summaryrefslogtreecommitdiff
path: root/program/lib
diff options
context:
space:
mode:
Diffstat (limited to 'program/lib')
-rw-r--r--program/lib/Crypt/GPG.php2542
-rw-r--r--program/lib/Crypt/GPG/DecryptStatusHandler.php336
-rw-r--r--program/lib/Crypt/GPG/Engine.php1758
-rw-r--r--program/lib/Crypt/GPG/Exceptions.php473
-rw-r--r--program/lib/Crypt/GPG/Key.php223
-rw-r--r--program/lib/Crypt/GPG/Signature.php428
-rw-r--r--program/lib/Crypt/GPG/SubKey.php649
-rw-r--r--program/lib/Crypt/GPG/UserId.php373
-rw-r--r--program/lib/Crypt/GPG/VerifyStatusHandler.php216
-rw-r--r--program/lib/Net/Sieve.php1274
-rw-r--r--program/lib/Roundcube/bootstrap.php30
-rw-r--r--program/lib/Roundcube/html.php70
-rw-r--r--program/lib/Roundcube/rcube.php302
-rw-r--r--program/lib/Roundcube/rcube_addressbook.php32
-rw-r--r--program/lib/Roundcube/rcube_base_replacer.php2
-rw-r--r--program/lib/Roundcube/rcube_browser.php2
-rw-r--r--program/lib/Roundcube/rcube_cache.php45
-rw-r--r--program/lib/Roundcube/rcube_config.php251
-rw-r--r--program/lib/Roundcube/rcube_contacts.php4
-rw-r--r--program/lib/Roundcube/rcube_content_filter.php2
-rw-r--r--program/lib/Roundcube/rcube_csv2vcard.php49
-rw-r--r--program/lib/Roundcube/rcube_db.php107
-rw-r--r--program/lib/Roundcube/rcube_db_mssql.php32
-rw-r--r--program/lib/Roundcube/rcube_db_mysql.php23
-rw-r--r--program/lib/Roundcube/rcube_db_pgsql.php47
-rw-r--r--program/lib/Roundcube/rcube_db_sqlite.php47
-rw-r--r--program/lib/Roundcube/rcube_db_sqlsrv.php29
-rw-r--r--program/lib/Roundcube/rcube_enriched.php2
-rw-r--r--program/lib/Roundcube/rcube_image.php99
-rw-r--r--program/lib/Roundcube/rcube_imap.php279
-rw-r--r--program/lib/Roundcube/rcube_imap_cache.php219
-rw-r--r--program/lib/Roundcube/rcube_imap_generic.php282
-rw-r--r--program/lib/Roundcube/rcube_ldap.php1322
-rw-r--r--program/lib/Roundcube/rcube_ldap_generic.php6
-rw-r--r--program/lib/Roundcube/rcube_message.php107
-rw-r--r--program/lib/Roundcube/rcube_mime.php32
-rw-r--r--program/lib/Roundcube/rcube_plugin.php36
-rw-r--r--program/lib/Roundcube/rcube_plugin_api.php122
-rw-r--r--program/lib/Roundcube/rcube_result_set.php47
-rw-r--r--program/lib/Roundcube/rcube_session.php142
-rw-r--r--program/lib/Roundcube/rcube_smtp.php12
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_atd.php204
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_enchant.php182
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_engine.php91
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_googie.php176
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_pspell.php189
-rw-r--r--program/lib/Roundcube/rcube_spellchecker.php163
-rw-r--r--program/lib/Roundcube/rcube_storage.php22
-rw-r--r--program/lib/Roundcube/rcube_string_replacer.php13
-rw-r--r--program/lib/Roundcube/rcube_user.php9
-rw-r--r--program/lib/Roundcube/rcube_utils.php84
-rw-r--r--program/lib/Roundcube/rcube_vcard.php15
-rw-r--r--program/lib/Roundcube/rcube_washtml.php21
53 files changed, 10780 insertions, 2442 deletions
diff --git a/program/lib/Crypt/GPG.php b/program/lib/Crypt/GPG.php
new file mode 100644
index 000000000..6e8e717e8
--- /dev/null
+++ b/program/lib/Crypt/GPG.php
@@ -0,0 +1,2542 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Crypt_GPG is a package to use GPG from PHP
+ *
+ * This package provides an object oriented interface to GNU Privacy
+ * Guard (GPG). It requires the GPG executable to be on the system.
+ *
+ * Though GPG can support symmetric-key cryptography, this package is intended
+ * only to facilitate public-key cryptography.
+ *
+ * This file contains the main GPG class. The class in this file lets you
+ * encrypt, decrypt, sign and verify data; import and delete keys; and perform
+ * other useful GPG tasks.
+ *
+ * Example usage:
+ * <code>
+ * <?php
+ * // encrypt some data
+ * $gpg = new Crypt_GPG();
+ * $gpg->addEncryptKey($mySecretKeyId);
+ * $encryptedData = $gpg->encrypt($data);
+ * ?>
+ * </code>
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Nathan Fredrickson <nathan@silverorange.com>
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2005-2010 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @version CVS: $Id: GPG.php 302814 2010-08-26 15:43:07Z gauthierm $
+ * @link http://pear.php.net/package/Crypt_GPG
+ * @link http://pear.php.net/manual/en/package.encryption.crypt-gpg.php
+ * @link http://www.gnupg.org/
+ */
+
+/**
+ * Signature handler class
+ */
+require_once 'Crypt/GPG/VerifyStatusHandler.php';
+
+/**
+ * Decryption handler class
+ */
+require_once 'Crypt/GPG/DecryptStatusHandler.php';
+
+/**
+ * GPG key class
+ */
+require_once 'Crypt/GPG/Key.php';
+
+/**
+ * GPG sub-key class
+ */
+require_once 'Crypt/GPG/SubKey.php';
+
+/**
+ * GPG user id class
+ */
+require_once 'Crypt/GPG/UserId.php';
+
+/**
+ * GPG process and I/O engine class
+ */
+require_once 'Crypt/GPG/Engine.php';
+
+/**
+ * GPG exception classes
+ */
+require_once 'Crypt/GPG/Exceptions.php';
+
+// {{{ class Crypt_GPG
+
+/**
+ * A class to use GPG from PHP
+ *
+ * This class provides an object oriented interface to GNU Privacy Guard (GPG).
+ *
+ * Though GPG can support symmetric-key cryptography, this class is intended
+ * only to facilitate public-key cryptography.
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Nathan Fredrickson <nathan@silverorange.com>
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2005-2010 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ * @link http://www.gnupg.org/
+ */
+class Crypt_GPG
+{
+ // {{{ class error constants
+
+ /**
+ * Error code returned when there is no error.
+ */
+ const ERROR_NONE = 0;
+
+ /**
+ * Error code returned when an unknown or unhandled error occurs.
+ */
+ const ERROR_UNKNOWN = 1;
+
+ /**
+ * Error code returned when a bad passphrase is used.
+ */
+ const ERROR_BAD_PASSPHRASE = 2;
+
+ /**
+ * Error code returned when a required passphrase is missing.
+ */
+ const ERROR_MISSING_PASSPHRASE = 3;
+
+ /**
+ * Error code returned when a key that is already in the keyring is
+ * imported.
+ */
+ const ERROR_DUPLICATE_KEY = 4;
+
+ /**
+ * Error code returned the required data is missing for an operation.
+ *
+ * This could be missing key data, missing encrypted data or missing
+ * signature data.
+ */
+ const ERROR_NO_DATA = 5;
+
+ /**
+ * Error code returned when an unsigned key is used.
+ */
+ const ERROR_UNSIGNED_KEY = 6;
+
+ /**
+ * Error code returned when a key that is not self-signed is used.
+ */
+ const ERROR_NOT_SELF_SIGNED = 7;
+
+ /**
+ * Error code returned when a public or private key that is not in the
+ * keyring is used.
+ */
+ const ERROR_KEY_NOT_FOUND = 8;
+
+ /**
+ * Error code returned when an attempt to delete public key having a
+ * private key is made.
+ */
+ const ERROR_DELETE_PRIVATE_KEY = 9;
+
+ /**
+ * Error code returned when one or more bad signatures are detected.
+ */
+ const ERROR_BAD_SIGNATURE = 10;
+
+ /**
+ * Error code returned when there is a problem reading GnuPG data files.
+ */
+ const ERROR_FILE_PERMISSIONS = 11;
+
+ // }}}
+ // {{{ class constants for data signing modes
+
+ /**
+ * Signing mode for normal signing of data. The signed message will not
+ * be readable without special software.
+ *
+ * This is the default signing mode.
+ *
+ * @see Crypt_GPG::sign()
+ * @see Crypt_GPG::signFile()
+ */
+ const SIGN_MODE_NORMAL = 1;
+
+ /**
+ * Signing mode for clearsigning data. Clearsigned signatures are ASCII
+ * armored data and are readable without special software. If the signed
+ * message is unencrypted, the message will still be readable. The message
+ * text will be in the original encoding.
+ *
+ * @see Crypt_GPG::sign()
+ * @see Crypt_GPG::signFile()
+ */
+ const SIGN_MODE_CLEAR = 2;
+
+ /**
+ * Signing mode for creating a detached signature. When using detached
+ * signatures, only the signature data is returned. The original message
+ * text may be distributed separately from the signature data. This is
+ * useful for miltipart/signed email messages as per
+ * {@link http://www.ietf.org/rfc/rfc3156.txt RFC 3156}.
+ *
+ * @see Crypt_GPG::sign()
+ * @see Crypt_GPG::signFile()
+ */
+ const SIGN_MODE_DETACHED = 3;
+
+ // }}}
+ // {{{ class constants for fingerprint formats
+
+ /**
+ * No formatting is performed.
+ *
+ * Example: C3BC615AD9C766E5A85C1F2716D27458B1BBA1C4
+ *
+ * @see Crypt_GPG::getFingerprint()
+ */
+ const FORMAT_NONE = 1;
+
+ /**
+ * Fingerprint is formatted in the format used by the GnuPG gpg command's
+ * default output.
+ *
+ * Example: C3BC 615A D9C7 66E5 A85C 1F27 16D2 7458 B1BB A1C4
+ *
+ * @see Crypt_GPG::getFingerprint()
+ */
+ const FORMAT_CANONICAL = 2;
+
+ /**
+ * Fingerprint is formatted in the format used when displaying X.509
+ * certificates
+ *
+ * Example: C3:BC:61:5A:D9:C7:66:E5:A8:5C:1F:27:16:D2:74:58:B1:BB:A1:C4
+ *
+ * @see Crypt_GPG::getFingerprint()
+ */
+ const FORMAT_X509 = 3;
+
+ // }}}
+ // {{{ other class constants
+
+ /**
+ * URI at which package bugs may be reported.
+ */
+ const BUG_URI = 'http://pear.php.net/bugs/report.php?package=Crypt_GPG';
+
+ // }}}
+ // {{{ protected class properties
+
+ /**
+ * Engine used to control the GPG subprocess
+ *
+ * @var Crypt_GPG_Engine
+ *
+ * @see Crypt_GPG::setEngine()
+ */
+ protected $engine = null;
+
+ /**
+ * Keys used to encrypt
+ *
+ * The array is of the form:
+ * <code>
+ * array(
+ * $key_id => array(
+ * 'fingerprint' => $fingerprint,
+ * 'passphrase' => null
+ * )
+ * );
+ * </code>
+ *
+ * @var array
+ * @see Crypt_GPG::addEncryptKey()
+ * @see Crypt_GPG::clearEncryptKeys()
+ */
+ protected $encryptKeys = array();
+
+ /**
+ * Keys used to decrypt
+ *
+ * The array is of the form:
+ * <code>
+ * array(
+ * $key_id => array(
+ * 'fingerprint' => $fingerprint,
+ * 'passphrase' => $passphrase
+ * )
+ * );
+ * </code>
+ *
+ * @var array
+ * @see Crypt_GPG::addSignKey()
+ * @see Crypt_GPG::clearSignKeys()
+ */
+ protected $signKeys = array();
+
+ /**
+ * Keys used to sign
+ *
+ * The array is of the form:
+ * <code>
+ * array(
+ * $key_id => array(
+ * 'fingerprint' => $fingerprint,
+ * 'passphrase' => $passphrase
+ * )
+ * );
+ * </code>
+ *
+ * @var array
+ * @see Crypt_GPG::addDecryptKey()
+ * @see Crypt_GPG::clearDecryptKeys()
+ */
+ protected $decryptKeys = array();
+
+ // }}}
+ // {{{ __construct()
+
+ /**
+ * Creates a new GPG object
+ *
+ * Available options are:
+ *
+ * - <kbd>string homedir</kbd> - the directory where the GPG
+ * keyring files are stored. If not
+ * specified, Crypt_GPG uses the
+ * default of <kbd>~/.gnupg</kbd>.
+ * - <kbd>string publicKeyring</kbd> - the file path of the public
+ * keyring. Use this if the public
+ * keyring is not in the homedir, or
+ * if the keyring is in a directory
+ * not writable by the process
+ * invoking GPG (like Apache). Then
+ * you can specify the path to the
+ * keyring with this option
+ * (/foo/bar/pubring.gpg), and specify
+ * a writable directory (like /tmp)
+ * using the <i>homedir</i> option.
+ * - <kbd>string privateKeyring</kbd> - the file path of the private
+ * keyring. Use this if the private
+ * keyring is not in the homedir, or
+ * if the keyring is in a directory
+ * not writable by the process
+ * invoking GPG (like Apache). Then
+ * you can specify the path to the
+ * keyring with this option
+ * (/foo/bar/secring.gpg), and specify
+ * a writable directory (like /tmp)
+ * using the <i>homedir</i> option.
+ * - <kbd>string trustDb</kbd> - the file path of the web-of-trust
+ * database. Use this if the trust
+ * database is not in the homedir, or
+ * if the database is in a directory
+ * not writable by the process
+ * invoking GPG (like Apache). Then
+ * you can specify the path to the
+ * trust database with this option
+ * (/foo/bar/trustdb.gpg), and specify
+ * a writable directory (like /tmp)
+ * using the <i>homedir</i> option.
+ * - <kbd>string binary</kbd> - the location of the GPG binary. If
+ * not specified, the driver attempts
+ * to auto-detect the GPG binary
+ * location using a list of known
+ * default locations for the current
+ * operating system. The option
+ * <kbd>gpgBinary</kbd> is a
+ * deprecated alias for this option.
+ * - <kbd>boolean debug</kbd> - whether or not to use debug mode.
+ * When debug mode is on, all
+ * communication to and from the GPG
+ * subprocess is logged. This can be
+ *
+ * @param array $options optional. An array of options used to create the
+ * GPG object. All options are optional and are
+ * represented as key-value pairs.
+ *
+ * @throws Crypt_GPG_FileException if the <kbd>homedir</kbd> does not exist
+ * and cannot be created. This can happen if <kbd>homedir</kbd> is
+ * not specified, Crypt_GPG is run as the web user, and the web
+ * user has no home directory. This exception is also thrown if any
+ * of the options <kbd>publicKeyring</kbd>,
+ * <kbd>privateKeyring</kbd> or <kbd>trustDb</kbd> options are
+ * specified but the files do not exist or are are not readable.
+ * This can happen if the user running the Crypt_GPG process (for
+ * example, the Apache user) does not have permission to read the
+ * files.
+ *
+ * @throws PEAR_Exception if the provided <kbd>binary</kbd> is invalid, or
+ * if no <kbd>binary</kbd> is provided and no suitable binary could
+ * be found.
+ */
+ public function __construct(array $options = array())
+ {
+ $this->setEngine(new Crypt_GPG_Engine($options));
+ }
+
+ // }}}
+ // {{{ importKey()
+
+ /**
+ * Imports a public or private key into the keyring
+ *
+ * Keys may be removed from the keyring using
+ * {@link Crypt_GPG::deletePublicKey()} or
+ * {@link Crypt_GPG::deletePrivateKey()}.
+ *
+ * @param string $data the key data to be imported.
+ *
+ * @return array an associative array containing the following elements:
+ * - <kbd>fingerprint</kbd> - the fingerprint of the
+ * imported key,
+ * - <kbd>public_imported</kbd> - the number of public
+ * keys imported,
+ * - <kbd>public_unchanged</kbd> - the number of unchanged
+ * public keys,
+ * - <kbd>private_imported</kbd> - the number of private
+ * keys imported,
+ * - <kbd>private_unchanged</kbd> - the number of unchanged
+ * private keys.
+ *
+ * @throws Crypt_GPG_NoDataException if the key data is missing or if the
+ * data is is not valid key data.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function importKey($data)
+ {
+ return $this->_importKey($data, false);
+ }
+
+ // }}}
+ // {{{ importKeyFile()
+
+ /**
+ * Imports a public or private key file into the keyring
+ *
+ * Keys may be removed from the keyring using
+ * {@link Crypt_GPG::deletePublicKey()} or
+ * {@link Crypt_GPG::deletePrivateKey()}.
+ *
+ * @param string $filename the key file to be imported.
+ *
+ * @return array an associative array containing the following elements:
+ * - <kbd>fingerprint</kbd> - the fingerprint of the
+ * imported key,
+ * - <kbd>public_imported</kbd> - the number of public
+ * keys imported,
+ * - <kbd>public_unchanged</kbd> - the number of unchanged
+ * public keys,
+ * - <kbd>private_imported</kbd> - the number of private
+ * keys imported,
+ * - <kbd>private_unchanged</kbd> - the number of unchanged
+ * private keys.
+ * private keys.
+ *
+ * @throws Crypt_GPG_NoDataException if the key data is missing or if the
+ * data is is not valid key data.
+ *
+ * @throws Crypt_GPG_FileException if the key file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function importKeyFile($filename)
+ {
+ return $this->_importKey($filename, true);
+ }
+
+ // }}}
+ // {{{ exportPublicKey()
+
+ /**
+ * Exports a public key from the keyring
+ *
+ * The exported key remains on the keyring. To delete the public key, use
+ * {@link Crypt_GPG::deletePublicKey()}.
+ *
+ * If more than one key fingerprint is available for the specified
+ * <kbd>$keyId</kbd> (for example, if you use a non-unique uid) only the
+ * first public key is exported.
+ *
+ * @param string $keyId either the full uid of the public key, the email
+ * part of the uid of the public key or the key id of
+ * the public key. For example,
+ * "Test User (example) <test@example.com>",
+ * "test@example.com" or a hexadecimal string.
+ * @param boolean $armor optional. If true, ASCII armored data is returned;
+ * otherwise, binary data is returned. Defaults to
+ * true.
+ *
+ * @return string the public key data.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if a public key with the given
+ * <kbd>$keyId</kbd> is not found.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function exportPublicKey($keyId, $armor = true)
+ {
+ $fingerprint = $this->getFingerprint($keyId);
+
+ if ($fingerprint === null) {
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'Public key not found: ' . $keyId,
+ Crypt_GPG::ERROR_KEY_NOT_FOUND, $keyId);
+ }
+
+ $keyData = '';
+ $operation = '--export ' . escapeshellarg($fingerprint);
+ $arguments = ($armor) ? array('--armor') : array();
+
+ $this->engine->reset();
+ $this->engine->setOutput($keyData);
+ $this->engine->setOperation($operation, $arguments);
+ $this->engine->run();
+
+ $code = $this->engine->getErrorCode();
+
+ if ($code !== Crypt_GPG::ERROR_NONE) {
+ throw new Crypt_GPG_Exception(
+ 'Unknown error exporting public key. Please use the ' .
+ '\'debug\' option when creating the Crypt_GPG object, and ' .
+ 'file a bug report at ' . self::BUG_URI, $code);
+ }
+
+ return $keyData;
+ }
+
+ // }}}
+ // {{{ deletePublicKey()
+
+ /**
+ * Deletes a public key from the keyring
+ *
+ * If more than one key fingerprint is available for the specified
+ * <kbd>$keyId</kbd> (for example, if you use a non-unique uid) only the
+ * first public key is deleted.
+ *
+ * The private key must be deleted first or an exception will be thrown.
+ * See {@link Crypt_GPG::deletePrivateKey()}.
+ *
+ * @param string $keyId either the full uid of the public key, the email
+ * part of the uid of the public key or the key id of
+ * the public key. For example,
+ * "Test User (example) <test@example.com>",
+ * "test@example.com" or a hexadecimal string.
+ *
+ * @return void
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if a public key with the given
+ * <kbd>$keyId</kbd> is not found.
+ *
+ * @throws Crypt_GPG_DeletePrivateKeyException if the specified public key
+ * has an associated private key on the keyring. The private key
+ * must be deleted first.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function deletePublicKey($keyId)
+ {
+ $fingerprint = $this->getFingerprint($keyId);
+
+ if ($fingerprint === null) {
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'Public key not found: ' . $keyId,
+ Crypt_GPG::ERROR_KEY_NOT_FOUND, $keyId);
+ }
+
+ $operation = '--delete-key ' . escapeshellarg($fingerprint);
+ $arguments = array(
+ '--batch',
+ '--yes'
+ );
+
+ $this->engine->reset();
+ $this->engine->setOperation($operation, $arguments);
+ $this->engine->run();
+
+ $code = $this->engine->getErrorCode();
+
+ switch ($code) {
+ case Crypt_GPG::ERROR_NONE:
+ break;
+ case Crypt_GPG::ERROR_DELETE_PRIVATE_KEY:
+ throw new Crypt_GPG_DeletePrivateKeyException(
+ 'Private key must be deleted before public key can be ' .
+ 'deleted.', $code, $keyId);
+ default:
+ throw new Crypt_GPG_Exception(
+ 'Unknown error deleting public key. Please use the ' .
+ '\'debug\' option when creating the Crypt_GPG object, and ' .
+ 'file a bug report at ' . self::BUG_URI, $code);
+ }
+ }
+
+ // }}}
+ // {{{ deletePrivateKey()
+
+ /**
+ * Deletes a private key from the keyring
+ *
+ * If more than one key fingerprint is available for the specified
+ * <kbd>$keyId</kbd> (for example, if you use a non-unique uid) only the
+ * first private key is deleted.
+ *
+ * Calls GPG with the <kbd>--delete-secret-key</kbd> command.
+ *
+ * @param string $keyId either the full uid of the private key, the email
+ * part of the uid of the private key or the key id of
+ * the private key. For example,
+ * "Test User (example) <test@example.com>",
+ * "test@example.com" or a hexadecimal string.
+ *
+ * @return void
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if a private key with the given
+ * <kbd>$keyId</kbd> is not found.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function deletePrivateKey($keyId)
+ {
+ $fingerprint = $this->getFingerprint($keyId);
+
+ if ($fingerprint === null) {
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'Private key not found: ' . $keyId,
+ Crypt_GPG::ERROR_KEY_NOT_FOUND, $keyId);
+ }
+
+ $operation = '--delete-secret-key ' . escapeshellarg($fingerprint);
+ $arguments = array(
+ '--batch',
+ '--yes'
+ );
+
+ $this->engine->reset();
+ $this->engine->setOperation($operation, $arguments);
+ $this->engine->run();
+
+ $code = $this->engine->getErrorCode();
+
+ switch ($code) {
+ case Crypt_GPG::ERROR_NONE:
+ break;
+ case Crypt_GPG::ERROR_KEY_NOT_FOUND:
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'Private key not found: ' . $keyId,
+ $code, $keyId);
+ default:
+ throw new Crypt_GPG_Exception(
+ 'Unknown error deleting private key. Please use the ' .
+ '\'debug\' option when creating the Crypt_GPG object, and ' .
+ 'file a bug report at ' . self::BUG_URI, $code);
+ }
+ }
+
+ // }}}
+ // {{{ getKeys()
+
+ /**
+ * Gets the available keys in the keyring
+ *
+ * Calls GPG with the <kbd>--list-keys</kbd> command and grabs keys. See
+ * the first section of <b>doc/DETAILS</b> in the
+ * {@link http://www.gnupg.org/download/ GPG package} for a detailed
+ * description of how the GPG command output is parsed.
+ *
+ * @param string $keyId optional. Only keys with that match the specified
+ * pattern are returned. The pattern may be part of
+ * a user id, a key id or a key fingerprint. If not
+ * specified, all keys are returned.
+ *
+ * @return array an array of {@link Crypt_GPG_Key} objects. If no keys
+ * match the specified <kbd>$keyId</kbd> an empty array is
+ * returned.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ *
+ * @see Crypt_GPG_Key
+ */
+ public function getKeys($keyId = '')
+ {
+ // get private key fingerprints
+ if ($keyId == '') {
+ $operation = '--list-secret-keys';
+ } else {
+ $operation = '--list-secret-keys ' . escapeshellarg($keyId);
+ }
+
+ // According to The file 'doc/DETAILS' in the GnuPG distribution, using
+ // double '--with-fingerprint' also prints the fingerprint for subkeys.
+ $arguments = array(
+ '--with-colons',
+ '--with-fingerprint',
+ '--with-fingerprint',
+ '--fixed-list-mode'
+ );
+
+ $output = '';
+
+ $this->engine->reset();
+ $this->engine->setOutput($output);
+ $this->engine->setOperation($operation, $arguments);
+ $this->engine->run();
+
+ $code = $this->engine->getErrorCode();
+
+ switch ($code) {
+ case Crypt_GPG::ERROR_NONE:
+ case Crypt_GPG::ERROR_KEY_NOT_FOUND:
+ // ignore not found key errors
+ break;
+ case Crypt_GPG::ERROR_FILE_PERMISSIONS:
+ $filename = $this->engine->getErrorFilename();
+ if ($filename) {
+ throw new Crypt_GPG_FileException(sprintf(
+ 'Error reading GnuPG data file \'%s\'. Check to make ' .
+ 'sure it is readable by the current user.', $filename),
+ $code, $filename);
+ }
+ throw new Crypt_GPG_FileException(
+ 'Error reading GnuPG data file. Check to make GnuPG data ' .
+ 'files are readable by the current user.', $code);
+ default:
+ throw new Crypt_GPG_Exception(
+ 'Unknown error getting keys. Please use the \'debug\' option ' .
+ 'when creating the Crypt_GPG object, and file a bug report ' .
+ 'at ' . self::BUG_URI, $code);
+ }
+
+ $privateKeyFingerprints = array();
+
+ $lines = explode(PHP_EOL, $output);
+ foreach ($lines as $line) {
+ $lineExp = explode(':', $line);
+ if ($lineExp[0] == 'fpr') {
+ $privateKeyFingerprints[] = $lineExp[9];
+ }
+ }
+
+ // get public keys
+ if ($keyId == '') {
+ $operation = '--list-public-keys';
+ } else {
+ $operation = '--list-public-keys ' . escapeshellarg($keyId);
+ }
+
+ $output = '';
+
+ $this->engine->reset();
+ $this->engine->setOutput($output);
+ $this->engine->setOperation($operation, $arguments);
+ $this->engine->run();
+
+ $code = $this->engine->getErrorCode();
+
+ switch ($code) {
+ case Crypt_GPG::ERROR_NONE:
+ case Crypt_GPG::ERROR_KEY_NOT_FOUND:
+ // ignore not found key errors
+ break;
+ case Crypt_GPG::ERROR_FILE_PERMISSIONS:
+ $filename = $this->engine->getErrorFilename();
+ if ($filename) {
+ throw new Crypt_GPG_FileException(sprintf(
+ 'Error reading GnuPG data file \'%s\'. Check to make ' .
+ 'sure it is readable by the current user.', $filename),
+ $code, $filename);
+ }
+ throw new Crypt_GPG_FileException(
+ 'Error reading GnuPG data file. Check to make GnuPG data ' .
+ 'files are readable by the current user.', $code);
+ default:
+ throw new Crypt_GPG_Exception(
+ 'Unknown error getting keys. Please use the \'debug\' option ' .
+ 'when creating the Crypt_GPG object, and file a bug report ' .
+ 'at ' . self::BUG_URI, $code);
+ }
+
+ $keys = array();
+
+ $key = null; // current key
+ $subKey = null; // current sub-key
+
+ $lines = explode(PHP_EOL, $output);
+ foreach ($lines as $line) {
+ $lineExp = explode(':', $line);
+
+ if ($lineExp[0] == 'pub') {
+
+ // new primary key means last key should be added to the array
+ if ($key !== null) {
+ $keys[] = $key;
+ }
+
+ $key = new Crypt_GPG_Key();
+
+ $subKey = Crypt_GPG_SubKey::parse($line);
+ $key->addSubKey($subKey);
+
+ } elseif ($lineExp[0] == 'sub') {
+
+ $subKey = Crypt_GPG_SubKey::parse($line);
+ $key->addSubKey($subKey);
+
+ } elseif ($lineExp[0] == 'fpr') {
+
+ $fingerprint = $lineExp[9];
+
+ // set current sub-key fingerprint
+ $subKey->setFingerprint($fingerprint);
+
+ // if private key exists, set has private to true
+ if (in_array($fingerprint, $privateKeyFingerprints)) {
+ $subKey->setHasPrivate(true);
+ }
+
+ } elseif ($lineExp[0] == 'uid') {
+
+ $string = stripcslashes($lineExp[9]); // as per documentation
+ $userId = new Crypt_GPG_UserId($string);
+
+ if ($lineExp[1] == 'r') {
+ $userId->setRevoked(true);
+ }
+
+ $key->addUserId($userId);
+
+ }
+ }
+
+ // add last key
+ if ($key !== null) {
+ $keys[] = $key;
+ }
+
+ return $keys;
+ }
+
+ // }}}
+ // {{{ getFingerprint()
+
+ /**
+ * Gets a key fingerprint from the keyring
+ *
+ * If more than one key fingerprint is available (for example, if you use
+ * a non-unique user id) only the first key fingerprint is returned.
+ *
+ * Calls the GPG <kbd>--list-keys</kbd> command with the
+ * <kbd>--with-fingerprint</kbd> option to retrieve a public key
+ * fingerprint.
+ *
+ * @param string $keyId either the full user id of the key, the email
+ * part of the user id of the key, or the key id of
+ * the key. For example,
+ * "Test User (example) <test@example.com>",
+ * "test@example.com" or a hexadecimal string.
+ * @param integer $format optional. How the fingerprint should be formatted.
+ * Use {@link Crypt_GPG::FORMAT_X509} for X.509
+ * certificate format,
+ * {@link Crypt_GPG::FORMAT_CANONICAL} for the format
+ * used by GnuPG output and
+ * {@link Crypt_GPG::FORMAT_NONE} for no formatting.
+ * Defaults to <code>Crypt_GPG::FORMAT_NONE</code>.
+ *
+ * @return string the fingerprint of the key, or null if no fingerprint
+ * is found for the given <kbd>$keyId</kbd>.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function getFingerprint($keyId, $format = Crypt_GPG::FORMAT_NONE)
+ {
+ $output = '';
+ $operation = '--list-keys ' . escapeshellarg($keyId);
+ $arguments = array(
+ '--with-colons',
+ '--with-fingerprint'
+ );
+
+ $this->engine->reset();
+ $this->engine->setOutput($output);
+ $this->engine->setOperation($operation, $arguments);
+ $this->engine->run();
+
+ $code = $this->engine->getErrorCode();
+
+ switch ($code) {
+ case Crypt_GPG::ERROR_NONE:
+ case Crypt_GPG::ERROR_KEY_NOT_FOUND:
+ // ignore not found key errors
+ break;
+ default:
+ throw new Crypt_GPG_Exception(
+ 'Unknown error getting key fingerprint. Please use the ' .
+ '\'debug\' option when creating the Crypt_GPG object, and ' .
+ 'file a bug report at ' . self::BUG_URI, $code);
+ }
+
+ $fingerprint = null;
+
+ $lines = explode(PHP_EOL, $output);
+ foreach ($lines as $line) {
+ if (substr($line, 0, 3) == 'fpr') {
+ $lineExp = explode(':', $line);
+ $fingerprint = $lineExp[9];
+
+ switch ($format) {
+ case Crypt_GPG::FORMAT_CANONICAL:
+ $fingerprintExp = str_split($fingerprint, 4);
+ $format = '%s %s %s %s %s %s %s %s %s %s';
+ $fingerprint = vsprintf($format, $fingerprintExp);
+ break;
+
+ case Crypt_GPG::FORMAT_X509:
+ $fingerprintExp = str_split($fingerprint, 2);
+ $fingerprint = implode(':', $fingerprintExp);
+ break;
+ }
+
+ break;
+ }
+ }
+
+ return $fingerprint;
+ }
+
+ // }}}
+ // {{{ encrypt()
+
+ /**
+ * Encrypts string data
+ *
+ * Data is ASCII armored by default but may optionally be returned as
+ * binary.
+ *
+ * @param string $data the data to be encrypted.
+ * @param boolean $armor optional. If true, ASCII armored data is returned;
+ * otherwise, binary data is returned. Defaults to
+ * true.
+ *
+ * @return string the encrypted data.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if no encryption key is specified.
+ * See {@link Crypt_GPG::addEncryptKey()}.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ *
+ * @sensitive $data
+ */
+ public function encrypt($data, $armor = true)
+ {
+ return $this->_encrypt($data, false, null, $armor);
+ }
+
+ // }}}
+ // {{{ encryptFile()
+
+ /**
+ * Encrypts a file
+ *
+ * Encrypted data is ASCII armored by default but may optionally be saved
+ * as binary.
+ *
+ * @param string $filename the filename of the file to encrypt.
+ * @param string $encryptedFile optional. The filename of the file in
+ * which to store the encrypted data. If null
+ * or unspecified, the encrypted data is
+ * returned as a string.
+ * @param boolean $armor optional. If true, ASCII armored data is
+ * returned; otherwise, binary data is
+ * returned. Defaults to true.
+ *
+ * @return void|string if the <kbd>$encryptedFile</kbd> parameter is null,
+ * a string containing the encrypted data is returned.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if no encryption key is specified.
+ * See {@link Crypt_GPG::addEncryptKey()}.
+ *
+ * @throws Crypt_GPG_FileException if the output file is not writeable or
+ * if the input file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function encryptFile($filename, $encryptedFile = null, $armor = true)
+ {
+ return $this->_encrypt($filename, true, $encryptedFile, $armor);
+ }
+
+ // }}}
+ // {{{ encryptAndSign()
+
+ /**
+ * Encrypts and signs data
+ *
+ * Data is encrypted and signed in a single pass.
+ *
+ * NOTE: Until GnuPG version 1.4.10, it was not possible to verify
+ * encrypted-signed data without decrypting it at the same time. If you try
+ * to use {@link Crypt_GPG::verify()} method on encrypted-signed data with
+ * earlier GnuPG versions, you will get an error. Please use
+ * {@link Crypt_GPG::decryptAndVerify()} to verify encrypted-signed data.
+ *
+ * @param string $data the data to be encrypted and signed.
+ * @param boolean $armor optional. If true, ASCII armored data is returned;
+ * otherwise, binary data is returned. Defaults to
+ * true.
+ *
+ * @return string the encrypted signed data.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if no encryption key is specified
+ * or if no signing key is specified. See
+ * {@link Crypt_GPG::addEncryptKey()} and
+ * {@link Crypt_GPG::addSignKey()}.
+ *
+ * @throws Crypt_GPG_BadPassphraseException if a specified passphrase is
+ * incorrect or if a required passphrase is not specified.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ *
+ * @see Crypt_GPG::decryptAndVerify()
+ */
+ public function encryptAndSign($data, $armor = true)
+ {
+ return $this->_encryptAndSign($data, false, null, $armor);
+ }
+
+ // }}}
+ // {{{ encryptAndSignFile()
+
+ /**
+ * Encrypts and signs a file
+ *
+ * The file is encrypted and signed in a single pass.
+ *
+ * NOTE: Until GnuPG version 1.4.10, it was not possible to verify
+ * encrypted-signed files without decrypting them at the same time. If you
+ * try to use {@link Crypt_GPG::verify()} method on encrypted-signed files
+ * with earlier GnuPG versions, you will get an error. Please use
+ * {@link Crypt_GPG::decryptAndVerifyFile()} to verify encrypted-signed
+ * files.
+ *
+ * @param string $filename the name of the file containing the data to
+ * be encrypted and signed.
+ * @param string $signedFile optional. The name of the file in which the
+ * encrypted, signed data should be stored. If
+ * null or unspecified, the encrypted, signed
+ * data is returned as a string.
+ * @param boolean $armor optional. If true, ASCII armored data is
+ * returned; otherwise, binary data is returned.
+ * Defaults to true.
+ *
+ * @return void|string if the <kbd>$signedFile</kbd> parameter is null, a
+ * string containing the encrypted, signed data is
+ * returned.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if no encryption key is specified
+ * or if no signing key is specified. See
+ * {@link Crypt_GPG::addEncryptKey()} and
+ * {@link Crypt_GPG::addSignKey()}.
+ *
+ * @throws Crypt_GPG_BadPassphraseException if a specified passphrase is
+ * incorrect or if a required passphrase is not specified.
+ *
+ * @throws Crypt_GPG_FileException if the output file is not writeable or
+ * if the input file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ *
+ * @see Crypt_GPG::decryptAndVerifyFile()
+ */
+ public function encryptAndSignFile($filename, $signedFile = null,
+ $armor = true
+ ) {
+ return $this->_encryptAndSign($filename, true, $signedFile, $armor);
+ }
+
+ // }}}
+ // {{{ decrypt()
+
+ /**
+ * Decrypts string data
+ *
+ * This method assumes the required private key is available in the keyring
+ * and throws an exception if the private key is not available. To add a
+ * private key to the keyring, use the {@link Crypt_GPG::importKey()} or
+ * {@link Crypt_GPG::importKeyFile()} methods.
+ *
+ * @param string $encryptedData the data to be decrypted.
+ *
+ * @return string the decrypted data.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if the private key needed to
+ * decrypt the data is not in the user's keyring.
+ *
+ * @throws Crypt_GPG_NoDataException if specified data does not contain
+ * GPG encrypted data.
+ *
+ * @throws Crypt_GPG_BadPassphraseException if a required passphrase is
+ * incorrect or if a required passphrase is not specified. See
+ * {@link Crypt_GPG::addDecryptKey()}.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function decrypt($encryptedData)
+ {
+ return $this->_decrypt($encryptedData, false, null);
+ }
+
+ // }}}
+ // {{{ decryptFile()
+
+ /**
+ * Decrypts a file
+ *
+ * This method assumes the required private key is available in the keyring
+ * and throws an exception if the private key is not available. To add a
+ * private key to the keyring, use the {@link Crypt_GPG::importKey()} or
+ * {@link Crypt_GPG::importKeyFile()} methods.
+ *
+ * @param string $encryptedFile the name of the encrypted file data to
+ * decrypt.
+ * @param string $decryptedFile optional. The name of the file to which the
+ * decrypted data should be written. If null
+ * or unspecified, the decrypted data is
+ * returned as a string.
+ *
+ * @return void|string if the <kbd>$decryptedFile</kbd> parameter is null,
+ * a string containing the decrypted data is returned.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if the private key needed to
+ * decrypt the data is not in the user's keyring.
+ *
+ * @throws Crypt_GPG_NoDataException if specified data does not contain
+ * GPG encrypted data.
+ *
+ * @throws Crypt_GPG_BadPassphraseException if a required passphrase is
+ * incorrect or if a required passphrase is not specified. See
+ * {@link Crypt_GPG::addDecryptKey()}.
+ *
+ * @throws Crypt_GPG_FileException if the output file is not writeable or
+ * if the input file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function decryptFile($encryptedFile, $decryptedFile = null)
+ {
+ return $this->_decrypt($encryptedFile, true, $decryptedFile);
+ }
+
+ // }}}
+ // {{{ decryptAndVerify()
+
+ /**
+ * Decrypts and verifies string data
+ *
+ * This method assumes the required private key is available in the keyring
+ * and throws an exception if the private key is not available. To add a
+ * private key to the keyring, use the {@link Crypt_GPG::importKey()} or
+ * {@link Crypt_GPG::importKeyFile()} methods.
+ *
+ * @param string $encryptedData the encrypted, signed data to be decrypted
+ * and verified.
+ *
+ * @return array two element array. The array has an element 'data'
+ * containing the decrypted data and an element
+ * 'signatures' containing an array of
+ * {@link Crypt_GPG_Signature} objects for the signed data.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if the private key needed to
+ * decrypt the data is not in the user's keyring.
+ *
+ * @throws Crypt_GPG_NoDataException if specified data does not contain
+ * GPG encrypted data.
+ *
+ * @throws Crypt_GPG_BadPassphraseException if a required passphrase is
+ * incorrect or if a required passphrase is not specified. See
+ * {@link Crypt_GPG::addDecryptKey()}.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function decryptAndVerify($encryptedData)
+ {
+ return $this->_decryptAndVerify($encryptedData, false, null);
+ }
+
+ // }}}
+ // {{{ decryptAndVerifyFile()
+
+ /**
+ * Decrypts and verifies a signed, encrypted file
+ *
+ * This method assumes the required private key is available in the keyring
+ * and throws an exception if the private key is not available. To add a
+ * private key to the keyring, use the {@link Crypt_GPG::importKey()} or
+ * {@link Crypt_GPG::importKeyFile()} methods.
+ *
+ * @param string $encryptedFile the name of the signed, encrypted file to
+ * to decrypt and verify.
+ * @param string $decryptedFile optional. The name of the file to which the
+ * decrypted data should be written. If null
+ * or unspecified, the decrypted data is
+ * returned in the results array.
+ *
+ * @return array two element array. The array has an element 'data'
+ * containing the decrypted data and an element
+ * 'signatures' containing an array of
+ * {@link Crypt_GPG_Signature} objects for the signed data.
+ * If the decrypted data is written to a file, the 'data'
+ * element is null.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if the private key needed to
+ * decrypt the data is not in the user's keyring.
+ *
+ * @throws Crypt_GPG_NoDataException if specified data does not contain
+ * GPG encrypted data.
+ *
+ * @throws Crypt_GPG_BadPassphraseException if a required passphrase is
+ * incorrect or if a required passphrase is not specified. See
+ * {@link Crypt_GPG::addDecryptKey()}.
+ *
+ * @throws Crypt_GPG_FileException if the output file is not writeable or
+ * if the input file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function decryptAndVerifyFile($encryptedFile, $decryptedFile = null)
+ {
+ return $this->_decryptAndVerify($encryptedFile, true, $decryptedFile);
+ }
+
+ // }}}
+ // {{{ sign()
+
+ /**
+ * Signs data
+ *
+ * Data may be signed using any one of the three available signing modes:
+ * - {@link Crypt_GPG::SIGN_MODE_NORMAL}
+ * - {@link Crypt_GPG::SIGN_MODE_CLEAR}
+ * - {@link Crypt_GPG::SIGN_MODE_DETACHED}
+ *
+ * @param string $data the data to be signed.
+ * @param boolean $mode optional. The data signing mode to use. Should
+ * be one of {@link Crypt_GPG::SIGN_MODE_NORMAL},
+ * {@link Crypt_GPG::SIGN_MODE_CLEAR} or
+ * {@link Crypt_GPG::SIGN_MODE_DETACHED}. If not
+ * specified, defaults to
+ * <kbd>Crypt_GPG::SIGN_MODE_NORMAL</kbd>.
+ * @param boolean $armor optional. If true, ASCII armored data is
+ * returned; otherwise, binary data is returned.
+ * Defaults to true. This has no effect if the
+ * mode <kbd>Crypt_GPG::SIGN_MODE_CLEAR</kbd> is
+ * used.
+ * @param boolean $textmode optional. If true, line-breaks in signed data
+ * are normalized. Use this option when signing
+ * e-mail, or for greater compatibility between
+ * systems with different line-break formats.
+ * Defaults to false. This has no effect if the
+ * mode <kbd>Crypt_GPG::SIGN_MODE_CLEAR</kbd> is
+ * used as clear-signing always uses textmode.
+ *
+ * @return string the signed data, or the signature data if a detached
+ * signature is requested.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if no signing key is specified.
+ * See {@link Crypt_GPG::addSignKey()}.
+ *
+ * @throws Crypt_GPG_BadPassphraseException if a specified passphrase is
+ * incorrect or if a required passphrase is not specified.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function sign($data, $mode = Crypt_GPG::SIGN_MODE_NORMAL,
+ $armor = true, $textmode = false
+ ) {
+ return $this->_sign($data, false, null, $mode, $armor, $textmode);
+ }
+
+ // }}}
+ // {{{ signFile()
+
+ /**
+ * Signs a file
+ *
+ * The file may be signed using any one of the three available signing
+ * modes:
+ * - {@link Crypt_GPG::SIGN_MODE_NORMAL}
+ * - {@link Crypt_GPG::SIGN_MODE_CLEAR}
+ * - {@link Crypt_GPG::SIGN_MODE_DETACHED}
+ *
+ * @param string $filename the name of the file containing the data to
+ * be signed.
+ * @param string $signedFile optional. The name of the file in which the
+ * signed data should be stored. If null or
+ * unspecified, the signed data is returned as a
+ * string.
+ * @param boolean $mode optional. The data signing mode to use. Should
+ * be one of {@link Crypt_GPG::SIGN_MODE_NORMAL},
+ * {@link Crypt_GPG::SIGN_MODE_CLEAR} or
+ * {@link Crypt_GPG::SIGN_MODE_DETACHED}. If not
+ * specified, defaults to
+ * <kbd>Crypt_GPG::SIGN_MODE_NORMAL</kbd>.
+ * @param boolean $armor optional. If true, ASCII armored data is
+ * returned; otherwise, binary data is returned.
+ * Defaults to true. This has no effect if the
+ * mode <kbd>Crypt_GPG::SIGN_MODE_CLEAR</kbd> is
+ * used.
+ * @param boolean $textmode optional. If true, line-breaks in signed data
+ * are normalized. Use this option when signing
+ * e-mail, or for greater compatibility between
+ * systems with different line-break formats.
+ * Defaults to false. This has no effect if the
+ * mode <kbd>Crypt_GPG::SIGN_MODE_CLEAR</kbd> is
+ * used as clear-signing always uses textmode.
+ *
+ * @return void|string if the <kbd>$signedFile</kbd> parameter is null, a
+ * string containing the signed data (or the signature
+ * data if a detached signature is requested) is
+ * returned.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if no signing key is specified.
+ * See {@link Crypt_GPG::addSignKey()}.
+ *
+ * @throws Crypt_GPG_BadPassphraseException if a specified passphrase is
+ * incorrect or if a required passphrase is not specified.
+ *
+ * @throws Crypt_GPG_FileException if the output file is not writeable or
+ * if the input file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function signFile($filename, $signedFile = null,
+ $mode = Crypt_GPG::SIGN_MODE_NORMAL, $armor = true, $textmode = false
+ ) {
+ return $this->_sign(
+ $filename,
+ true,
+ $signedFile,
+ $mode,
+ $armor,
+ $textmode
+ );
+ }
+
+ // }}}
+ // {{{ verify()
+
+ /**
+ * Verifies signed data
+ *
+ * The {@link Crypt_GPG::decrypt()} method may be used to get the original
+ * message if the signed data is not clearsigned and does not use a
+ * detached signature.
+ *
+ * @param string $signedData the signed data to be verified.
+ * @param string $signature optional. If verifying data signed using a
+ * detached signature, this must be the detached
+ * signature data. The data that was signed is
+ * specified in <kbd>$signedData</kbd>.
+ *
+ * @return array an array of {@link Crypt_GPG_Signature} objects for the
+ * signed data. For each signature that is valid, the
+ * {@link Crypt_GPG_Signature::isValid()} will return true.
+ *
+ * @throws Crypt_GPG_NoDataException if the provided data is not signed
+ * data.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ *
+ * @see Crypt_GPG_Signature
+ */
+ public function verify($signedData, $signature = '')
+ {
+ return $this->_verify($signedData, false, $signature);
+ }
+
+ // }}}
+ // {{{ verifyFile()
+
+ /**
+ * Verifies a signed file
+ *
+ * The {@link Crypt_GPG::decryptFile()} method may be used to get the
+ * original message if the signed data is not clearsigned and does not use
+ * a detached signature.
+ *
+ * @param string $filename the signed file to be verified.
+ * @param string $signature optional. If verifying a file signed using a
+ * detached signature, this must be the detached
+ * signature data. The file that was signed is
+ * specified in <kbd>$filename</kbd>.
+ *
+ * @return array an array of {@link Crypt_GPG_Signature} objects for the
+ * signed data. For each signature that is valid, the
+ * {@link Crypt_GPG_Signature::isValid()} will return true.
+ *
+ * @throws Crypt_GPG_NoDataException if the provided data is not signed
+ * data.
+ *
+ * @throws Crypt_GPG_FileException if the input file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ *
+ * @see Crypt_GPG_Signature
+ */
+ public function verifyFile($filename, $signature = '')
+ {
+ return $this->_verify($filename, true, $signature);
+ }
+
+ // }}}
+ // {{{ addDecryptKey()
+
+ /**
+ * Adds a key to use for decryption
+ *
+ * @param mixed $key the key to use. This may be a key identifier,
+ * user id, fingerprint, {@link Crypt_GPG_Key} or
+ * {@link Crypt_GPG_SubKey}. The key must be able
+ * to encrypt.
+ * @param string $passphrase optional. The passphrase of the key required
+ * for decryption.
+ *
+ * @return void
+ *
+ * @see Crypt_GPG::decrypt()
+ * @see Crypt_GPG::decryptFile()
+ * @see Crypt_GPG::clearDecryptKeys()
+ * @see Crypt_GPG::_addKey()
+ * @see Crypt_GPG_DecryptStatusHandler
+ *
+ * @sensitive $passphrase
+ */
+ public function addDecryptKey($key, $passphrase = null)
+ {
+ $this->_addKey($this->decryptKeys, true, false, $key, $passphrase);
+ }
+
+ // }}}
+ // {{{ addEncryptKey()
+
+ /**
+ * Adds a key to use for encryption
+ *
+ * @param mixed $key the key to use. This may be a key identifier, user id
+ * user id, fingerprint, {@link Crypt_GPG_Key} or
+ * {@link Crypt_GPG_SubKey}. The key must be able to
+ * encrypt.
+ *
+ * @return void
+ *
+ * @see Crypt_GPG::encrypt()
+ * @see Crypt_GPG::encryptFile()
+ * @see Crypt_GPG::clearEncryptKeys()
+ * @see Crypt_GPG::_addKey()
+ */
+ public function addEncryptKey($key)
+ {
+ $this->_addKey($this->encryptKeys, true, false, $key);
+ }
+
+ // }}}
+ // {{{ addSignKey()
+
+ /**
+ * Adds a key to use for signing
+ *
+ * @param mixed $key the key to use. This may be a key identifier,
+ * user id, fingerprint, {@link Crypt_GPG_Key} or
+ * {@link Crypt_GPG_SubKey}. The key must be able
+ * to sign.
+ * @param string $passphrase optional. The passphrase of the key required
+ * for signing.
+ *
+ * @return void
+ *
+ * @see Crypt_GPG::sign()
+ * @see Crypt_GPG::signFile()
+ * @see Crypt_GPG::clearSignKeys()
+ * @see Crypt_GPG::handleSignStatus()
+ * @see Crypt_GPG::_addKey()
+ *
+ * @sensitive $passphrase
+ */
+ public function addSignKey($key, $passphrase = null)
+ {
+ $this->_addKey($this->signKeys, false, true, $key, $passphrase);
+ }
+
+ // }}}
+ // {{{ clearDecryptKeys()
+
+ /**
+ * Clears all decryption keys
+ *
+ * @return void
+ *
+ * @see Crypt_GPG::decrypt()
+ * @see Crypt_GPG::addDecryptKey()
+ */
+ public function clearDecryptKeys()
+ {
+ $this->decryptKeys = array();
+ }
+
+ // }}}
+ // {{{ clearEncryptKeys()
+
+ /**
+ * Clears all encryption keys
+ *
+ * @return void
+ *
+ * @see Crypt_GPG::encrypt()
+ * @see Crypt_GPG::addEncryptKey()
+ */
+ public function clearEncryptKeys()
+ {
+ $this->encryptKeys = array();
+ }
+
+ // }}}
+ // {{{ clearSignKeys()
+
+ /**
+ * Clears all signing keys
+ *
+ * @return void
+ *
+ * @see Crypt_GPG::sign()
+ * @see Crypt_GPG::addSignKey()
+ */
+ public function clearSignKeys()
+ {
+ $this->signKeys = array();
+ }
+
+ // }}}
+ // {{{ handleSignStatus()
+
+ /**
+ * Handles the status output from GPG for the sign operation
+ *
+ * This method is responsible for sending the passphrase commands when
+ * required by the {@link Crypt_GPG::sign()} method. See <b>doc/DETAILS</b>
+ * in the {@link http://www.gnupg.org/download/ GPG distribution} for
+ * detailed information on GPG's status output.
+ *
+ * @param string $line the status line to handle.
+ *
+ * @return void
+ *
+ * @see Crypt_GPG::sign()
+ */
+ public function handleSignStatus($line)
+ {
+ $tokens = explode(' ', $line);
+ switch ($tokens[0]) {
+ case 'NEED_PASSPHRASE':
+ $subKeyId = $tokens[1];
+ if (array_key_exists($subKeyId, $this->signKeys)) {
+ $passphrase = $this->signKeys[$subKeyId]['passphrase'];
+ $this->engine->sendCommand($passphrase);
+ } else {
+ $this->engine->sendCommand('');
+ }
+ break;
+ }
+ }
+
+ // }}}
+ // {{{ handleImportKeyStatus()
+
+ /**
+ * Handles the status output from GPG for the import operation
+ *
+ * This method is responsible for building the result array that is
+ * returned from the {@link Crypt_GPG::importKey()} method. See
+ * <b>doc/DETAILS</b> in the
+ * {@link http://www.gnupg.org/download/ GPG distribution} for detailed
+ * information on GPG's status output.
+ *
+ * @param string $line the status line to handle.
+ * @param array &$result the current result array being processed.
+ *
+ * @return void
+ *
+ * @see Crypt_GPG::importKey()
+ * @see Crypt_GPG::importKeyFile()
+ * @see Crypt_GPG_Engine::addStatusHandler()
+ */
+ public function handleImportKeyStatus($line, array &$result)
+ {
+ $tokens = explode(' ', $line);
+ switch ($tokens[0]) {
+ case 'IMPORT_OK':
+ $result['fingerprint'] = $tokens[2];
+ break;
+
+ case 'IMPORT_RES':
+ $result['public_imported'] = intval($tokens[3]);
+ $result['public_unchanged'] = intval($tokens[5]);
+ $result['private_imported'] = intval($tokens[11]);
+ $result['private_unchanged'] = intval($tokens[12]);
+ break;
+ }
+ }
+
+ // }}}
+ // {{{ setEngine()
+
+ /**
+ * Sets the I/O engine to use for GnuPG operations
+ *
+ * Normally this method does not need to be used. It provides a means for
+ * dependency injection.
+ *
+ * @param Crypt_GPG_Engine $engine the engine to use.
+ *
+ * @return void
+ */
+ public function setEngine(Crypt_GPG_Engine $engine)
+ {
+ $this->engine = $engine;
+ }
+
+ // }}}
+ // {{{ _addKey()
+
+ /**
+ * Adds a key to one of the internal key arrays
+ *
+ * This handles resolving full key objects from the provided
+ * <kbd>$key</kbd> value.
+ *
+ * @param array &$array the array to which the key should be added.
+ * @param boolean $encrypt whether or not the key must be able to
+ * encrypt.
+ * @param boolean $sign whether or not the key must be able to sign.
+ * @param mixed $key the key to add. This may be a key identifier,
+ * user id, fingerprint, {@link Crypt_GPG_Key} or
+ * {@link Crypt_GPG_SubKey}.
+ * @param string $passphrase optional. The passphrase associated with the
+ * key.
+ *
+ * @return void
+ *
+ * @sensitive $passphrase
+ */
+ private function _addKey(array &$array, $encrypt, $sign, $key,
+ $passphrase = null
+ ) {
+ $subKeys = array();
+
+ if (is_scalar($key)) {
+ $keys = $this->getKeys($key);
+ if (count($keys) == 0) {
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'Key "' . $key . '" not found.', 0, $key);
+ }
+ $key = $keys[0];
+ }
+
+ if ($key instanceof Crypt_GPG_Key) {
+ if ($encrypt && !$key->canEncrypt()) {
+ throw new InvalidArgumentException(
+ 'Key "' . $key . '" cannot encrypt.');
+ }
+
+ if ($sign && !$key->canSign()) {
+ throw new InvalidArgumentException(
+ 'Key "' . $key . '" cannot sign.');
+ }
+
+ foreach ($key->getSubKeys() as $subKey) {
+ $canEncrypt = $subKey->canEncrypt();
+ $canSign = $subKey->canSign();
+ if ( ($encrypt && $sign && $canEncrypt && $canSign)
+ || ($encrypt && !$sign && $canEncrypt)
+ || (!$encrypt && $sign && $canSign)
+ ) {
+ // We add all subkeys that meet the requirements because we
+ // were not told which subkey is required.
+ $subKeys[] = $subKey;
+ }
+ }
+ } elseif ($key instanceof Crypt_GPG_SubKey) {
+ $subKeys[] = $key;
+ }
+
+ if (count($subKeys) === 0) {
+ throw new InvalidArgumentException(
+ 'Key "' . $key . '" is not in a recognized format.');
+ }
+
+ foreach ($subKeys as $subKey) {
+ if ($encrypt && !$subKey->canEncrypt()) {
+ throw new InvalidArgumentException(
+ 'Key "' . $key . '" cannot encrypt.');
+ }
+
+ if ($sign && !$subKey->canSign()) {
+ throw new InvalidArgumentException(
+ 'Key "' . $key . '" cannot sign.');
+ }
+
+ $array[$subKey->getId()] = array(
+ 'fingerprint' => $subKey->getFingerprint(),
+ 'passphrase' => $passphrase
+ );
+ }
+ }
+
+ // }}}
+ // {{{ _importKey()
+
+ /**
+ * Imports a public or private key into the keyring
+ *
+ * @param string $key the key to be imported.
+ * @param boolean $isFile whether or not the input is a filename.
+ *
+ * @return array an associative array containing the following elements:
+ * - <kbd>fingerprint</kbd> - the fingerprint of the
+ * imported key,
+ * - <kbd>public_imported</kbd> - the number of public
+ * keys imported,
+ * - <kbd>public_unchanged</kbd> - the number of unchanged
+ * public keys,
+ * - <kbd>private_imported</kbd> - the number of private
+ * keys imported,
+ * - <kbd>private_unchanged</kbd> - the number of unchanged
+ * private keys.
+ *
+ * @throws Crypt_GPG_NoDataException if the key data is missing or if the
+ * data is is not valid key data.
+ *
+ * @throws Crypt_GPG_FileException if the key file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ private function _importKey($key, $isFile)
+ {
+ $result = array();
+
+ if ($isFile) {
+ $input = @fopen($key, 'rb');
+ if ($input === false) {
+ throw new Crypt_GPG_FileException('Could not open key file "' .
+ $key . '" for importing.', 0, $key);
+ }
+ } else {
+ $input = strval($key);
+ if ($input == '') {
+ throw new Crypt_GPG_NoDataException(
+ 'No valid GPG key data found.', Crypt_GPG::ERROR_NO_DATA);
+ }
+ }
+
+ $arguments = array();
+ $version = $this->engine->getVersion();
+
+ if ( version_compare($version, '1.0.5', 'ge')
+ && version_compare($version, '1.0.7', 'lt')
+ ) {
+ $arguments[] = '--allow-secret-key-import';
+ }
+
+ $this->engine->reset();
+ $this->engine->addStatusHandler(
+ array($this, 'handleImportKeyStatus'),
+ array(&$result)
+ );
+
+ $this->engine->setOperation('--import', $arguments);
+ $this->engine->setInput($input);
+ $this->engine->run();
+
+ if ($isFile) {
+ fclose($input);
+ }
+
+ $code = $this->engine->getErrorCode();
+
+ switch ($code) {
+ case Crypt_GPG::ERROR_DUPLICATE_KEY:
+ case Crypt_GPG::ERROR_NONE:
+ // ignore duplicate key import errors
+ break;
+ case Crypt_GPG::ERROR_NO_DATA:
+ throw new Crypt_GPG_NoDataException(
+ 'No valid GPG key data found.', $code);
+ default:
+ throw new Crypt_GPG_Exception(
+ 'Unknown error importing GPG key. Please use the \'debug\' ' .
+ 'option when creating the Crypt_GPG object, and file a bug ' .
+ 'report at ' . self::BUG_URI, $code);
+ }
+
+ return $result;
+ }
+
+ // }}}
+ // {{{ _encrypt()
+
+ /**
+ * Encrypts data
+ *
+ * @param string $data the data to encrypt.
+ * @param boolean $isFile whether or not the data is a filename.
+ * @param string $outputFile the filename of the file in which to store
+ * the encrypted data. If null, the encrypted
+ * data is returned as a string.
+ * @param boolean $armor if true, ASCII armored data is returned;
+ * otherwise, binary data is returned.
+ *
+ * @return void|string if the <kbd>$outputFile</kbd> parameter is null, a
+ * string containing the encrypted data is returned.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if no encryption key is specified.
+ * See {@link Crypt_GPG::addEncryptKey()}.
+ *
+ * @throws Crypt_GPG_FileException if the output file is not writeable or
+ * if the input file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ private function _encrypt($data, $isFile, $outputFile, $armor)
+ {
+ if (count($this->encryptKeys) === 0) {
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'No encryption keys specified.');
+ }
+
+ if ($isFile) {
+ $input = @fopen($data, 'rb');
+ if ($input === false) {
+ throw new Crypt_GPG_FileException('Could not open input file "' .
+ $data . '" for encryption.', 0, $data);
+ }
+ } else {
+ $input = strval($data);
+ }
+
+ if ($outputFile === null) {
+ $output = '';
+ } else {
+ $output = @fopen($outputFile, 'wb');
+ if ($output === false) {
+ if ($isFile) {
+ fclose($input);
+ }
+ throw new Crypt_GPG_FileException('Could not open output ' .
+ 'file "' . $outputFile . '" for storing encrypted data.',
+ 0, $outputFile);
+ }
+ }
+
+ $arguments = ($armor) ? array('--armor') : array();
+ foreach ($this->encryptKeys as $key) {
+ $arguments[] = '--recipient ' . escapeshellarg($key['fingerprint']);
+ }
+
+ $this->engine->reset();
+ $this->engine->setInput($input);
+ $this->engine->setOutput($output);
+ $this->engine->setOperation('--encrypt', $arguments);
+ $this->engine->run();
+
+ if ($isFile) {
+ fclose($input);
+ }
+
+ if ($outputFile !== null) {
+ fclose($output);
+ }
+
+ $code = $this->engine->getErrorCode();
+
+ if ($code !== Crypt_GPG::ERROR_NONE) {
+ throw new Crypt_GPG_Exception(
+ 'Unknown error encrypting data. Please use the \'debug\' ' .
+ 'option when creating the Crypt_GPG object, and file a bug ' .
+ 'report at ' . self::BUG_URI, $code);
+ }
+
+ if ($outputFile === null) {
+ return $output;
+ }
+ }
+
+ // }}}
+ // {{{ _decrypt()
+
+ /**
+ * Decrypts data
+ *
+ * @param string $data the data to be decrypted.
+ * @param boolean $isFile whether or not the data is a filename.
+ * @param string $outputFile the name of the file to which the decrypted
+ * data should be written. If null, the decrypted
+ * data is returned as a string.
+ *
+ * @return void|string if the <kbd>$outputFile</kbd> parameter is null, a
+ * string containing the decrypted data is returned.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if the private key needed to
+ * decrypt the data is not in the user's keyring.
+ *
+ * @throws Crypt_GPG_NoDataException if specified data does not contain
+ * GPG encrypted data.
+ *
+ * @throws Crypt_GPG_BadPassphraseException if a required passphrase is
+ * incorrect or if a required passphrase is not specified. See
+ * {@link Crypt_GPG::addDecryptKey()}.
+ *
+ * @throws Crypt_GPG_FileException if the output file is not writeable or
+ * if the input file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ private function _decrypt($data, $isFile, $outputFile)
+ {
+ if ($isFile) {
+ $input = @fopen($data, 'rb');
+ if ($input === false) {
+ throw new Crypt_GPG_FileException('Could not open input file "' .
+ $data . '" for decryption.', 0, $data);
+ }
+ } else {
+ $input = strval($data);
+ if ($input == '') {
+ throw new Crypt_GPG_NoDataException(
+ 'Cannot decrypt data. No PGP encrypted data was found in '.
+ 'the provided data.', Crypt_GPG::ERROR_NO_DATA);
+ }
+ }
+
+ if ($outputFile === null) {
+ $output = '';
+ } else {
+ $output = @fopen($outputFile, 'wb');
+ if ($output === false) {
+ if ($isFile) {
+ fclose($input);
+ }
+ throw new Crypt_GPG_FileException('Could not open output ' .
+ 'file "' . $outputFile . '" for storing decrypted data.',
+ 0, $outputFile);
+ }
+ }
+
+ $handler = new Crypt_GPG_DecryptStatusHandler($this->engine,
+ $this->decryptKeys);
+
+ $this->engine->reset();
+ $this->engine->addStatusHandler(array($handler, 'handle'));
+ $this->engine->setOperation('--decrypt');
+ $this->engine->setInput($input);
+ $this->engine->setOutput($output);
+ $this->engine->run();
+
+ if ($isFile) {
+ fclose($input);
+ }
+
+ if ($outputFile !== null) {
+ fclose($output);
+ }
+
+ // if there was any problem decrypting the data, the handler will
+ // deal with it here.
+ $handler->throwException();
+
+ if ($outputFile === null) {
+ return $output;
+ }
+ }
+
+ // }}}
+ // {{{ _sign()
+
+ /**
+ * Signs data
+ *
+ * @param string $data the data to be signed.
+ * @param boolean $isFile whether or not the data is a filename.
+ * @param string $outputFile the name of the file in which the signed data
+ * should be stored. If null, the signed data is
+ * returned as a string.
+ * @param boolean $mode the data signing mode to use. Should be one of
+ * {@link Crypt_GPG::SIGN_MODE_NORMAL},
+ * {@link Crypt_GPG::SIGN_MODE_CLEAR} or
+ * {@link Crypt_GPG::SIGN_MODE_DETACHED}.
+ * @param boolean $armor if true, ASCII armored data is returned;
+ * otherwise, binary data is returned. This has
+ * no effect if the mode
+ * <kbd>Crypt_GPG::SIGN_MODE_CLEAR</kbd> is
+ * used.
+ * @param boolean $textmode if true, line-breaks in signed data be
+ * normalized. Use this option when signing
+ * e-mail, or for greater compatibility between
+ * systems with different line-break formats.
+ * Defaults to false. This has no effect if the
+ * mode <kbd>Crypt_GPG::SIGN_MODE_CLEAR</kbd> is
+ * used as clear-signing always uses textmode.
+ *
+ * @return void|string if the <kbd>$outputFile</kbd> parameter is null, a
+ * string containing the signed data (or the signature
+ * data if a detached signature is requested) is
+ * returned.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if no signing key is specified.
+ * See {@link Crypt_GPG::addSignKey()}.
+ *
+ * @throws Crypt_GPG_BadPassphraseException if a specified passphrase is
+ * incorrect or if a required passphrase is not specified.
+ *
+ * @throws Crypt_GPG_FileException if the output file is not writeable or
+ * if the input file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ private function _sign($data, $isFile, $outputFile, $mode, $armor,
+ $textmode
+ ) {
+ if (count($this->signKeys) === 0) {
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'No signing keys specified.');
+ }
+
+ if ($isFile) {
+ $input = @fopen($data, 'rb');
+ if ($input === false) {
+ throw new Crypt_GPG_FileException('Could not open input ' .
+ 'file "' . $data . '" for signing.', 0, $data);
+ }
+ } else {
+ $input = strval($data);
+ }
+
+ if ($outputFile === null) {
+ $output = '';
+ } else {
+ $output = @fopen($outputFile, 'wb');
+ if ($output === false) {
+ if ($isFile) {
+ fclose($input);
+ }
+ throw new Crypt_GPG_FileException('Could not open output ' .
+ 'file "' . $outputFile . '" for storing signed ' .
+ 'data.', 0, $outputFile);
+ }
+ }
+
+ switch ($mode) {
+ case Crypt_GPG::SIGN_MODE_DETACHED:
+ $operation = '--detach-sign';
+ break;
+ case Crypt_GPG::SIGN_MODE_CLEAR:
+ $operation = '--clearsign';
+ break;
+ case Crypt_GPG::SIGN_MODE_NORMAL:
+ default:
+ $operation = '--sign';
+ break;
+ }
+
+ $arguments = array();
+
+ if ($armor) {
+ $arguments[] = '--armor';
+ }
+ if ($textmode) {
+ $arguments[] = '--textmode';
+ }
+
+ foreach ($this->signKeys as $key) {
+ $arguments[] = '--local-user ' .
+ escapeshellarg($key['fingerprint']);
+ }
+
+ $this->engine->reset();
+ $this->engine->addStatusHandler(array($this, 'handleSignStatus'));
+ $this->engine->setInput($input);
+ $this->engine->setOutput($output);
+ $this->engine->setOperation($operation, $arguments);
+ $this->engine->run();
+
+ if ($isFile) {
+ fclose($input);
+ }
+
+ if ($outputFile !== null) {
+ fclose($output);
+ }
+
+ $code = $this->engine->getErrorCode();
+
+ switch ($code) {
+ case Crypt_GPG::ERROR_NONE:
+ break;
+ case Crypt_GPG::ERROR_KEY_NOT_FOUND:
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'Cannot sign data. Private key not found. Import the '.
+ 'private key before trying to sign data.', $code,
+ $this->engine->getErrorKeyId());
+ case Crypt_GPG::ERROR_BAD_PASSPHRASE:
+ throw new Crypt_GPG_BadPassphraseException(
+ 'Cannot sign data. Incorrect passphrase provided.', $code);
+ case Crypt_GPG::ERROR_MISSING_PASSPHRASE:
+ throw new Crypt_GPG_BadPassphraseException(
+ 'Cannot sign data. No passphrase provided.', $code);
+ default:
+ throw new Crypt_GPG_Exception(
+ 'Unknown error signing data. Please use the \'debug\' option ' .
+ 'when creating the Crypt_GPG object, and file a bug report ' .
+ 'at ' . self::BUG_URI, $code);
+ }
+
+ if ($outputFile === null) {
+ return $output;
+ }
+ }
+
+ // }}}
+ // {{{ _encryptAndSign()
+
+ /**
+ * Encrypts and signs data
+ *
+ * @param string $data the data to be encrypted and signed.
+ * @param boolean $isFile whether or not the data is a filename.
+ * @param string $outputFile the name of the file in which the encrypted,
+ * signed data should be stored. If null, the
+ * encrypted, signed data is returned as a
+ * string.
+ * @param boolean $armor if true, ASCII armored data is returned;
+ * otherwise, binary data is returned.
+ *
+ * @return void|string if the <kbd>$outputFile</kbd> parameter is null, a
+ * string containing the encrypted, signed data is
+ * returned.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if no encryption key is specified
+ * or if no signing key is specified. See
+ * {@link Crypt_GPG::addEncryptKey()} and
+ * {@link Crypt_GPG::addSignKey()}.
+ *
+ * @throws Crypt_GPG_BadPassphraseException if a specified passphrase is
+ * incorrect or if a required passphrase is not specified.
+ *
+ * @throws Crypt_GPG_FileException if the output file is not writeable or
+ * if the input file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ */
+ private function _encryptAndSign($data, $isFile, $outputFile, $armor)
+ {
+ if (count($this->signKeys) === 0) {
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'No signing keys specified.');
+ }
+
+ if (count($this->encryptKeys) === 0) {
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'No encryption keys specified.');
+ }
+
+
+ if ($isFile) {
+ $input = @fopen($data, 'rb');
+ if ($input === false) {
+ throw new Crypt_GPG_FileException('Could not open input ' .
+ 'file "' . $data . '" for encrypting and signing.', 0,
+ $data);
+ }
+ } else {
+ $input = strval($data);
+ }
+
+ if ($outputFile === null) {
+ $output = '';
+ } else {
+ $output = @fopen($outputFile, 'wb');
+ if ($output === false) {
+ if ($isFile) {
+ fclose($input);
+ }
+ throw new Crypt_GPG_FileException('Could not open output ' .
+ 'file "' . $outputFile . '" for storing encrypted, ' .
+ 'signed data.', 0, $outputFile);
+ }
+ }
+
+ $arguments = ($armor) ? array('--armor') : array();
+
+ foreach ($this->signKeys as $key) {
+ $arguments[] = '--local-user ' .
+ escapeshellarg($key['fingerprint']);
+ }
+
+ foreach ($this->encryptKeys as $key) {
+ $arguments[] = '--recipient ' . escapeshellarg($key['fingerprint']);
+ }
+
+ $this->engine->reset();
+ $this->engine->addStatusHandler(array($this, 'handleSignStatus'));
+ $this->engine->setInput($input);
+ $this->engine->setOutput($output);
+ $this->engine->setOperation('--encrypt --sign', $arguments);
+ $this->engine->run();
+
+ if ($isFile) {
+ fclose($input);
+ }
+
+ if ($outputFile !== null) {
+ fclose($output);
+ }
+
+ $code = $this->engine->getErrorCode();
+
+ switch ($code) {
+ case Crypt_GPG::ERROR_NONE:
+ break;
+ case Crypt_GPG::ERROR_KEY_NOT_FOUND:
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'Cannot sign encrypted data. Private key not found. Import '.
+ 'the private key before trying to sign the encrypted data.',
+ $code, $this->engine->getErrorKeyId());
+ case Crypt_GPG::ERROR_BAD_PASSPHRASE:
+ throw new Crypt_GPG_BadPassphraseException(
+ 'Cannot sign encrypted data. Incorrect passphrase provided.',
+ $code);
+ case Crypt_GPG::ERROR_MISSING_PASSPHRASE:
+ throw new Crypt_GPG_BadPassphraseException(
+ 'Cannot sign encrypted data. No passphrase provided.', $code);
+ default:
+ throw new Crypt_GPG_Exception(
+ 'Unknown error encrypting and signing data. Please use the ' .
+ '\'debug\' option when creating the Crypt_GPG object, and ' .
+ 'file a bug report at ' . self::BUG_URI, $code);
+ }
+
+ if ($outputFile === null) {
+ return $output;
+ }
+ }
+
+ // }}}
+ // {{{ _verify()
+
+ /**
+ * Verifies data
+ *
+ * @param string $data the signed data to be verified.
+ * @param boolean $isFile whether or not the data is a filename.
+ * @param string $signature if verifying a file signed using a detached
+ * signature, this must be the detached signature
+ * data. Otherwise, specify ''.
+ *
+ * @return array an array of {@link Crypt_GPG_Signature} objects for the
+ * signed data.
+ *
+ * @throws Crypt_GPG_NoDataException if the provided data is not signed
+ * data.
+ *
+ * @throws Crypt_GPG_FileException if the input file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ *
+ * @see Crypt_GPG_Signature
+ */
+ private function _verify($data, $isFile, $signature)
+ {
+ if ($signature == '') {
+ $operation = '--verify';
+ $arguments = array();
+ } else {
+ // Signed data goes in FD_MESSAGE, detached signature data goes in
+ // FD_INPUT.
+ $operation = '--verify - "-&' . Crypt_GPG_Engine::FD_MESSAGE. '"';
+ $arguments = array('--enable-special-filenames');
+ }
+
+ $handler = new Crypt_GPG_VerifyStatusHandler();
+
+ if ($isFile) {
+ $input = @fopen($data, 'rb');
+ if ($input === false) {
+ throw new Crypt_GPG_FileException('Could not open input ' .
+ 'file "' . $data . '" for verifying.', 0, $data);
+ }
+ } else {
+ $input = strval($data);
+ if ($input == '') {
+ throw new Crypt_GPG_NoDataException(
+ 'No valid signature data found.', Crypt_GPG::ERROR_NO_DATA);
+ }
+ }
+
+ $this->engine->reset();
+ $this->engine->addStatusHandler(array($handler, 'handle'));
+
+ if ($signature == '') {
+ // signed or clearsigned data
+ $this->engine->setInput($input);
+ } else {
+ // detached signature
+ $this->engine->setInput($signature);
+ $this->engine->setMessage($input);
+ }
+
+ $this->engine->setOperation($operation, $arguments);
+ $this->engine->run();
+
+ if ($isFile) {
+ fclose($input);
+ }
+
+ $code = $this->engine->getErrorCode();
+
+ switch ($code) {
+ case Crypt_GPG::ERROR_NONE:
+ case Crypt_GPG::ERROR_BAD_SIGNATURE:
+ break;
+ case Crypt_GPG::ERROR_NO_DATA:
+ throw new Crypt_GPG_NoDataException(
+ 'No valid signature data found.', $code);
+ case Crypt_GPG::ERROR_KEY_NOT_FOUND:
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'Public key required for data verification not in keyring.',
+ $code, $this->engine->getErrorKeyId());
+ default:
+ throw new Crypt_GPG_Exception(
+ 'Unknown error validating signature details. Please use the ' .
+ '\'debug\' option when creating the Crypt_GPG object, and ' .
+ 'file a bug report at ' . self::BUG_URI, $code);
+ }
+
+ return $handler->getSignatures();
+ }
+
+ // }}}
+ // {{{ _decryptAndVerify()
+
+ /**
+ * Decrypts and verifies encrypted, signed data
+ *
+ * @param string $data the encrypted signed data to be decrypted and
+ * verified.
+ * @param boolean $isFile whether or not the data is a filename.
+ * @param string $outputFile the name of the file to which the decrypted
+ * data should be written. If null, the decrypted
+ * data is returned in the results array.
+ *
+ * @return array two element array. The array has an element 'data'
+ * containing the decrypted data and an element
+ * 'signatures' containing an array of
+ * {@link Crypt_GPG_Signature} objects for the signed data.
+ * If the decrypted data is written to a file, the 'data'
+ * element is null.
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if the private key needed to
+ * decrypt the data is not in the user's keyring or it the public
+ * key needed for verification is not in the user's keyring.
+ *
+ * @throws Crypt_GPG_NoDataException if specified data does not contain
+ * GPG signed, encrypted data.
+ *
+ * @throws Crypt_GPG_BadPassphraseException if a required passphrase is
+ * incorrect or if a required passphrase is not specified. See
+ * {@link Crypt_GPG::addDecryptKey()}.
+ *
+ * @throws Crypt_GPG_FileException if the output file is not writeable or
+ * if the input file is not readable.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ *
+ * @see Crypt_GPG_Signature
+ */
+ private function _decryptAndVerify($data, $isFile, $outputFile)
+ {
+ if ($isFile) {
+ $input = @fopen($data, 'rb');
+ if ($input === false) {
+ throw new Crypt_GPG_FileException('Could not open input ' .
+ 'file "' . $data . '" for decrypting and verifying.', 0,
+ $data);
+ }
+ } else {
+ $input = strval($data);
+ if ($input == '') {
+ throw new Crypt_GPG_NoDataException(
+ 'No valid encrypted signed data found.',
+ Crypt_GPG::ERROR_NO_DATA);
+ }
+ }
+
+ if ($outputFile === null) {
+ $output = '';
+ } else {
+ $output = @fopen($outputFile, 'wb');
+ if ($output === false) {
+ if ($isFile) {
+ fclose($input);
+ }
+ throw new Crypt_GPG_FileException('Could not open output ' .
+ 'file "' . $outputFile . '" for storing decrypted data.',
+ 0, $outputFile);
+ }
+ }
+
+ $verifyHandler = new Crypt_GPG_VerifyStatusHandler();
+
+ $decryptHandler = new Crypt_GPG_DecryptStatusHandler($this->engine,
+ $this->decryptKeys);
+
+ $this->engine->reset();
+ $this->engine->addStatusHandler(array($verifyHandler, 'handle'));
+ $this->engine->addStatusHandler(array($decryptHandler, 'handle'));
+ $this->engine->setInput($input);
+ $this->engine->setOutput($output);
+ $this->engine->setOperation('--decrypt');
+ $this->engine->run();
+
+ if ($isFile) {
+ fclose($input);
+ }
+
+ if ($outputFile !== null) {
+ fclose($output);
+ }
+
+ $return = array(
+ 'data' => null,
+ 'signatures' => $verifyHandler->getSignatures()
+ );
+
+ // if there was any problem decrypting the data, the handler will
+ // deal with it here.
+ try {
+ $decryptHandler->throwException();
+ } catch (Exception $e) {
+ if ($e instanceof Crypt_GPG_KeyNotFoundException) {
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'Public key required for data verification not in ',
+ 'the keyring. Either no suitable private decryption key ' .
+ 'is in the keyring or the public key required for data ' .
+ 'verification is not in the keyring. Import a suitable ' .
+ 'key before trying to decrypt and verify this data.',
+ self::ERROR_KEY_NOT_FOUND, $this->engine->getErrorKeyId());
+ }
+
+ if ($e instanceof Crypt_GPG_NoDataException) {
+ throw new Crypt_GPG_NoDataException(
+ 'Cannot decrypt and verify data. No PGP encrypted data ' .
+ 'was found in the provided data.', self::ERROR_NO_DATA);
+ }
+
+ throw $e;
+ }
+
+ if ($outputFile === null) {
+ $return['data'] = $output;
+ }
+
+ return $return;
+ }
+
+ // }}}
+}
+
+// }}}
+
+?>
diff --git a/program/lib/Crypt/GPG/DecryptStatusHandler.php b/program/lib/Crypt/GPG/DecryptStatusHandler.php
new file mode 100644
index 000000000..40e8d50ed
--- /dev/null
+++ b/program/lib/Crypt/GPG/DecryptStatusHandler.php
@@ -0,0 +1,336 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Crypt_GPG is a package to use GPG from PHP
+ *
+ * This file contains an object that handles GPG's status output for the
+ * decrypt operation.
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2008-2009 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @version CVS: $Id: DecryptStatusHandler.php 302814 2010-08-26 15:43:07Z gauthierm $
+ * @link http://pear.php.net/package/Crypt_GPG
+ * @link http://www.gnupg.org/
+ */
+
+/**
+ * Crypt_GPG base class
+ */
+require_once 'Crypt/GPG.php';
+
+/**
+ * GPG exception classes
+ */
+require_once 'Crypt/GPG/Exceptions.php';
+
+
+/**
+ * Status line handler for the decrypt operation
+ *
+ * This class is used internally by Crypt_GPG and does not need be used
+ * directly. See the {@link Crypt_GPG} class for end-user API.
+ *
+ * This class is responsible for sending the passphrase commands when required
+ * by the {@link Crypt_GPG::decrypt()} method. See <b>doc/DETAILS</b> in the
+ * {@link http://www.gnupg.org/download/ GPG distribution} for detailed
+ * information on GPG's status output for the decrypt operation.
+ *
+ * This class is also responsible for parsing error status and throwing a
+ * meaningful exception in the event that decryption fails.
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2008 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ * @link http://www.gnupg.org/
+ */
+class Crypt_GPG_DecryptStatusHandler
+{
+ // {{{ protected properties
+
+ /**
+ * Keys used to decrypt
+ *
+ * The array is of the form:
+ * <code>
+ * array(
+ * $key_id => array(
+ * 'fingerprint' => $fingerprint,
+ * 'passphrase' => $passphrase
+ * )
+ * );
+ * </code>
+ *
+ * @var array
+ */
+ protected $keys = array();
+
+ /**
+ * Engine used to which passphrases are passed
+ *
+ * @var Crypt_GPG_Engine
+ */
+ protected $engine = null;
+
+ /**
+ * The id of the current sub-key used for decryption
+ *
+ * @var string
+ */
+ protected $currentSubKey = '';
+
+ /**
+ * Whether or not decryption succeeded
+ *
+ * If the message is only signed (compressed) and not encrypted, this is
+ * always true. If the message is encrypted, this flag is set to false
+ * until we know the decryption succeeded.
+ *
+ * @var boolean
+ */
+ protected $decryptionOkay = true;
+
+ /**
+ * Whether or not there was no data for decryption
+ *
+ * @var boolean
+ */
+ protected $noData = false;
+
+ /**
+ * Keys for which the passhprase is missing
+ *
+ * This contains primary user ids indexed by sub-key id and is used to
+ * create helpful exception messages.
+ *
+ * @var array
+ */
+ protected $missingPassphrases = array();
+
+ /**
+ * Keys for which the passhprase is incorrect
+ *
+ * This contains primary user ids indexed by sub-key id and is used to
+ * create helpful exception messages.
+ *
+ * @var array
+ */
+ protected $badPassphrases = array();
+
+ /**
+ * Keys that can be used to decrypt the data but are missing from the
+ * keychain
+ *
+ * This is an array with both the key and value being the sub-key id of
+ * the missing keys.
+ *
+ * @var array
+ */
+ protected $missingKeys = array();
+
+ // }}}
+ // {{{ __construct()
+
+ /**
+ * Creates a new decryption status handler
+ *
+ * @param Crypt_GPG_Engine $engine the GPG engine to which passphrases are
+ * passed.
+ * @param array $keys the decryption keys to use.
+ */
+ public function __construct(Crypt_GPG_Engine $engine, array $keys)
+ {
+ $this->engine = $engine;
+ $this->keys = $keys;
+ }
+
+ // }}}
+ // {{{ handle()
+
+ /**
+ * Handles a status line
+ *
+ * @param string $line the status line to handle.
+ *
+ * @return void
+ */
+ public function handle($line)
+ {
+ $tokens = explode(' ', $line);
+ switch ($tokens[0]) {
+ case 'ENC_TO':
+ // Now we know the message is encrypted. Set flag to check if
+ // decryption succeeded.
+ $this->decryptionOkay = false;
+
+ // this is the new key message
+ $this->currentSubKeyId = $tokens[1];
+ break;
+
+ case 'NEED_PASSPHRASE':
+ // send passphrase to the GPG engine
+ $subKeyId = $tokens[1];
+ if (array_key_exists($subKeyId, $this->keys)) {
+ $passphrase = $this->keys[$subKeyId]['passphrase'];
+ $this->engine->sendCommand($passphrase);
+ } else {
+ $this->engine->sendCommand('');
+ }
+ break;
+
+ case 'USERID_HINT':
+ // remember the user id for pretty exception messages
+ $this->badPassphrases[$tokens[1]]
+ = implode(' ', array_splice($tokens, 2));
+
+ break;
+
+ case 'GOOD_PASSPHRASE':
+ // if we got a good passphrase, remove the key from the list of
+ // bad passphrases.
+ unset($this->badPassphrases[$this->currentSubKeyId]);
+ break;
+
+ case 'MISSING_PASSPHRASE':
+ $this->missingPassphrases[$this->currentSubKeyId]
+ = $this->currentSubKeyId;
+
+ break;
+
+ case 'NO_SECKEY':
+ // note: this message is also received if there are multiple
+ // recipients and a previous key had a correct passphrase.
+ $this->missingKeys[$tokens[1]] = $tokens[1];
+ break;
+
+ case 'NODATA':
+ $this->noData = true;
+ break;
+
+ case 'DECRYPTION_OKAY':
+ // If the message is encrypted, this is the all-clear signal.
+ $this->decryptionOkay = true;
+ break;
+ }
+ }
+
+ // }}}
+ // {{{ throwException()
+
+ /**
+ * Takes the final status of the decrypt operation and throws an
+ * appropriate exception
+ *
+ * If decryption was successful, no exception is thrown.
+ *
+ * @return void
+ *
+ * @throws Crypt_GPG_KeyNotFoundException if the private key needed to
+ * decrypt the data is not in the user's keyring.
+ *
+ * @throws Crypt_GPG_NoDataException if specified data does not contain
+ * GPG encrypted data.
+ *
+ * @throws Crypt_GPG_BadPassphraseException if a required passphrase is
+ * incorrect or if a required passphrase is not specified. See
+ * {@link Crypt_GPG::addDecryptKey()}.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <i>debug</i> option and file a bug report if these
+ * exceptions occur.
+ */
+ public function throwException()
+ {
+ $code = Crypt_GPG::ERROR_NONE;
+
+ if (!$this->decryptionOkay) {
+ if (count($this->badPassphrases) > 0) {
+ $code = Crypt_GPG::ERROR_BAD_PASSPHRASE;
+ } elseif (count($this->missingKeys) > 0) {
+ $code = Crypt_GPG::ERROR_KEY_NOT_FOUND;
+ } else {
+ $code = Crypt_GPG::ERROR_UNKNOWN;
+ }
+ } elseif ($this->noData) {
+ $code = Crypt_GPG::ERROR_NO_DATA;
+ }
+
+ switch ($code) {
+ case Crypt_GPG::ERROR_NONE:
+ break;
+
+ case Crypt_GPG::ERROR_KEY_NOT_FOUND:
+ if (count($this->missingKeys) > 0) {
+ $keyId = reset($this->missingKeys);
+ } else {
+ $keyId = '';
+ }
+ throw new Crypt_GPG_KeyNotFoundException(
+ 'Cannot decrypt data. No suitable private key is in the ' .
+ 'keyring. Import a suitable private key before trying to ' .
+ 'decrypt this data.', $code, $keyId);
+
+ case Crypt_GPG::ERROR_BAD_PASSPHRASE:
+ $badPassphrases = array_diff_key(
+ $this->badPassphrases,
+ $this->missingPassphrases
+ );
+
+ $missingPassphrases = array_intersect_key(
+ $this->badPassphrases,
+ $this->missingPassphrases
+ );
+
+ $message = 'Cannot decrypt data.';
+ if (count($badPassphrases) > 0) {
+ $message = ' Incorrect passphrase provided for keys: "' .
+ implode('", "', $badPassphrases) . '".';
+ }
+ if (count($missingPassphrases) > 0) {
+ $message = ' No passphrase provided for keys: "' .
+ implode('", "', $badPassphrases) . '".';
+ }
+
+ throw new Crypt_GPG_BadPassphraseException($message, $code,
+ $badPassphrases, $missingPassphrases);
+
+ case Crypt_GPG::ERROR_NO_DATA:
+ throw new Crypt_GPG_NoDataException(
+ 'Cannot decrypt data. No PGP encrypted data was found in '.
+ 'the provided data.', $code);
+
+ default:
+ throw new Crypt_GPG_Exception(
+ 'Unknown error decrypting data.', $code);
+ }
+ }
+
+ // }}}
+}
+
+?>
diff --git a/program/lib/Crypt/GPG/Engine.php b/program/lib/Crypt/GPG/Engine.php
new file mode 100644
index 000000000..081be8e21
--- /dev/null
+++ b/program/lib/Crypt/GPG/Engine.php
@@ -0,0 +1,1758 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Crypt_GPG is a package to use GPG from PHP
+ *
+ * This file contains an engine that handles GPG subprocess control and I/O.
+ * PHP's process manipulation functions are used to handle the GPG subprocess.
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Nathan Fredrickson <nathan@silverorange.com>
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2005-2010 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @version CVS: $Id: Engine.php 302822 2010-08-26 17:30:57Z gauthierm $
+ * @link http://pear.php.net/package/Crypt_GPG
+ * @link http://www.gnupg.org/
+ */
+
+/**
+ * Crypt_GPG base class.
+ */
+require_once 'Crypt/GPG.php';
+
+/**
+ * GPG exception classes.
+ */
+require_once 'Crypt/GPG/Exceptions.php';
+
+/**
+ * Standard PEAR exception is used if GPG binary is not found.
+ */
+require_once 'PEAR/Exception.php';
+
+// {{{ class Crypt_GPG_Engine
+
+/**
+ * Native PHP Crypt_GPG I/O engine
+ *
+ * This class is used internally by Crypt_GPG and does not need be used
+ * directly. See the {@link Crypt_GPG} class for end-user API.
+ *
+ * This engine uses PHP's native process control functions to directly control
+ * the GPG process. The GPG executable is required to be on the system.
+ *
+ * All data is passed to the GPG subprocess using file descriptors. This is the
+ * most secure method of passing data to the GPG subprocess.
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Nathan Fredrickson <nathan@silverorange.com>
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2005-2010 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ * @link http://www.gnupg.org/
+ */
+class Crypt_GPG_Engine
+{
+ // {{{ constants
+
+ /**
+ * Size of data chunks that are sent to and retrieved from the IPC pipes.
+ *
+ * PHP reads 8192 bytes. If this is set to less than 8192, PHP reads 8192
+ * and buffers the rest so we might as well just read 8192.
+ *
+ * Using values other than 8192 also triggers PHP bugs.
+ *
+ * @see http://bugs.php.net/bug.php?id=35224
+ */
+ const CHUNK_SIZE = 8192;
+
+ /**
+ * Standard input file descriptor. This is used to pass data to the GPG
+ * process.
+ */
+ const FD_INPUT = 0;
+
+ /**
+ * Standard output file descriptor. This is used to receive normal output
+ * from the GPG process.
+ */
+ const FD_OUTPUT = 1;
+
+ /**
+ * Standard output file descriptor. This is used to receive error output
+ * from the GPG process.
+ */
+ const FD_ERROR = 2;
+
+ /**
+ * GPG status output file descriptor. The status file descriptor outputs
+ * detailed information for many GPG commands. See the second section of
+ * the file <b>doc/DETAILS</b> in the
+ * {@link http://www.gnupg.org/download/ GPG package} for a detailed
+ * description of GPG's status output.
+ */
+ const FD_STATUS = 3;
+
+ /**
+ * Command input file descriptor. This is used for methods requiring
+ * passphrases.
+ */
+ const FD_COMMAND = 4;
+
+ /**
+ * Extra message input file descriptor. This is used for passing signed
+ * data when verifying a detached signature.
+ */
+ const FD_MESSAGE = 5;
+
+ /**
+ * Minimum version of GnuPG that is supported.
+ */
+ const MIN_VERSION = '1.0.2';
+
+ // }}}
+ // {{{ private class properties
+
+ /**
+ * Whether or not to use debugging mode
+ *
+ * When set to true, every GPG command is echoed before it is run. Sensitive
+ * data is always handled using pipes and is not specified as part of the
+ * command. As a result, sensitive data is never displayed when debug is
+ * enabled. Sensitive data includes private key data and passphrases.
+ *
+ * Debugging is off by default.
+ *
+ * @var boolean
+ * @see Crypt_GPG_Engine::__construct()
+ */
+ private $_debug = false;
+
+ /**
+ * Location of GPG binary
+ *
+ * @var string
+ * @see Crypt_GPG_Engine::__construct()
+ * @see Crypt_GPG_Engine::_getBinary()
+ */
+ private $_binary = '';
+
+ /**
+ * Directory containing the GPG key files
+ *
+ * This property only contains the path when the <i>homedir</i> option
+ * is specified in the constructor.
+ *
+ * @var string
+ * @see Crypt_GPG_Engine::__construct()
+ */
+ private $_homedir = '';
+
+ /**
+ * File path of the public keyring
+ *
+ * This property only contains the file path when the <i>public_keyring</i>
+ * option is specified in the constructor.
+ *
+ * If the specified file path starts with <kbd>~/</kbd>, the path is
+ * relative to the <i>homedir</i> if specified, otherwise to
+ * <kbd>~/.gnupg</kbd>.
+ *
+ * @var string
+ * @see Crypt_GPG_Engine::__construct()
+ */
+ private $_publicKeyring = '';
+
+ /**
+ * File path of the private (secret) keyring
+ *
+ * This property only contains the file path when the <i>private_keyring</i>
+ * option is specified in the constructor.
+ *
+ * If the specified file path starts with <kbd>~/</kbd>, the path is
+ * relative to the <i>homedir</i> if specified, otherwise to
+ * <kbd>~/.gnupg</kbd>.
+ *
+ * @var string
+ * @see Crypt_GPG_Engine::__construct()
+ */
+ private $_privateKeyring = '';
+
+ /**
+ * File path of the trust database
+ *
+ * This property only contains the file path when the <i>trust_db</i>
+ * option is specified in the constructor.
+ *
+ * If the specified file path starts with <kbd>~/</kbd>, the path is
+ * relative to the <i>homedir</i> if specified, otherwise to
+ * <kbd>~/.gnupg</kbd>.
+ *
+ * @var string
+ * @see Crypt_GPG_Engine::__construct()
+ */
+ private $_trustDb = '';
+
+ /**
+ * Array of pipes used for communication with the GPG binary
+ *
+ * This is an array of file descriptor resources.
+ *
+ * @var array
+ */
+ private $_pipes = array();
+
+ /**
+ * Array of currently opened pipes
+ *
+ * This array is used to keep track of remaining opened pipes so they can
+ * be closed when the GPG subprocess is finished. This array is a subset of
+ * the {@link Crypt_GPG_Engine::$_pipes} array and contains opened file
+ * descriptor resources.
+ *
+ * @var array
+ * @see Crypt_GPG_Engine::_closePipe()
+ */
+ private $_openPipes = array();
+
+ /**
+ * A handle for the GPG process
+ *
+ * @var resource
+ */
+ private $_process = null;
+
+ /**
+ * Whether or not the operating system is Darwin (OS X)
+ *
+ * @var boolean
+ */
+ private $_isDarwin = false;
+
+ /**
+ * Commands to be sent to GPG's command input stream
+ *
+ * @var string
+ * @see Crypt_GPG_Engine::sendCommand()
+ */
+ private $_commandBuffer = '';
+
+ /**
+ * Array of status line handlers
+ *
+ * @var array
+ * @see Crypt_GPG_Engine::addStatusHandler()
+ */
+ private $_statusHandlers = array();
+
+ /**
+ * Array of error line handlers
+ *
+ * @var array
+ * @see Crypt_GPG_Engine::addErrorHandler()
+ */
+ private $_errorHandlers = array();
+
+ /**
+ * The error code of the current operation
+ *
+ * @var integer
+ * @see Crypt_GPG_Engine::getErrorCode()
+ */
+ private $_errorCode = Crypt_GPG::ERROR_NONE;
+
+ /**
+ * File related to the error code of the current operation
+ *
+ * @var string
+ * @see Crypt_GPG_Engine::getErrorFilename()
+ */
+ private $_errorFilename = '';
+
+ /**
+ * Key id related to the error code of the current operation
+ *
+ * @var string
+ * @see Crypt_GPG_Engine::getErrorKeyId()
+ */
+ private $_errorkeyId = '';
+
+ /**
+ * The number of currently needed passphrases
+ *
+ * If this is not zero when the GPG command is completed, the error code is
+ * set to {@link Crypt_GPG::ERROR_MISSING_PASSPHRASE}.
+ *
+ * @var integer
+ */
+ private $_needPassphrase = 0;
+
+ /**
+ * The input source
+ *
+ * This is data to send to GPG. Either a string or a stream resource.
+ *
+ * @var string|resource
+ * @see Crypt_GPG_Engine::setInput()
+ */
+ private $_input = null;
+
+ /**
+ * The extra message input source
+ *
+ * Either a string or a stream resource.
+ *
+ * @var string|resource
+ * @see Crypt_GPG_Engine::setMessage()
+ */
+ private $_message = null;
+
+ /**
+ * The output location
+ *
+ * This is where the output from GPG is sent. Either a string or a stream
+ * resource.
+ *
+ * @var string|resource
+ * @see Crypt_GPG_Engine::setOutput()
+ */
+ private $_output = '';
+
+ /**
+ * The GPG operation to execute
+ *
+ * @var string
+ * @see Crypt_GPG_Engine::setOperation()
+ */
+ private $_operation;
+
+ /**
+ * Arguments for the current operation
+ *
+ * @var array
+ * @see Crypt_GPG_Engine::setOperation()
+ */
+ private $_arguments = array();
+
+ /**
+ * The version number of the GPG binary
+ *
+ * @var string
+ * @see Crypt_GPG_Engine::getVersion()
+ */
+ private $_version = '';
+
+ /**
+ * Cached value indicating whether or not mbstring function overloading is
+ * on for strlen
+ *
+ * This is cached for optimal performance inside the I/O loop.
+ *
+ * @var boolean
+ * @see Crypt_GPG_Engine::_byteLength()
+ * @see Crypt_GPG_Engine::_byteSubstring()
+ */
+ private static $_mbStringOverload = null;
+
+ // }}}
+ // {{{ __construct()
+
+ /**
+ * Creates a new GPG engine
+ *
+ * Available options are:
+ *
+ * - <kbd>string homedir</kbd> - the directory where the GPG
+ * keyring files are stored. If not
+ * specified, Crypt_GPG uses the
+ * default of <kbd>~/.gnupg</kbd>.
+ * - <kbd>string publicKeyring</kbd> - the file path of the public
+ * keyring. Use this if the public
+ * keyring is not in the homedir, or
+ * if the keyring is in a directory
+ * not writable by the process
+ * invoking GPG (like Apache). Then
+ * you can specify the path to the
+ * keyring with this option
+ * (/foo/bar/pubring.gpg), and specify
+ * a writable directory (like /tmp)
+ * using the <i>homedir</i> option.
+ * - <kbd>string privateKeyring</kbd> - the file path of the private
+ * keyring. Use this if the private
+ * keyring is not in the homedir, or
+ * if the keyring is in a directory
+ * not writable by the process
+ * invoking GPG (like Apache). Then
+ * you can specify the path to the
+ * keyring with this option
+ * (/foo/bar/secring.gpg), and specify
+ * a writable directory (like /tmp)
+ * using the <i>homedir</i> option.
+ * - <kbd>string trustDb</kbd> - the file path of the web-of-trust
+ * database. Use this if the trust
+ * database is not in the homedir, or
+ * if the database is in a directory
+ * not writable by the process
+ * invoking GPG (like Apache). Then
+ * you can specify the path to the
+ * trust database with this option
+ * (/foo/bar/trustdb.gpg), and specify
+ * a writable directory (like /tmp)
+ * using the <i>homedir</i> option.
+ * - <kbd>string binary</kbd> - the location of the GPG binary. If
+ * not specified, the driver attempts
+ * to auto-detect the GPG binary
+ * location using a list of known
+ * default locations for the current
+ * operating system. The option
+ * <kbd>gpgBinary</kbd> is a
+ * deprecated alias for this option.
+ * - <kbd>boolean debug</kbd> - whether or not to use debug mode.
+ * When debug mode is on, all
+ * communication to and from the GPG
+ * subprocess is logged. This can be
+ * useful to diagnose errors when
+ * using Crypt_GPG.
+ *
+ * @param array $options optional. An array of options used to create the
+ * GPG object. All options are optional and are
+ * represented as key-value pairs.
+ *
+ * @throws Crypt_GPG_FileException if the <kbd>homedir</kbd> does not exist
+ * and cannot be created. This can happen if <kbd>homedir</kbd> is
+ * not specified, Crypt_GPG is run as the web user, and the web
+ * user has no home directory. This exception is also thrown if any
+ * of the options <kbd>publicKeyring</kbd>,
+ * <kbd>privateKeyring</kbd> or <kbd>trustDb</kbd> options are
+ * specified but the files do not exist or are are not readable.
+ * This can happen if the user running the Crypt_GPG process (for
+ * example, the Apache user) does not have permission to read the
+ * files.
+ *
+ * @throws PEAR_Exception if the provided <kbd>binary</kbd> is invalid, or
+ * if no <kbd>binary</kbd> is provided and no suitable binary could
+ * be found.
+ */
+ public function __construct(array $options = array())
+ {
+ $this->_isDarwin = (strncmp(strtoupper(PHP_OS), 'DARWIN', 6) === 0);
+
+ // populate mbstring overloading cache if not set
+ if (self::$_mbStringOverload === null) {
+ self::$_mbStringOverload = (extension_loaded('mbstring')
+ && (ini_get('mbstring.func_overload') & 0x02) === 0x02);
+ }
+
+ // get homedir
+ if (array_key_exists('homedir', $options)) {
+ $this->_homedir = (string)$options['homedir'];
+ } else {
+ // note: this requires the package OS dep exclude 'windows'
+ $info = posix_getpwuid(posix_getuid());
+ $this->_homedir = $info['dir'].'/.gnupg';
+ }
+
+ // attempt to create homedir if it does not exist
+ if (!is_dir($this->_homedir)) {
+ if (@mkdir($this->_homedir, 0777, true)) {
+ // Set permissions on homedir. Parent directories are created
+ // with 0777, homedir is set to 0700.
+ chmod($this->_homedir, 0700);
+ } else {
+ throw new Crypt_GPG_FileException('The \'homedir\' "' .
+ $this->_homedir . '" is not readable or does not exist '.
+ 'and cannot be created. This can happen if \'homedir\' '.
+ 'is not specified in the Crypt_GPG options, Crypt_GPG is '.
+ 'run as the web user, and the web user has no home '.
+ 'directory.',
+ 0, $this->_homedir);
+ }
+ }
+
+ // get binary
+ if (array_key_exists('binary', $options)) {
+ $this->_binary = (string)$options['binary'];
+ } elseif (array_key_exists('gpgBinary', $options)) {
+ // deprecated alias
+ $this->_binary = (string)$options['gpgBinary'];
+ } else {
+ $this->_binary = $this->_getBinary();
+ }
+
+ if ($this->_binary == '' || !is_executable($this->_binary)) {
+ throw new PEAR_Exception('GPG binary not found. If you are sure '.
+ 'the GPG binary is installed, please specify the location of '.
+ 'the GPG binary using the \'binary\' driver option.');
+ }
+
+ /*
+ * Note:
+ *
+ * Normally, GnuPG expects keyrings to be in the homedir and expects
+ * to be able to write temporary files in the homedir. Sometimes,
+ * keyrings are not in the homedir, or location of the keyrings does
+ * not allow writing temporary files. In this case, the <i>homedir</i>
+ * option by itself is not enough to specify the keyrings because GnuPG
+ * can not write required temporary files. Additional options are
+ * provided so you can specify the location of the keyrings separately
+ * from the homedir.
+ */
+
+ // get public keyring
+ if (array_key_exists('publicKeyring', $options)) {
+ $this->_publicKeyring = (string)$options['publicKeyring'];
+ if (!is_readable($this->_publicKeyring)) {
+ throw new Crypt_GPG_FileException('The \'publicKeyring\' "' .
+ $this->_publicKeyring . '" does not exist or is ' .
+ 'not readable. Check the location and ensure the file ' .
+ 'permissions are correct.', 0, $this->_publicKeyring);
+ }
+ }
+
+ // get private keyring
+ if (array_key_exists('privateKeyring', $options)) {
+ $this->_privateKeyring = (string)$options['privateKeyring'];
+ if (!is_readable($this->_privateKeyring)) {
+ throw new Crypt_GPG_FileException('The \'privateKeyring\' "' .
+ $this->_privateKeyring . '" does not exist or is ' .
+ 'not readable. Check the location and ensure the file ' .
+ 'permissions are correct.', 0, $this->_privateKeyring);
+ }
+ }
+
+ // get trust database
+ if (array_key_exists('trustDb', $options)) {
+ $this->_trustDb = (string)$options['trustDb'];
+ if (!is_readable($this->_trustDb)) {
+ throw new Crypt_GPG_FileException('The \'trustDb\' "' .
+ $this->_trustDb . '" does not exist or is not readable. ' .
+ 'Check the location and ensure the file permissions are ' .
+ 'correct.', 0, $this->_trustDb);
+ }
+ }
+
+ if (array_key_exists('debug', $options)) {
+ $this->_debug = (boolean)$options['debug'];
+ }
+ }
+
+ // }}}
+ // {{{ __destruct()
+
+ /**
+ * Closes open GPG subprocesses when this object is destroyed
+ *
+ * Subprocesses should never be left open by this class unless there is
+ * an unknown error and unexpected script termination occurs.
+ */
+ public function __destruct()
+ {
+ $this->_closeSubprocess();
+ }
+
+ // }}}
+ // {{{ addErrorHandler()
+
+ /**
+ * Adds an error handler method
+ *
+ * The method is run every time a new error line is received from the GPG
+ * subprocess. The handler method must accept the error line to be handled
+ * as its first parameter.
+ *
+ * @param callback $callback the callback method to use.
+ * @param array $args optional. Additional arguments to pass as
+ * parameters to the callback method.
+ *
+ * @return void
+ */
+ public function addErrorHandler($callback, array $args = array())
+ {
+ $this->_errorHandlers[] = array(
+ 'callback' => $callback,
+ 'args' => $args
+ );
+ }
+
+ // }}}
+ // {{{ addStatusHandler()
+
+ /**
+ * Adds a status handler method
+ *
+ * The method is run every time a new status line is received from the
+ * GPG subprocess. The handler method must accept the status line to be
+ * handled as its first parameter.
+ *
+ * @param callback $callback the callback method to use.
+ * @param array $args optional. Additional arguments to pass as
+ * parameters to the callback method.
+ *
+ * @return void
+ */
+ public function addStatusHandler($callback, array $args = array())
+ {
+ $this->_statusHandlers[] = array(
+ 'callback' => $callback,
+ 'args' => $args
+ );
+ }
+
+ // }}}
+ // {{{ sendCommand()
+
+ /**
+ * Sends a command to the GPG subprocess over the command file-descriptor
+ * pipe
+ *
+ * @param string $command the command to send.
+ *
+ * @return void
+ *
+ * @sensitive $command
+ */
+ public function sendCommand($command)
+ {
+ if (array_key_exists(self::FD_COMMAND, $this->_openPipes)) {
+ $this->_commandBuffer .= $command . PHP_EOL;
+ }
+ }
+
+ // }}}
+ // {{{ reset()
+
+ /**
+ * Resets the GPG engine, preparing it for a new operation
+ *
+ * @return void
+ *
+ * @see Crypt_GPG_Engine::run()
+ * @see Crypt_GPG_Engine::setOperation()
+ */
+ public function reset()
+ {
+ $this->_operation = '';
+ $this->_arguments = array();
+ $this->_input = null;
+ $this->_message = null;
+ $this->_output = '';
+ $this->_errorCode = Crypt_GPG::ERROR_NONE;
+ $this->_needPassphrase = 0;
+ $this->_commandBuffer = '';
+
+ $this->_statusHandlers = array();
+ $this->_errorHandlers = array();
+
+ $this->addStatusHandler(array($this, '_handleErrorStatus'));
+ $this->addErrorHandler(array($this, '_handleErrorError'));
+
+ if ($this->_debug) {
+ $this->addStatusHandler(array($this, '_handleDebugStatus'));
+ $this->addErrorHandler(array($this, '_handleDebugError'));
+ }
+ }
+
+ // }}}
+ // {{{ run()
+
+ /**
+ * Runs the current GPG operation
+ *
+ * This creates and manages the GPG subprocess.
+ *
+ * The operation must be set with {@link Crypt_GPG_Engine::setOperation()}
+ * before this method is called.
+ *
+ * @return void
+ *
+ * @throws Crypt_GPG_InvalidOperationException if no operation is specified.
+ *
+ * @see Crypt_GPG_Engine::reset()
+ * @see Crypt_GPG_Engine::setOperation()
+ */
+ public function run()
+ {
+ if ($this->_operation === '') {
+ throw new Crypt_GPG_InvalidOperationException('No GPG operation ' .
+ 'specified. Use Crypt_GPG_Engine::setOperation() before ' .
+ 'calling Crypt_GPG_Engine::run().');
+ }
+
+ $this->_openSubprocess();
+ $this->_process();
+ $this->_closeSubprocess();
+ }
+
+ // }}}
+ // {{{ getErrorCode()
+
+ /**
+ * Gets the error code of the last executed operation
+ *
+ * This value is only meaningful after {@link Crypt_GPG_Engine::run()} has
+ * been executed.
+ *
+ * @return integer the error code of the last executed operation.
+ */
+ public function getErrorCode()
+ {
+ return $this->_errorCode;
+ }
+
+ // }}}
+ // {{{ getErrorFilename()
+
+ /**
+ * Gets the file related to the error code of the last executed operation
+ *
+ * This value is only meaningful after {@link Crypt_GPG_Engine::run()} has
+ * been executed. If there is no file related to the error, an empty string
+ * is returned.
+ *
+ * @return string the file related to the error code of the last executed
+ * operation.
+ */
+ public function getErrorFilename()
+ {
+ return $this->_errorFilename;
+ }
+
+ // }}}
+ // {{{ getErrorKeyId()
+
+ /**
+ * Gets the key id related to the error code of the last executed operation
+ *
+ * This value is only meaningful after {@link Crypt_GPG_Engine::run()} has
+ * been executed. If there is no key id related to the error, an empty
+ * string is returned.
+ *
+ * @return string the key id related to the error code of the last executed
+ * operation.
+ */
+ public function getErrorKeyId()
+ {
+ return $this->_errorKeyId;
+ }
+
+ // }}}
+ // {{{ setInput()
+
+ /**
+ * Sets the input source for the current GPG operation
+ *
+ * @param string|resource &$input either a reference to the string
+ * containing the input data or an open
+ * stream resource containing the input
+ * data.
+ *
+ * @return void
+ */
+ public function setInput(&$input)
+ {
+ $this->_input =& $input;
+ }
+
+ // }}}
+ // {{{ setMessage()
+
+ /**
+ * Sets the message source for the current GPG operation
+ *
+ * Detached signature data should be specified here.
+ *
+ * @param string|resource &$message either a reference to the string
+ * containing the message data or an open
+ * stream resource containing the message
+ * data.
+ *
+ * @return void
+ */
+ public function setMessage(&$message)
+ {
+ $this->_message =& $message;
+ }
+
+ // }}}
+ // {{{ setOutput()
+
+ /**
+ * Sets the output destination for the current GPG operation
+ *
+ * @param string|resource &$output either a reference to the string in
+ * which to store GPG output or an open
+ * stream resource to which the output data
+ * should be written.
+ *
+ * @return void
+ */
+ public function setOutput(&$output)
+ {
+ $this->_output =& $output;
+ }
+
+ // }}}
+ // {{{ setOperation()
+
+ /**
+ * Sets the operation to perform
+ *
+ * @param string $operation the operation to perform. This should be one
+ * of GPG's operations. For example,
+ * <kbd>--encrypt</kbd>, <kbd>--decrypt</kbd>,
+ * <kbd>--sign</kbd>, etc.
+ * @param array $arguments optional. Additional arguments for the GPG
+ * subprocess. See the GPG manual for specific
+ * values.
+ *
+ * @return void
+ *
+ * @see Crypt_GPG_Engine::reset()
+ * @see Crypt_GPG_Engine::run()
+ */
+ public function setOperation($operation, array $arguments = array())
+ {
+ $this->_operation = $operation;
+ $this->_arguments = $arguments;
+ }
+
+ // }}}
+ // {{{ getVersion()
+
+ /**
+ * Gets the version of the GnuPG binary
+ *
+ * @return string a version number string containing the version of GnuPG
+ * being used. This value is suitable to use with PHP's
+ * version_compare() function.
+ *
+ * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
+ * Use the <kbd>debug</kbd> option and file a bug report if these
+ * exceptions occur.
+ *
+ * @throws Crypt_GPG_UnsupportedException if the provided binary is not
+ * GnuPG or if the GnuPG version is less than 1.0.2.
+ */
+ public function getVersion()
+ {
+ if ($this->_version == '') {
+
+ $options = array(
+ 'homedir' => $this->_homedir,
+ 'binary' => $this->_binary,
+ 'debug' => $this->_debug
+ );
+
+ $engine = new self($options);
+ $info = '';
+
+ // Set a garbage version so we do not end up looking up the version
+ // recursively.
+ $engine->_version = '1.0.0';
+
+ $engine->reset();
+ $engine->setOutput($info);
+ $engine->setOperation('--version');
+ $engine->run();
+
+ $code = $this->getErrorCode();
+
+ if ($code !== Crypt_GPG::ERROR_NONE) {
+ throw new Crypt_GPG_Exception(
+ 'Unknown error getting GnuPG version information. Please ' .
+ 'use the \'debug\' option when creating the Crypt_GPG ' .
+ 'object, and file a bug report at ' . Crypt_GPG::BUG_URI,
+ $code);
+ }
+
+ $matches = array();
+ $expression = '/gpg \(GnuPG\) (\S+)/';
+
+ if (preg_match($expression, $info, $matches) === 1) {
+ $this->_version = $matches[1];
+ } else {
+ throw new Crypt_GPG_Exception(
+ 'No GnuPG version information provided by the binary "' .
+ $this->_binary . '". Are you sure it is GnuPG?');
+ }
+
+ if (version_compare($this->_version, self::MIN_VERSION, 'lt')) {
+ throw new Crypt_GPG_Exception(
+ 'The version of GnuPG being used (' . $this->_version .
+ ') is not supported by Crypt_GPG. The minimum version ' .
+ 'required by Crypt_GPG is ' . self::MIN_VERSION);
+ }
+ }
+
+
+ return $this->_version;
+ }
+
+ // }}}
+ // {{{ _handleErrorStatus()
+
+ /**
+ * Handles error values in the status output from GPG
+ *
+ * This method is responsible for setting the
+ * {@link Crypt_GPG_Engine::$_errorCode}. See <b>doc/DETAILS</b> in the
+ * {@link http://www.gnupg.org/download/ GPG distribution} for detailed
+ * information on GPG's status output.
+ *
+ * @param string $line the status line to handle.
+ *
+ * @return void
+ */
+ private function _handleErrorStatus($line)
+ {
+ $tokens = explode(' ', $line);
+ switch ($tokens[0]) {
+ case 'BAD_PASSPHRASE':
+ $this->_errorCode = Crypt_GPG::ERROR_BAD_PASSPHRASE;
+ break;
+
+ case 'MISSING_PASSPHRASE':
+ $this->_errorCode = Crypt_GPG::ERROR_MISSING_PASSPHRASE;
+ break;
+
+ case 'NODATA':
+ $this->_errorCode = Crypt_GPG::ERROR_NO_DATA;
+ break;
+
+ case 'DELETE_PROBLEM':
+ if ($tokens[1] == '1') {
+ $this->_errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND;
+ break;
+ } elseif ($tokens[1] == '2') {
+ $this->_errorCode = Crypt_GPG::ERROR_DELETE_PRIVATE_KEY;
+ break;
+ }
+ break;
+
+ case 'IMPORT_RES':
+ if ($tokens[12] > 0) {
+ $this->_errorCode = Crypt_GPG::ERROR_DUPLICATE_KEY;
+ }
+ break;
+
+ case 'NO_PUBKEY':
+ case 'NO_SECKEY':
+ $this->_errorKeyId = $tokens[1];
+ $this->_errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND;
+ break;
+
+ case 'NEED_PASSPHRASE':
+ $this->_needPassphrase++;
+ break;
+
+ case 'GOOD_PASSPHRASE':
+ $this->_needPassphrase--;
+ break;
+
+ case 'EXPSIG':
+ case 'EXPKEYSIG':
+ case 'REVKEYSIG':
+ case 'BADSIG':
+ $this->_errorCode = Crypt_GPG::ERROR_BAD_SIGNATURE;
+ break;
+
+ }
+ }
+
+ // }}}
+ // {{{ _handleErrorError()
+
+ /**
+ * Handles error values in the error output from GPG
+ *
+ * This method is responsible for setting the
+ * {@link Crypt_GPG_Engine::$_errorCode}.
+ *
+ * @param string $line the error line to handle.
+ *
+ * @return void
+ */
+ private function _handleErrorError($line)
+ {
+ if ($this->_errorCode === Crypt_GPG::ERROR_NONE) {
+ $pattern = '/no valid OpenPGP data found/';
+ if (preg_match($pattern, $line) === 1) {
+ $this->_errorCode = Crypt_GPG::ERROR_NO_DATA;
+ }
+ }
+
+ if ($this->_errorCode === Crypt_GPG::ERROR_NONE) {
+ $pattern = '/No secret key|secret key not available/';
+ if (preg_match($pattern, $line) === 1) {
+ $this->_errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND;
+ }
+ }
+
+ if ($this->_errorCode === Crypt_GPG::ERROR_NONE) {
+ $pattern = '/No public key|public key not found/';
+ if (preg_match($pattern, $line) === 1) {
+ $this->_errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND;
+ }
+ }
+
+ if ($this->_errorCode === Crypt_GPG::ERROR_NONE) {
+ $matches = array();
+ $pattern = '/can\'t (?:access|open) `(.*?)\'/';
+ if (preg_match($pattern, $line, $matches) === 1) {
+ $this->_errorFilename = $matches[1];
+ $this->_errorCode = Crypt_GPG::ERROR_FILE_PERMISSIONS;
+ }
+ }
+ }
+
+ // }}}
+ // {{{ _handleDebugStatus()
+
+ /**
+ * Displays debug output for status lines
+ *
+ * @param string $line the status line to handle.
+ *
+ * @return void
+ */
+ private function _handleDebugStatus($line)
+ {
+ $this->_debug('STATUS: ' . $line);
+ }
+
+ // }}}
+ // {{{ _handleDebugError()
+
+ /**
+ * Displays debug output for error lines
+ *
+ * @param string $line the error line to handle.
+ *
+ * @return void
+ */
+ private function _handleDebugError($line)
+ {
+ $this->_debug('ERROR: ' . $line);
+ }
+
+ // }}}
+ // {{{ _process()
+
+ /**
+ * Performs internal streaming operations for the subprocess using either
+ * strings or streams as input / output points
+ *
+ * This is the main I/O loop for streaming to and from the GPG subprocess.
+ *
+ * The implementation of this method is verbose mainly for performance
+ * reasons. Adding streams to a lookup array and looping the array inside
+ * the main I/O loop would be siginficantly slower for large streams.
+ *
+ * @return void
+ *
+ * @throws Crypt_GPG_Exception if there is an error selecting streams for
+ * reading or writing. If this occurs, please file a bug report at
+ * http://pear.php.net/bugs/report.php?package=Crypt_GPG.
+ */
+ private function _process()
+ {
+ $this->_debug('BEGIN PROCESSING');
+
+ $this->_commandBuffer = ''; // buffers input to GPG
+ $messageBuffer = ''; // buffers input to GPG
+ $inputBuffer = ''; // buffers input to GPG
+ $outputBuffer = ''; // buffers output from GPG
+ $statusBuffer = ''; // buffers output from GPG
+ $errorBuffer = ''; // buffers output from GPG
+ $inputComplete = false; // input stream is completely buffered
+ $messageComplete = false; // message stream is completely buffered
+
+ if (is_string($this->_input)) {
+ $inputBuffer = $this->_input;
+ $inputComplete = true;
+ }
+
+ if (is_string($this->_message)) {
+ $messageBuffer = $this->_message;
+ $messageComplete = true;
+ }
+
+ if (is_string($this->_output)) {
+ $outputBuffer =& $this->_output;
+ }
+
+ // convenience variables
+ $fdInput = $this->_pipes[self::FD_INPUT];
+ $fdOutput = $this->_pipes[self::FD_OUTPUT];
+ $fdError = $this->_pipes[self::FD_ERROR];
+ $fdStatus = $this->_pipes[self::FD_STATUS];
+ $fdCommand = $this->_pipes[self::FD_COMMAND];
+ $fdMessage = $this->_pipes[self::FD_MESSAGE];
+
+ while (true) {
+
+ $inputStreams = array();
+ $outputStreams = array();
+ $exceptionStreams = array();
+
+ // set up input streams
+ if (is_resource($this->_input) && !$inputComplete) {
+ if (feof($this->_input)) {
+ $inputComplete = true;
+ } else {
+ $inputStreams[] = $this->_input;
+ }
+ }
+
+ // close GPG input pipe if there is no more data
+ if ($inputBuffer == '' && $inputComplete) {
+ $this->_debug('=> closing GPG input pipe');
+ $this->_closePipe(self::FD_INPUT);
+ }
+
+ if (is_resource($this->_message) && !$messageComplete) {
+ if (feof($this->_message)) {
+ $messageComplete = true;
+ } else {
+ $inputStreams[] = $this->_message;
+ }
+ }
+
+ // close GPG message pipe if there is no more data
+ if ($messageBuffer == '' && $messageComplete) {
+ $this->_debug('=> closing GPG message pipe');
+ $this->_closePipe(self::FD_MESSAGE);
+ }
+
+ if (!feof($fdOutput)) {
+ $inputStreams[] = $fdOutput;
+ }
+
+ if (!feof($fdStatus)) {
+ $inputStreams[] = $fdStatus;
+ }
+
+ if (!feof($fdError)) {
+ $inputStreams[] = $fdError;
+ }
+
+ // set up output streams
+ if ($outputBuffer != '' && is_resource($this->_output)) {
+ $outputStreams[] = $this->_output;
+ }
+
+ if ($this->_commandBuffer != '') {
+ $outputStreams[] = $fdCommand;
+ }
+
+ if ($messageBuffer != '') {
+ $outputStreams[] = $fdMessage;
+ }
+
+ if ($inputBuffer != '') {
+ $outputStreams[] = $fdInput;
+ }
+
+ // no streams left to read or write, we're all done
+ if (count($inputStreams) === 0 && count($outputStreams) === 0) {
+ break;
+ }
+
+ $this->_debug('selecting streams');
+
+ $ready = stream_select(
+ $inputStreams,
+ $outputStreams,
+ $exceptionStreams,
+ null
+ );
+
+ $this->_debug('=> got ' . $ready);
+
+ if ($ready === false) {
+ throw new Crypt_GPG_Exception(
+ 'Error selecting stream for communication with GPG ' .
+ 'subprocess. Please file a bug report at: ' .
+ 'http://pear.php.net/bugs/report.php?package=Crypt_GPG');
+ }
+
+ if ($ready === 0) {
+ throw new Crypt_GPG_Exception(
+ 'stream_select() returned 0. This can not happen! Please ' .
+ 'file a bug report at: ' .
+ 'http://pear.php.net/bugs/report.php?package=Crypt_GPG');
+ }
+
+ // write input (to GPG)
+ if (in_array($fdInput, $outputStreams)) {
+ $this->_debug('GPG is ready for input');
+
+ $chunk = self::_byteSubstring(
+ $inputBuffer,
+ 0,
+ self::CHUNK_SIZE
+ );
+
+ $length = self::_byteLength($chunk);
+
+ $this->_debug(
+ '=> about to write ' . $length . ' bytes to GPG input'
+ );
+
+ $length = fwrite($fdInput, $chunk, $length);
+
+ $this->_debug('=> wrote ' . $length . ' bytes');
+
+ $inputBuffer = self::_byteSubstring(
+ $inputBuffer,
+ $length
+ );
+ }
+
+ // read input (from PHP stream)
+ if (in_array($this->_input, $inputStreams)) {
+ $this->_debug('input stream is ready for reading');
+ $this->_debug(
+ '=> about to read ' . self::CHUNK_SIZE .
+ ' bytes from input stream'
+ );
+
+ $chunk = fread($this->_input, self::CHUNK_SIZE);
+ $length = self::_byteLength($chunk);
+ $inputBuffer .= $chunk;
+
+ $this->_debug('=> read ' . $length . ' bytes');
+ }
+
+ // write message (to GPG)
+ if (in_array($fdMessage, $outputStreams)) {
+ $this->_debug('GPG is ready for message data');
+
+ $chunk = self::_byteSubstring(
+ $messageBuffer,
+ 0,
+ self::CHUNK_SIZE
+ );
+
+ $length = self::_byteLength($chunk);
+
+ $this->_debug(
+ '=> about to write ' . $length . ' bytes to GPG message'
+ );
+
+ $length = fwrite($fdMessage, $chunk, $length);
+ $this->_debug('=> wrote ' . $length . ' bytes');
+
+ $messageBuffer = self::_byteSubstring($messageBuffer, $length);
+ }
+
+ // read message (from PHP stream)
+ if (in_array($this->_message, $inputStreams)) {
+ $this->_debug('message stream is ready for reading');
+ $this->_debug(
+ '=> about to read ' . self::CHUNK_SIZE .
+ ' bytes from message stream'
+ );
+
+ $chunk = fread($this->_message, self::CHUNK_SIZE);
+ $length = self::_byteLength($chunk);
+ $messageBuffer .= $chunk;
+
+ $this->_debug('=> read ' . $length . ' bytes');
+ }
+
+ // read output (from GPG)
+ if (in_array($fdOutput, $inputStreams)) {
+ $this->_debug('GPG output stream ready for reading');
+ $this->_debug(
+ '=> about to read ' . self::CHUNK_SIZE .
+ ' bytes from GPG output'
+ );
+
+ $chunk = fread($fdOutput, self::CHUNK_SIZE);
+ $length = self::_byteLength($chunk);
+ $outputBuffer .= $chunk;
+
+ $this->_debug('=> read ' . $length . ' bytes');
+ }
+
+ // write output (to PHP stream)
+ if (in_array($this->_output, $outputStreams)) {
+ $this->_debug('output stream is ready for data');
+
+ $chunk = self::_byteSubstring(
+ $outputBuffer,
+ 0,
+ self::CHUNK_SIZE
+ );
+
+ $length = self::_byteLength($chunk);
+
+ $this->_debug(
+ '=> about to write ' . $length . ' bytes to output stream'
+ );
+
+ $length = fwrite($this->_output, $chunk, $length);
+
+ $this->_debug('=> wrote ' . $length . ' bytes');
+
+ $outputBuffer = self::_byteSubstring($outputBuffer, $length);
+ }
+
+ // read error (from GPG)
+ if (in_array($fdError, $inputStreams)) {
+ $this->_debug('GPG error stream ready for reading');
+ $this->_debug(
+ '=> about to read ' . self::CHUNK_SIZE .
+ ' bytes from GPG error'
+ );
+
+ $chunk = fread($fdError, self::CHUNK_SIZE);
+ $length = self::_byteLength($chunk);
+ $errorBuffer .= $chunk;
+
+ $this->_debug('=> read ' . $length . ' bytes');
+
+ // pass lines to error handlers
+ while (($pos = strpos($errorBuffer, PHP_EOL)) !== false) {
+ $line = self::_byteSubstring($errorBuffer, 0, $pos);
+ foreach ($this->_errorHandlers as $handler) {
+ array_unshift($handler['args'], $line);
+ call_user_func_array(
+ $handler['callback'],
+ $handler['args']
+ );
+
+ array_shift($handler['args']);
+ }
+ $errorBuffer = self::_byteSubString(
+ $errorBuffer,
+ $pos + self::_byteLength(PHP_EOL)
+ );
+ }
+ }
+
+ // read status (from GPG)
+ if (in_array($fdStatus, $inputStreams)) {
+ $this->_debug('GPG status stream ready for reading');
+ $this->_debug(
+ '=> about to read ' . self::CHUNK_SIZE .
+ ' bytes from GPG status'
+ );
+
+ $chunk = fread($fdStatus, self::CHUNK_SIZE);
+ $length = self::_byteLength($chunk);
+ $statusBuffer .= $chunk;
+
+ $this->_debug('=> read ' . $length . ' bytes');
+
+ // pass lines to status handlers
+ while (($pos = strpos($statusBuffer, PHP_EOL)) !== false) {
+ $line = self::_byteSubstring($statusBuffer, 0, $pos);
+ // only pass lines beginning with magic prefix
+ if (self::_byteSubstring($line, 0, 9) == '[GNUPG:] ') {
+ $line = self::_byteSubstring($line, 9);
+ foreach ($this->_statusHandlers as $handler) {
+ array_unshift($handler['args'], $line);
+ call_user_func_array(
+ $handler['callback'],
+ $handler['args']
+ );
+
+ array_shift($handler['args']);
+ }
+ }
+ $statusBuffer = self::_byteSubString(
+ $statusBuffer,
+ $pos + self::_byteLength(PHP_EOL)
+ );
+ }
+ }
+
+ // write command (to GPG)
+ if (in_array($fdCommand, $outputStreams)) {
+ $this->_debug('GPG is ready for command data');
+
+ // send commands
+ $chunk = self::_byteSubstring(
+ $this->_commandBuffer,
+ 0,
+ self::CHUNK_SIZE
+ );
+
+ $length = self::_byteLength($chunk);
+
+ $this->_debug(
+ '=> about to write ' . $length . ' bytes to GPG command'
+ );
+
+ $length = fwrite($fdCommand, $chunk, $length);
+
+ $this->_debug('=> wrote ' . $length);
+
+ $this->_commandBuffer = self::_byteSubstring(
+ $this->_commandBuffer,
+ $length
+ );
+ }
+
+ } // end loop while streams are open
+
+ $this->_debug('END PROCESSING');
+ }
+
+ // }}}
+ // {{{ _openSubprocess()
+
+ /**
+ * Opens an internal GPG subprocess for the current operation
+ *
+ * Opens a GPG subprocess, then connects the subprocess to some pipes. Sets
+ * the private class property {@link Crypt_GPG_Engine::$_process} to
+ * the new subprocess.
+ *
+ * @return void
+ *
+ * @throws Crypt_GPG_OpenSubprocessException if the subprocess could not be
+ * opened.
+ *
+ * @see Crypt_GPG_Engine::setOperation()
+ * @see Crypt_GPG_Engine::_closeSubprocess()
+ * @see Crypt_GPG_Engine::$_process
+ */
+ private function _openSubprocess()
+ {
+ $version = $this->getVersion();
+
+ $env = $_ENV;
+
+ // Newer versions of GnuPG return localized results. Crypt_GPG only
+ // works with English, so set the locale to 'C' for the subprocess.
+ $env['LC_ALL'] = 'C';
+
+ $commandLine = $this->_binary;
+
+ $defaultArguments = array(
+ '--status-fd ' . escapeshellarg(self::FD_STATUS),
+ '--command-fd ' . escapeshellarg(self::FD_COMMAND),
+ '--no-secmem-warning',
+ '--no-tty',
+ '--no-default-keyring', // ignored if keying files are not specified
+ '--no-options' // prevent creation of ~/.gnupg directory
+ );
+
+ if (version_compare($version, '1.0.7', 'ge')) {
+ if (version_compare($version, '2.0.0', 'lt')) {
+ $defaultArguments[] = '--no-use-agent';
+ }
+ $defaultArguments[] = '--no-permission-warning';
+ }
+
+ if (version_compare($version, '1.4.2', 'ge')) {
+ $defaultArguments[] = '--exit-on-status-write-error';
+ }
+
+ if (version_compare($version, '1.3.2', 'ge')) {
+ $defaultArguments[] = '--trust-model always';
+ } else {
+ $defaultArguments[] = '--always-trust';
+ }
+
+ $arguments = array_merge($defaultArguments, $this->_arguments);
+
+ if ($this->_homedir) {
+ $arguments[] = '--homedir ' . escapeshellarg($this->_homedir);
+
+ // the random seed file makes subsequent actions faster so only
+ // disable it if we have to.
+ if (!is_writeable($this->_homedir)) {
+ $arguments[] = '--no-random-seed-file';
+ }
+ }
+
+ if ($this->_publicKeyring) {
+ $arguments[] = '--keyring ' . escapeshellarg($this->_publicKeyring);
+ }
+
+ if ($this->_privateKeyring) {
+ $arguments[] = '--secret-keyring ' .
+ escapeshellarg($this->_privateKeyring);
+ }
+
+ if ($this->_trustDb) {
+ $arguments[] = '--trustdb-name ' . escapeshellarg($this->_trustDb);
+ }
+
+ $commandLine .= ' ' . implode(' ', $arguments) . ' ' .
+ $this->_operation;
+
+ // Binary operations will not work on Windows with PHP < 5.2.6. This is
+ // in case stream_select() ever works on Windows.
+ $rb = (version_compare(PHP_VERSION, '5.2.6') < 0) ? 'r' : 'rb';
+ $wb = (version_compare(PHP_VERSION, '5.2.6') < 0) ? 'w' : 'wb';
+
+ $descriptorSpec = array(
+ self::FD_INPUT => array('pipe', $rb), // stdin
+ self::FD_OUTPUT => array('pipe', $wb), // stdout
+ self::FD_ERROR => array('pipe', $wb), // stderr
+ self::FD_STATUS => array('pipe', $wb), // status
+ self::FD_COMMAND => array('pipe', $rb), // command
+ self::FD_MESSAGE => array('pipe', $rb) // message
+ );
+
+ $this->_debug('OPENING SUBPROCESS WITH THE FOLLOWING COMMAND:');
+ $this->_debug($commandLine);
+
+ $this->_process = proc_open(
+ $commandLine,
+ $descriptorSpec,
+ $this->_pipes,
+ null,
+ $env,
+ array('binary_pipes' => true)
+ );
+
+ if (!is_resource($this->_process)) {
+ throw new Crypt_GPG_OpenSubprocessException(
+ 'Unable to open GPG subprocess.', 0, $commandLine);
+ }
+
+ $this->_openPipes = $this->_pipes;
+ $this->_errorCode = Crypt_GPG::ERROR_NONE;
+ }
+
+ // }}}
+ // {{{ _closeSubprocess()
+
+ /**
+ * Closes a the internal GPG subprocess
+ *
+ * Closes the internal GPG subprocess. Sets the private class property
+ * {@link Crypt_GPG_Engine::$_process} to null.
+ *
+ * @return void
+ *
+ * @see Crypt_GPG_Engine::_openSubprocess()
+ * @see Crypt_GPG_Engine::$_process
+ */
+ private function _closeSubprocess()
+ {
+ if (is_resource($this->_process)) {
+ $this->_debug('CLOSING SUBPROCESS');
+
+ // close remaining open pipes
+ foreach (array_keys($this->_openPipes) as $pipeNumber) {
+ $this->_closePipe($pipeNumber);
+ }
+
+ $exitCode = proc_close($this->_process);
+
+ if ($exitCode != 0) {
+ $this->_debug(
+ '=> subprocess returned an unexpected exit code: ' .
+ $exitCode
+ );
+
+ if ($this->_errorCode === Crypt_GPG::ERROR_NONE) {
+ if ($this->_needPassphrase > 0) {
+ $this->_errorCode = Crypt_GPG::ERROR_MISSING_PASSPHRASE;
+ } else {
+ $this->_errorCode = Crypt_GPG::ERROR_UNKNOWN;
+ }
+ }
+ }
+
+ $this->_process = null;
+ $this->_pipes = array();
+ }
+ }
+
+ // }}}
+ // {{{ _closePipe()
+
+ /**
+ * Closes an opened pipe used to communicate with the GPG subprocess
+ *
+ * If the pipe is already closed, it is ignored. If the pipe is open, it
+ * is flushed and then closed.
+ *
+ * @param integer $pipeNumber the file descriptor number of the pipe to
+ * close.
+ *
+ * @return void
+ */
+ private function _closePipe($pipeNumber)
+ {
+ $pipeNumber = intval($pipeNumber);
+ if (array_key_exists($pipeNumber, $this->_openPipes)) {
+ fflush($this->_openPipes[$pipeNumber]);
+ fclose($this->_openPipes[$pipeNumber]);
+ unset($this->_openPipes[$pipeNumber]);
+ }
+ }
+
+ // }}}
+ // {{{ _getBinary()
+
+ /**
+ * Gets the name of the GPG binary for the current operating system
+ *
+ * This method is called if the '<kbd>binary</kbd>' option is <i>not</i>
+ * specified when creating this driver.
+ *
+ * @return string the name of the GPG binary for the current operating
+ * system. If no suitable binary could be found, an empty
+ * string is returned.
+ */
+ private function _getBinary()
+ {
+ $binary = '';
+
+ if ($this->_isDarwin) {
+ $binaryFiles = array(
+ '/opt/local/bin/gpg', // MacPorts
+ '/usr/local/bin/gpg', // Mac GPG
+ '/sw/bin/gpg', // Fink
+ '/usr/bin/gpg'
+ );
+ } else {
+ $binaryFiles = array(
+ '/usr/bin/gpg',
+ '/usr/local/bin/gpg'
+ );
+ }
+
+ foreach ($binaryFiles as $binaryFile) {
+ if (is_executable($binaryFile)) {
+ $binary = $binaryFile;
+ break;
+ }
+ }
+
+ return $binary;
+ }
+
+ // }}}
+ // {{{ _debug()
+
+ /**
+ * Displays debug text if debugging is turned on
+ *
+ * Debugging text is prepended with a debug identifier and echoed to stdout.
+ *
+ * @param string $text the debugging text to display.
+ *
+ * @return void
+ */
+ private function _debug($text)
+ {
+ if ($this->_debug) {
+ if (array_key_exists('SHELL', $_ENV)) {
+ foreach (explode(PHP_EOL, $text) as $line) {
+ echo "Crypt_GPG DEBUG: ", $line, PHP_EOL;
+ }
+ } else {
+ // running on a web server, format debug output nicely
+ foreach (explode(PHP_EOL, $text) as $line) {
+ echo "Crypt_GPG DEBUG: <strong>", $line,
+ '</strong><br />', PHP_EOL;
+ }
+ }
+ }
+ }
+
+ // }}}
+ // {{{ _byteLength()
+
+ /**
+ * Gets the length of a string in bytes even if mbstring function
+ * overloading is turned on
+ *
+ * This is used for stream-based communication with the GPG subprocess.
+ *
+ * @param string $string the string for which to get the length.
+ *
+ * @return integer the length of the string in bytes.
+ *
+ * @see Crypt_GPG_Engine::$_mbStringOverload
+ */
+ private static function _byteLength($string)
+ {
+ if (self::$_mbStringOverload) {
+ return mb_strlen($string, '8bit');
+ }
+
+ return strlen((binary)$string);
+ }
+
+ // }}}
+ // {{{ _byteSubstring()
+
+ /**
+ * Gets the substring of a string in bytes even if mbstring function
+ * overloading is turned on
+ *
+ * This is used for stream-based communication with the GPG subprocess.
+ *
+ * @param string $string the input string.
+ * @param integer $start the starting point at which to get the substring.
+ * @param integer $length optional. The length of the substring.
+ *
+ * @return string the extracted part of the string. Unlike the default PHP
+ * <kbd>substr()</kbd> function, the returned value is
+ * always a string and never false.
+ *
+ * @see Crypt_GPG_Engine::$_mbStringOverload
+ */
+ private static function _byteSubstring($string, $start, $length = null)
+ {
+ if (self::$_mbStringOverload) {
+ if ($length === null) {
+ return mb_substr(
+ $string,
+ $start,
+ self::_byteLength($string) - $start, '8bit'
+ );
+ }
+
+ return mb_substr($string, $start, $length, '8bit');
+ }
+
+ if ($length === null) {
+ return (string)substr((binary)$string, $start);
+ }
+
+ return (string)substr((binary)$string, $start, $length);
+ }
+
+ // }}}
+}
+
+// }}}
+
+?>
diff --git a/program/lib/Crypt/GPG/Exceptions.php b/program/lib/Crypt/GPG/Exceptions.php
new file mode 100644
index 000000000..744acf5d4
--- /dev/null
+++ b/program/lib/Crypt/GPG/Exceptions.php
@@ -0,0 +1,473 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Various exception handling classes for Crypt_GPG
+ *
+ * Crypt_GPG provides an object oriented interface to GNU Privacy
+ * Guard (GPG). It requires the GPG executable to be on the system.
+ *
+ * This file contains various exception classes used by the Crypt_GPG package.
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Nathan Fredrickson <nathan@silverorange.com>
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2005 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @version CVS: $Id: Exceptions.php 273745 2009-01-18 05:24:25Z gauthierm $
+ * @link http://pear.php.net/package/Crypt_GPG
+ */
+
+/**
+ * PEAR Exception handler and base class
+ */
+require_once 'PEAR/Exception.php';
+
+// {{{ class Crypt_GPG_Exception
+
+/**
+ * An exception thrown by the Crypt_GPG package
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2005 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ */
+class Crypt_GPG_Exception extends PEAR_Exception
+{
+}
+
+// }}}
+// {{{ class Crypt_GPG_FileException
+
+/**
+ * An exception thrown when a file is used in ways it cannot be used
+ *
+ * For example, if an output file is specified and the file is not writeable, or
+ * if an input file is specified and the file is not readable, this exception
+ * is thrown.
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2007-2008 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ */
+class Crypt_GPG_FileException extends Crypt_GPG_Exception
+{
+ // {{{ private class properties
+
+ /**
+ * The name of the file that caused this exception
+ *
+ * @var string
+ */
+ private $_filename = '';
+
+ // }}}
+ // {{{ __construct()
+
+ /**
+ * Creates a new Crypt_GPG_FileException
+ *
+ * @param string $message an error message.
+ * @param integer $code a user defined error code.
+ * @param string $filename the name of the file that caused this exception.
+ */
+ public function __construct($message, $code = 0, $filename = '')
+ {
+ $this->_filename = $filename;
+ parent::__construct($message, $code);
+ }
+
+ // }}}
+ // {{{ getFilename()
+
+ /**
+ * Returns the filename of the file that caused this exception
+ *
+ * @return string the filename of the file that caused this exception.
+ *
+ * @see Crypt_GPG_FileException::$_filename
+ */
+ public function getFilename()
+ {
+ return $this->_filename;
+ }
+
+ // }}}
+}
+
+// }}}
+// {{{ class Crypt_GPG_OpenSubprocessException
+
+/**
+ * An exception thrown when the GPG subprocess cannot be opened
+ *
+ * This exception is thrown when the {@link Crypt_GPG_Engine} tries to open a
+ * new subprocess and fails.
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2005 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ */
+class Crypt_GPG_OpenSubprocessException extends Crypt_GPG_Exception
+{
+ // {{{ private class properties
+
+ /**
+ * The command used to try to open the subprocess
+ *
+ * @var string
+ */
+ private $_command = '';
+
+ // }}}
+ // {{{ __construct()
+
+ /**
+ * Creates a new Crypt_GPG_OpenSubprocessException
+ *
+ * @param string $message an error message.
+ * @param integer $code a user defined error code.
+ * @param string $command the command that was called to open the
+ * new subprocess.
+ *
+ * @see Crypt_GPG::_openSubprocess()
+ */
+ public function __construct($message, $code = 0, $command = '')
+ {
+ $this->_command = $command;
+ parent::__construct($message, $code);
+ }
+
+ // }}}
+ // {{{ getCommand()
+
+ /**
+ * Returns the contents of the internal _command property
+ *
+ * @return string the command used to open the subprocess.
+ *
+ * @see Crypt_GPG_OpenSubprocessException::$_command
+ */
+ public function getCommand()
+ {
+ return $this->_command;
+ }
+
+ // }}}
+}
+
+// }}}
+// {{{ class Crypt_GPG_InvalidOperationException
+
+/**
+ * An exception thrown when an invalid GPG operation is attempted
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2008 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ */
+class Crypt_GPG_InvalidOperationException extends Crypt_GPG_Exception
+{
+ // {{{ private class properties
+
+ /**
+ * The attempted operation
+ *
+ * @var string
+ */
+ private $_operation = '';
+
+ // }}}
+ // {{{ __construct()
+
+ /**
+ * Creates a new Crypt_GPG_OpenSubprocessException
+ *
+ * @param string $message an error message.
+ * @param integer $code a user defined error code.
+ * @param string $operation the operation.
+ */
+ public function __construct($message, $code = 0, $operation = '')
+ {
+ $this->_operation = $operation;
+ parent::__construct($message, $code);
+ }
+
+ // }}}
+ // {{{ getOperation()
+
+ /**
+ * Returns the contents of the internal _operation property
+ *
+ * @return string the attempted operation.
+ *
+ * @see Crypt_GPG_InvalidOperationException::$_operation
+ */
+ public function getOperation()
+ {
+ return $this->_operation;
+ }
+
+ // }}}
+}
+
+// }}}
+// {{{ class Crypt_GPG_KeyNotFoundException
+
+/**
+ * An exception thrown when Crypt_GPG fails to find the key for various
+ * operations
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2005 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ */
+class Crypt_GPG_KeyNotFoundException extends Crypt_GPG_Exception
+{
+ // {{{ private class properties
+
+ /**
+ * The key identifier that was searched for
+ *
+ * @var string
+ */
+ private $_keyId = '';
+
+ // }}}
+ // {{{ __construct()
+
+ /**
+ * Creates a new Crypt_GPG_KeyNotFoundException
+ *
+ * @param string $message an error message.
+ * @param integer $code a user defined error code.
+ * @param string $keyId the key identifier of the key.
+ */
+ public function __construct($message, $code = 0, $keyId= '')
+ {
+ $this->_keyId = $keyId;
+ parent::__construct($message, $code);
+ }
+
+ // }}}
+ // {{{ getKeyId()
+
+ /**
+ * Gets the key identifier of the key that was not found
+ *
+ * @return string the key identifier of the key that was not found.
+ */
+ public function getKeyId()
+ {
+ return $this->_keyId;
+ }
+
+ // }}}
+}
+
+// }}}
+// {{{ class Crypt_GPG_NoDataException
+
+/**
+ * An exception thrown when Crypt_GPG cannot find valid data for various
+ * operations
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2006 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ */
+class Crypt_GPG_NoDataException extends Crypt_GPG_Exception
+{
+}
+
+// }}}
+// {{{ class Crypt_GPG_BadPassphraseException
+
+/**
+ * An exception thrown when a required passphrase is incorrect or missing
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2006-2008 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ */
+class Crypt_GPG_BadPassphraseException extends Crypt_GPG_Exception
+{
+ // {{{ private class properties
+
+ /**
+ * Keys for which the passhprase is missing
+ *
+ * This contains primary user ids indexed by sub-key id.
+ *
+ * @var array
+ */
+ private $_missingPassphrases = array();
+
+ /**
+ * Keys for which the passhprase is incorrect
+ *
+ * This contains primary user ids indexed by sub-key id.
+ *
+ * @var array
+ */
+ private $_badPassphrases = array();
+
+ // }}}
+ // {{{ __construct()
+
+ /**
+ * Creates a new Crypt_GPG_BadPassphraseException
+ *
+ * @param string $message an error message.
+ * @param integer $code a user defined error code.
+ * @param string $badPassphrases an array containing user ids of keys
+ * for which the passphrase is incorrect.
+ * @param string $missingPassphrases an array containing user ids of keys
+ * for which the passphrase is missing.
+ */
+ public function __construct($message, $code = 0,
+ array $badPassphrases = array(), array $missingPassphrases = array()
+ ) {
+ $this->_badPassphrases = $badPassphrases;
+ $this->_missingPassphrases = $missingPassphrases;
+
+ parent::__construct($message, $code);
+ }
+
+ // }}}
+ // {{{ getBadPassphrases()
+
+ /**
+ * Gets keys for which the passhprase is incorrect
+ *
+ * @return array an array of keys for which the passphrase is incorrect.
+ * The array contains primary user ids indexed by the sub-key
+ * id.
+ */
+ public function getBadPassphrases()
+ {
+ return $this->_badPassphrases;
+ }
+
+ // }}}
+ // {{{ getMissingPassphrases()
+
+ /**
+ * Gets keys for which the passhprase is missing
+ *
+ * @return array an array of keys for which the passphrase is missing.
+ * The array contains primary user ids indexed by the sub-key
+ * id.
+ */
+ public function getMissingPassphrases()
+ {
+ return $this->_missingPassphrases;
+ }
+
+ // }}}
+}
+
+// }}}
+// {{{ class Crypt_GPG_DeletePrivateKeyException
+
+/**
+ * An exception thrown when an attempt is made to delete public key that has an
+ * associated private key on the keyring
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2008 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ */
+class Crypt_GPG_DeletePrivateKeyException extends Crypt_GPG_Exception
+{
+ // {{{ private class properties
+
+ /**
+ * The key identifier the deletion attempt was made upon
+ *
+ * @var string
+ */
+ private $_keyId = '';
+
+ // }}}
+ // {{{ __construct()
+
+ /**
+ * Creates a new Crypt_GPG_DeletePrivateKeyException
+ *
+ * @param string $message an error message.
+ * @param integer $code a user defined error code.
+ * @param string $keyId the key identifier of the public key that was
+ * attempted to delete.
+ *
+ * @see Crypt_GPG::deletePublicKey()
+ */
+ public function __construct($message, $code = 0, $keyId = '')
+ {
+ $this->_keyId = $keyId;
+ parent::__construct($message, $code);
+ }
+
+ // }}}
+ // {{{ getKeyId()
+
+ /**
+ * Gets the key identifier of the key that was not found
+ *
+ * @return string the key identifier of the key that was not found.
+ */
+ public function getKeyId()
+ {
+ return $this->_keyId;
+ }
+
+ // }}}
+}
+
+// }}}
+
+?>
diff --git a/program/lib/Crypt/GPG/Key.php b/program/lib/Crypt/GPG/Key.php
new file mode 100644
index 000000000..67a4b9c7d
--- /dev/null
+++ b/program/lib/Crypt/GPG/Key.php
@@ -0,0 +1,223 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Contains a class representing GPG keys
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2008-2010 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @version CVS: $Id: Key.php 295621 2010-03-01 04:18:54Z gauthierm $
+ * @link http://pear.php.net/package/Crypt_GPG
+ */
+
+/**
+ * Sub-key class definition
+ */
+require_once 'Crypt/GPG/SubKey.php';
+
+/**
+ * User id class definition
+ */
+require_once 'Crypt/GPG/UserId.php';
+
+// {{{ class Crypt_GPG_Key
+
+/**
+ * A data class for GPG key information
+ *
+ * This class is used to store the results of the {@link Crypt_GPG::getKeys()}
+ * method.
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2008-2010 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ * @see Crypt_GPG::getKeys()
+ */
+class Crypt_GPG_Key
+{
+ // {{{ class properties
+
+ /**
+ * The user ids associated with this key
+ *
+ * This is an array of {@link Crypt_GPG_UserId} objects.
+ *
+ * @var array
+ *
+ * @see Crypt_GPG_Key::addUserId()
+ * @see Crypt_GPG_Key::getUserIds()
+ */
+ private $_userIds = array();
+
+ /**
+ * The subkeys of this key
+ *
+ * This is an array of {@link Crypt_GPG_SubKey} objects.
+ *
+ * @var array
+ *
+ * @see Crypt_GPG_Key::addSubKey()
+ * @see Crypt_GPG_Key::getSubKeys()
+ */
+ private $_subKeys = array();
+
+ // }}}
+ // {{{ getSubKeys()
+
+ /**
+ * Gets the sub-keys of this key
+ *
+ * @return array the sub-keys of this key.
+ *
+ * @see Crypt_GPG_Key::addSubKey()
+ */
+ public function getSubKeys()
+ {
+ return $this->_subKeys;
+ }
+
+ // }}}
+ // {{{ getUserIds()
+
+ /**
+ * Gets the user ids of this key
+ *
+ * @return array the user ids of this key.
+ *
+ * @see Crypt_GPG_Key::addUserId()
+ */
+ public function getUserIds()
+ {
+ return $this->_userIds;
+ }
+
+ // }}}
+ // {{{ getPrimaryKey()
+
+ /**
+ * Gets the primary sub-key of this key
+ *
+ * The primary key is the first added sub-key.
+ *
+ * @return Crypt_GPG_SubKey the primary sub-key of this key.
+ */
+ public function getPrimaryKey()
+ {
+ $primary_key = null;
+ if (count($this->_subKeys) > 0) {
+ $primary_key = $this->_subKeys[0];
+ }
+ return $primary_key;
+ }
+
+ // }}}
+ // {{{ canSign()
+
+ /**
+ * Gets whether or not this key can sign data
+ *
+ * This key can sign data if any sub-key of this key can sign data.
+ *
+ * @return boolean true if this key can sign data and false if this key
+ * cannot sign data.
+ */
+ public function canSign()
+ {
+ $canSign = false;
+ foreach ($this->_subKeys as $subKey) {
+ if ($subKey->canSign()) {
+ $canSign = true;
+ break;
+ }
+ }
+ return $canSign;
+ }
+
+ // }}}
+ // {{{ canEncrypt()
+
+ /**
+ * Gets whether or not this key can encrypt data
+ *
+ * This key can encrypt data if any sub-key of this key can encrypt data.
+ *
+ * @return boolean true if this key can encrypt data and false if this
+ * key cannot encrypt data.
+ */
+ public function canEncrypt()
+ {
+ $canEncrypt = false;
+ foreach ($this->_subKeys as $subKey) {
+ if ($subKey->canEncrypt()) {
+ $canEncrypt = true;
+ break;
+ }
+ }
+ return $canEncrypt;
+ }
+
+ // }}}
+ // {{{ addSubKey()
+
+ /**
+ * Adds a sub-key to this key
+ *
+ * The first added sub-key will be the primary key of this key.
+ *
+ * @param Crypt_GPG_SubKey $subKey the sub-key to add.
+ *
+ * @return Crypt_GPG_Key the current object, for fluent interface.
+ */
+ public function addSubKey(Crypt_GPG_SubKey $subKey)
+ {
+ $this->_subKeys[] = $subKey;
+ return $this;
+ }
+
+ // }}}
+ // {{{ addUserId()
+
+ /**
+ * Adds a user id to this key
+ *
+ * @param Crypt_GPG_UserId $userId the user id to add.
+ *
+ * @return Crypt_GPG_Key the current object, for fluent interface.
+ */
+ public function addUserId(Crypt_GPG_UserId $userId)
+ {
+ $this->_userIds[] = $userId;
+ return $this;
+ }
+
+ // }}}
+}
+
+// }}}
+
+?>
diff --git a/program/lib/Crypt/GPG/Signature.php b/program/lib/Crypt/GPG/Signature.php
new file mode 100644
index 000000000..03ab44c53
--- /dev/null
+++ b/program/lib/Crypt/GPG/Signature.php
@@ -0,0 +1,428 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * A class representing GPG signatures
+ *
+ * This file contains a data class representing a GPG signature.
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Nathan Fredrickson <nathan@silverorange.com>
+ * @copyright 2005-2010 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @version CVS: $Id: Signature.php 302773 2010-08-25 14:16:28Z gauthierm $
+ * @link http://pear.php.net/package/Crypt_GPG
+ */
+
+/**
+ * User id class definition
+ */
+require_once 'Crypt/GPG/UserId.php';
+
+// {{{ class Crypt_GPG_Signature
+
+/**
+ * A class for GPG signature information
+ *
+ * This class is used to store the results of the Crypt_GPG::verify() method.
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Nathan Fredrickson <nathan@silverorange.com>
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2005-2010 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ * @see Crypt_GPG::verify()
+ */
+class Crypt_GPG_Signature
+{
+ // {{{ class properties
+
+ /**
+ * A base64-encoded string containing a unique id for this signature if
+ * this signature has been verified as ok
+ *
+ * This id is used to prevent replay attacks and is not present for all
+ * types of signatures.
+ *
+ * @var string
+ */
+ private $_id = '';
+
+ /**
+ * The fingerprint of the key used to create the signature
+ *
+ * @var string
+ */
+ private $_keyFingerprint = '';
+
+ /**
+ * The id of the key used to create the signature
+ *
+ * @var string
+ */
+ private $_keyId = '';
+
+ /**
+ * The creation date of this signature
+ *
+ * This is a Unix timestamp.
+ *
+ * @var integer
+ */
+ private $_creationDate = 0;
+
+ /**
+ * The expiration date of the signature
+ *
+ * This is a Unix timestamp. If this signature does not expire, this will
+ * be zero.
+ *
+ * @var integer
+ */
+ private $_expirationDate = 0;
+
+ /**
+ * The user id associated with this signature
+ *
+ * @var Crypt_GPG_UserId
+ */
+ private $_userId = null;
+
+ /**
+ * Whether or not this signature is valid
+ *
+ * @var boolean
+ */
+ private $_isValid = false;
+
+ // }}}
+ // {{{ __construct()
+
+ /**
+ * Creates a new signature
+ *
+ * Signatures can be initialized from an array of named values. Available
+ * names are:
+ *
+ * - <kbd>string id</kbd> - the unique id of this signature.
+ * - <kbd>string fingerprint</kbd> - the fingerprint of the key used to
+ * create the signature. The fingerprint
+ * should not contain formatting
+ * characters.
+ * - <kbd>string keyId</kbd> - the id of the key used to create the
+ * the signature.
+ * - <kbd>integer creation</kbd> - the date the signature was created.
+ * This is a UNIX timestamp.
+ * - <kbd>integer expiration</kbd> - the date the signature expired. This
+ * is a UNIX timestamp. If the signature
+ * does not expire, use 0.
+ * - <kbd>boolean valid</kbd> - whether or not the signature is valid.
+ * - <kbd>string userId</kbd> - the user id associated with the
+ * signature. This may also be a
+ * {@link Crypt_GPG_UserId} object.
+ *
+ * @param Crypt_GPG_Signature|array $signature optional. Either an existing
+ * signature object, which is copied; or an array of initial values.
+ */
+ public function __construct($signature = null)
+ {
+ // copy from object
+ if ($signature instanceof Crypt_GPG_Signature) {
+ $this->_id = $signature->_id;
+ $this->_keyFingerprint = $signature->_keyFingerprint;
+ $this->_keyId = $signature->_keyId;
+ $this->_creationDate = $signature->_creationDate;
+ $this->_expirationDate = $signature->_expirationDate;
+ $this->_isValid = $signature->_isValid;
+
+ if ($signature->_userId instanceof Crypt_GPG_UserId) {
+ $this->_userId = clone $signature->_userId;
+ } else {
+ $this->_userId = $signature->_userId;
+ }
+ }
+
+ // initialize from array
+ if (is_array($signature)) {
+ if (array_key_exists('id', $signature)) {
+ $this->setId($signature['id']);
+ }
+
+ if (array_key_exists('fingerprint', $signature)) {
+ $this->setKeyFingerprint($signature['fingerprint']);
+ }
+
+ if (array_key_exists('keyId', $signature)) {
+ $this->setKeyId($signature['keyId']);
+ }
+
+ if (array_key_exists('creation', $signature)) {
+ $this->setCreationDate($signature['creation']);
+ }
+
+ if (array_key_exists('expiration', $signature)) {
+ $this->setExpirationDate($signature['expiration']);
+ }
+
+ if (array_key_exists('valid', $signature)) {
+ $this->setValid($signature['valid']);
+ }
+
+ if (array_key_exists('userId', $signature)) {
+ $userId = new Crypt_GPG_UserId($signature['userId']);
+ $this->setUserId($userId);
+ }
+ }
+ }
+
+ // }}}
+ // {{{ getId()
+
+ /**
+ * Gets the id of this signature
+ *
+ * @return string a base64-encoded string containing a unique id for this
+ * signature. This id is used to prevent replay attacks and
+ * is not present for all types of signatures.
+ */
+ public function getId()
+ {
+ return $this->_id;
+ }
+
+ // }}}
+ // {{{ getKeyFingerprint()
+
+ /**
+ * Gets the fingerprint of the key used to create this signature
+ *
+ * @return string the fingerprint of the key used to create this signature.
+ */
+ public function getKeyFingerprint()
+ {
+ return $this->_keyFingerprint;
+ }
+
+ // }}}
+ // {{{ getKeyId()
+
+ /**
+ * Gets the id of the key used to create this signature
+ *
+ * Whereas the fingerprint of the signing key may not always be available
+ * (for example if the signature is bad), the id should always be
+ * available.
+ *
+ * @return string the id of the key used to create this signature.
+ */
+ public function getKeyId()
+ {
+ return $this->_keyId;
+ }
+
+ // }}}
+ // {{{ getCreationDate()
+
+ /**
+ * Gets the creation date of this signature
+ *
+ * @return integer the creation date of this signature. This is a Unix
+ * timestamp.
+ */
+ public function getCreationDate()
+ {
+ return $this->_creationDate;
+ }
+
+ // }}}
+ // {{{ getExpirationDate()
+
+ /**
+ * Gets the expiration date of the signature
+ *
+ * @return integer the expiration date of this signature. This is a Unix
+ * timestamp. If this signature does not expire, this will
+ * be zero.
+ */
+ public function getExpirationDate()
+ {
+ return $this->_expirationDate;
+ }
+
+ // }}}
+ // {{{ getUserId()
+
+ /**
+ * Gets the user id associated with this signature
+ *
+ * @return Crypt_GPG_UserId the user id associated with this signature.
+ */
+ public function getUserId()
+ {
+ return $this->_userId;
+ }
+
+ // }}}
+ // {{{ isValid()
+
+ /**
+ * Gets whether or no this signature is valid
+ *
+ * @return boolean true if this signature is valid and false if it is not.
+ */
+ public function isValid()
+ {
+ return $this->_isValid;
+ }
+
+ // }}}
+ // {{{ setId()
+
+ /**
+ * Sets the id of this signature
+ *
+ * @param string $id a base64-encoded string containing a unique id for
+ * this signature.
+ *
+ * @return Crypt_GPG_Signature the current object, for fluent interface.
+ *
+ * @see Crypt_GPG_Signature::getId()
+ */
+ public function setId($id)
+ {
+ $this->_id = strval($id);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setKeyFingerprint()
+
+ /**
+ * Sets the key fingerprint of this signature
+ *
+ * @param string $fingerprint the key fingerprint of this signature. This
+ * is the fingerprint of the primary key used to
+ * create this signature.
+ *
+ * @return Crypt_GPG_Signature the current object, for fluent interface.
+ */
+ public function setKeyFingerprint($fingerprint)
+ {
+ $this->_keyFingerprint = strval($fingerprint);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setKeyId()
+
+ /**
+ * Sets the key id of this signature
+ *
+ * @param string $id the key id of this signature. This is the id of the
+ * primary key used to create this signature.
+ *
+ * @return Crypt_GPG_Signature the current object, for fluent interface.
+ */
+ public function setKeyId($id)
+ {
+ $this->_keyId = strval($id);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setCreationDate()
+
+ /**
+ * Sets the creation date of this signature
+ *
+ * @param integer $creationDate the creation date of this signature. This
+ * is a Unix timestamp.
+ *
+ * @return Crypt_GPG_Signature the current object, for fluent interface.
+ */
+ public function setCreationDate($creationDate)
+ {
+ $this->_creationDate = intval($creationDate);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setExpirationDate()
+
+ /**
+ * Sets the expiration date of this signature
+ *
+ * @param integer $expirationDate the expiration date of this signature.
+ * This is a Unix timestamp. Specify zero if
+ * this signature does not expire.
+ *
+ * @return Crypt_GPG_Signature the current object, for fluent interface.
+ */
+ public function setExpirationDate($expirationDate)
+ {
+ $this->_expirationDate = intval($expirationDate);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setUserId()
+
+ /**
+ * Sets the user id associated with this signature
+ *
+ * @param Crypt_GPG_UserId $userId the user id associated with this
+ * signature.
+ *
+ * @return Crypt_GPG_Signature the current object, for fluent interface.
+ */
+ public function setUserId(Crypt_GPG_UserId $userId)
+ {
+ $this->_userId = $userId;
+ return $this;
+ }
+
+ // }}}
+ // {{{ setValid()
+
+ /**
+ * Sets whether or not this signature is valid
+ *
+ * @param boolean $isValid true if this signature is valid and false if it
+ * is not.
+ *
+ * @return Crypt_GPG_Signature the current object, for fluent interface.
+ */
+ public function setValid($isValid)
+ {
+ $this->_isValid = ($isValid) ? true : false;
+ return $this;
+ }
+
+ // }}}
+}
+
+// }}}
+
+?>
diff --git a/program/lib/Crypt/GPG/SubKey.php b/program/lib/Crypt/GPG/SubKey.php
new file mode 100644
index 000000000..b6316e99f
--- /dev/null
+++ b/program/lib/Crypt/GPG/SubKey.php
@@ -0,0 +1,649 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Contains a class representing GPG sub-keys and constants for GPG algorithms
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @author Nathan Fredrickson <nathan@silverorange.com>
+ * @copyright 2005-2010 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @version CVS: $Id: SubKey.php 302768 2010-08-25 13:45:52Z gauthierm $
+ * @link http://pear.php.net/package/Crypt_GPG
+ */
+
+// {{{ class Crypt_GPG_SubKey
+
+/**
+ * A class for GPG sub-key information
+ *
+ * This class is used to store the results of the {@link Crypt_GPG::getKeys()}
+ * method. Sub-key objects are members of a {@link Crypt_GPG_Key} object.
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @author Nathan Fredrickson <nathan@silverorange.com>
+ * @copyright 2005-2010 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ * @see Crypt_GPG::getKeys()
+ * @see Crypt_GPG_Key::getSubKeys()
+ */
+class Crypt_GPG_SubKey
+{
+ // {{{ class constants
+
+ /**
+ * RSA encryption algorithm.
+ */
+ const ALGORITHM_RSA = 1;
+
+ /**
+ * Elgamal encryption algorithm (encryption only).
+ */
+ const ALGORITHM_ELGAMAL_ENC = 16;
+
+ /**
+ * DSA encryption algorithm (sometimes called DH, sign only).
+ */
+ const ALGORITHM_DSA = 17;
+
+ /**
+ * Elgamal encryption algorithm (signage and encryption - should not be
+ * used).
+ */
+ const ALGORITHM_ELGAMAL_ENC_SGN = 20;
+
+ // }}}
+ // {{{ class properties
+
+ /**
+ * The id of this sub-key
+ *
+ * @var string
+ */
+ private $_id = '';
+
+ /**
+ * The algorithm used to create this sub-key
+ *
+ * The value is one of the Crypt_GPG_SubKey::ALGORITHM_* constants.
+ *
+ * @var integer
+ */
+ private $_algorithm = 0;
+
+ /**
+ * The fingerprint of this sub-key
+ *
+ * @var string
+ */
+ private $_fingerprint = '';
+
+ /**
+ * Length of this sub-key in bits
+ *
+ * @var integer
+ */
+ private $_length = 0;
+
+ /**
+ * Date this sub-key was created
+ *
+ * This is a Unix timestamp.
+ *
+ * @var integer
+ */
+ private $_creationDate = 0;
+
+ /**
+ * Date this sub-key expires
+ *
+ * This is a Unix timestamp. If this sub-key does not expire, this will be
+ * zero.
+ *
+ * @var integer
+ */
+ private $_expirationDate = 0;
+
+ /**
+ * Whether or not this sub-key can sign data
+ *
+ * @var boolean
+ */
+ private $_canSign = false;
+
+ /**
+ * Whether or not this sub-key can encrypt data
+ *
+ * @var boolean
+ */
+ private $_canEncrypt = false;
+
+ /**
+ * Whether or not the private key for this sub-key exists in the keyring
+ *
+ * @var boolean
+ */
+ private $_hasPrivate = false;
+
+ /**
+ * Whether or not this sub-key is revoked
+ *
+ * @var boolean
+ */
+ private $_isRevoked = false;
+
+ // }}}
+ // {{{ __construct()
+
+ /**
+ * Creates a new sub-key object
+ *
+ * Sub-keys can be initialized from an array of named values. Available
+ * names are:
+ *
+ * - <kbd>string id</kbd> - the key id of the sub-key.
+ * - <kbd>integer algorithm</kbd> - the encryption algorithm of the
+ * sub-key.
+ * - <kbd>string fingerprint</kbd> - the fingerprint of the sub-key. The
+ * fingerprint should not contain
+ * formatting characters.
+ * - <kbd>integer length</kbd> - the length of the sub-key in bits.
+ * - <kbd>integer creation</kbd> - the date the sub-key was created.
+ * This is a UNIX timestamp.
+ * - <kbd>integer expiration</kbd> - the date the sub-key expires. This
+ * is a UNIX timestamp. If the sub-key
+ * does not expire, use 0.
+ * - <kbd>boolean canSign</kbd> - whether or not the sub-key can be
+ * used to sign data.
+ * - <kbd>boolean canEncrypt</kbd> - whether or not the sub-key can be
+ * used to encrypt data.
+ * - <kbd>boolean hasPrivate</kbd> - whether or not the private key for
+ * the sub-key exists in the keyring.
+ * - <kbd>boolean isRevoked</kbd> - whether or not this sub-key is
+ * revoked.
+ *
+ * @param Crypt_GPG_SubKey|string|array $key optional. Either an existing
+ * sub-key object, which is copied; a sub-key string, which is
+ * parsed; or an array of initial values.
+ */
+ public function __construct($key = null)
+ {
+ // parse from string
+ if (is_string($key)) {
+ $key = self::parse($key);
+ }
+
+ // copy from object
+ if ($key instanceof Crypt_GPG_SubKey) {
+ $this->_id = $key->_id;
+ $this->_algorithm = $key->_algorithm;
+ $this->_fingerprint = $key->_fingerprint;
+ $this->_length = $key->_length;
+ $this->_creationDate = $key->_creationDate;
+ $this->_expirationDate = $key->_expirationDate;
+ $this->_canSign = $key->_canSign;
+ $this->_canEncrypt = $key->_canEncrypt;
+ $this->_hasPrivate = $key->_hasPrivate;
+ $this->_isRevoked = $key->_isRevoked;
+ }
+
+ // initialize from array
+ if (is_array($key)) {
+ if (array_key_exists('id', $key)) {
+ $this->setId($key['id']);
+ }
+
+ if (array_key_exists('algorithm', $key)) {
+ $this->setAlgorithm($key['algorithm']);
+ }
+
+ if (array_key_exists('fingerprint', $key)) {
+ $this->setFingerprint($key['fingerprint']);
+ }
+
+ if (array_key_exists('length', $key)) {
+ $this->setLength($key['length']);
+ }
+
+ if (array_key_exists('creation', $key)) {
+ $this->setCreationDate($key['creation']);
+ }
+
+ if (array_key_exists('expiration', $key)) {
+ $this->setExpirationDate($key['expiration']);
+ }
+
+ if (array_key_exists('canSign', $key)) {
+ $this->setCanSign($key['canSign']);
+ }
+
+ if (array_key_exists('canEncrypt', $key)) {
+ $this->setCanEncrypt($key['canEncrypt']);
+ }
+
+ if (array_key_exists('hasPrivate', $key)) {
+ $this->setHasPrivate($key['hasPrivate']);
+ }
+
+ if (array_key_exists('isRevoked', $key)) {
+ $this->setRevoked($key['isRevoked']);
+ }
+ }
+ }
+
+ // }}}
+ // {{{ getId()
+
+ /**
+ * Gets the id of this sub-key
+ *
+ * @return string the id of this sub-key.
+ */
+ public function getId()
+ {
+ return $this->_id;
+ }
+
+ // }}}
+ // {{{ getAlgorithm()
+
+ /**
+ * Gets the algorithm used by this sub-key
+ *
+ * The algorithm should be one of the Crypt_GPG_SubKey::ALGORITHM_*
+ * constants.
+ *
+ * @return integer the algorithm used by this sub-key.
+ */
+ public function getAlgorithm()
+ {
+ return $this->_algorithm;
+ }
+
+ // }}}
+ // {{{ getCreationDate()
+
+ /**
+ * Gets the creation date of this sub-key
+ *
+ * This is a Unix timestamp.
+ *
+ * @return integer the creation date of this sub-key.
+ */
+ public function getCreationDate()
+ {
+ return $this->_creationDate;
+ }
+
+ // }}}
+ // {{{ getExpirationDate()
+
+ /**
+ * Gets the date this sub-key expires
+ *
+ * This is a Unix timestamp. If this sub-key does not expire, this will be
+ * zero.
+ *
+ * @return integer the date this sub-key expires.
+ */
+ public function getExpirationDate()
+ {
+ return $this->_expirationDate;
+ }
+
+ // }}}
+ // {{{ getFingerprint()
+
+ /**
+ * Gets the fingerprint of this sub-key
+ *
+ * @return string the fingerprint of this sub-key.
+ */
+ public function getFingerprint()
+ {
+ return $this->_fingerprint;
+ }
+
+ // }}}
+ // {{{ getLength()
+
+ /**
+ * Gets the length of this sub-key in bits
+ *
+ * @return integer the length of this sub-key in bits.
+ */
+ public function getLength()
+ {
+ return $this->_length;
+ }
+
+ // }}}
+ // {{{ canSign()
+
+ /**
+ * Gets whether or not this sub-key can sign data
+ *
+ * @return boolean true if this sub-key can sign data and false if this
+ * sub-key can not sign data.
+ */
+ public function canSign()
+ {
+ return $this->_canSign;
+ }
+
+ // }}}
+ // {{{ canEncrypt()
+
+ /**
+ * Gets whether or not this sub-key can encrypt data
+ *
+ * @return boolean true if this sub-key can encrypt data and false if this
+ * sub-key can not encrypt data.
+ */
+ public function canEncrypt()
+ {
+ return $this->_canEncrypt;
+ }
+
+ // }}}
+ // {{{ hasPrivate()
+
+ /**
+ * Gets whether or not the private key for this sub-key exists in the
+ * keyring
+ *
+ * @return boolean true the private key for this sub-key exists in the
+ * keyring and false if it does not.
+ */
+ public function hasPrivate()
+ {
+ return $this->_hasPrivate;
+ }
+
+ // }}}
+ // {{{ isRevoked()
+
+ /**
+ * Gets whether or not this sub-key is revoked
+ *
+ * @return boolean true if this sub-key is revoked and false if it is not.
+ */
+ public function isRevoked()
+ {
+ return $this->_isRevoked;
+ }
+
+ // }}}
+ // {{{ setCreationDate()
+
+ /**
+ * Sets the creation date of this sub-key
+ *
+ * The creation date is a Unix timestamp.
+ *
+ * @param integer $creationDate the creation date of this sub-key.
+ *
+ * @return Crypt_GPG_SubKey the current object, for fluent interface.
+ */
+ public function setCreationDate($creationDate)
+ {
+ $this->_creationDate = intval($creationDate);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setExpirationDate()
+
+ /**
+ * Sets the expiration date of this sub-key
+ *
+ * The expiration date is a Unix timestamp. Specify zero if this sub-key
+ * does not expire.
+ *
+ * @param integer $expirationDate the expiration date of this sub-key.
+ *
+ * @return Crypt_GPG_SubKey the current object, for fluent interface.
+ */
+ public function setExpirationDate($expirationDate)
+ {
+ $this->_expirationDate = intval($expirationDate);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setId()
+
+ /**
+ * Sets the id of this sub-key
+ *
+ * @param string $id the id of this sub-key.
+ *
+ * @return Crypt_GPG_SubKey the current object, for fluent interface.
+ */
+ public function setId($id)
+ {
+ $this->_id = strval($id);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setAlgorithm()
+
+ /**
+ * Sets the algorithm used by this sub-key
+ *
+ * @param integer $algorithm the algorithm used by this sub-key.
+ *
+ * @return Crypt_GPG_SubKey the current object, for fluent interface.
+ */
+ public function setAlgorithm($algorithm)
+ {
+ $this->_algorithm = intval($algorithm);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setFingerprint()
+
+ /**
+ * Sets the fingerprint of this sub-key
+ *
+ * @param string $fingerprint the fingerprint of this sub-key.
+ *
+ * @return Crypt_GPG_SubKey the current object, for fluent interface.
+ */
+ public function setFingerprint($fingerprint)
+ {
+ $this->_fingerprint = strval($fingerprint);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setLength()
+
+ /**
+ * Sets the length of this sub-key in bits
+ *
+ * @param integer $length the length of this sub-key in bits.
+ *
+ * @return Crypt_GPG_SubKey the current object, for fluent interface.
+ */
+ public function setLength($length)
+ {
+ $this->_length = intval($length);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setCanSign()
+
+ /**
+ * Sets whether of not this sub-key can sign data
+ *
+ * @param boolean $canSign true if this sub-key can sign data and false if
+ * it can not.
+ *
+ * @return Crypt_GPG_SubKey the current object, for fluent interface.
+ */
+ public function setCanSign($canSign)
+ {
+ $this->_canSign = ($canSign) ? true : false;
+ return $this;
+ }
+
+ // }}}
+ // {{{ setCanEncrypt()
+
+ /**
+ * Sets whether of not this sub-key can encrypt data
+ *
+ * @param boolean $canEncrypt true if this sub-key can encrypt data and
+ * false if it can not.
+ *
+ * @return Crypt_GPG_SubKey the current object, for fluent interface.
+ */
+ public function setCanEncrypt($canEncrypt)
+ {
+ $this->_canEncrypt = ($canEncrypt) ? true : false;
+ return $this;
+ }
+
+ // }}}
+ // {{{ setHasPrivate()
+
+ /**
+ * Sets whether of not the private key for this sub-key exists in the
+ * keyring
+ *
+ * @param boolean $hasPrivate true if the private key for this sub-key
+ * exists in the keyring and false if it does
+ * not.
+ *
+ * @return Crypt_GPG_SubKey the current object, for fluent interface.
+ */
+ public function setHasPrivate($hasPrivate)
+ {
+ $this->_hasPrivate = ($hasPrivate) ? true : false;
+ return $this;
+ }
+
+ // }}}
+ // {{{ setRevoked()
+
+ /**
+ * Sets whether or not this sub-key is revoked
+ *
+ * @param boolean $isRevoked whether or not this sub-key is revoked.
+ *
+ * @return Crypt_GPG_SubKey the current object, for fluent interface.
+ */
+ public function setRevoked($isRevoked)
+ {
+ $this->_isRevoked = ($isRevoked) ? true : false;
+ return $this;
+ }
+
+ // }}}
+ // {{{ parse()
+
+ /**
+ * Parses a sub-key object from a sub-key string
+ *
+ * See <b>doc/DETAILS</b> in the
+ * {@link http://www.gnupg.org/download/ GPG distribution} for information
+ * on how the sub-key string is parsed.
+ *
+ * @param string $string the string containing the sub-key.
+ *
+ * @return Crypt_GPG_SubKey the sub-key object parsed from the string.
+ */
+ public static function parse($string)
+ {
+ $tokens = explode(':', $string);
+
+ $subKey = new Crypt_GPG_SubKey();
+
+ $subKey->setId($tokens[4]);
+ $subKey->setLength($tokens[2]);
+ $subKey->setAlgorithm($tokens[3]);
+ $subKey->setCreationDate(self::_parseDate($tokens[5]));
+ $subKey->setExpirationDate(self::_parseDate($tokens[6]));
+
+ if ($tokens[1] == 'r') {
+ $subKey->setRevoked(true);
+ }
+
+ if (strpos($tokens[11], 's') !== false) {
+ $subKey->setCanSign(true);
+ }
+
+ if (strpos($tokens[11], 'e') !== false) {
+ $subKey->setCanEncrypt(true);
+ }
+
+ return $subKey;
+ }
+
+ // }}}
+ // {{{ _parseDate()
+
+ /**
+ * Parses a date string as provided by GPG into a UNIX timestamp
+ *
+ * @param string $string the date string.
+ *
+ * @return integer the UNIX timestamp corresponding to the provided date
+ * string.
+ */
+ private static function _parseDate($string)
+ {
+ if ($string == '') {
+ $timestamp = 0;
+ } else {
+ // all times are in UTC according to GPG documentation
+ $timeZone = new DateTimeZone('UTC');
+
+ if (strpos($string, 'T') === false) {
+ // interpret as UNIX timestamp
+ $string = '@' . $string;
+ }
+
+ $date = new DateTime($string, $timeZone);
+
+ // convert to UNIX timestamp
+ $timestamp = intval($date->format('U'));
+ }
+
+ return $timestamp;
+ }
+
+ // }}}
+}
+
+// }}}
+
+?>
diff --git a/program/lib/Crypt/GPG/UserId.php b/program/lib/Crypt/GPG/UserId.php
new file mode 100644
index 000000000..04435708c
--- /dev/null
+++ b/program/lib/Crypt/GPG/UserId.php
@@ -0,0 +1,373 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Contains a data class representing a GPG user id
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2008-2010 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @version CVS: $Id: UserId.php 295621 2010-03-01 04:18:54Z gauthierm $
+ * @link http://pear.php.net/package/Crypt_GPG
+ */
+
+// {{{ class Crypt_GPG_UserId
+
+/**
+ * A class for GPG user id information
+ *
+ * This class is used to store the results of the {@link Crypt_GPG::getKeys()}
+ * method. User id objects are members of a {@link Crypt_GPG_Key} object.
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2008-2010 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ * @see Crypt_GPG::getKeys()
+ * @see Crypt_GPG_Key::getUserIds()
+ */
+class Crypt_GPG_UserId
+{
+ // {{{ class properties
+
+ /**
+ * The name field of this user id
+ *
+ * @var string
+ */
+ private $_name = '';
+
+ /**
+ * The comment field of this user id
+ *
+ * @var string
+ */
+ private $_comment = '';
+
+ /**
+ * The email field of this user id
+ *
+ * @var string
+ */
+ private $_email = '';
+
+ /**
+ * Whether or not this user id is revoked
+ *
+ * @var boolean
+ */
+ private $_isRevoked = false;
+
+ /**
+ * Whether or not this user id is valid
+ *
+ * @var boolean
+ */
+ private $_isValid = true;
+
+ // }}}
+ // {{{ __construct()
+
+ /**
+ * Creates a new user id
+ *
+ * User ids can be initialized from an array of named values. Available
+ * names are:
+ *
+ * - <kbd>string name</kbd> - the name field of the user id.
+ * - <kbd>string comment</kbd> - the comment field of the user id.
+ * - <kbd>string email</kbd> - the email field of the user id.
+ * - <kbd>boolean valid</kbd> - whether or not the user id is valid.
+ * - <kbd>boolean revoked</kbd> - whether or not the user id is revoked.
+ *
+ * @param Crypt_GPG_UserId|string|array $userId optional. Either an
+ * existing user id object, which is copied; a user id string, which
+ * is parsed; or an array of initial values.
+ */
+ public function __construct($userId = null)
+ {
+ // parse from string
+ if (is_string($userId)) {
+ $userId = self::parse($userId);
+ }
+
+ // copy from object
+ if ($userId instanceof Crypt_GPG_UserId) {
+ $this->_name = $userId->_name;
+ $this->_comment = $userId->_comment;
+ $this->_email = $userId->_email;
+ $this->_isRevoked = $userId->_isRevoked;
+ $this->_isValid = $userId->_isValid;
+ }
+
+ // initialize from array
+ if (is_array($userId)) {
+ if (array_key_exists('name', $userId)) {
+ $this->setName($userId['name']);
+ }
+
+ if (array_key_exists('comment', $userId)) {
+ $this->setComment($userId['comment']);
+ }
+
+ if (array_key_exists('email', $userId)) {
+ $this->setEmail($userId['email']);
+ }
+
+ if (array_key_exists('revoked', $userId)) {
+ $this->setRevoked($userId['revoked']);
+ }
+
+ if (array_key_exists('valid', $userId)) {
+ $this->setValid($userId['valid']);
+ }
+ }
+ }
+
+ // }}}
+ // {{{ getName()
+
+ /**
+ * Gets the name field of this user id
+ *
+ * @return string the name field of this user id.
+ */
+ public function getName()
+ {
+ return $this->_name;
+ }
+
+ // }}}
+ // {{{ getComment()
+
+ /**
+ * Gets the comments field of this user id
+ *
+ * @return string the comments field of this user id.
+ */
+ public function getComment()
+ {
+ return $this->_comment;
+ }
+
+ // }}}
+ // {{{ getEmail()
+
+ /**
+ * Gets the email field of this user id
+ *
+ * @return string the email field of this user id.
+ */
+ public function getEmail()
+ {
+ return $this->_email;
+ }
+
+ // }}}
+ // {{{ isRevoked()
+
+ /**
+ * Gets whether or not this user id is revoked
+ *
+ * @return boolean true if this user id is revoked and false if it is not.
+ */
+ public function isRevoked()
+ {
+ return $this->_isRevoked;
+ }
+
+ // }}}
+ // {{{ isValid()
+
+ /**
+ * Gets whether or not this user id is valid
+ *
+ * @return boolean true if this user id is valid and false if it is not.
+ */
+ public function isValid()
+ {
+ return $this->_isValid;
+ }
+
+ // }}}
+ // {{{ __toString()
+
+ /**
+ * Gets a string representation of this user id
+ *
+ * The string is formatted as:
+ * <b><kbd>name (comment) <email-address></kbd></b>.
+ *
+ * @return string a string representation of this user id.
+ */
+ public function __toString()
+ {
+ $components = array();
+
+ if (strlen($this->_name) > 0) {
+ $components[] = $this->_name;
+ }
+
+ if (strlen($this->_comment) > 0) {
+ $components[] = '(' . $this->_comment . ')';
+ }
+
+ if (strlen($this->_email) > 0) {
+ $components[] = '<' . $this->_email. '>';
+ }
+
+ return implode(' ', $components);
+ }
+
+ // }}}
+ // {{{ setName()
+
+ /**
+ * Sets the name field of this user id
+ *
+ * @param string $name the name field of this user id.
+ *
+ * @return Crypt_GPG_UserId the current object, for fluent interface.
+ */
+ public function setName($name)
+ {
+ $this->_name = strval($name);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setComment()
+
+ /**
+ * Sets the comment field of this user id
+ *
+ * @param string $comment the comment field of this user id.
+ *
+ * @return Crypt_GPG_UserId the current object, for fluent interface.
+ */
+ public function setComment($comment)
+ {
+ $this->_comment = strval($comment);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setEmail()
+
+ /**
+ * Sets the email field of this user id
+ *
+ * @param string $email the email field of this user id.
+ *
+ * @return Crypt_GPG_UserId the current object, for fluent interface.
+ */
+ public function setEmail($email)
+ {
+ $this->_email = strval($email);
+ return $this;
+ }
+
+ // }}}
+ // {{{ setRevoked()
+
+ /**
+ * Sets whether or not this user id is revoked
+ *
+ * @param boolean $isRevoked whether or not this user id is revoked.
+ *
+ * @return Crypt_GPG_UserId the current object, for fluent interface.
+ */
+ public function setRevoked($isRevoked)
+ {
+ $this->_isRevoked = ($isRevoked) ? true : false;
+ return $this;
+ }
+
+ // }}}
+ // {{{ setValid()
+
+ /**
+ * Sets whether or not this user id is valid
+ *
+ * @param boolean $isValid whether or not this user id is valid.
+ *
+ * @return Crypt_GPG_UserId the current object, for fluent interface.
+ */
+ public function setValid($isValid)
+ {
+ $this->_isValid = ($isValid) ? true : false;
+ return $this;
+ }
+
+ // }}}
+ // {{{ parse()
+
+ /**
+ * Parses a user id object from a user id string
+ *
+ * A user id string is of the form:
+ * <b><kbd>name (comment) <email-address></kbd></b> with the <i>comment</i>
+ * and <i>email-address</i> fields being optional.
+ *
+ * @param string $string the user id string to parse.
+ *
+ * @return Crypt_GPG_UserId the user id object parsed from the string.
+ */
+ public static function parse($string)
+ {
+ $userId = new Crypt_GPG_UserId();
+ $email = '';
+ $comment = '';
+
+ // get email address from end of string if it exists
+ $matches = array();
+ if (preg_match('/^(.+?) <([^>]+)>$/', $string, $matches) === 1) {
+ $string = $matches[1];
+ $email = $matches[2];
+ }
+
+ // get comment from end of string if it exists
+ $matches = array();
+ if (preg_match('/^(.+?) \(([^\)]+)\)$/', $string, $matches) === 1) {
+ $string = $matches[1];
+ $comment = $matches[2];
+ }
+
+ $name = $string;
+
+ $userId->setName($name);
+ $userId->setComment($comment);
+ $userId->setEmail($email);
+
+ return $userId;
+ }
+
+ // }}}
+}
+
+// }}}
+
+?>
diff --git a/program/lib/Crypt/GPG/VerifyStatusHandler.php b/program/lib/Crypt/GPG/VerifyStatusHandler.php
new file mode 100644
index 000000000..083bd3012
--- /dev/null
+++ b/program/lib/Crypt/GPG/VerifyStatusHandler.php
@@ -0,0 +1,216 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Crypt_GPG is a package to use GPG from PHP
+ *
+ * This file contains an object that handles GPG's status output for the verify
+ * operation.
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * This library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of the
+ * License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2008 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @version CVS: $Id: VerifyStatusHandler.php 302908 2010-08-31 03:56:54Z gauthierm $
+ * @link http://pear.php.net/package/Crypt_GPG
+ * @link http://www.gnupg.org/
+ */
+
+/**
+ * Signature object class definition
+ */
+require_once 'Crypt/GPG/Signature.php';
+
+/**
+ * Status line handler for the verify operation
+ *
+ * This class is used internally by Crypt_GPG and does not need be used
+ * directly. See the {@link Crypt_GPG} class for end-user API.
+ *
+ * This class is responsible for building signature objects that are returned
+ * by the {@link Crypt_GPG::verify()} method. See <b>doc/DETAILS</b> in the
+ * {@link http://www.gnupg.org/download/ GPG distribution} for detailed
+ * information on GPG's status output for the verify operation.
+ *
+ * @category Encryption
+ * @package Crypt_GPG
+ * @author Michael Gauthier <mike@silverorange.com>
+ * @copyright 2008 silverorange
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
+ * @link http://pear.php.net/package/Crypt_GPG
+ * @link http://www.gnupg.org/
+ */
+class Crypt_GPG_VerifyStatusHandler
+{
+ // {{{ protected properties
+
+ /**
+ * The current signature id
+ *
+ * Ths signature id is emitted by GPG before the new signature line so we
+ * must remember it temporarily.
+ *
+ * @var string
+ */
+ protected $signatureId = '';
+
+ /**
+ * List of parsed {@link Crypt_GPG_Signature} objects
+ *
+ * @var array
+ */
+ protected $signatures = array();
+
+ /**
+ * Array index of the current signature
+ *
+ * @var integer
+ */
+ protected $index = -1;
+
+ // }}}
+ // {{{ handle()
+
+ /**
+ * Handles a status line
+ *
+ * @param string $line the status line to handle.
+ *
+ * @return void
+ */
+ public function handle($line)
+ {
+ $tokens = explode(' ', $line);
+ switch ($tokens[0]) {
+ case 'GOODSIG':
+ case 'EXPSIG':
+ case 'EXPKEYSIG':
+ case 'REVKEYSIG':
+ case 'BADSIG':
+ $signature = new Crypt_GPG_Signature();
+
+ // if there was a signature id, set it on the new signature
+ if ($this->signatureId != '') {
+ $signature->setId($this->signatureId);
+ $this->signatureId = '';
+ }
+
+ // Detect whether fingerprint or key id was returned and set
+ // signature values appropriately. Key ids are strings of either
+ // 16 or 8 hexadecimal characters. Fingerprints are strings of 40
+ // hexadecimal characters. The key id is the last 16 characters of
+ // the key fingerprint.
+ if (strlen($tokens[1]) > 16) {
+ $signature->setKeyFingerprint($tokens[1]);
+ $signature->setKeyId(substr($tokens[1], -16));
+ } else {
+ $signature->setKeyId($tokens[1]);
+ }
+
+ // get user id string
+ $string = implode(' ', array_splice($tokens, 2));
+ $string = rawurldecode($string);
+
+ $signature->setUserId(Crypt_GPG_UserId::parse($string));
+
+ $this->index++;
+ $this->signatures[$this->index] = $signature;
+ break;
+
+ case 'ERRSIG':
+ $signature = new Crypt_GPG_Signature();
+
+ // if there was a signature id, set it on the new signature
+ if ($this->signatureId != '') {
+ $signature->setId($this->signatureId);
+ $this->signatureId = '';
+ }
+
+ // Detect whether fingerprint or key id was returned and set
+ // signature values appropriately. Key ids are strings of either
+ // 16 or 8 hexadecimal characters. Fingerprints are strings of 40
+ // hexadecimal characters. The key id is the last 16 characters of
+ // the key fingerprint.
+ if (strlen($tokens[1]) > 16) {
+ $signature->setKeyFingerprint($tokens[1]);
+ $signature->setKeyId(substr($tokens[1], -16));
+ } else {
+ $signature->setKeyId($tokens[1]);
+ }
+
+ $this->index++;
+ $this->signatures[$this->index] = $signature;
+
+ break;
+
+ case 'VALIDSIG':
+ if (!array_key_exists($this->index, $this->signatures)) {
+ break;
+ }
+
+ $signature = $this->signatures[$this->index];
+
+ $signature->setValid(true);
+ $signature->setKeyFingerprint($tokens[1]);
+
+ if (strpos($tokens[3], 'T') === false) {
+ $signature->setCreationDate($tokens[3]);
+ } else {
+ $signature->setCreationDate(strtotime($tokens[3]));
+ }
+
+ if (array_key_exists(4, $tokens)) {
+ if (strpos($tokens[4], 'T') === false) {
+ $signature->setExpirationDate($tokens[4]);
+ } else {
+ $signature->setExpirationDate(strtotime($tokens[4]));
+ }
+ }
+
+ break;
+
+ case 'SIG_ID':
+ // note: signature id comes before new signature line and may not
+ // exist for some signature types
+ $this->signatureId = $tokens[1];
+ break;
+ }
+ }
+
+ // }}}
+ // {{{ getSignatures()
+
+ /**
+ * Gets the {@link Crypt_GPG_Signature} objects parsed by this handler
+ *
+ * @return array the signature objects parsed by this handler.
+ */
+ public function getSignatures()
+ {
+ return $this->signatures;
+ }
+
+ // }}}
+}
+
+?>
diff --git a/program/lib/Net/Sieve.php b/program/lib/Net/Sieve.php
new file mode 100644
index 000000000..8ebdf0958
--- /dev/null
+++ b/program/lib/Net/Sieve.php
@@ -0,0 +1,1274 @@
+<?php
+/**
+ * This file contains the Net_Sieve class.
+ *
+ * PHP version 4
+ *
+ * +-----------------------------------------------------------------------+
+ * | All rights reserved. |
+ * | |
+ * | Redistribution and use in source and binary forms, with or without |
+ * | modification, are permitted provided that the following conditions |
+ * | are met: |
+ * | |
+ * | o Redistributions of source code must retain the above copyright |
+ * | notice, this list of conditions and the following disclaimer. |
+ * | o Redistributions in binary form must reproduce the above copyright |
+ * | notice, this list of conditions and the following disclaimer in the |
+ * | documentation and/or other materials provided with the distribution.|
+ * | |
+ * | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
+ * | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
+ * | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
+ * | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
+ * | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
+ * | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
+ * | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+ * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+ * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
+ * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
+ * | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+ * +-----------------------------------------------------------------------+
+ *
+ * @category Networking
+ * @package Net_Sieve
+ * @author Richard Heyes <richard@phpguru.org>
+ * @author Damian Fernandez Sosa <damlists@cnba.uba.ar>
+ * @author Anish Mistry <amistry@am-productions.biz>
+ * @author Jan Schneider <jan@horde.org>
+ * @copyright 2002-2003 Richard Heyes
+ * @copyright 2006-2008 Anish Mistry
+ * @license http://www.opensource.org/licenses/bsd-license.php BSD
+ * @version SVN: $Id$
+ * @link http://pear.php.net/package/Net_Sieve
+ */
+
+require_once 'PEAR.php';
+require_once 'Net/Socket.php';
+
+/**
+ * TODO
+ *
+ * o supportsAuthMech()
+ */
+
+/**
+ * Disconnected state
+ * @const NET_SIEVE_STATE_DISCONNECTED
+ */
+define('NET_SIEVE_STATE_DISCONNECTED', 1, true);
+
+/**
+ * Authorisation state
+ * @const NET_SIEVE_STATE_AUTHORISATION
+ */
+define('NET_SIEVE_STATE_AUTHORISATION', 2, true);
+
+/**
+ * Transaction state
+ * @const NET_SIEVE_STATE_TRANSACTION
+ */
+define('NET_SIEVE_STATE_TRANSACTION', 3, true);
+
+
+/**
+ * A class for talking to the timsieved server which comes with Cyrus IMAP.
+ *
+ * @category Networking
+ * @package Net_Sieve
+ * @author Richard Heyes <richard@phpguru.org>
+ * @author Damian Fernandez Sosa <damlists@cnba.uba.ar>
+ * @author Anish Mistry <amistry@am-productions.biz>
+ * @author Jan Schneider <jan@horde.org>
+ * @copyright 2002-2003 Richard Heyes
+ * @copyright 2006-2008 Anish Mistry
+ * @license http://www.opensource.org/licenses/bsd-license.php BSD
+ * @version Release: 1.3.2
+ * @link http://pear.php.net/package/Net_Sieve
+ * @link http://tools.ietf.org/html/rfc5228 RFC 5228 (Sieve: An Email
+ * Filtering Language)
+ * @link http://tools.ietf.org/html/rfc5804 RFC 5804 A Protocol for
+ * Remotely Managing Sieve Scripts
+ */
+class Net_Sieve
+{
+ /**
+ * The authentication methods this class supports.
+ *
+ * Can be overwritten if having problems with certain methods.
+ *
+ * @var array
+ */
+ var $supportedAuthMethods = array('DIGEST-MD5', 'CRAM-MD5', 'EXTERNAL',
+ 'PLAIN' , 'LOGIN');
+
+ /**
+ * SASL authentication methods that require Auth_SASL.
+ *
+ * @var array
+ */
+ var $supportedSASLAuthMethods = array('DIGEST-MD5', 'CRAM-MD5');
+
+ /**
+ * The socket handle.
+ *
+ * @var resource
+ */
+ var $_sock;
+
+ /**
+ * Parameters and connection information.
+ *
+ * @var array
+ */
+ var $_data;
+
+ /**
+ * Current state of the connection.
+ *
+ * One of the NET_SIEVE_STATE_* constants.
+ *
+ * @var integer
+ */
+ var $_state;
+
+ /**
+ * Constructor error.
+ *
+ * @var PEAR_Error
+ */
+ var $_error;
+
+ /**
+ * Whether to enable debugging.
+ *
+ * @var boolean
+ */
+ var $_debug = false;
+
+ /**
+ * Debug output handler.
+ *
+ * This has to be a valid callback.
+ *
+ * @var string|array
+ */
+ var $_debug_handler = null;
+
+ /**
+ * Whether to pick up an already established connection.
+ *
+ * @var boolean
+ */
+ var $_bypassAuth = false;
+
+ /**
+ * Whether to use TLS if available.
+ *
+ * @var boolean
+ */
+ var $_useTLS = true;
+
+ /**
+ * Additional options for stream_context_create().
+ *
+ * @var array
+ */
+ var $_options = null;
+
+ /**
+ * Maximum number of referral loops
+ *
+ * @var array
+ */
+ var $_maxReferralCount = 15;
+
+ /**
+ * Constructor.
+ *
+ * Sets up the object, connects to the server and logs in. Stores any
+ * generated error in $this->_error, which can be retrieved using the
+ * getError() method.
+ *
+ * @param string $user Login username.
+ * @param string $pass Login password.
+ * @param string $host Hostname of server.
+ * @param string $port Port of server.
+ * @param string $logintype Type of login to perform (see
+ * $supportedAuthMethods).
+ * @param string $euser Effective user. If authenticating as an
+ * administrator, login as this user.
+ * @param boolean $debug Whether to enable debugging (@see setDebug()).
+ * @param string $bypassAuth Skip the authentication phase. Useful if the
+ * socket is already open.
+ * @param boolean $useTLS Use TLS if available.
+ * @param array $options Additional options for
+ * stream_context_create().
+ * @param mixed $handler A callback handler for the debug output.
+ */
+ function Net_Sieve($user = null, $pass = null, $host = 'localhost',
+ $port = 2000, $logintype = '', $euser = '',
+ $debug = false, $bypassAuth = false, $useTLS = true,
+ $options = null, $handler = null)
+ {
+ $this->_state = NET_SIEVE_STATE_DISCONNECTED;
+ $this->_data['user'] = $user;
+ $this->_data['pass'] = $pass;
+ $this->_data['host'] = $host;
+ $this->_data['port'] = $port;
+ $this->_data['logintype'] = $logintype;
+ $this->_data['euser'] = $euser;
+ $this->_sock = new Net_Socket();
+ $this->_bypassAuth = $bypassAuth;
+ $this->_useTLS = $useTLS;
+ $this->_options = $options;
+ $this->setDebug($debug, $handler);
+
+ /* Try to include the Auth_SASL package. If the package is not
+ * available, we disable the authentication methods that depend upon
+ * it. */
+ if ((@include_once 'Auth/SASL.php') === false) {
+ $this->_debug('Auth_SASL not present');
+ foreach ($this->supportedSASLAuthMethods as $SASLMethod) {
+ $pos = array_search($SASLMethod, $this->supportedAuthMethods);
+ $this->_debug('Disabling method ' . $SASLMethod);
+ unset($this->supportedAuthMethods[$pos]);
+ }
+ }
+
+ if (strlen($user) && strlen($pass)) {
+ $this->_error = $this->_handleConnectAndLogin();
+ }
+ }
+
+ /**
+ * Returns any error that may have been generated in the constructor.
+ *
+ * @return boolean|PEAR_Error False if no error, PEAR_Error otherwise.
+ */
+ function getError()
+ {
+ return PEAR::isError($this->_error) ? $this->_error : false;
+ }
+
+ /**
+ * Sets the debug state and handler function.
+ *
+ * @param boolean $debug Whether to enable debugging.
+ * @param string $handler A custom debug handler. Must be a valid callback.
+ *
+ * @return void
+ */
+ function setDebug($debug = true, $handler = null)
+ {
+ $this->_debug = $debug;
+ $this->_debug_handler = $handler;
+ }
+
+ /**
+ * Connects to the server and logs in.
+ *
+ * @return boolean True on success, PEAR_Error on failure.
+ */
+ function _handleConnectAndLogin()
+ {
+ if (PEAR::isError($res = $this->connect($this->_data['host'], $this->_data['port'], $this->_options, $this->_useTLS))) {
+ return $res;
+ }
+ if ($this->_bypassAuth === false) {
+ if (PEAR::isError($res = $this->login($this->_data['user'], $this->_data['pass'], $this->_data['logintype'], $this->_data['euser'], $this->_bypassAuth))) {
+ return $res;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Handles connecting to the server and checks the response validity.
+ *
+ * @param string $host Hostname of server.
+ * @param string $port Port of server.
+ * @param array $options List of options to pass to
+ * stream_context_create().
+ * @param boolean $useTLS Use TLS if available.
+ *
+ * @return boolean True on success, PEAR_Error otherwise.
+ */
+ function connect($host, $port, $options = null, $useTLS = true)
+ {
+ $this->_data['host'] = $host;
+ $this->_data['port'] = $port;
+ $this->_useTLS = $useTLS;
+ if (is_array($options)) {
+ $this->_options = array_merge($this->_options, $options);
+ }
+
+ if (NET_SIEVE_STATE_DISCONNECTED != $this->_state) {
+ return PEAR::raiseError('Not currently in DISCONNECTED state', 1);
+ }
+
+ if (PEAR::isError($res = $this->_sock->connect($host, $port, false, 5, $options))) {
+ return $res;
+ }
+
+ if ($this->_bypassAuth) {
+ $this->_state = NET_SIEVE_STATE_TRANSACTION;
+ } else {
+ $this->_state = NET_SIEVE_STATE_AUTHORISATION;
+ if (PEAR::isError($res = $this->_doCmd())) {
+ return $res;
+ }
+ }
+
+ // Explicitly ask for the capabilities in case the connection is
+ // picked up from an existing connection.
+ if (PEAR::isError($res = $this->_cmdCapability())) {
+ return PEAR::raiseError(
+ 'Failed to connect, server said: ' . $res->getMessage(), 2
+ );
+ }
+
+ // Check if we can enable TLS via STARTTLS.
+ if ($useTLS && !empty($this->_capability['starttls'])
+ && function_exists('stream_socket_enable_crypto')
+ ) {
+ if (PEAR::isError($res = $this->_startTLS())) {
+ return $res;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Disconnect from the Sieve server.
+ *
+ * @param boolean $sendLogoutCMD Whether to send LOGOUT command before
+ * disconnecting.
+ *
+ * @return boolean True on success, PEAR_Error otherwise.
+ */
+ function disconnect($sendLogoutCMD = true)
+ {
+ return $this->_cmdLogout($sendLogoutCMD);
+ }
+
+ /**
+ * Logs into server.
+ *
+ * @param string $user Login username.
+ * @param string $pass Login password.
+ * @param string $logintype Type of login method to use.
+ * @param string $euser Effective UID (perform on behalf of $euser).
+ * @param boolean $bypassAuth Do not perform authentication.
+ *
+ * @return boolean True on success, PEAR_Error otherwise.
+ */
+ function login($user, $pass, $logintype = null, $euser = '', $bypassAuth = false)
+ {
+ $this->_data['user'] = $user;
+ $this->_data['pass'] = $pass;
+ $this->_data['logintype'] = $logintype;
+ $this->_data['euser'] = $euser;
+ $this->_bypassAuth = $bypassAuth;
+
+ if (NET_SIEVE_STATE_AUTHORISATION != $this->_state) {
+ return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
+ }
+
+ if (!$bypassAuth ) {
+ if (PEAR::isError($res = $this->_cmdAuthenticate($user, $pass, $logintype, $euser))) {
+ return $res;
+ }
+ }
+ $this->_state = NET_SIEVE_STATE_TRANSACTION;
+
+ return true;
+ }
+
+ /**
+ * Returns an indexed array of scripts currently on the server.
+ *
+ * @return array Indexed array of scriptnames.
+ */
+ function listScripts()
+ {
+ if (is_array($scripts = $this->_cmdListScripts())) {
+ $this->_active = $scripts[1];
+ return $scripts[0];
+ } else {
+ return $scripts;
+ }
+ }
+
+ /**
+ * Returns the active script.
+ *
+ * @return string The active scriptname.
+ */
+ function getActive()
+ {
+ if (!empty($this->_active)) {
+ return $this->_active;
+ }
+ if (is_array($scripts = $this->_cmdListScripts())) {
+ $this->_active = $scripts[1];
+ return $scripts[1];
+ }
+ }
+
+ /**
+ * Sets the active script.
+ *
+ * @param string $scriptname The name of the script to be set as active.
+ *
+ * @return boolean True on success, PEAR_Error on failure.
+ */
+ function setActive($scriptname)
+ {
+ return $this->_cmdSetActive($scriptname);
+ }
+
+ /**
+ * Retrieves a script.
+ *
+ * @param string $scriptname The name of the script to be retrieved.
+ *
+ * @return string The script on success, PEAR_Error on failure.
+ */
+ function getScript($scriptname)
+ {
+ return $this->_cmdGetScript($scriptname);
+ }
+
+ /**
+ * Adds a script to the server.
+ *
+ * @param string $scriptname Name of the script.
+ * @param string $script The script content.
+ * @param boolean $makeactive Whether to make this the active script.
+ *
+ * @return boolean True on success, PEAR_Error on failure.
+ */
+ function installScript($scriptname, $script, $makeactive = false)
+ {
+ if (PEAR::isError($res = $this->_cmdPutScript($scriptname, $script))) {
+ return $res;
+ }
+ if ($makeactive) {
+ return $this->_cmdSetActive($scriptname);
+ }
+ return true;
+ }
+
+ /**
+ * Removes a script from the server.
+ *
+ * @param string $scriptname Name of the script.
+ *
+ * @return boolean True on success, PEAR_Error on failure.
+ */
+ function removeScript($scriptname)
+ {
+ return $this->_cmdDeleteScript($scriptname);
+ }
+
+ /**
+ * Checks if the server has space to store the script by the server.
+ *
+ * @param string $scriptname The name of the script to mark as active.
+ * @param integer $size The size of the script.
+ *
+ * @return boolean|PEAR_Error True if there is space, PEAR_Error otherwise.
+ *
+ * @todo Rename to hasSpace()
+ */
+ function haveSpace($scriptname, $size)
+ {
+ if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
+ return PEAR::raiseError('Not currently in TRANSACTION state', 1);
+ }
+
+ if (PEAR::isError($res = $this->_doCmd(sprintf('HAVESPACE %s %d', $this->_escape($scriptname), $size)))) {
+ return $res;
+ }
+ return true;
+ }
+
+ /**
+ * Returns the list of extensions the server supports.
+ *
+ * @return array List of extensions or PEAR_Error on failure.
+ */
+ function getExtensions()
+ {
+ if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
+ return PEAR::raiseError('Not currently connected', 7);
+ }
+ return $this->_capability['extensions'];
+ }
+
+ /**
+ * Returns whether the server supports an extension.
+ *
+ * @param string $extension The extension to check.
+ *
+ * @return boolean Whether the extension is supported or PEAR_Error on
+ * failure.
+ */
+ function hasExtension($extension)
+ {
+ if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
+ return PEAR::raiseError('Not currently connected', 7);
+ }
+
+ $extension = trim($this->_toUpper($extension));
+ if (is_array($this->_capability['extensions'])) {
+ foreach ($this->_capability['extensions'] as $ext) {
+ if ($ext == $extension) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the list of authentication methods the server supports.
+ *
+ * @return array List of authentication methods or PEAR_Error on failure.
+ */
+ function getAuthMechs()
+ {
+ if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
+ return PEAR::raiseError('Not currently connected', 7);
+ }
+ return $this->_capability['sasl'];
+ }
+
+ /**
+ * Returns whether the server supports an authentication method.
+ *
+ * @param string $method The method to check.
+ *
+ * @return boolean Whether the method is supported or PEAR_Error on
+ * failure.
+ */
+ function hasAuthMech($method)
+ {
+ if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
+ return PEAR::raiseError('Not currently connected', 7);
+ }
+
+ $method = trim($this->_toUpper($method));
+ if (is_array($this->_capability['sasl'])) {
+ foreach ($this->_capability['sasl'] as $sasl) {
+ if ($sasl == $method) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Handles the authentication using any known method.
+ *
+ * @param string $uid The userid to authenticate as.
+ * @param string $pwd The password to authenticate with.
+ * @param string $userMethod The method to use. If empty, the class chooses
+ * the best (strongest) available method.
+ * @param string $euser The effective uid to authenticate as.
+ *
+ * @return void
+ */
+ function _cmdAuthenticate($uid, $pwd, $userMethod = null, $euser = '')
+ {
+ if (PEAR::isError($method = $this->_getBestAuthMethod($userMethod))) {
+ return $method;
+ }
+ switch ($method) {
+ case 'DIGEST-MD5':
+ return $this->_authDigestMD5($uid, $pwd, $euser);
+ case 'CRAM-MD5':
+ $result = $this->_authCRAMMD5($uid, $pwd, $euser);
+ break;
+ case 'LOGIN':
+ $result = $this->_authLOGIN($uid, $pwd, $euser);
+ break;
+ case 'PLAIN':
+ $result = $this->_authPLAIN($uid, $pwd, $euser);
+ break;
+ case 'EXTERNAL':
+ $result = $this->_authEXTERNAL($uid, $pwd, $euser);
+ break;
+ default :
+ $result = PEAR::raiseError(
+ $method . ' is not a supported authentication method'
+ );
+ break;
+ }
+
+ if (PEAR::isError($res = $this->_doCmd())) {
+ return $res;
+ }
+
+ // Query the server capabilities again now that we are authenticated.
+ if (PEAR::isError($res = $this->_cmdCapability())) {
+ return PEAR::raiseError(
+ 'Failed to connect, server said: ' . $res->getMessage(), 2
+ );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Authenticates the user using the PLAIN method.
+ *
+ * @param string $user The userid to authenticate as.
+ * @param string $pass The password to authenticate with.
+ * @param string $euser The effective uid to authenticate as.
+ *
+ * @return void
+ */
+ function _authPLAIN($user, $pass, $euser)
+ {
+ return $this->_sendCmd(
+ sprintf(
+ 'AUTHENTICATE "PLAIN" "%s"',
+ base64_encode($euser . chr(0) . $user . chr(0) . $pass)
+ )
+ );
+ }
+
+ /**
+ * Authenticates the user using the LOGIN method.
+ *
+ * @param string $user The userid to authenticate as.
+ * @param string $pass The password to authenticate with.
+ * @param string $euser The effective uid to authenticate as.
+ *
+ * @return void
+ */
+ function _authLOGIN($user, $pass, $euser)
+ {
+ if (PEAR::isError($result = $this->_sendCmd('AUTHENTICATE "LOGIN"'))) {
+ return $result;
+ }
+ if (PEAR::isError($result = $this->_doCmd('"' . base64_encode($user) . '"', true))) {
+ return $result;
+ }
+ return $this->_doCmd('"' . base64_encode($pass) . '"', true);
+ }
+
+ /**
+ * Authenticates the user using the CRAM-MD5 method.
+ *
+ * @param string $user The userid to authenticate as.
+ * @param string $pass The password to authenticate with.
+ * @param string $euser The effective uid to authenticate as.
+ *
+ * @return void
+ */
+ function _authCRAMMD5($user, $pass, $euser)
+ {
+ if (PEAR::isError($challenge = $this->_doCmd('AUTHENTICATE "CRAM-MD5"', true))) {
+ return $challenge;
+ }
+
+ $challenge = base64_decode(trim($challenge));
+ $cram = Auth_SASL::factory('crammd5');
+ if (PEAR::isError($response = $cram->getResponse($user, $pass, $challenge))) {
+ return $response;
+ }
+
+ return $this->_sendStringResponse(base64_encode($response));
+ }
+
+ /**
+ * Authenticates the user using the DIGEST-MD5 method.
+ *
+ * @param string $user The userid to authenticate as.
+ * @param string $pass The password to authenticate with.
+ * @param string $euser The effective uid to authenticate as.
+ *
+ * @return void
+ */
+ function _authDigestMD5($user, $pass, $euser)
+ {
+ if (PEAR::isError($challenge = $this->_doCmd('AUTHENTICATE "DIGEST-MD5"', true))) {
+ return $challenge;
+ }
+
+ $challenge = base64_decode(trim($challenge));
+ $digest = Auth_SASL::factory('digestmd5');
+ // @todo Really 'localhost'?
+ if (PEAR::isError($response = $digest->getResponse($user, $pass, $challenge, 'localhost', 'sieve', $euser))) {
+ return $response;
+ }
+
+ if (PEAR::isError($result = $this->_sendStringResponse(base64_encode($response)))) {
+ return $result;
+ }
+ if (PEAR::isError($result = $this->_doCmd('', true))) {
+ return $result;
+ }
+ if ($this->_toUpper(substr($result, 0, 2)) == 'OK') {
+ return;
+ }
+
+ /* We don't use the protocol's third step because SIEVE doesn't allow
+ * subsequent authentication, so we just silently ignore it. */
+ if (PEAR::isError($result = $this->_sendStringResponse(''))) {
+ return $result;
+ }
+
+ return $this->_doCmd();
+ }
+
+ /**
+ * Authenticates the user using the EXTERNAL method.
+ *
+ * @param string $user The userid to authenticate as.
+ * @param string $pass The password to authenticate with.
+ * @param string $euser The effective uid to authenticate as.
+ *
+ * @return void
+ *
+ * @since 1.1.7
+ */
+ function _authEXTERNAL($user, $pass, $euser)
+ {
+ $cmd = sprintf(
+ 'AUTHENTICATE "EXTERNAL" "%s"',
+ base64_encode(strlen($euser) ? $euser : $user)
+ );
+ return $this->_sendCmd($cmd);
+ }
+
+ /**
+ * Removes a script from the server.
+ *
+ * @param string $scriptname Name of the script to delete.
+ *
+ * @return boolean True on success, PEAR_Error otherwise.
+ */
+ function _cmdDeleteScript($scriptname)
+ {
+ if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
+ return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
+ }
+
+ if (PEAR::isError($res = $this->_doCmd(sprintf('DELETESCRIPT %s', $this->_escape($scriptname))))) {
+ return $res;
+ }
+ return true;
+ }
+
+ /**
+ * Retrieves the contents of the named script.
+ *
+ * @param string $scriptname Name of the script to retrieve.
+ *
+ * @return string The script if successful, PEAR_Error otherwise.
+ */
+ function _cmdGetScript($scriptname)
+ {
+ if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
+ return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
+ }
+
+ if (PEAR::isError($res = $this->_doCmd(sprintf('GETSCRIPT %s', $this->_escape($scriptname))))) {
+ return $res;
+ }
+
+ return preg_replace('/^{[0-9]+}\r\n/', '', $res);
+ }
+
+ /**
+ * Sets the active script, i.e. the one that gets run on new mail by the
+ * server.
+ *
+ * @param string $scriptname The name of the script to mark as active.
+ *
+ * @return boolean True on success, PEAR_Error otherwise.
+ */
+ function _cmdSetActive($scriptname)
+ {
+ if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
+ return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
+ }
+
+ if (PEAR::isError($res = $this->_doCmd(sprintf('SETACTIVE %s', $this->_escape($scriptname))))) {
+ return $res;
+ }
+
+ $this->_activeScript = $scriptname;
+ return true;
+ }
+
+ /**
+ * Returns the list of scripts on the server.
+ *
+ * @return array An array with the list of scripts in the first element
+ * and the active script in the second element on success,
+ * PEAR_Error otherwise.
+ */
+ function _cmdListScripts()
+ {
+ if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
+ return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
+ }
+
+ if (PEAR::isError($res = $this->_doCmd('LISTSCRIPTS'))) {
+ return $res;
+ }
+
+ $scripts = array();
+ $activescript = null;
+ $res = explode("\r\n", $res);
+ foreach ($res as $value) {
+ if (preg_match('/^"(.*)"( ACTIVE)?$/i', $value, $matches)) {
+ $script_name = stripslashes($matches[1]);
+ $scripts[] = $script_name;
+ if (!empty($matches[2])) {
+ $activescript = $script_name;
+ }
+ }
+ }
+
+ return array($scripts, $activescript);
+ }
+
+ /**
+ * Adds a script to the server.
+ *
+ * @param string $scriptname Name of the new script.
+ * @param string $scriptdata The new script.
+ *
+ * @return boolean True on success, PEAR_Error otherwise.
+ */
+ function _cmdPutScript($scriptname, $scriptdata)
+ {
+ if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
+ return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
+ }
+
+ $stringLength = $this->_getLineLength($scriptdata);
+ $command = sprintf("PUTSCRIPT %s {%d+}\r\n%s",
+ $this->_escape($scriptname),
+ $stringLength,
+ $scriptdata);
+ if (PEAR::isError($res = $this->_doCmd($command))) {
+ return $res;
+ }
+
+ return true;
+ }
+
+ /**
+ * Logs out of the server and terminates the connection.
+ *
+ * @param boolean $sendLogoutCMD Whether to send LOGOUT command before
+ * disconnecting.
+ *
+ * @return boolean True on success, PEAR_Error otherwise.
+ */
+ function _cmdLogout($sendLogoutCMD = true)
+ {
+ if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
+ return PEAR::raiseError('Not currently connected', 1);
+ }
+
+ if ($sendLogoutCMD) {
+ if (PEAR::isError($res = $this->_doCmd('LOGOUT'))) {
+ return $res;
+ }
+ }
+
+ $this->_sock->disconnect();
+ $this->_state = NET_SIEVE_STATE_DISCONNECTED;
+
+ return true;
+ }
+
+ /**
+ * Sends the CAPABILITY command
+ *
+ * @return boolean True on success, PEAR_Error otherwise.
+ */
+ function _cmdCapability()
+ {
+ if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
+ return PEAR::raiseError('Not currently connected', 1);
+ }
+ if (PEAR::isError($res = $this->_doCmd('CAPABILITY'))) {
+ return $res;
+ }
+ $this->_parseCapability($res);
+ return true;
+ }
+
+ /**
+ * Parses the response from the CAPABILITY command and stores the result
+ * in $_capability.
+ *
+ * @param string $data The response from the capability command.
+ *
+ * @return void
+ */
+ function _parseCapability($data)
+ {
+ // Clear the cached capabilities.
+ $this->_capability = array('sasl' => array(),
+ 'extensions' => array());
+
+ $data = preg_split('/\r?\n/', $this->_toUpper($data), -1, PREG_SPLIT_NO_EMPTY);
+
+ for ($i = 0; $i < count($data); $i++) {
+ if (!preg_match('/^"([A-Z]+)"( "(.*)")?$/', $data[$i], $matches)) {
+ continue;
+ }
+ switch ($matches[1]) {
+ case 'IMPLEMENTATION':
+ $this->_capability['implementation'] = $matches[3];
+ break;
+
+ case 'SASL':
+ $this->_capability['sasl'] = preg_split('/\s+/', $matches[3]);
+ break;
+
+ case 'SIEVE':
+ $this->_capability['extensions'] = preg_split('/\s+/', $matches[3]);
+ break;
+
+ case 'STARTTLS':
+ $this->_capability['starttls'] = true;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Sends a command to the server
+ *
+ * @param string $cmd The command to send.
+ *
+ * @return void
+ */
+ function _sendCmd($cmd)
+ {
+ $status = $this->_sock->getStatus();
+ if (PEAR::isError($status) || $status['eof']) {
+ return PEAR::raiseError('Failed to write to socket: connection lost');
+ }
+ if (PEAR::isError($error = $this->_sock->write($cmd . "\r\n"))) {
+ return PEAR::raiseError(
+ 'Failed to write to socket: ' . $error->getMessage()
+ );
+ }
+ $this->_debug("C: $cmd");
+ }
+
+ /**
+ * Sends a string response to the server.
+ *
+ * @param string $str The string to send.
+ *
+ * @return void
+ */
+ function _sendStringResponse($str)
+ {
+ return $this->_sendCmd('{' . $this->_getLineLength($str) . "+}\r\n" . $str);
+ }
+
+ /**
+ * Receives a single line from the server.
+ *
+ * @return string The server response line.
+ */
+ function _recvLn()
+ {
+ if (PEAR::isError($lastline = $this->_sock->gets(8192))) {
+ return PEAR::raiseError(
+ 'Failed to read from socket: ' . $lastline->getMessage()
+ );
+ }
+
+ $lastline = rtrim($lastline);
+ $this->_debug("S: $lastline");
+
+ if ($lastline === '') {
+ return PEAR::raiseError('Failed to read from socket');
+ }
+
+ return $lastline;
+ }
+
+ /**
+ * Receives a number of bytes from the server.
+ *
+ * @param integer $length Number of bytes to read.
+ *
+ * @return string The server response.
+ */
+ function _recvBytes($length)
+ {
+ $response = '';
+ $response_length = 0;
+ while ($response_length < $length) {
+ $response .= $this->_sock->read($length - $response_length);
+ $response_length = $this->_getLineLength($response);
+ }
+ $this->_debug('S: ' . rtrim($response));
+ return $response;
+ }
+
+ /**
+ * Send a command and retrieves a response from the server.
+ *
+ * @param string $cmd The command to send.
+ * @param boolean $auth Whether this is an authentication command.
+ *
+ * @return string|PEAR_Error Reponse string if an OK response, PEAR_Error
+ * if a NO response.
+ */
+ function _doCmd($cmd = '', $auth = false)
+ {
+ $referralCount = 0;
+ while ($referralCount < $this->_maxReferralCount) {
+ if (strlen($cmd)) {
+ if (PEAR::isError($error = $this->_sendCmd($cmd))) {
+ return $error;
+ }
+ }
+
+ $response = '';
+ while (true) {
+ if (PEAR::isError($line = $this->_recvLn())) {
+ return $line;
+ }
+ $uc_line = $this->_toUpper($line);
+
+ if ('OK' == substr($uc_line, 0, 2)) {
+ $response .= $line;
+ return rtrim($response);
+ }
+
+ if ('NO' == substr($uc_line, 0, 2)) {
+ // Check for string literal error message.
+ if (preg_match('/{([0-9]+)}$/', $line, $matches)) {
+ $line = substr($line, 0, -(strlen($matches[1])+2))
+ . str_replace(
+ "\r\n", ' ', $this->_recvBytes($matches[1] + 2)
+ );
+ }
+ return PEAR::raiseError(trim($response . substr($line, 2)), 3);
+ }
+
+ if ('BYE' == substr($uc_line, 0, 3)) {
+ if (PEAR::isError($error = $this->disconnect(false))) {
+ return PEAR::raiseError(
+ 'Cannot handle BYE, the error was: '
+ . $error->getMessage(),
+ 4
+ );
+ }
+ // Check for referral, then follow it. Otherwise, carp an
+ // error.
+ if (preg_match('/^bye \(referral "(sieve:\/\/)?([^"]+)/i', $line, $matches)) {
+ // Replace the old host with the referral host
+ // preserving any protocol prefix.
+ $this->_data['host'] = preg_replace(
+ '/\w+(?!(\w|\:\/\/)).*/', $matches[2],
+ $this->_data['host']
+ );
+ if (PEAR::isError($error = $this->_handleConnectAndLogin())) {
+ return PEAR::raiseError(
+ 'Cannot follow referral to '
+ . $this->_data['host'] . ', the error was: '
+ . $error->getMessage(),
+ 5
+ );
+ }
+ break;
+ }
+ return PEAR::raiseError(trim($response . $line), 6);
+ }
+
+ if (preg_match('/^{([0-9]+)}/', $line, $matches)) {
+ // Matches literal string responses.
+ $line = $this->_recvBytes($matches[1] + 2);
+ if (!$auth) {
+ // Receive the pending OK only if we aren't
+ // authenticating since string responses during
+ // authentication don't need an OK.
+ $this->_recvLn();
+ }
+ return $line;
+ }
+
+ if ($auth) {
+ // String responses during authentication don't need an
+ // OK.
+ $response .= $line;
+ return rtrim($response);
+ }
+
+ $response .= $line . "\r\n";
+ $referralCount++;
+ }
+ }
+
+ return PEAR::raiseError('Max referral count (' . $referralCount . ') reached. Cyrus murder loop error?', 7);
+ }
+
+ /**
+ * Returns the name of the best authentication method that the server
+ * has advertised.
+ *
+ * @param string $userMethod Only consider this method as available.
+ *
+ * @return string The name of the best supported authentication method or
+ * a PEAR_Error object on failure.
+ */
+ function _getBestAuthMethod($userMethod = null)
+ {
+ if (!isset($this->_capability['sasl'])) {
+ return PEAR::raiseError('This server doesn\'t support any authentication methods. SASL problem?');
+ }
+ if (!$this->_capability['sasl']) {
+ return PEAR::raiseError('This server doesn\'t support any authentication methods.');
+ }
+
+ if ($userMethod) {
+ if (in_array($userMethod, $this->_capability['sasl'])) {
+ return $userMethod;
+ }
+ return PEAR::raiseError(
+ sprintf('No supported authentication method found. The server supports these methods: %s, but we want to use: %s',
+ implode(', ', $this->_capability['sasl']),
+ $userMethod));
+ }
+
+ foreach ($this->supportedAuthMethods as $method) {
+ if (in_array($method, $this->_capability['sasl'])) {
+ return $method;
+ }
+ }
+
+ return PEAR::raiseError(
+ sprintf('No supported authentication method found. The server supports these methods: %s, but we only support: %s',
+ implode(', ', $this->_capability['sasl']),
+ implode(', ', $this->supportedAuthMethods)));
+ }
+
+ /**
+ * Starts a TLS connection.
+ *
+ * @return boolean True on success, PEAR_Error on failure.
+ */
+ function _startTLS()
+ {
+ if (PEAR::isError($res = $this->_doCmd('STARTTLS'))) {
+ return $res;
+ }
+
+ if (!stream_socket_enable_crypto($this->_sock->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
+ return PEAR::raiseError('Failed to establish TLS connection', 2);
+ }
+
+ $this->_debug('STARTTLS negotiation successful');
+
+ // The server should be sending a CAPABILITY response after
+ // negotiating TLS. Read it, and ignore if it doesn't.
+ // Unfortunately old Cyrus versions are broken and don't send a
+ // CAPABILITY response, thus we would wait here forever. Parse the
+ // Cyrus version and work around this broken behavior.
+ if (!preg_match('/^CYRUS TIMSIEVED V([0-9.]+)/', $this->_capability['implementation'], $matches) ||
+ version_compare($matches[1], '2.3.10', '>=')) {
+ $this->_doCmd();
+ }
+
+ // Query the server capabilities again now that we are under
+ // encryption.
+ if (PEAR::isError($res = $this->_cmdCapability())) {
+ return PEAR::raiseError(
+ 'Failed to connect, server said: ' . $res->getMessage(), 2
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the length of a string.
+ *
+ * @param string $string A string.
+ *
+ * @return integer The length of the string.
+ */
+ function _getLineLength($string)
+ {
+ if (extension_loaded('mbstring')) {
+ return mb_strlen($string, 'latin1');
+ } else {
+ return strlen($string);
+ }
+ }
+
+ /**
+ * Locale independant strtoupper() implementation.
+ *
+ * @param string $string The string to convert to lowercase.
+ *
+ * @return string The lowercased string, based on ASCII encoding.
+ */
+ function _toUpper($string)
+ {
+ $language = setlocale(LC_CTYPE, 0);
+ setlocale(LC_CTYPE, 'C');
+ $string = strtoupper($string);
+ setlocale(LC_CTYPE, $language);
+ return $string;
+ }
+
+ /**
+ * Converts strings into RFC's quoted-string or literal-c2s form.
+ *
+ * @param string $string The string to convert.
+ *
+ * @return string Result string.
+ */
+ function _escape($string)
+ {
+ // Some implementations don't allow UTF-8 characters in quoted-string,
+ // use literal-c2s.
+ if (preg_match('/[^\x01-\x09\x0B-\x0C\x0E-\x7F]/', $string)) {
+ return sprintf("{%d+}\r\n%s", $this->_getLineLength($string), $string);
+ }
+
+ return '"' . addcslashes($string, '\\"') . '"';
+ }
+
+ /**
+ * Write debug text to the current debug output handler.
+ *
+ * @param string $message Debug message text.
+ *
+ * @return void
+ */
+ function _debug($message)
+ {
+ if ($this->_debug) {
+ if ($this->_debug_handler) {
+ call_user_func_array($this->_debug_handler, array(&$this, $message));
+ } else {
+ echo "$message\n";
+ }
+ }
+ }
+}
diff --git a/program/lib/Roundcube/bootstrap.php b/program/lib/Roundcube/bootstrap.php
index 6e5143382..c3fac1f4d 100644
--- a/program/lib/Roundcube/bootstrap.php
+++ b/program/lib/Roundcube/bootstrap.php
@@ -54,11 +54,11 @@ foreach ($config as $optname => $optval) {
}
// framework constants
-define('RCUBE_VERSION', '1.0-git');
+define('RCUBE_VERSION', '0.9.5');
define('RCUBE_CHARSET', 'UTF-8');
if (!defined('RCUBE_LIB_DIR')) {
- define('RCUBE_LIB_DIR', dirname(__FILE__).DIRECTORY_SEPARATOR);
+ define('RCUBE_LIB_DIR', dirname(__FILE__).'/');
}
if (!defined('RCUBE_INSTALL_PATH')) {
@@ -303,6 +303,32 @@ function is_ascii($str, $control_chars = true)
/**
+ * Remove single and double quotes from a given string
+ *
+ * @param string Input value
+ *
+ * @return string Dequoted string
+ */
+function strip_quotes($str)
+{
+ return str_replace(array("'", '"'), '', $str);
+}
+
+
+/**
+ * Remove new lines characters from given string
+ *
+ * @param string $str Input value
+ *
+ * @return string Stripped string
+ */
+function strip_newlines($str)
+{
+ return preg_replace('/[\r\n]/', '', $str);
+}
+
+
+/**
* Compose a valid representation of name and e-mail address
*
* @param string $email E-mail address
diff --git a/program/lib/Roundcube/html.php b/program/lib/Roundcube/html.php
index a36711281..1a4c3beba 100644
--- a/program/lib/Roundcube/html.php
+++ b/program/lib/Roundcube/html.php
@@ -21,7 +21,7 @@
* Class for HTML code creation
*
* @package Framework
- * @subpackage View
+ * @subpackage HTML
*/
class html
{
@@ -218,7 +218,7 @@ class html
$attr = array('src' => $attr);
}
return self::tag('iframe', $attr, $cont, array_merge(self::$common_attrib,
- array('src','name','width','height','border','frameborder','onload')));
+ array('src','name','width','height','border','frameborder')));
}
/**
@@ -288,7 +288,7 @@ class html
}
// attributes with no value
- if (in_array($key, array('checked', 'multiple', 'disabled', 'selected', 'autofocus'))) {
+ if (in_array($key, array('checked', 'multiple', 'disabled', 'selected'))) {
if ($value) {
$attrib_arr[] = $key . '="' . $key . '"';
}
@@ -350,18 +350,16 @@ class html
/**
* Class to create an HTML input field
*
- * @package Framework
- * @subpackage View
+ * @package HTML
*/
class html_inputfield extends html
{
protected $tagname = 'input';
protected $type = 'text';
protected $allowed = array(
- 'type','name','value','size','tabindex','autocapitalize','required',
+ 'type','name','value','size','tabindex','autocapitalize',
'autocomplete','checked','onchange','onclick','disabled','readonly',
- 'spellcheck','results','maxlength','src','multiple','accept',
- 'placeholder','autofocus',
+ 'spellcheck','results','maxlength','src','multiple','placeholder',
);
/**
@@ -407,8 +405,7 @@ class html_inputfield extends html
/**
* Class to create an HTML password field
*
- * @package Framework
- * @subpackage View
+ * @package HTML
*/
class html_passwordfield extends html_inputfield
{
@@ -418,9 +415,9 @@ class html_passwordfield extends html_inputfield
/**
* Class to create an hidden HTML input field
*
- * @package Framework
- * @subpackage View
+ * @package HTML
*/
+
class html_hiddenfield extends html
{
protected $tagname = 'input';
@@ -468,8 +465,7 @@ class html_hiddenfield extends html
/**
* Class to create HTML radio buttons
*
- * @package Framework
- * @subpackage View
+ * @package HTML
*/
class html_radiobutton extends html_inputfield
{
@@ -499,8 +495,7 @@ class html_radiobutton extends html_inputfield
/**
* Class to create HTML checkboxes
*
- * @package Framework
- * @subpackage View
+ * @package HTML
*/
class html_checkbox extends html_inputfield
{
@@ -530,8 +525,7 @@ class html_checkbox extends html_inputfield
/**
* Class to create an HTML textarea
*
- * @package Framework
- * @subpackage View
+ * @package HTML
*/
class html_textarea extends html
{
@@ -589,8 +583,7 @@ class html_textarea extends html
* print $select->show('CH');
* </pre>
*
- * @package Framework
- * @subpackage View
+ * @package HTML
*/
class html_select extends html
{
@@ -655,8 +648,7 @@ class html_select extends html
/**
* Class to build an HTML table
*
- * @package Framework
- * @subpackage View
+ * @package HTML
*/
class html_table extends html
{
@@ -678,11 +670,6 @@ class html_table extends html
{
$default_attrib = self::$doctype == 'xhtml' ? array('summary' => '', 'border' => 0) : array();
$this->attrib = array_merge($attrib, $default_attrib);
-
- if (!empty($attrib['tagname']) && $attrib['tagname'] != 'table') {
- $this->tagname = $attrib['tagname'];
- $this->allowed = self::$common_attrib;
- }
}
/**
@@ -826,20 +813,19 @@ class html_table extends html
if (!empty($this->header)) {
$rowcontent = '';
foreach ($this->header as $c => $col) {
- $rowcontent .= self::tag($this->_col_tagname(), $col->attrib, $col->content);
+ $rowcontent .= self::tag('td', $col->attrib, $col->content);
}
- $thead = $this->tagname == 'table' ? self::tag('thead', null, self::tag('tr', null, $rowcontent, parent::$common_attrib)) :
- self::tag($this->_row_tagname(), array('class' => 'thead'), $rowcontent, parent::$common_attrib);
+ $thead = self::tag('thead', null, self::tag('tr', null, $rowcontent, parent::$common_attrib));
}
foreach ($this->rows as $r => $row) {
$rowcontent = '';
foreach ($row->cells as $c => $col) {
- $rowcontent .= self::tag($this->_col_tagname(), $col->attrib, $col->content);
+ $rowcontent .= self::tag('td', $col->attrib, $col->content);
}
if ($r < $this->rowindex || count($row->cells)) {
- $tbody .= self::tag($this->_row_tagname(), $row->attrib, $rowcontent, parent::$common_attrib);
+ $tbody .= self::tag('tr', $row->attrib, $rowcontent, parent::$common_attrib);
}
}
@@ -848,7 +834,7 @@ class html_table extends html
}
// add <tbody>
- $this->content = $thead . ($this->tagname == 'table' ? self::tag('tbody', null, $tbody) : $tbody);
+ $this->content = $thead . self::tag('tbody', null, $tbody);
unset($this->attrib['cols'], $this->attrib['rowsonly']);
return parent::show();
@@ -873,22 +859,4 @@ class html_table extends html
$this->rowindex = 0;
}
- /**
- * Getter for the corresponding tag name for table row elements
- */
- private function _row_tagname()
- {
- static $row_tagnames = array('table' => 'tr', 'ul' => 'li', '*' => 'div');
- return $row_tagnames[$this->tagname] ?: $row_tagnames['*'];
- }
-
- /**
- * Getter for the corresponding tag name for table cell elements
- */
- private function _col_tagname()
- {
- static $col_tagnames = array('table' => 'td', '*' => 'span');
- return $col_tagnames[$this->tagname] ?: $col_tagnames['*'];
- }
-
}
diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php
index d9c3dd8b9..7329b09fb 100644
--- a/program/lib/Roundcube/rcube.php
+++ b/program/lib/Roundcube/rcube.php
@@ -99,20 +99,20 @@ class rcube
protected $texts;
protected $caches = array();
protected $shutdown_functions = array();
+ protected $expunge_cache = false;
/**
* This implements the 'singleton' design pattern
*
* @param integer Options to initialize with this instance. See rcube::INIT_WITH_* constants
- * @param string Environment name to run (e.g. live, dev, test)
*
* @return rcube The one and only instance
*/
- static function get_instance($mode = 0, $env = '')
+ static function get_instance($mode = 0)
{
if (!self::$instance) {
- self::$instance = new rcube($env);
+ self::$instance = new rcube();
self::$instance->init($mode);
}
@@ -123,10 +123,10 @@ class rcube
/**
* Private constructor
*/
- protected function __construct($env = '')
+ protected function __construct()
{
// load configuration
- $this->config = new rcube_config($env);
+ $this->config = new rcube_config;
$this->plugins = new rcube_dummy_plugin_api;
register_shutdown_function(array($this, 'shutdown'));
@@ -258,39 +258,6 @@ class rcube
/**
- * Initialize and get shared cache object
- *
- * @param string $name Cache identifier
- * @param bool $packed Enables/disables data serialization
- *
- * @return rcube_cache_shared Cache object
- */
- public function get_cache_shared($name, $packed=true)
- {
- $shared_name = "shared_$name";
-
- if (!array_key_exists($shared_name, $this->caches)) {
- $opt = strtolower($name) . '_cache';
- $type = $this->config->get($opt);
- $ttl = $this->config->get($opt . '_ttl');
-
- if (!$type) {
- // cache is disabled
- return $this->caches[$shared_name] = null;
- }
-
- if ($ttl === null) {
- $ttl = $this->config->get('shared_cache_ttl', '10d');
- }
-
- $this->caches[$shared_name] = new rcube_cache_shared($type, $name, $ttl, $packed);
- }
-
- return $this->caches[$shared_name];
- }
-
-
- /**
* Create SMTP object and connect to server
*
* @param boolean True if connection should be established
@@ -378,7 +345,6 @@ class rcube
'auth_pw' => $this->config->get("{$driver}_auth_pw"),
'debug' => (bool) $this->config->get("{$driver}_debug"),
'force_caps' => (bool) $this->config->get("{$driver}_force_caps"),
- 'disabled_caps' => $this->config->get("{$driver}_disabled_caps"),
'timeout' => (int) $this->config->get("{$driver}_timeout"),
'skip_deleted' => (bool) $this->config->get('skip_deleted'),
'driver' => $driver,
@@ -458,12 +424,15 @@ class rcube
ini_set('session.name', $sess_name ? $sess_name : 'roundcube_sessid');
ini_set('session.use_cookies', 1);
ini_set('session.use_only_cookies', 1);
+ ini_set('session.serialize_handler', 'php');
ini_set('session.cookie_httponly', 1);
// use database for storing session data
$this->session = new rcube_session($this->get_dbh(), $this->config);
- $this->session->register_gc_handler(array($this, 'gc'));
+ $this->session->register_gc_handler(array($this, 'temp_gc'));
+ $this->session->register_gc_handler(array($this, 'cache_gc'));
+
$this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME']));
$this->session->set_ip_check($this->config->get('ip_check'));
@@ -473,47 +442,27 @@ class rcube
// start PHP session (if not in CLI mode)
if ($_SERVER['REMOTE_ADDR']) {
- $this->session->start();
+ session_start();
}
}
/**
- * Garbage collector - cache/temp cleaner
- */
- public function gc()
- {
- rcube_cache::gc();
- rcube_cache_shared::gc();
- $this->get_storage()->cache_gc();
-
- $this->gc_temp();
- }
-
-
- /**
* Garbage collector function for temp files.
* Remove temp files older than two days
*/
- public function gc_temp()
+ public function temp_gc()
{
$tmp = unslashify($this->config->get('temp_dir'));
-
- // expire in 48 hours by default
- $temp_dir_ttl = $this->config->get('temp_dir_ttl', '48h');
- $temp_dir_ttl = get_offset_sec($temp_dir_ttl);
- if ($temp_dir_ttl < 6*3600)
- $temp_dir_ttl = 6*3600; // 6 hours sensible lower bound.
-
- $expire = time() - $temp_dir_ttl;
+ $expire = time() - 172800; // expire in 48 hours
if ($tmp && ($dir = opendir($tmp))) {
while (($fname = readdir($dir)) !== false) {
- if ($fname[0] == '.') {
+ if ($fname{0} == '.') {
continue;
}
- if (@filemtime($tmp.'/'.$fname) < $expire) {
+ if (filemtime($tmp.'/'.$fname) < $expire) {
@unlink($tmp.'/'.$fname);
}
}
@@ -524,21 +473,14 @@ class rcube
/**
- * Runs garbage collector with probability based on
- * session settings. This is intended for environments
- * without a session.
+ * Garbage collector for cache entries.
+ * Set flag to expunge caches on shutdown
*/
- public function gc_run()
+ public function cache_gc()
{
- $probability = (int) ini_get('session.gc_probability');
- $divisor = (int) ini_get('session.gc_divisor');
-
- if ($divisor > 0 && $probability > 0) {
- $random = mt_rand(1, $divisor);
- if ($random <= $probability) {
- $this->gc();
- }
- }
+ // because this gc function is called before storage is initialized,
+ // we just set a flag to expunge storage cache on shutdown.
+ $this->expunge_cache = true;
}
@@ -797,7 +739,7 @@ class rcube
mcrypt_module_close($td);
}
else {
- @include_once 'des.inc';
+ // @include_once 'des.inc'; (not shipped with this distribution)
if (function_exists('des')) {
$des_iv_size = 8;
@@ -852,7 +794,7 @@ class rcube
mcrypt_module_close($td);
}
else {
- @include_once 'des.inc';
+ // @include_once 'des.inc'; (not shipped with this distribution)
if (function_exists('des')) {
$des_iv_size = 8;
@@ -922,14 +864,6 @@ class rcube
call_user_func($function);
}
- // write session data as soon as possible and before
- // closing database connection, don't do this before
- // registered shutdown functions, they may need the session
- // Note: this will run registered gc handlers (ie. cache gc)
- if ($_SERVER['REMOTE_ADDR'] && is_object($this->session)) {
- $this->session->write_close();
- }
-
if (is_object($this->smtp)) {
$this->smtp->disconnect();
}
@@ -941,6 +875,9 @@ class rcube
}
if (is_object($this->storage)) {
+ if ($this->expunge_cache) {
+ $this->storage->expunge_cache();
+ }
$this->storage->close();
}
}
@@ -1132,8 +1069,8 @@ class rcube
* - code: Error code (required)
* - type: Error type [php|db|imap|javascript] (required)
* - message: Error message
- * - file: File where error occured
- * - line: Line where error occured
+ * - file: File where error occurred
+ * - line: Line where error occurred
* @param boolean True to log the error
* @param boolean Terminate script execution
*/
@@ -1359,191 +1296,6 @@ class rcube
}
}
- /**
- * Unique Message-ID generator.
- *
- * @return string Message-ID
- */
- public function gen_message_id()
- {
- $local_part = md5(uniqid('rcube'.mt_rand(), true));
- $domain_part = $this->user->get_username('domain');
-
- // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
- if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
- foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) {
- $host = preg_replace('/:[0-9]+$/', '', $host);
- if ($host && preg_match('/\.[a-z]+$/i', $host)) {
- $domain_part = $host;
- }
- }
- }
-
- return sprintf('<%s@%s>', $local_part, $domain_part);
- }
-
- /**
- * Send the given message using the configured method.
- *
- * @param object $message Reference to Mail_MIME object
- * @param string $from Sender address string
- * @param array $mailto Array of recipient address strings
- * @param array $error SMTP error array (reference)
- * @param string $body_file Location of file with saved message body (reference),
- * used when delay_file_io is enabled
- * @param array $options SMTP options (e.g. DSN request)
- *
- * @return boolean Send status.
- */
- public function deliver_message(&$message, $from, $mailto, &$error, &$body_file = null, $options = null)
- {
- $plugin = $this->plugins->exec_hook('message_before_send', array(
- 'message' => $message,
- 'from' => $from,
- 'mailto' => $mailto,
- 'options' => $options,
- ));
-
- if ($plugin['abort']) {
- return isset($plugin['result']) ? $plugin['result'] : false;
- }
-
- $from = $plugin['from'];
- $mailto = $plugin['mailto'];
- $options = $plugin['options'];
- $message = $plugin['message'];
- $headers = $message->headers();
-
- // send thru SMTP server using custom SMTP library
- if ($this->config->get('smtp_server')) {
- // generate list of recipients
- $a_recipients = array($mailto);
-
- if (strlen($headers['Cc']))
- $a_recipients[] = $headers['Cc'];
- if (strlen($headers['Bcc']))
- $a_recipients[] = $headers['Bcc'];
-
- // clean Bcc from header for recipients
- $send_headers = $headers;
- unset($send_headers['Bcc']);
- // here too, it because txtHeaders() below use $message->_headers not only $send_headers
- unset($message->_headers['Bcc']);
-
- $smtp_headers = $message->txtHeaders($send_headers, true);
-
- if ($message->getParam('delay_file_io')) {
- // use common temp dir
- $temp_dir = $this->config->get('temp_dir');
- $body_file = tempnam($temp_dir, 'rcmMsg');
- if (PEAR::isError($mime_result = $message->saveMessageBody($body_file))) {
- self::raise_error(array('code' => 650, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Could not create message: ".$mime_result->getMessage()),
- TRUE, FALSE);
- return false;
- }
- $msg_body = fopen($body_file, 'r');
- }
- else {
- $msg_body = $message->get();
- }
-
- // send message
- if (!is_object($this->smtp)) {
- $this->smtp_init(true);
- }
-
- $sent = $this->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $options);
- $response = $this->smtp->get_response();
- $error = $this->smtp->get_error();
-
- // log error
- if (!$sent) {
- self::raise_error(array('code' => 800, 'type' => 'smtp',
- 'line' => __LINE__, 'file' => __FILE__,
- 'message' => "SMTP error: ".join("\n", $response)), TRUE, FALSE);
- }
- }
- // send mail using PHP's mail() function
- else {
- // unset some headers because they will be added by the mail() function
- $headers_enc = $message->headers($headers);
- $headers_php = $message->_headers;
- unset($headers_php['To'], $headers_php['Subject']);
-
- // reset stored headers and overwrite
- $message->_headers = array();
- $header_str = $message->txtHeaders($headers_php);
-
- // #1485779
- if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
- if (preg_match_all('/<([^@]+@[^>]+)>/', $headers_enc['To'], $m)) {
- $headers_enc['To'] = implode(', ', $m[1]);
- }
- }
-
- $msg_body = $message->get();
-
- if (PEAR::isError($msg_body)) {
- self::raise_error(array('code' => 650, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Could not create message: ".$msg_body->getMessage()),
- TRUE, FALSE);
- }
- else {
- $delim = $this->config->header_delimiter();
- $to = $headers_enc['To'];
- $subject = $headers_enc['Subject'];
- $header_str = rtrim($header_str);
-
- if ($delim != "\r\n") {
- $header_str = str_replace("\r\n", $delim, $header_str);
- $msg_body = str_replace("\r\n", $delim, $msg_body);
- $to = str_replace("\r\n", $delim, $to);
- $subject = str_replace("\r\n", $delim, $subject);
- }
-
- if (filter_var(ini_get('safe_mode'), FILTER_VALIDATE_BOOLEAN))
- $sent = mail($to, $subject, $msg_body, $header_str);
- else
- $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
- }
- }
-
- if ($sent) {
- $this->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body));
-
- // remove MDN headers after sending
- unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
-
- // get all recipients
- if ($headers['Cc'])
- $mailto .= $headers['Cc'];
- if ($headers['Bcc'])
- $mailto .= $headers['Bcc'];
- if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
- $mailto = implode(', ', array_unique($m[1]));
-
- if ($this->config->get('smtp_log')) {
- self::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
- $this->user->get_username(),
- $_SERVER['REMOTE_ADDR'],
- $mailto,
- !empty($response) ? join('; ', $response) : ''));
- }
- }
-
- if (is_resource($msg_body)) {
- fclose($msg_body);
- }
-
- $message->_headers = array();
- $message->headers($headers);
-
- return $sent;
- }
-
}
diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php
index 9301211ff..13016ecc7 100644
--- a/program/lib/Roundcube/rcube_addressbook.php
+++ b/program/lib/Roundcube/rcube_addressbook.php
@@ -3,7 +3,7 @@
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2013, The Roundcube Dev Team |
+ | Copyright (C) 2006-2012, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
@@ -35,7 +35,6 @@ abstract class rcube_addressbook
/** public properties (mandatory) */
public $primary_key;
public $groups = false;
- public $export_groups = true;
public $readonly = true;
public $searchonly = false;
public $undelete = false;
@@ -134,7 +133,7 @@ abstract class rcube_addressbook
abstract function get_record($id, $assoc=false);
/**
- * Returns the last error occured (e.g. when updating/inserting failed)
+ * Returns the last error occurred (e.g. when updating/inserting failed)
*
* @return array Hash array with the following fields: type, message
*/
@@ -424,7 +423,7 @@ abstract class rcube_addressbook
* @param boolean True to return one array with all values, False for hash array with values grouped by type
* @return array List of column values
*/
- public static function get_col_values($col, $data, $flat = false)
+ function get_col_values($col, $data, $flat = false)
{
$out = array();
foreach ((array)$data as $c => $values) {
@@ -433,7 +432,7 @@ abstract class rcube_addressbook
$out = array_merge($out, (array)$values);
}
else {
- list(, $type) = explode(':', $c);
+ list($f, $type) = explode(':', $c);
$out[$type] = array_merge((array)$out[$type], (array)$values);
}
}
@@ -477,8 +476,7 @@ abstract class rcube_addressbook
$fn = trim(join(' ', array_filter(array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']))));
// use email address part for name
- $email = self::get_col_values('email', $contact, true);
- $email = $email[0];
+ $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email'];
if ($email && (empty($fn) || $fn == $email)) {
// return full email
@@ -525,9 +523,9 @@ abstract class rcube_addressbook
$fn = $contact['name'];
// fallback to email address
- if (empty($fn) && ($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
- return $email[0];
- }
+ $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email'];
+ if (empty($fn) && $email)
+ return $email;
return $fn;
}
@@ -540,11 +538,11 @@ abstract class rcube_addressbook
$key = $contact[$sort_col] . ':' . $contact['sourceid'];
// add email to a key to not skip contacts with the same name (#1488375)
- if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
- $key .= ':' . implode(':', (array)$email);
- }
+ if (!empty($contact['email'])) {
+ $key .= ':' . implode(':', (array)$contact['email']);
+ }
- return $key;
+ return $key;
}
/**
@@ -563,9 +561,9 @@ abstract class rcube_addressbook
// use only strict comparison (mode = 1)
// @TODO: partial search, e.g. match only day and month
if (in_array($colname, $this->date_cols)) {
- return (($value = rcube_utils::anytodatetime($value))
- && ($search = rcube_utils::anytodatetime($search))
- && $value->format('Ymd') == $search->format('Ymd'));
+ return (($value = rcube_utils::strtotime($value))
+ && ($search = rcube_utils::strtotime($search))
+ && date('Ymd', $value) == date('Ymd', $search));
}
// composite field, e.g. address
diff --git a/program/lib/Roundcube/rcube_base_replacer.php b/program/lib/Roundcube/rcube_base_replacer.php
index a59bba926..aaaa2028c 100644
--- a/program/lib/Roundcube/rcube_base_replacer.php
+++ b/program/lib/Roundcube/rcube_base_replacer.php
@@ -21,7 +21,7 @@
* using a predefined base
*
* @package Framework
- * @subpackage Utils
+ * @subpackage Core
* @author Thomas Bruederli <roundcube@gmail.com>
*/
class rcube_base_replacer
diff --git a/program/lib/Roundcube/rcube_browser.php b/program/lib/Roundcube/rcube_browser.php
index 34128291b..d10fe2a2c 100644
--- a/program/lib/Roundcube/rcube_browser.php
+++ b/program/lib/Roundcube/rcube_browser.php
@@ -20,7 +20,7 @@
* Provide details about the client's browser based on the User-Agent header
*
* @package Framework
- * @subpackage Utils
+ * @subpackage Core
*/
class rcube_browser
{
diff --git a/program/lib/Roundcube/rcube_cache.php b/program/lib/Roundcube/rcube_cache.php
index a708cb292..deaba68e9 100644
--- a/program/lib/Roundcube/rcube_cache.php
+++ b/program/lib/Roundcube/rcube_cache.php
@@ -38,7 +38,6 @@ class rcube_cache
private $type;
private $userid;
private $prefix;
- private $table;
private $ttl;
private $packed;
private $index;
@@ -72,9 +71,8 @@ class rcube_cache
$this->db = function_exists('apc_exists'); // APC 3.1.4 required
}
else {
- $this->type = 'db';
- $this->db = $rcube->get_dbh();
- $this->table = $this->db->table_name('cache');
+ $this->type = 'db';
+ $this->db = $rcube->get_dbh();
}
// convert ttl string to seconds
@@ -194,31 +192,20 @@ class rcube_cache
*/
function expunge()
{
- if ($this->type == 'db' && $this->db && $this->ttl) {
+ if ($this->type == 'db' && $this->db) {
$this->db->query(
- "DELETE FROM ".$this->table.
+ "DELETE FROM ".$this->db->table_name('cache').
" WHERE user_id = ?".
" AND cache_key LIKE ?".
- " AND expires < " . $this->db->now(),
+ " AND " . $this->db->unixtimestamp('created')." < ?",
$this->userid,
- $this->prefix.'.%');
+ $this->prefix.'.%',
+ time() - $this->ttl);
}
}
/**
- * Remove expired records of all caches
- */
- static function gc()
- {
- $rcube = rcube::get_instance();
- $db = $rcube->get_dbh();
-
- $db->query("DELETE FROM " . $db->table_name('cache') . " WHERE expires < " . $db->now());
- }
-
-
- /**
* Writes the cache back to the DB.
*/
function close()
@@ -284,7 +271,7 @@ class rcube_cache
else {
$sql_result = $this->db->limitquery(
"SELECT data, cache_key".
- " FROM " . $this->table.
+ " FROM ".$this->db->table_name('cache').
" WHERE user_id = ?".
" AND cache_key = ?".
// for better performance we allow more records for one key
@@ -339,7 +326,7 @@ class rcube_cache
// Remove NULL rows (here we don't need to check if the record exist)
if ($data == 'N;') {
$this->db->query(
- "DELETE FROM " . $this->table.
+ "DELETE FROM ".$this->db->table_name('cache').
" WHERE user_id = ?".
" AND cache_key = ?",
$this->userid, $key);
@@ -350,10 +337,8 @@ class rcube_cache
// update existing cache record
if ($key_exists) {
$result = $this->db->query(
- "UPDATE " . $this->table.
- " SET created = " . $this->db->now().
- ", expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL').
- ", data = ?".
+ "UPDATE ".$this->db->table_name('cache').
+ " SET created = ". $this->db->now().", data = ?".
" WHERE user_id = ?".
" AND cache_key = ?",
$data, $this->userid, $key);
@@ -363,9 +348,9 @@ class rcube_cache
// for better performance we allow more records for one key
// so, no need to check if record exist (see rcube_cache::read_record())
$result = $this->db->query(
- "INSERT INTO " . $this->table.
- " (created, expires, user_id, cache_key, data)".
- " VALUES (" . $this->db->now() . ", " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?, ?, ?)",
+ "INSERT INTO ".$this->db->table_name('cache').
+ " (created, user_id, cache_key, data)".
+ " VALUES (".$this->db->now().", ?, ?, ?)",
$this->userid, $key, $data);
}
@@ -426,7 +411,7 @@ class rcube_cache
}
$this->db->query(
- "DELETE FROM " . $this->table.
+ "DELETE FROM ".$this->db->table_name('cache').
" WHERE user_id = ?" . $where,
$this->userid);
}
diff --git a/program/lib/Roundcube/rcube_config.php b/program/lib/Roundcube/rcube_config.php
index ac3ea678c..3edec4242 100644
--- a/program/lib/Roundcube/rcube_config.php
+++ b/program/lib/Roundcube/rcube_config.php
@@ -3,7 +3,7 @@
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2013, The Roundcube Dev Team |
+ | Copyright (C) 2008-2012, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
@@ -26,8 +26,6 @@ class rcube_config
{
const DEFAULT_SKIN = 'larry';
- private $env = '';
- private $paths = array();
private $prop = array();
private $errors = array();
private $userprefs = array();
@@ -45,46 +43,14 @@ class rcube_config
'reply_mode' => 'top_posting',
'refresh_interval' => 'keep_alive',
'min_refresh_interval' => 'min_keep_alive',
- 'messages_cache_ttl' => 'message_cache_lifetime',
- 'redundant_attachments_cache_ttl' => 'redundant_attachments_memcache_ttl',
);
/**
* Object constructor
- *
- * @param string Environment suffix for config files to load
*/
- public function __construct($env = '')
+ public function __construct()
{
- $this->env = $env;
-
- if ($paths = getenv('RCUBE_CONFIG_PATH')) {
- $this->paths = explode(PATH_SEPARATOR, $paths);
- // make all paths absolute
- foreach ($this->paths as $i => $path) {
- if (!$this->_is_absolute($path)) {
- if ($realpath = realpath(RCUBE_INSTALL_PATH . $path)) {
- $this->paths[$i] = unslashify($realpath) . '/';
- }
- else {
- unset($this->paths[$i]);
- }
- }
- else {
- $this->paths[$i] = unslashify($path) . '/';
- }
- }
- }
-
- if (defined('RCUBE_CONFIG_DIR') && !in_array(RCUBE_CONFIG_DIR, $this->paths)) {
- $this->paths[] = RCUBE_CONFIG_DIR;
- }
-
- if (empty($this->paths)) {
- $this->paths[] = RCUBE_INSTALL_PATH . 'config/';
- }
-
$this->load();
// Defaults, that we do not require you to configure,
@@ -101,26 +67,16 @@ class rcube_config
*/
private function load()
{
- // Load default settings
- if (!$this->load_from_file('defaults.inc.php')) {
- $this->errors[] = 'defaults.inc.php was not found.';
- }
-
// load main config file
- if (!$this->load_from_file('config.inc.php')) {
- // Old configuration files
- if (!$this->load_from_file('main.inc.php') ||
- !$this->load_from_file('db.inc.php')) {
- $this->errors[] = 'config.inc.php was not found.';
- }
- else if (rand(1,100) == 10) { // log warning on every 100th request (average)
- trigger_error("config.inc.php was not found. Please migrate your config by running bin/update.sh", E_USER_WARNING);
- }
- }
+ if (!$this->load_from_file(RCUBE_CONFIG_DIR . 'main.inc.php'))
+ $this->errors[] = 'main.inc.php was not found.';
+
+ // load database config
+ if (!$this->load_from_file(RCUBE_CONFIG_DIR . 'db.inc.php'))
+ $this->errors[] = 'db.inc.php was not found.';
// load host-specific configuration
- if (!empty($_SERVER['HTTP_HOST']))
- $this->load_host_config();
+ $this->load_host_config();
// set skin (with fallback to old 'skin_path' property)
if (empty($this->prop['skin'])) {
@@ -163,6 +119,17 @@ class rcube_config
// enable display_errors in 'show' level, but not for ajax requests
ini_set('display_errors', intval(empty($_REQUEST['_remote']) && ($this->prop['debug_level'] & 4)));
+ // set timezone auto settings values
+ if ($this->prop['timezone'] == 'auto') {
+ $this->prop['_timezone_value'] = $this->client_timezone();
+ }
+ else if (is_numeric($this->prop['timezone']) && ($tz = timezone_name_from_abbr("", $this->prop['timezone'] * 3600, 0))) {
+ $this->prop['timezone'] = $tz;
+ }
+ else if (empty($this->prop['timezone'])) {
+ $this->prop['timezone'] = 'UTC';
+ }
+
// remove deprecated properties
unset($this->prop['dst_active']);
@@ -186,7 +153,7 @@ class rcube_config
}
if ($fname) {
- $this->load_from_file($fname);
+ $this->load_from_file(RCUBE_CONFIG_DIR . $fname);
}
}
@@ -195,78 +162,26 @@ class rcube_config
* Read configuration from a file
* and merge with the already stored config values
*
- * @param string $file Name of the config file to be loaded
+ * @param string $fpath Full path to the config file to be loaded
* @return booelan True on success, false on failure
*/
- public function load_from_file($file)
- {
- $success = false;
-
- foreach ($this->resolve_paths($file) as $fpath) {
- if ($fpath && is_file($fpath) && is_readable($fpath)) {
- // use output buffering, we don't need any output here
- ob_start();
- include($fpath);
- ob_end_clean();
-
- if (is_array($config)) {
- $this->merge($config);
- $success = true;
- }
- // deprecated name of config variable
- else if (is_array($rcmail_config)) {
- $this->merge($rcmail_config);
- $success = true;
- }
- }
- }
-
- return $success;
- }
-
- /**
- * Helper method to resolve absolute paths to the given config file.
- * This also takes the 'env' property into account.
- *
- * @param string Filename or absolute file path
- * @param boolean Return -$env file path if exists
- * @return array List of candidates in config dir path(s)
- */
- public function resolve_paths($file, $use_env = true)
+ public function load_from_file($fpath)
{
- $files = array();
- $abs_path = $this->_is_absolute($file);
-
- foreach ($this->paths as $basepath) {
- $realpath = $abs_path ? $file : realpath($basepath . '/' . $file);
-
- // check if <file>-env.ini exists
- if ($realpath && $use_env && !empty($this->env)) {
- $envfile = preg_replace('/\.(inc.php)$/', '-' . $this->env . '.\\1', $realpath);
- if (is_file($envfile))
- $realpath = $envfile;
- }
-
- if ($realpath) {
- $files[] = $realpath;
-
- // no need to continue the loop if an absolute file path is given
- if ($abs_path) {
- break;
- }
+ if (is_file($fpath) && is_readable($fpath)) {
+ // use output buffering, we don't need any output here
+ ob_start();
+ include($fpath);
+ ob_end_clean();
+
+ if (is_array($rcmail_config)) {
+ $this->merge($rcmail_config);
+ return true;
}
}
- return $files;
+ return false;
}
- /**
- * Determine whether the given file path is absolute or relative
- */
- private function _is_absolute($path)
- {
- return $path[0] == DIRECTORY_SEPARATOR || preg_match('!^[a-z]:[\\\\/]!i', $path);
- }
/**
* Getter for a specific config parameter
@@ -286,10 +201,8 @@ class rcube_config
$rcube = rcube::get_instance();
- if ($name == 'timezone') {
- if (empty($result) || $result == 'auto') {
- $result = $this->client_timezone();
- }
+ if ($name == 'timezone' && isset($this->prop['_timezone_value'])) {
+ $result = $this->prop['_timezone_value'];
}
else if ($name == 'client_mimetypes') {
if ($result == null && $def == null)
@@ -347,6 +260,11 @@ class rcube_config
}
}
+ // convert user's timezone into the new format
+ if (is_numeric($prefs['timezone']) && ($tz = timezone_name_from_abbr('', $prefs['timezone'] * 3600, 0))) {
+ $prefs['timezone'] = $tz;
+ }
+
// larry is the new default skin :-)
if ($prefs['skin'] == 'default') {
$prefs['skin'] = self::DEFAULT_SKIN;
@@ -354,6 +272,13 @@ class rcube_config
$this->userprefs = $prefs;
$this->prop = array_merge($this->prop, $prefs);
+
+ // override timezone settings with client values
+ if ($this->prop['timezone'] == 'auto') {
+ $this->prop['_timezone_value'] = isset($_SESSION['timezone']) ? $this->client_timezone() : $this->prop['_timezone_value'];
+ }
+ else if (isset($this->prop['_timezone_value']))
+ unset($this->prop['_timezone_value']);
}
@@ -494,12 +419,13 @@ class rcube_config
*/
private function client_timezone()
{
- // @TODO: remove this legacy timezone handling in the future
- $props = $this->fix_legacy_props(array('timezone' => $_SESSION['timezone']));
-
- if (!empty($props['timezone'])) {
+ if (isset($_SESSION['timezone']) && is_numeric($_SESSION['timezone'])
+ && ($ctz = timezone_name_from_abbr("", $_SESSION['timezone'] * 3600, 0))) {
+ return $ctz;
+ }
+ else if (!empty($_SESSION['timezone'])) {
try {
- $tz = new DateTimeZone($props['timezone']);
+ $tz = timezone_open($_SESSION['timezone']);
return $tz->getName();
}
catch (Exception $e) { /* gracefully ignore */ }
@@ -527,77 +453,6 @@ class rcube_config
}
}
- // convert deprecated numeric timezone value
- if (isset($props['timezone']) && is_numeric($props['timezone'])) {
- if ($tz = self::timezone_name_from_abbr($props['timezone'])) {
- $props['timezone'] = $tz;
- }
- else {
- unset($props['timezone']);
- }
- }
-
return $props;
}
-
- /**
- * timezone_name_from_abbr() replacement. Converts timezone offset
- * into timezone name abbreviation.
- *
- * @param float $offset Timezone offset (in hours)
- *
- * @return string Timezone abbreviation
- */
- static public function timezone_name_from_abbr($offset)
- {
- // List of timezones here is not complete - https://bugs.php.net/bug.php?id=44780
- if ($tz = timezone_name_from_abbr('', $offset * 3600, 0)) {
- return $tz;
- }
-
- // try with more complete list (#1489261)
- $timezones = array(
- '-660' => "Pacific/Apia",
- '-600' => "Pacific/Honolulu",
- '-570' => "Pacific/Marquesas",
- '-540' => "America/Anchorage",
- '-480' => "America/Los_Angeles",
- '-420' => "America/Denver",
- '-360' => "America/Chicago",
- '-300' => "America/New_York",
- '-270' => "America/Caracas",
- '-240' => "America/Halifax",
- '-210' => "Canada/Newfoundland",
- '-180' => "America/Sao_Paulo",
- '-60' => "Atlantic/Azores",
- '0' => "Europe/London",
- '60' => "Europe/Paris",
- '120' => "Europe/Helsinki",
- '180' => "Europe/Moscow",
- '210' => "Asia/Tehran",
- '240' => "Asia/Dubai",
- '300' => "Asia/Karachi",
- '270' => "Asia/Kabul",
- '300' => "Asia/Karachi",
- '330' => "Asia/Kolkata",
- '345' => "Asia/Katmandu",
- '360' => "Asia/Yekaterinburg",
- '390' => "Asia/Rangoon",
- '420' => "Asia/Krasnoyarsk",
- '480' => "Asia/Shanghai",
- '525' => "Australia/Eucla",
- '540' => "Asia/Tokyo",
- '570' => "Australia/Adelaide",
- '600' => "Australia/Melbourne",
- '630' => "Australia/Lord_Howe",
- '660' => "Asia/Vladivostok",
- '690' => "Pacific/Norfolk",
- '720' => "Pacific/Auckland",
- '765' => "Pacific/Chatham",
- '780' => "Pacific/Enderbury",
- '840' => "Pacific/Kiritimati",
- );
-
- return $timezones[(string) intval($offset * 60)];
- }
}
diff --git a/program/lib/Roundcube/rcube_contacts.php b/program/lib/Roundcube/rcube_contacts.php
index 6d01368a1..3919cdc6e 100644
--- a/program/lib/Roundcube/rcube_contacts.php
+++ b/program/lib/Roundcube/rcube_contacts.php
@@ -718,10 +718,6 @@ class rcube_contacts extends rcube_addressbook
foreach ($save_data as $key => $values) {
list($field, $section) = explode(':', $key);
$fulltext = in_array($field, $this->fulltext_cols);
- // avoid casting DateTime objects to array
- if (is_object($values) && is_a($values, 'DateTime')) {
- $values = array(0 => $values);
- }
foreach ((array)$values as $value) {
if (isset($value))
$vcard->set($field, $value, $section);
diff --git a/program/lib/Roundcube/rcube_content_filter.php b/program/lib/Roundcube/rcube_content_filter.php
index ae6617d1b..b814bb71d 100644
--- a/program/lib/Roundcube/rcube_content_filter.php
+++ b/program/lib/Roundcube/rcube_content_filter.php
@@ -20,7 +20,7 @@
* PHP stream filter to detect html/javascript code in attachments
*
* @package Framework
- * @subpackage Utils
+ * @subpackage Core
*/
class rcube_content_filter extends php_user_filter
{
diff --git a/program/lib/Roundcube/rcube_csv2vcard.php b/program/lib/Roundcube/rcube_csv2vcard.php
index 00e6d4e20..506a4b740 100644
--- a/program/lib/Roundcube/rcube_csv2vcard.php
+++ b/program/lib/Roundcube/rcube_csv2vcard.php
@@ -130,22 +130,6 @@ class rcube_csv2vcard
'work_state' => 'region:work',
'home_city_short' => 'locality:home',
'home_state_short' => 'region:home',
-
- // Atmail
- 'date_of_birth' => 'birthday',
- 'email' => 'email:pref',
- 'home_mobile' => 'phone:cell',
- 'home_zip' => 'zipcode:home',
- 'info' => 'notes',
- 'user_photo' => 'photo',
- 'url' => 'website:homepage',
- 'work_company' => 'organization',
- 'work_dept' => 'departament',
- 'work_fax' => 'phone:work,fax',
- 'work_mobile' => 'phone:work,cell',
- 'work_title' => 'jobtitle',
- 'work_zip' => 'zipcode:work',
- 'group' => 'groups',
);
/**
@@ -246,30 +230,8 @@ class rcube_csv2vcard
'work_phone' => "Work Phone",
'work_address' => "Work Address",
//'work_address_2' => "Work Address 2",
- 'work_city' => "Work City",
'work_country' => "Work Country",
- 'work_state' => "Work State",
'work_zipcode' => "Work ZipCode",
-
- // Atmail
- 'date_of_birth' => "Date of Birth",
- 'email' => "Email",
- //'email_2' => "Email2",
- //'email_3' => "Email3",
- //'email_4' => "Email4",
- //'email_5' => "Email5",
- 'home_mobile' => "Home Mobile",
- 'home_zip' => "Home Zip",
- 'info' => "Info",
- 'user_photo' => "User Photo",
- 'url' => "URL",
- 'work_company' => "Work Company",
- 'work_dept' => "Work Dept",
- 'work_fax' => "Work Fax",
- 'work_mobile' => "Work Mobile",
- 'work_title' => "Work Title",
- 'work_zip' => "Work Zip",
- 'groups' => "Group",
);
protected $local_label_map = array();
@@ -306,6 +268,7 @@ class rcube_csv2vcard
{
// convert to UTF-8
$head = substr($csv, 0, 4096);
+ $fallback = rcube::get_instance()->config->get('default_charset', 'ISO-8859-1'); // fallback to Latin-1?
$charset = rcube_charset::detect($head, RCUBE_CHARSET);
$csv = rcube_charset::convert($csv, $charset);
$head = '';
@@ -313,7 +276,7 @@ class rcube_csv2vcard
$this->map = array();
// Parse file
- foreach (preg_split("/[\r\n]+/", $csv) as $line) {
+ foreach (preg_split("/[\r\n]+/", $csv) as $i => $line) {
$elements = $this->parse_line($line);
if (empty($elements)) {
continue;
@@ -427,13 +390,9 @@ class rcube_csv2vcard
$contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d'];
}
- // Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00"
foreach (array('birthday', 'anniversary') as $key) {
- if (!empty($contact[$key])) {
- $date = preg_replace('/[0[:^word:]]/', '', $contact[$key]);
- if (empty($date)) {
- unset($contact[$key]);
- }
+ if (!empty($contact[$key]) && $contact[$key] == '0/0/00') { // @TODO: localization?
+ unset($contact[$key]);
}
}
diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php
index 852070073..5083a0dfe 100644
--- a/program/lib/Roundcube/rcube_db.php
+++ b/program/lib/Roundcube/rcube_db.php
@@ -47,7 +47,6 @@ class rcube_db
'identifier_end' => '"',
);
- const DEBUG_LINE_LENGTH = 4096;
/**
* Factory, returns driver-specific instance of the class
@@ -63,6 +62,7 @@ class rcube_db
$driver = strtolower(substr($db_dsnw, 0, strpos($db_dsnw, ':')));
$driver_map = array(
'sqlite2' => 'sqlite',
+ 'sqlite3' => 'sqlite',
'sybase' => 'mssql',
'dblib' => 'mssql',
'mysqli' => 'mysql',
@@ -100,15 +100,27 @@ class rcube_db
$this->db_dsnw_array = self::parse_dsn($db_dsnw);
$this->db_dsnr_array = self::parse_dsn($db_dsnr);
+
+ // Initialize driver class
+ $this->init();
+ }
+
+ /**
+ * Initialization of the object with driver specific code
+ */
+ protected function init()
+ {
+ // To be used by driver classes
}
/**
* Connect to specific database
*
- * @param array $dsn DSN for DB connections
- * @param string $mode Connection mode (r|w)
+ * @param array $dsn DSN for DB connections
+ *
+ * @return PDO database handle
*/
- protected function dsn_connect($dsn, $mode)
+ protected function dsn_connect($dsn)
{
$this->db_error = false;
$this->db_error_msg = null;
@@ -146,10 +158,9 @@ class rcube_db
return null;
}
- $this->dbh = $dbh;
- $this->db_mode = $mode;
- $this->db_connected = true;
$this->conn_configure($dsn, $dbh);
+
+ return $dbh;
}
/**
@@ -172,6 +183,16 @@ class rcube_db
}
/**
+ * Driver-specific database character set setting
+ *
+ * @param string $charset Character set name
+ */
+ protected function set_charset($charset)
+ {
+ $this->query("SET NAMES 'utf8'");
+ }
+
+ /**
* Connect to appropriate database depending on the operation
*
* @param string $mode Connection mode (r|w)
@@ -198,14 +219,23 @@ class rcube_db
$dsn = ($mode == 'r') ? $this->db_dsnr_array : $this->db_dsnw_array;
- $this->dsn_connect($dsn, $mode);
+ $this->dbh = $this->dsn_connect($dsn);
+ $this->db_connected = is_object($this->dbh);
// use write-master when read-only fails
- if (!$this->db_connected && $mode == 'r' && $this->is_replicated()) {
- $this->dsn_connect($this->db_dsnw_array, 'w');
+ if (!$this->db_connected && $mode == 'r') {
+ $mode = 'w';
+ $this->dbh = $this->dsn_connect($this->db_dsnw_array);
+ $this->db_connected = is_object($this->dbh);
}
- $this->conn_failure = !$this->db_connected;
+ if ($this->db_connected) {
+ $this->db_mode = $mode;
+ $this->set_charset('utf8');
+ }
+ else {
+ $this->conn_failure = true;
+ }
}
/**
@@ -226,11 +256,6 @@ class rcube_db
protected function debug($query)
{
if ($this->options['debug_mode']) {
- if (($len = strlen($query)) > self::DEBUG_LINE_LENGTH) {
- $diff = $len - self::DEBUG_LINE_LENGTH;
- $query = substr($query, 0, self::DEBUG_LINE_LENGTH)
- . "... [truncated $diff bytes]";
- }
rcube::write_log('sql', '[' . (++$this->db_index) . '] ' . $query . ';');
}
}
@@ -338,10 +363,8 @@ class rcube_db
*/
protected function _query($query, $offset, $numrows, $params)
{
- $query = trim($query);
-
// Read or write ?
- $mode = preg_match('/^(select|show|set)/i', $query) ? 'r' : 'w';
+ $mode = preg_match('/^(select|show)/i', ltrim($query)) ? 'r' : 'w';
$this->db_connect($mode);
@@ -387,16 +410,13 @@ class rcube_db
if ($result === false) {
$error = $this->dbh->errorInfo();
+ $this->db_error = true;
+ $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
- if (empty($this->options['ignore_key_errors']) || $error[0] != '23000') {
- $this->db_error = true;
- $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
-
- rcube::raise_error(array('code' => 500, 'type' => 'db',
- 'line' => __LINE__, 'file' => __FILE__,
- 'message' => $this->db_error_msg . " (SQL Query: $query)"
- ), true, false);
- }
+ rcube::raise_error(array('code' => 500, 'type' => 'db',
+ 'line' => __LINE__, 'file' => __FILE__,
+ 'message' => $this->db_error_msg . " (SQL Query: $query)"
+ ), true, false);
}
$this->last_result = $result;
@@ -683,19 +703,11 @@ class rcube_db
/**
* Return SQL function for current time and date
*
- * @param int $interval Optional interval (in seconds) to add/subtract
- *
* @return string SQL function to use in query
*/
- public function now($interval = 0)
+ public function now()
{
- if ($interval) {
- $add = ' ' . ($interval > 0 ? '+' : '-') . ' INTERVAL ';
- $add .= $interval > 0 ? intval($interval) : intval($interval) * -1;
- $add .= ' SECOND';
- }
-
- return "now()" . $add;
+ return "now()";
}
/**
@@ -856,26 +868,17 @@ class rcube_db
{
$rcube = rcube::get_instance();
- // add prefix to the table name if configured
- if ($prefix = $rcube->config->get('db_prefix')) {
- return $prefix . $table;
+ // return table name if configured
+ $config_key = 'db_table_'.$table;
+
+ if ($name = $rcube->config->get($config_key)) {
+ return $name;
}
return $table;
}
/**
- * Set class option value
- *
- * @param string $name Option name
- * @param mixed $value Option value
- */
- public function set_option($name, $value)
- {
- $this->options[$name] = $value;
- }
-
- /**
* MDB2 DSN string parser
*
* @param string $sequence Secuence name
diff --git a/program/lib/Roundcube/rcube_db_mssql.php b/program/lib/Roundcube/rcube_db_mssql.php
index 3c1b9d71f..37a42678a 100644
--- a/program/lib/Roundcube/rcube_db_mssql.php
+++ b/program/lib/Roundcube/rcube_db_mssql.php
@@ -29,52 +29,38 @@ class rcube_db_mssql extends rcube_db
public $db_provider = 'mssql';
/**
- * Object constructor
- *
- * @param string $db_dsnw DSN for read/write operations
- * @param string $db_dsnr Optional DSN for read only operations
- * @param bool $pconn Enables persistent connections
+ * Driver initialization
*/
- public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
+ protected function init()
{
- parent::__construct($db_dsnw, $db_dsnr, $pconn);
-
$this->options['identifier_start'] = '[';
$this->options['identifier_end'] = ']';
}
/**
- * Driver-specific configuration of database connection
- *
- * @param array $dsn DSN for DB connections
- * @param PDO $dbh Connection handler
+ * Character setting
*/
- protected function conn_configure($dsn, $dbh)
+ protected function set_charset($charset)
{
- // Set date format in case of non-default language (#1488918)
- $this->query("SET DATEFORMAT ymd");
+ // UTF-8 is default
}
/**
* Return SQL function for current time and date
*
- * @param int $interval Optional interval (in seconds) to add/subtract
- *
* @return string SQL function to use in query
*/
- public function now($interval = 0)
+ public function now()
{
- if ($interval) {
- $interval = intval($interval);
- return "dateadd(second, $interval, getdate())";
- }
-
return "getdate()";
}
/**
* Return SQL statement to convert a field value into a unix timestamp
*
+ * This method is deprecated and should not be used anymore due to limitations
+ * of timestamp functions in Mysql (year 2038 problem)
+ *
* @param string $field Field name
*
* @return string SQL statement to use in query
diff --git a/program/lib/Roundcube/rcube_db_mysql.php b/program/lib/Roundcube/rcube_db_mysql.php
index 6fa5ad768..7f5ad2b36 100644
--- a/program/lib/Roundcube/rcube_db_mysql.php
+++ b/program/lib/Roundcube/rcube_db_mysql.php
@@ -30,13 +30,9 @@ class rcube_db_mysql extends rcube_db
public $db_provider = 'mysql';
/**
- * Object constructor
- *
- * @param string $db_dsnw DSN for read/write operations
- * @param string $db_dsnr Optional DSN for read only operations
- * @param bool $pconn Enables persistent connections
+ * Driver initialization/configuration
*/
- public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
+ protected function init()
{
if (version_compare(PHP_VERSION, '5.3.0', '<')) {
rcube::raise_error(array('code' => 600, 'type' => 'db',
@@ -45,25 +41,12 @@ class rcube_db_mysql extends rcube_db
true, true);
}
- parent::__construct($db_dsnw, $db_dsnr, $pconn);
-
// SQL identifiers quoting
$this->options['identifier_start'] = '`';
$this->options['identifier_end'] = '`';
}
/**
- * Driver-specific configuration of database connection
- *
- * @param array $dsn DSN for DB connections
- * @param PDO $dbh Connection handler
- */
- protected function conn_configure($dsn, $dbh)
- {
- $this->query("SET NAMES 'utf8'");
- }
-
- /**
* Abstract SQL statement for value concatenation
*
* @return string SQL statement to be used in query
@@ -151,7 +134,7 @@ class rcube_db_mysql extends rcube_db
$result[PDO::MYSQL_ATTR_FOUND_ROWS] = true;
// Enable AUTOCOMMIT mode (#1488902)
- $result[PDO::ATTR_AUTOCOMMIT] = true;
+ $dsn_options[PDO::ATTR_AUTOCOMMIT] = true;
return $result;
}
diff --git a/program/lib/Roundcube/rcube_db_pgsql.php b/program/lib/Roundcube/rcube_db_pgsql.php
index d72c9d6b3..a06a37c10 100644
--- a/program/lib/Roundcube/rcube_db_pgsql.php
+++ b/program/lib/Roundcube/rcube_db_pgsql.php
@@ -29,17 +29,6 @@ class rcube_db_pgsql extends rcube_db
public $db_provider = 'postgres';
/**
- * Driver-specific configuration of database connection
- *
- * @param array $dsn DSN for DB connections
- * @param PDO $dbh Connection handler
- */
- protected function conn_configure($dsn, $dbh)
- {
- $this->query("SET NAMES 'utf8'");
- }
-
- /**
* Get last inserted record ID
*
* @param string $table Table name (to find the incremented sequence)
@@ -64,20 +53,19 @@ class rcube_db_pgsql extends rcube_db
/**
* Return correct name for a specific database sequence
*
- * @param string $table Table name
+ * @param string $sequence Secuence name
*
* @return string Translated sequence name
*/
- protected function sequence_name($table)
+ protected function sequence_name($sequence)
{
- // Note: we support only one sequence per table
- // Note: The sequence name must be <table_name>_seq
- $sequence = $table . '_seq';
- $rcube = rcube::get_instance();
+ $rcube = rcube::get_instance();
// return sequence name if configured
- if ($prefix = $rcube->config->get('db_prefix')) {
- return $prefix . $sequence;
+ $config_key = 'db_sequence_'.$sequence;
+
+ if ($name = $rcube->config->get($config_key)) {
+ return $name;
}
return $sequence;
@@ -86,6 +74,9 @@ class rcube_db_pgsql extends rcube_db
/**
* Return SQL statement to convert a field value into a unix timestamp
*
+ * This method is deprecated and should not be used anymore due to limitations
+ * of timestamp functions in Mysql (year 2038 problem)
+ *
* @param string $field Field name
*
* @return string SQL statement to use in query
@@ -97,24 +88,6 @@ class rcube_db_pgsql extends rcube_db
}
/**
- * Return SQL function for current time and date
- *
- * @param int $interval Optional interval (in seconds) to add/subtract
- *
- * @return string SQL function to use in query
- */
- public function now($interval = 0)
- {
- if ($interval) {
- $add = ' ' . ($interval > 0 ? '+' : '-') . " interval '";
- $add .= $interval > 0 ? intval($interval) : intval($interval) * -1;
- $add .= " seconds'";
- }
-
- return "now()" . $add;
- }
-
- /**
* Return SQL statement for case insensitive LIKE
*
* @param string $column Field name
diff --git a/program/lib/Roundcube/rcube_db_sqlite.php b/program/lib/Roundcube/rcube_db_sqlite.php
index b66c56097..145b8a371 100644
--- a/program/lib/Roundcube/rcube_db_sqlite.php
+++ b/program/lib/Roundcube/rcube_db_sqlite.php
@@ -29,6 +29,13 @@ class rcube_db_sqlite extends rcube_db
public $db_provider = 'sqlite';
/**
+ * Database character set
+ */
+ protected function set_charset($charset)
+ {
+ }
+
+ /**
* Prepare connection
*/
protected function conn_prepare($dsn)
@@ -49,6 +56,10 @@ class rcube_db_sqlite extends rcube_db
*/
protected function conn_configure($dsn, $dbh)
{
+ // we emulate via callback some missing functions
+ $dbh->sqliteCreateFunction('unix_timestamp', array('rcube_db_sqlite', 'sqlite_unix_timestamp'), 1);
+ $dbh->sqliteCreateFunction('now', array('rcube_db_sqlite', 'sqlite_now'), 0);
+
// Initialize database structure in file is empty
if (!empty($dsn['database']) && !filesize($dsn['database'])) {
$data = file_get_contents(RCUBE_INSTALL_PATH . 'SQL/sqlite.initial.sql');
@@ -72,32 +83,30 @@ class rcube_db_sqlite extends rcube_db
}
/**
- * Return SQL statement to convert a field value into a unix timestamp
- *
- * @param string $field Field name
- *
- * @return string SQL statement to use in query
- * @deprecated
+ * Callback for sqlite: unix_timestamp()
*/
- public function unixtimestamp($field)
+ public static function sqlite_unix_timestamp($timestamp = '')
{
- return "strftime('%s', $field)";
+ $timestamp = trim($timestamp);
+ if (!$timestamp) {
+ $ret = time();
+ }
+ else if (!preg_match('/^[0-9]+$/s', $timestamp)) {
+ $ret = strtotime($timestamp);
+ }
+ else {
+ $ret = $timestamp;
+ }
+
+ return $ret;
}
/**
- * Return SQL function for current time and date
- *
- * @param int $interval Optional interval (in seconds) to add/subtract
- *
- * @return string SQL function to use in query
+ * Callback for sqlite: now()
*/
- public function now($interval = 0)
+ public static function sqlite_now()
{
- if ($interval) {
- $add = ($interval > 0 ? '+' : '') . intval($interval) . ' seconds';
- }
-
- return "datetime('now'" . ($add ? ",'$add'" : "") . ")";
+ return date("Y-m-d H:i:s");
}
/**
diff --git a/program/lib/Roundcube/rcube_db_sqlsrv.php b/program/lib/Roundcube/rcube_db_sqlsrv.php
index 45c41cdaf..e5dfb1154 100644
--- a/program/lib/Roundcube/rcube_db_sqlsrv.php
+++ b/program/lib/Roundcube/rcube_db_sqlsrv.php
@@ -29,46 +29,29 @@ class rcube_db_sqlsrv extends rcube_db
public $db_provider = 'mssql';
/**
- * Object constructor
- *
- * @param string $db_dsnw DSN for read/write operations
- * @param string $db_dsnr Optional DSN for read only operations
- * @param bool $pconn Enables persistent connections
+ * Driver initialization
*/
- public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
+ protected function init()
{
- parent::__construct($db_dsnw, $db_dsnr, $pconn);
-
$this->options['identifier_start'] = '[';
$this->options['identifier_end'] = ']';
}
/**
- * Driver-specific configuration of database connection
- *
- * @param array $dsn DSN for DB connections
- * @param PDO $dbh Connection handler
+ * Database character set setting
*/
- protected function conn_configure($dsn, $dbh)
+ protected function set_charset($charset)
{
- // Set date format in case of non-default language (#1488918)
- $this->query("SET DATEFORMAT ymd");
+ // UTF-8 is default
}
/**
* Return SQL function for current time and date
*
- * @param int $interval Optional interval (in seconds) to add/subtract
- *
* @return string SQL function to use in query
*/
- public function now($interval = 0)
+ public function now()
{
- if ($interval) {
- $interval = intval($interval);
- return "dateadd(second, $interval, getdate())";
- }
-
return "getdate()";
}
diff --git a/program/lib/Roundcube/rcube_enriched.php b/program/lib/Roundcube/rcube_enriched.php
index 12deb33ce..8c628c912 100644
--- a/program/lib/Roundcube/rcube_enriched.php
+++ b/program/lib/Roundcube/rcube_enriched.php
@@ -118,7 +118,7 @@ class rcube_enriched
$quoted = '';
$lines = explode('<br>', $a[2]);
- foreach ($lines as $line)
+ foreach ($lines as $n => $line)
$quoted .= '&gt;'.$line.'<br>';
$body = $a[1].'<span class="quotes">'.$quoted.'</span>'.$a[3];
diff --git a/program/lib/Roundcube/rcube_image.php b/program/lib/Roundcube/rcube_image.php
index 4e4caae93..ffcfd4b1d 100644
--- a/program/lib/Roundcube/rcube_image.php
+++ b/program/lib/Roundcube/rcube_image.php
@@ -93,10 +93,6 @@ class rcube_image
$convert = $rcube->config->get('im_convert_path', false);
$props = $this->props();
- if (empty($props)) {
- return false;
- }
-
if (!$filename) {
$filename = $this->image_file;
}
@@ -105,6 +101,7 @@ class rcube_image
if ($convert) {
$p['out'] = $filename;
$p['in'] = $this->image_file;
+ $p['size'] = $size.'x'.$size;
$type = $props['type'];
if (!$type && ($data = $this->identify())) {
@@ -119,37 +116,11 @@ class rcube_image
$type = 'jpg';
}
- // If only one dimension is greater than the limit convert doesn't
- // work as expected, we need to calculate new dimensions
- $scale = $size / max($props['width'], $props['height']);
+ $p += array('type' => $type, 'types' => "bmp,eps,gif,jp2,jpg,png,svg,tif", 'quality' => 75);
+ $p['-opts'] = array('-resize' => $p['size'].'>');
- // if file is smaller than the limit, we do nothing
- // but copy original file to destination file
- if ($scale >= 1 && $p['intype'] == $type) {
- $result = ($this->image_file == $filename || copy($this->image_file, $filename)) ? '' : false;
- }
- else {
- if ($scale >= 1) {
- $width = $props['width'];
- $height = $props['height'];
- }
- else {
- $width = intval($props['width'] * $scale);
- $height = intval($props['height'] * $scale);
- }
-
- $valid_types = "bmp,eps,gif,jp2,jpg,png,svg,tif";
-
- $p += array(
- 'type' => $type,
- 'quality' => 75,
- 'size' => $width . 'x' . $height,
- );
-
- if (in_array($type, explode(',', $valid_types))) { // Valid type?
- $result = rcube::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace sRGB -strip'
- . ' -quality {quality} -resize {size} {intype}:{in} {type}:{out}', $p);
- }
+ if (in_array($type, explode(',', $p['types']))) { // Valid type?
+ $result = rcube::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace sRGB -quality {quality} {-opts} {intype}:{in} {type}:{out}', $p);
}
if ($result === '') {
@@ -177,43 +148,39 @@ class rcube_image
return false;
}
- if ($image === false) {
- return false;
- }
-
$scale = $size / max($props['width'], $props['height']);
// Imagemagick resize is implemented in shrinking mode (see -resize argument above)
// we do the same here, if an image is smaller than specified size
// we do nothing but copy original file to destination file
- if ($scale >= 1) {
- $result = $this->image_file == $filename || copy($this->image_file, $filename);
+ if ($scale > 1) {
+ return $this->image_file == $filename || copy($this->image_file, $filename) ? $type : false;
}
- else {
- $width = intval($props['width'] * $scale);
- $height = intval($props['height'] * $scale);
- $new_image = imagecreatetruecolor($width, $height);
-
- // Fix transparency of gif/png image
- if ($props['gd_type'] != IMAGETYPE_JPEG) {
- imagealphablending($new_image, false);
- imagesavealpha($new_image, true);
- $transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127);
- imagefilledrectangle($new_image, 0, 0, $width, $height, $transparent);
- }
-
- imagecopyresampled($new_image, $image, 0, 0, 0, 0, $width, $height, $props['width'], $props['height']);
- $image = $new_image;
-
- if ($props['gd_type'] == IMAGETYPE_JPEG) {
- $result = imagejpeg($image, $filename, 75);
- }
- elseif($props['gd_type'] == IMAGETYPE_GIF) {
- $result = imagegif($image, $filename);
- }
- elseif($props['gd_type'] == IMAGETYPE_PNG) {
- $result = imagepng($image, $filename, 6, PNG_ALL_FILTERS);
- }
+
+ $width = $props['width'] * $scale;
+ $height = $props['height'] * $scale;
+
+ $new_image = imagecreatetruecolor($width, $height);
+
+ // Fix transparency of gif/png image
+ if ($props['gd_type'] != IMAGETYPE_JPEG) {
+ imagealphablending($new_image, false);
+ imagesavealpha($new_image, true);
+ $transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127);
+ imagefilledrectangle($new_image, 0, 0, $width, $height, $transparent);
+ }
+
+ imagecopyresampled($new_image, $image, 0, 0, 0, 0, $width, $height, $props['width'], $props['height']);
+ $image = $new_image;
+
+ if ($props['gd_type'] == IMAGETYPE_JPEG) {
+ $result = imagejpeg($image, $filename, 75);
+ }
+ elseif($props['gd_type'] == IMAGETYPE_GIF) {
+ $result = imagegif($image, $filename);
+ }
+ elseif($props['gd_type'] == IMAGETYPE_PNG) {
+ $result = imagepng($image, $filename, 6, PNG_ALL_FILTERS);
}
if ($result) {
@@ -255,7 +222,7 @@ class rcube_image
$p['out'] = $filename;
$p['type'] = self::$extensions[$type];
- $result = rcube::exec($convert . ' 2>&1 -colorspace sRGB -strip -quality 75 {in} {type}:{out}', $p);
+ $result = rcube::exec($convert . ' 2>&1 -colorspace sRGB -quality 75 {in} {type}:{out}', $p);
if ($result === '') {
@chmod($filename, 0600);
diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php
index aa074233f..ca5e35f2c 100644
--- a/program/lib/Roundcube/rcube_imap.php
+++ b/program/lib/Roundcube/rcube_imap.php
@@ -308,7 +308,14 @@ class rcube_imap extends rcube_storage
*/
public function set_folder($folder)
{
+ if ($this->folder == $folder) {
+ return;
+ }
+
$this->folder = $folder;
+
+ // clear messagecount cache for this folder
+ $this->clear_messagecount($folder);
}
@@ -606,7 +613,7 @@ class rcube_imap extends rcube_storage
}
if ($mode == 'THREADS') {
- $res = $this->threads($folder);
+ $res = $this->fetch_threads($folder, $force);
$count = $res->count();
if ($status) {
@@ -636,11 +643,11 @@ class rcube_imap extends rcube_storage
$keys[] = 'ALL';
}
if ($status) {
- $keys[] = 'MAX';
+ $keys[] = 'MAX';
}
}
- // @TODO: if $mode == 'ALL' we could try to use cache index here
+ // @TODO: if $force==false && $mode == 'ALL' we could try to use cache index here
// get message count using (E)SEARCH
// not very performant but more precise (using UNDELETED)
@@ -771,7 +778,7 @@ class rcube_imap extends rcube_storage
$threads = $mcache->get_thread($folder);
}
else {
- $threads = $this->threads($folder);
+ $threads = $this->fetch_threads($folder);
}
return $this->fetch_thread_headers($folder, $threads, $page, $slice);
@@ -780,47 +787,32 @@ class rcube_imap extends rcube_storage
/**
* Method for fetching threads data
*
- * @param string $folder Folder name
+ * @param string $folder Folder name
+ * @param bool $force Use IMAP server, no cache
*
* @return rcube_imap_thread Thread data object
*/
- function threads($folder)
+ function fetch_threads($folder, $force = false)
{
- if ($mcache = $this->get_mcache_engine()) {
+ if (!$force && ($mcache = $this->get_mcache_engine())) {
// don't store in self's internal cache, cache has it's own internal cache
return $mcache->get_thread($folder);
}
- if (!empty($this->icache['threads'])) {
- if ($this->icache['threads']->get_parameters('MAILBOX') == $folder) {
- return $this->icache['threads'];
+ if (empty($this->icache['threads'])) {
+ if (!$this->check_connection()) {
+ return new rcube_result_thread();
}
- }
- // get all threads
- $result = $this->threads_direct($folder);
+ // get all threads
+ $result = $this->conn->thread($folder, $this->threading,
+ $this->options['skip_deleted'] ? 'UNDELETED' : '', true);
- // add to internal (fast) cache
- return $this->icache['threads'] = $result;
- }
-
-
- /**
- * Method for direct fetching of threads data
- *
- * @param string $folder Folder name
- *
- * @return rcube_imap_thread Thread data object
- */
- function threads_direct($folder)
- {
- if (!$this->check_connection()) {
- return new rcube_result_thread();
+ // add to internal (fast) cache
+ $this->icache['threads'] = $result;
}
- // get all threads
- return $this->conn->thread($folder, $this->threading,
- $this->options['skip_deleted'] ? 'UNDELETED' : '', true);
+ return $this->icache['threads'];
}
@@ -1091,17 +1083,16 @@ class rcube_imap extends rcube_storage
/**
- * Returns current status of a folder (compared to the last time use)
+ * Returns current status of folder
*
* We compare the maximum UID to determine the number of
* new messages because the RECENT flag is not reliable.
*
* @param string $folder Folder name
- * @param array $diff Difference data
*
- * @return int Folder status
+ * @return int Folder status
*/
- public function folder_status($folder = null, &$diff = array())
+ public function folder_status($folder = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
@@ -1122,9 +1113,6 @@ class rcube_imap extends rcube_storage
// got new messages
if ($new['maxuid'] > $old['maxuid']) {
$result += 1;
- // get new message UIDs range, that can be used for example
- // to get the data of these messages
- $diff['new'] = ($old['maxuid'] + 1 < $new['maxuid'] ? ($old['maxuid']+1).':' : '') . $new['maxuid'];
}
// some messages has been deleted
if ($new['cnt'] < $old['cnt']) {
@@ -1175,15 +1163,12 @@ class rcube_imap extends rcube_storage
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
- * @param bool $no_threads Get not threaded index
- * @param bool $no_search Get index not limited to search result (optionally)
*
* @return rcube_result_index|rcube_result_thread List of messages (UIDs)
*/
- public function index($folder = '', $sort_field = NULL, $sort_order = NULL,
- $no_threads = false, $no_search = false
- ) {
- if (!$no_threads && $this->threading) {
+ public function index($folder = '', $sort_field = NULL, $sort_order = NULL)
+ {
+ if ($this->threading) {
return $this->thread_index($folder, $sort_field, $sort_order);
}
@@ -1195,50 +1180,43 @@ class rcube_imap extends rcube_storage
// we have a saved search result, get index from there
if ($this->search_string) {
- if ($this->search_set->is_empty()) {
- return new rcube_result_index($folder, '* SORT');
+ if ($this->search_threads) {
+ $this->search($folder, $this->search_string, $this->search_charset, $this->sort_field);
}
- // search result is an index with the same sorting?
- if (($this->search_set instanceof rcube_result_index)
- && ((!$this->sort_field && !$this->search_sorted) ||
- ($this->search_sorted && $this->search_sort_field == $this->sort_field))
- ) {
+ // use message index sort as default sorting
+ if (!$this->sort_field || $this->search_sorted) {
+ if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
+ $this->search($folder, $this->search_string, $this->search_charset, $this->sort_field);
+ }
$index = $this->search_set;
}
- // $no_search is enabled when we are not interested in
- // fetching index for search result, e.g. to sort
- // threaded search result we can use full mailbox index.
- // This makes possible to use index from cache
- else if (!$no_search) {
- if (!$this->sort_field) {
- // No sorting needed, just build index from the search result
- // @TODO: do we need to sort by UID here?
- $search = $this->search_set->get_compressed();
- $index = new rcube_result_index($folder, '* ESEARCH ALL ' . $search);
- }
- else {
- $index = $this->index_direct($folder, $this->search_charset,
- $this->sort_field, $this->search_set);
- }
+ else if (!$this->check_connection()) {
+ return new rcube_result_index();
+ }
+ else {
+ $index = $this->conn->index($folder, $this->search_set->get(),
+ $this->sort_field, $this->options['skip_deleted'], true, true);
}
- if (isset($index)) {
- if ($this->sort_order != $index->get_parameters('ORDER')) {
- $index->revert();
- }
-
- return $index;
+ if ($this->sort_order != $index->get_parameters('ORDER')) {
+ $index->revert();
}
+
+ return $index;
}
// check local cache
if ($mcache = $this->get_mcache_engine()) {
- return $mcache->get_index($folder, $this->sort_field, $this->sort_order);
+ $index = $mcache->get_index($folder, $this->sort_field, $this->sort_order);
}
-
// fetch from IMAP server
- return $this->index_direct($folder, $this->sort_field, $this->sort_order);
+ else {
+ $index = $this->index_direct(
+ $folder, $this->sort_field, $this->sort_order);
+ }
+
+ return $index;
}
@@ -1246,24 +1224,22 @@ class rcube_imap extends rcube_storage
* Return sorted list of message UIDs ignoring current search settings.
* Doesn't uses cache by default.
*
- * @param string $folder Folder to get index from
- * @param string $sort_field Sort column
- * @param string $sort_order Sort order [ASC, DESC]
- * @param rcube_result_* $search Optional messages set to limit the result
+ * @param string $folder Folder to get index from
+ * @param string $sort_field Sort column
+ * @param string $sort_order Sort order [ASC, DESC]
+ * @param bool $skip_cache Disables cache usage
*
* @return rcube_result_index Sorted list of message UIDs
*/
- public function index_direct($folder, $sort_field = null, $sort_order = null, $search = null)
+ public function index_direct($folder, $sort_field = null, $sort_order = null, $skip_cache = true)
{
- if (!empty($search)) {
- $search = $this->search_set->get_compressed();
+ if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
+ $index = $mcache->get_index($folder, $sort_field, $sort_order);
}
-
// use message index sort as default sorting
- if (!$sort_field) {
+ else if (!$sort_field) {
// use search result from count() if possible
- if (empty($search) && $this->options['skip_deleted']
- && !empty($this->icache['undeleted_idx'])
+ if ($this->options['skip_deleted'] && !empty($this->icache['undeleted_idx'])
&& $this->icache['undeleted_idx']->get_parameters('ALL') !== null
&& $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
) {
@@ -1273,12 +1249,8 @@ class rcube_imap extends rcube_storage
return new rcube_result_index();
}
else {
- $query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
- if ($search) {
- $query = trim($query . ' UID ' . $search);
- }
-
- $index = $this->conn->search($folder, $query, true);
+ $index = $this->conn->search($folder,
+ 'ALL' .($this->options['skip_deleted'] ? ' UNDELETED' : ''), true);
}
}
else if (!$this->check_connection()) {
@@ -1287,18 +1259,13 @@ class rcube_imap extends rcube_storage
// fetch complete message index
else {
if ($this->get_capability('SORT')) {
- $query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
- if ($search) {
- $query = trim($query . ' UID ' . $search);
- }
-
- $index = $this->conn->sort($folder, $sort_field, $query, true);
+ $index = $this->conn->sort($folder, $sort_field,
+ $this->options['skip_deleted'] ? 'UNDELETED' : '', true);
}
if (empty($index) || $index->is_error()) {
- $index = $this->conn->index($folder, $search ? $search : "1:*",
- $sort_field, $this->options['skip_deleted'],
- $search ? true : false, true);
+ $index = $this->conn->index($folder, "1:*", $sort_field,
+ $this->options['skip_deleted'], false, true);
}
}
@@ -1331,7 +1298,7 @@ class rcube_imap extends rcube_storage
}
else {
// get all threads (default sort order)
- $threads = $this->threads($folder);
+ $threads = $this->fetch_threads($folder);
}
$this->set_sort_order($sort_field, $sort_order);
@@ -1342,10 +1309,9 @@ class rcube_imap extends rcube_storage
/**
- * Sort threaded result, using THREAD=REFS method if available.
- * If not, use any method and re-sort the result in THREAD=REFS way.
+ * Sort threaded result, using THREAD=REFS method
*
- * @param rcube_result_thread $threads Threads result set
+ * @param rcube_result_thread $threads Threads result set
*/
protected function sort_threads($threads)
{
@@ -1359,7 +1325,7 @@ class rcube_imap extends rcube_storage
if ($this->threading != 'REFS' || ($this->sort_field && $this->sort_field != 'date')) {
$sortby = $this->sort_field ? $this->sort_field : 'date';
- $index = $this->index($this->folder, $sortby, $this->sort_order, true, true);
+ $index = $this->index_direct($this->folder, $sortby, $this->sort_order, false);
if (!$index->is_empty()) {
$threads->sort($index);
@@ -1439,6 +1405,8 @@ class rcube_imap extends rcube_storage
*/
protected function search_index($folder, $criteria='ALL', $charset=NULL, $sort_field=NULL)
{
+ $orig_criteria = $criteria;
+
if (!$this->check_connection()) {
if ($this->threading) {
return new rcube_result_thread();
@@ -2079,18 +2047,17 @@ class rcube_imap extends rcube_storage
/**
* Fetch message body of a specific message from the server
*
- * @param int Message UID
- * @param string Part number
- * @param rcube_message_part Part object created by get_structure()
- * @param mixed True to print part, resource to write part contents in
- * @param resource File pointer to save the message part
- * @param boolean Disables charset conversion
- * @param int Only read this number of bytes
- * @param boolean Enables formatting of text/* parts bodies
+ * @param int $uid Message UID
+ * @param string $part Part number
+ * @param rcube_message_part $o_part Part object created by get_structure()
+ * @param mixed $print True to print part, ressource to write part contents in
+ * @param resource $fp File pointer to save the message part
+ * @param boolean $skip_charset_conv Disables charset conversion
+ * @param int $max_bytes Only read this number of bytes
*
* @return string Message/part body if not printed
*/
- public function get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false, $max_bytes=0, $formatted=true)
+ public function get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false, $max_bytes=0)
{
if (!$this->check_connection()) {
return null;
@@ -2109,9 +2076,8 @@ class rcube_imap extends rcube_storage
}
if ($o_part && $o_part->size) {
- $formatted = $formatted && $o_part->ctype_primary == 'text';
$body = $this->conn->handlePartBody($this->folder, $uid, true,
- $part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $formatted, $max_bytes);
+ $part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $o_part->ctype_primary == 'text', $max_bytes);
}
if ($fp || $print) {
@@ -2256,14 +2222,13 @@ class rcube_imap extends rcube_storage
/**
* Append a mail message (source) to a specific folder
*
- * @param string $folder Target folder
- * @param string|array $message The message source string or filename
- * or array (of strings and file pointers)
- * @param string $headers Headers string if $message contains only the body
- * @param boolean $is_file True if $message is a filename
- * @param array $flags Message flags
- * @param mixed $date Message internal date
- * @param bool $binary Enables BINARY append
+ * @param string $folder Target folder
+ * @param string $message The message source string or filename
+ * @param string $headers Headers string if $message contains only the body
+ * @param boolean $is_file True if $message is a filename
+ * @param array $flags Message flags
+ * @param mixed $date Message internal date
+ * @param bool $binary Enables BINARY append
*
* @return int|bool Appended message UID or True on success, False on error
*/
@@ -2354,7 +2319,10 @@ class rcube_imap extends rcube_storage
// move messages
$moved = $this->conn->move($uids, $from_mbox, $to_mbox);
+ // send expunge command in order to have the moved message
+ // really deleted from the source folder
if ($moved) {
+ $this->expunge_message($uids, $from_mbox, false);
$this->clear_messagecount($from_mbox);
$this->clear_messagecount($to_mbox);
}
@@ -2656,6 +2624,7 @@ class rcube_imap extends rcube_storage
if ($list_extended) {
// unsubscribe non-existent folders, remove from the list
+ // we can do this only when LIST response is available
if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
foreach ($a_folders as $idx => $folder) {
if (($opts = $this->conn->data['LIST'][$folder])
@@ -2668,14 +2637,19 @@ class rcube_imap extends rcube_storage
}
}
else {
- // unsubscribe non-existent folders, remove them from the list
- if (is_array($a_folders) && !empty($a_folders) && $name == '*') {
- $existing = $this->list_folders($root, $name);
- $nonexisting = array_diff($a_folders, $existing);
- $a_folders = array_diff($a_folders, $nonexisting);
-
- foreach ($nonexisting as $folder) {
- $this->conn->unsubscribe($folder);
+ // unsubscribe non-existent folders, remove them from the list,
+ // we can do this only when LIST response is available
+ if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
+ foreach ($a_folders as $idx => $folder) {
+ if (!isset($this->conn->data['LIST'][$folder])
+ || in_array('\\Noselect', $this->conn->data['LIST'][$folder])
+ ) {
+ // Some servers returns \Noselect for existing folders
+ if (!$this->folder_exists($folder)) {
+ $this->conn->unsubscribe($folder);
+ unset($a_folders[$idx]);
+ }
+ }
}
}
}
@@ -2794,6 +2768,7 @@ class rcube_imap extends rcube_storage
*/
private function list_folders_update(&$result, $type = null)
{
+ $delim = $this->get_hierarchy_delimiter();
$namespace = $this->get_namespace();
$search = array();
@@ -3704,7 +3679,7 @@ class rcube_imap extends rcube_storage
{
if ($this->caching && !$this->cache) {
$rcube = rcube::get_instance();
- $ttl = $rcube->config->get('imap_cache_ttl', '10d');
+ $ttl = $rcube->config->get('message_cache_lifetime', '10d');
$this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl);
}
@@ -3752,6 +3727,21 @@ class rcube_imap extends rcube_storage
}
}
+ /**
+ * Delete outdated cache entries
+ */
+ public function expunge_cache()
+ {
+ if ($this->mcache) {
+ $ttl = rcube::get_instance()->config->get('message_cache_lifetime', '10d');
+ $this->mcache->expunge($ttl);
+ }
+
+ if ($this->cache) {
+ $this->cache->expunge();
+ }
+ }
+
/* --------------------------------
* message caching methods
@@ -3785,10 +3775,8 @@ class rcube_imap extends rcube_storage
if ($this->messages_caching && !$this->mcache) {
$rcube = rcube::get_instance();
if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) {
- $ttl = $rcube->config->get('messages_cache_ttl', '10d');
- $threshold = $rcube->config->get('messages_cache_threshold', 50);
$this->mcache = new rcube_imap_cache(
- $dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold);
+ $dbh, $this, $userid, $this->options['skip_deleted']);
}
}
@@ -3810,15 +3798,6 @@ class rcube_imap extends rcube_storage
}
- /**
- * Delete outdated cache entries
- */
- function cache_gc()
- {
- rcube_imap_cache::gc();
- }
-
-
/* --------------------------------
* protected methods
* --------------------------------*/
@@ -3852,7 +3831,7 @@ class rcube_imap extends rcube_storage
$delimiter = $this->get_hierarchy_delimiter();
// find default folders and skip folders starting with '.'
- foreach ($a_folders as $folder) {
+ foreach ($a_folders as $i => $folder) {
if ($folder[0] == '.') {
continue;
}
@@ -4112,9 +4091,9 @@ class rcube_imap extends rcube_storage
return $this->index($folder, $sort_field, $sort_order);
}
- public function message_index_direct($folder, $sort_field = null, $sort_order = null)
+ public function message_index_direct($folder, $sort_field = null, $sort_order = null, $skip_cache = true)
{
- return $this->index_direct($folder, $sort_field, $sort_order);
+ return $this->index_direct($folder, $sort_field, $sort_order, $skip_cache);
}
public function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
diff --git a/program/lib/Roundcube/rcube_imap_cache.php b/program/lib/Roundcube/rcube_imap_cache.php
index d72bfe0ab..5170e9e21 100644
--- a/program/lib/Roundcube/rcube_imap_cache.php
+++ b/program/lib/Roundcube/rcube_imap_cache.php
@@ -49,20 +49,6 @@ class rcube_imap_cache
private $userid;
/**
- * Expiration time in seconds
- *
- * @var int
- */
- private $ttl;
-
- /**
- * Maximum cached message size
- *
- * @var int
- */
- private $threshold;
-
- /**
* Internal (in-memory) cache
*
* @var array
@@ -97,26 +83,13 @@ class rcube_imap_cache
/**
* Object constructor.
- *
- * @param rcube_db $db DB handler
- * @param rcube_imap $imap IMAP handler
- * @param int $userid User identifier
- * @param bool $skip_deleted skip_deleted flag
- * @param string $ttl Expiration time of memcache/apc items
- * @param int $threshold Maximum cached message size
*/
- function __construct($db, $imap, $userid, $skip_deleted, $ttl=0, $threshold=0)
+ function __construct($db, $imap, $userid, $skip_deleted)
{
- // convert ttl string to seconds
- $ttl = get_offset_sec($ttl);
- if ($ttl > 2592000) $ttl = 2592000;
-
$this->db = $db;
$this->imap = $imap;
$this->userid = $userid;
$this->skip_deleted = $skip_deleted;
- $this->ttl = $ttl;
- $this->threshold = $threshold;
}
@@ -242,7 +215,9 @@ class rcube_imap_cache
* Return messages thread.
* If threaded index doesn't exist or is invalid, will be updated.
*
- * @param string $mailbox Folder name
+ * @param string $mailbox Folder name
+ * @param string $sort_field Sorting column
+ * @param string $sort_order Sorting order (ASC|DESC)
*
* @return array Messages threaded index
*/
@@ -281,11 +256,19 @@ class rcube_imap_cache
if ($index === null) {
// Get mailbox data (UIDVALIDITY, counters, etc.) for status check
$mbox_data = $this->imap->folder_data($mailbox);
- // Get THREADS result
- $index['object'] = $this->get_thread_data($mailbox, $mbox_data);
+
+ if ($mbox_data['EXISTS']) {
+ // get all threads (default sort order)
+ $threads = $this->imap->fetch_threads($mailbox, true);
+ }
+ else {
+ $threads = new rcube_result_thread($mailbox, '* THREAD');
+ }
+
+ $index['object'] = $threads;
// insert/update
- $this->add_thread_row($mailbox, $index['object'], $mbox_data, $exists);
+ $this->add_thread_row($mailbox, $threads, $mbox_data, $exists);
}
$this->icache[$mailbox]['thread'] = $index;
@@ -443,40 +426,23 @@ class rcube_imap_cache
if (!$force) {
$res = $this->db->query(
"UPDATE ".$this->db->table_name('cache_messages')
- ." SET flags = ?, data = ?, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+ ." SET flags = ?, data = ?, changed = ".$this->db->now()
." WHERE user_id = ?"
." AND mailbox = ?"
." AND uid = ?",
$flags, $msg, $this->userid, $mailbox, (int) $message->uid);
- if ($this->db->affected_rows($res)) {
+ if ($this->db->affected_rows()) {
return;
}
}
- $this->db->set_option('ignore_key_errors', true);
-
// insert new record
- $res = $this->db->query(
+ $this->db->query(
"INSERT INTO ".$this->db->table_name('cache_messages')
- ." (user_id, mailbox, uid, flags, expires, data)"
- ." VALUES (?, ?, ?, ?, ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?)",
+ ." (user_id, mailbox, uid, flags, changed, data)"
+ ." VALUES (?, ?, ?, ?, ".$this->db->now().", ?)",
$this->userid, $mailbox, (int) $message->uid, $flags, $msg);
-
- // race-condition, insert failed so try update (#1489146)
- // thanks to ignore_key_errors "duplicate row" errors will be ignored
- if ($force && !$res && !$this->db->is_error($res)) {
- $this->db->query(
- "UPDATE ".$this->db->table_name('cache_messages')
- ." SET expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- .", flags = ?, data = ?"
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- ." AND uid = ?",
- $flags, $msg, $this->userid, $mailbox, (int) $message->uid);
- }
-
- $this->db->set_option('ignore_key_errors', false);
}
@@ -517,7 +483,7 @@ class rcube_imap_cache
$this->db->query(
"UPDATE ".$this->db->table_name('cache_messages')
- ." SET expires = ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+ ." SET changed = ".$this->db->now()
.", flags = flags ".($enabled ? "+ $idx" : "- $idx")
." WHERE user_id = ?"
." AND mailbox = ?"
@@ -640,21 +606,23 @@ class rcube_imap_cache
/**
- * Delete expired cache entries
+ * Delete cache entries older than TTL
+ *
+ * @param string $ttl Lifetime of message cache entries
*/
- static function gc()
+ function expunge($ttl)
{
- $rcube = rcube::get_instance();
- $db = $rcube->get_dbh();
+ // get expiration timestamp
+ $ts = get_offset_time($ttl, -1);
- $db->query("DELETE FROM ".$db->table_name('cache_messages')
- ." WHERE expires < " . $db->now());
+ $this->db->query("DELETE FROM ".$this->db->table_name('cache_messages')
+ ." WHERE changed < " . $this->db->fromunixtime($ts));
- $db->query("DELETE FROM ".$db->table_name('cache_index')
- ." WHERE expires < " . $db->now());
+ $this->db->query("DELETE FROM ".$this->db->table_name('cache_index')
+ ." WHERE changed < " . $this->db->fromunixtime($ts));
- $db->query("DELETE FROM ".$db->table_name('cache_thread')
- ." WHERE expires < " . $db->now());
+ $this->db->query("DELETE FROM ".$this->db->table_name('cache_thread')
+ ." WHERE changed < " . $this->db->fromunixtime($ts));
}
@@ -746,38 +714,20 @@ class rcube_imap_cache
$data = implode('@', $data);
if ($exists) {
- $res = $this->db->query(
+ $sql_result = $this->db->query(
"UPDATE ".$this->db->table_name('cache_index')
- ." SET data = ?, valid = 1, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+ ." SET data = ?, valid = 1, changed = ".$this->db->now()
." WHERE user_id = ?"
." AND mailbox = ?",
$data, $this->userid, $mailbox);
-
- if ($this->db->affected_rows($res)) {
- return;
- }
}
-
- $this->db->set_option('ignore_key_errors', true);
-
- $res = $this->db->query(
- "INSERT INTO ".$this->db->table_name('cache_index')
- ." (user_id, mailbox, valid, expires, data)"
- ." VALUES (?, ?, 1, ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') .", ?)",
- $this->userid, $mailbox, $data);
-
- // race-condition, insert failed so try update (#1489146)
- // thanks to ignore_key_errors "duplicate row" errors will be ignored
- if (!$exists && !$res && !$this->db->is_error($res)) {
- $res = $this->db->query(
- "UPDATE ".$this->db->table_name('cache_index')
- ." SET data = ?, valid = 1, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
- $data, $this->userid, $mailbox);
+ else {
+ $sql_result = $this->db->query(
+ "INSERT INTO ".$this->db->table_name('cache_index')
+ ." (user_id, mailbox, data, valid, changed)"
+ ." VALUES (?, ?, ?, 1, ".$this->db->now().")",
+ $this->userid, $mailbox, $data);
}
-
- $this->db->set_option('ignore_key_errors', false);
}
@@ -794,41 +744,21 @@ class rcube_imap_cache
);
$data = implode('@', $data);
- $expires = ($this->ttl ? $this->db->now($this->ttl) : 'NULL');
-
if ($exists) {
- $res = $this->db->query(
+ $sql_result = $this->db->query(
"UPDATE ".$this->db->table_name('cache_thread')
- ." SET data = ?, expires = $expires"
+ ." SET data = ?, changed = ".$this->db->now()
." WHERE user_id = ?"
." AND mailbox = ?",
$data, $this->userid, $mailbox);
-
- if ($this->db->affected_rows($res)) {
- return;
- }
}
-
- $this->db->set_option('ignore_key_errors', true);
-
- $res = $this->db->query(
- "INSERT INTO ".$this->db->table_name('cache_thread')
- ." (user_id, mailbox, expires, data)"
- ." VALUES (?, ?, $expires, ?)",
- $this->userid, $mailbox, $data);
-
- // race-condition, insert failed so try update (#1489146)
- // thanks to ignore_key_errors "duplicate row" errors will be ignored
- if (!$exists && !$res && !$this->db->is_error($res)) {
- $this->db->query(
- "UPDATE ".$this->db->table_name('cache_thread')
- ." SET expires = $expires, data = ?"
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
- $data, $this->userid, $mailbox);
+ else {
+ $sql_result = $this->db->query(
+ "INSERT INTO ".$this->db->table_name('cache_thread')
+ ." (user_id, mailbox, data, changed)"
+ ." VALUES (?, ?, ?, ".$this->db->now().")",
+ $this->userid, $mailbox, $data);
}
-
- $this->db->set_option('ignore_key_errors', false);
}
@@ -1055,7 +985,7 @@ class rcube_imap_cache
$uids, true, array('FLAGS'), $index['modseq'], $qresync);
if (!empty($result)) {
- foreach ($result as $msg) {
+ foreach ($result as $id => $msg) {
$uid = $msg->uid;
// Remove deleted message
if ($this->skip_deleted && !empty($msg->flags['DELETED'])) {
@@ -1076,7 +1006,7 @@ class rcube_imap_cache
$this->db->query(
"UPDATE ".$this->db->table_name('cache_messages')
- ." SET flags = ?, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+ ." SET flags = ?, changed = ".$this->db->now()
." WHERE user_id = ?"
." AND mailbox = ?"
." AND uid = ?"
@@ -1104,18 +1034,17 @@ class rcube_imap_cache
}
}
+ // Invalidate thread index (?)
+ if (!$index['valid']) {
+ $this->remove_thread($mailbox);
+ }
+
$sort_field = $index['sort_field'];
$sort_order = $index['object']->get_parameters('ORDER');
$exists = true;
// Validate index
if (!$this->validate($mailbox, $index, $exists)) {
- // Invalidate (remove) thread index
- // if $exists=false it was already removed in validate()
- if ($exists) {
- $this->remove_thread($mailbox);
- }
-
// Update index
$data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
}
@@ -1182,16 +1111,11 @@ class rcube_imap_cache
*
* @param rcube_message_header|rcube_message_part
*/
- private function message_object_prepare(&$msg, &$size = 0)
+ private function message_object_prepare(&$msg)
{
- // Remove body too big
- if ($msg->body && ($length = strlen($msg->body))) {
- $size += $length;
-
- if ($size > $this->threshold * 1024) {
- $size -= $length;
- unset($msg->body);
- }
+ // Remove body too big (>25kB)
+ if ($msg->body && strlen($msg->body) > 25 * 1024) {
+ unset($msg->body);
}
// Fix mimetype which might be broken by some code when message is displayed
@@ -1205,13 +1129,13 @@ class rcube_imap_cache
if (is_array($msg->structure->parts)) {
foreach ($msg->structure->parts as $part) {
- $this->message_object_prepare($part, $size);
+ $this->message_object_prepare($part);
}
}
if (is_array($msg->parts)) {
foreach ($msg->parts as $part) {
- $this->message_object_prepare($part, $size);
+ $this->message_object_prepare($part);
}
}
}
@@ -1236,25 +1160,6 @@ class rcube_imap_cache
return $index;
}
-
-
- /**
- * Fetches thread data from IMAP server
- */
- private function get_thread_data($mailbox, $mbox_data = array())
- {
- if (empty($mbox_data)) {
- $mbox_data = $this->imap->folder_data($mailbox);
- }
-
- if ($mbox_data['EXISTS']) {
- // get all threads (default sort order)
- return $this->imap->threads_direct($mailbox);
- }
-
- return new rcube_result_thread($mailbox, '* THREAD');
- }
-
}
// for backward compat.
diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php
index bce4cd4e2..1b28c3bd7 100644
--- a/program/lib/Roundcube/rcube_imap_generic.php
+++ b/program/lib/Roundcube/rcube_imap_generic.php
@@ -72,8 +72,6 @@ class rcube_imap_generic
const COMMAND_CAPABILITY = 2;
const COMMAND_LASTLINE = 4;
- const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n
-
/**
* Object constructor
*/
@@ -789,21 +787,23 @@ class rcube_imap_generic
// TLS connection
if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
- $res = $this->execute('STARTTLS');
+ if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
+ $res = $this->execute('STARTTLS');
- if ($res[0] != self::ERROR_OK) {
- $this->closeConnection();
- return false;
- }
+ if ($res[0] != self::ERROR_OK) {
+ $this->closeConnection();
+ return false;
+ }
- if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
- $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
- $this->closeConnection();
- return false;
- }
+ if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
+ $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
+ $this->closeConnection();
+ return false;
+ }
- // Now we're secure, capabilities need to be reread
- $this->clearCapability();
+ // Now we're secure, capabilities need to be reread
+ $this->clearCapability();
+ }
}
// Send ID info
@@ -902,11 +902,6 @@ class rcube_imap_generic
$this->prefs['auth_type'] = 'CHECK';
}
- // disabled capabilities
- if (!empty($this->prefs['disabled_caps'])) {
- $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']);
- }
-
// additional message flags
if (!empty($this->prefs['message_flags'])) {
$this->flags = array_merge($this->flags, $this->prefs['message_flags']);
@@ -1088,8 +1083,8 @@ class rcube_imap_generic
/**
* Executes EXPUNGE command
*
- * @param string $mailbox Mailbox name
- * @param string|array $messages Message UIDs to expunge
+ * @param string $mailbox Mailbox name
+ * @param string $messages Message UIDs to expunge
*
* @return boolean True on success, False on error
*/
@@ -1107,13 +1102,10 @@ class rcube_imap_generic
// Clear internal status cache
unset($this->data['STATUS:'.$mailbox]);
- if (!empty($messages) && $messages != '*' && $this->hasCapability('UIDPLUS')) {
- $messages = self::compressMessageSet($messages);
- $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE);
- }
- else {
+ if ($messages)
+ $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE);
+ else
$result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE);
- }
if ($result == self::ERROR_OK) {
$this->selected = null; // state has changed, need to reselect
@@ -1350,8 +1342,9 @@ class rcube_imap_generic
$folders[$mailbox] = array();
}
- // store folder options
- if ($cmd == 'LIST') {
+ // store LSUB options only if not empty, this way
+ // we can detect a situation when LIST doesn't return specified folder
+ if (!empty($opts) || $cmd == 'LIST') {
// Add to options array
if (empty($this->data['LIST'][$mailbox]))
$this->data['LIST'][$mailbox] = $opts;
@@ -1583,12 +1576,11 @@ class rcube_imap_generic
}
// message IDs
- if (!empty($add)) {
+ if (!empty($add))
$add = $this->compressMessageSet($add);
- }
list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT',
- array("($field)", $encoding, !empty($add) ? $add : 'ALL'));
+ array("($field)", $encoding, 'ALL' . (!empty($add) ? ' '.$add : '')));
if ($code != self::ERROR_OK) {
$response = null;
@@ -1675,6 +1667,7 @@ class rcube_imap_generic
}
if (!empty($criteria)) {
+ $modseq = stripos($criteria, 'MODSEQ') !== false;
$params .= ($params ? ' ' : '') . $criteria;
}
else {
@@ -1813,6 +1806,7 @@ class rcube_imap_generic
if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
$flags = explode(' ', strtoupper($matches[1]));
if (in_array('\\DELETED', $flags)) {
+ $deleted[$id] = $id;
continue;
}
}
@@ -2004,6 +1998,7 @@ class rcube_imap_generic
/**
* Moves message(s) from one folder to another.
+ * Original message(s) will be marked as deleted.
*
* @param string|array $messages Message UID(s)
* @param string $from Mailbox name
@@ -2022,41 +2017,15 @@ class rcube_imap_generic
return false;
}
- // use MOVE command (RFC 6851)
- if ($this->hasCapability('MOVE')) {
- // Clear last COPYUID data
- unset($this->data['COPYUID']);
+ $r = $this->copy($messages, $from, $to);
+ if ($r) {
// Clear internal status cache
- unset($this->data['STATUS:'.$to]);
unset($this->data['STATUS:'.$from]);
- $result = $this->execute('UID MOVE', array(
- $this->compressMessageSet($messages), $this->escape($to)),
- self::COMMAND_NORESPONSE);
-
- return ($result == self::ERROR_OK);
- }
-
- // use COPY + STORE +FLAGS.SILENT \Deleted + EXPUNGE
- $result = $this->copy($messages, $from, $to);
-
- if ($result) {
- // Clear internal status cache
- unset($this->data['STATUS:'.$from]);
-
- $result = $this->flag($from, $messages, 'DELETED');
-
- if ($messages == '*') {
- // CLOSE+SELECT should be faster than EXPUNGE
- $this->close();
- }
- else {
- $this->expunge($from, $messages);
- }
+ return $this->flag($from, $messages, 'DELETED');
}
-
- return $result;
+ return $r;
}
/**
@@ -2197,7 +2166,7 @@ class rcube_imap_generic
// create array with header field:data
if (!empty($headers)) {
$headers = explode("\n", trim($headers));
- foreach ($headers as $resln) {
+ foreach ($headers as $hid => $resln) {
if (ord($resln[0]) <= 32) {
$lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln);
} else {
@@ -2205,7 +2174,7 @@ class rcube_imap_generic
}
}
- foreach ($lines as $str) {
+ while (list($lines_key, $str) = each($lines)) {
list($field, $string) = explode(':', $str, 2);
$field = strtolower($field);
@@ -2509,7 +2478,7 @@ class rcube_imap_generic
}
if ($binary) {
- // WARNING: Use $formatted argument with care, this may break binary data stream
+ // WARNING: Use $formatting argument with care, this may break binary data stream
$mode = -1;
}
@@ -2530,7 +2499,6 @@ class rcube_imap_generic
// handle one line response
if ($line[0] == '(' && substr($line, -1) == ')') {
// tokenize content inside brackets
- // the content can be e.g.: (UID 9844 BODY[2.4] NIL)
$tokens = $this->tokenizeResponse(preg_replace('/(^\(|\)$)/', '', $line));
for ($i=0; $i<count($tokens); $i+=2) {
@@ -2645,11 +2613,11 @@ class rcube_imap_generic
/**
* Handler for IMAP APPEND command
*
- * @param string $mailbox Mailbox name
- * @param string|array $message The message source string or array (of strings and file pointers)
- * @param array $flags Message flags
- * @param string $date Message internal date
- * @param bool $binary Enable BINARY append (RFC3516)
+ * @param string $mailbox Mailbox name
+ * @param string $message Message content
+ * @param array $flags Message flags
+ * @param string $date Message internal date
+ * @param bool $binary Enable BINARY append (RFC3516)
*
* @return string|bool On success APPENDUID response (if available) or True, False on failure
*/
@@ -2663,28 +2631,13 @@ class rcube_imap_generic
$binary = $binary && $this->getCapability('BINARY');
$literal_plus = !$binary && $this->prefs['literal+'];
- $len = 0;
- $msg = is_array($message) ? $message : array(&$message);
- $chunk_size = 512000;
-
- for ($i=0, $cnt=count($msg); $i<$cnt; $i++) {
- if (is_resource($msg[$i])) {
- $stat = fstat($msg[$i]);
- if ($stat === false) {
- return false;
- }
- $len += $stat['size'];
- }
- else {
- if (!$binary) {
- $msg[$i] = str_replace("\r", '', $msg[$i]);
- $msg[$i] = str_replace("\n", "\r\n", $msg[$i]);
- }
- $len += strlen($msg[$i]);
- }
+ if (!$binary) {
+ $message = str_replace("\r", '', $message);
+ $message = str_replace("\n", "\r\n", $message);
}
+ $len = strlen($message);
if (!$len) {
return false;
}
@@ -2709,32 +2662,7 @@ class rcube_imap_generic
}
}
- foreach ($msg as $msg_part) {
- // file pointer
- if (is_resource($msg_part)) {
- rewind($msg_part);
- while (!feof($msg_part) && $this->fp) {
- $buffer = fread($msg_part, $chunk_size);
- $this->putLine($buffer, false);
- }
- fclose($msg_part);
- }
- // string
- else {
- $size = strlen($msg_part);
-
- // Break up the data by sending one chunk (up to 512k) at a time.
- // This approach reduces our peak memory usage
- for ($offset = 0; $offset < $size; $offset += $chunk_size) {
- $chunk = substr($msg_part, $offset, $chunk_size);
- if (!$this->putLine($chunk, false)) {
- return false;
- }
- }
- }
- }
-
- if (!$this->putLine('')) { // \r\n
+ if (!$this->putLine($message)) {
return false;
}
@@ -2773,23 +2701,94 @@ class rcube_imap_generic
*/
function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null, $binary = false)
{
+ unset($this->data['APPENDUID']);
+
+ if ($mailbox === null || $mailbox === '') {
+ return false;
+ }
+
// open message file
+ $in_fp = false;
if (file_exists(realpath($path))) {
- $fp = fopen($path, 'r');
+ $in_fp = fopen($path, 'r');
}
- if (!$fp) {
+ if (!$in_fp) {
$this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");
return false;
}
- $message = array();
+ $body_separator = "\r\n\r\n";
+ $len = filesize($path);
+
+ if (!$len) {
+ return false;
+ }
+
if ($headers) {
- $message[] = trim($headers, "\r\n") . "\r\n\r\n";
+ $headers = preg_replace('/[\r\n]+$/', '', $headers);
+ $len += strlen($headers) + strlen($body_separator);
+ }
+
+ $binary = $binary && $this->getCapability('BINARY');
+ $literal_plus = !$binary && $this->prefs['literal+'];
+
+ // build APPEND command
+ $key = $this->nextTag();
+ $request = "$key APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')';
+ if (!empty($date)) {
+ $request .= ' ' . $this->escape($date);
}
- $message[] = $fp;
+ $request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}';
+
+ // send APPEND command
+ if ($this->putLine($request)) {
+ // Don't wait when LITERAL+ is supported
+ if (!$literal_plus) {
+ $line = $this->readReply();
- return $this->append($mailbox, $message, $flags, $date, $binary);
+ if ($line[0] != '+') {
+ $this->parseResult($line, 'APPEND: ');
+ return false;
+ }
+ }
+
+ // send headers with body separator
+ if ($headers) {
+ $this->putLine($headers . $body_separator, false);
+ }
+
+ // send file
+ while (!feof($in_fp) && $this->fp) {
+ $buffer = fgets($in_fp, 4096);
+ $this->putLine($buffer, false);
+ }
+ fclose($in_fp);
+
+ if (!$this->putLine('')) { // \r\n
+ return false;
+ }
+
+ // read response
+ do {
+ $line = $this->readLine();
+ } while (!$this->startsWith($line, $key, true, true));
+
+ // Clear internal status cache
+ unset($this->data['STATUS:'.$mailbox]);
+
+ if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK)
+ return false;
+ else if (!empty($this->data['APPENDUID']))
+ return $this->data['APPENDUID'];
+ else
+ return true;
+ }
+ else {
+ $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
+ }
+
+ return false;
}
/**
@@ -3538,7 +3537,7 @@ class rcube_imap_generic
if (is_array($element)) {
reset($element);
- foreach ($element as $value) {
+ while (list($key, $value) = each($element)) {
$string .= ' ' . self::r_implode($value);
}
}
@@ -3566,7 +3565,7 @@ class rcube_imap_generic
// if less than 255 bytes long, let's not bother
if (!$force && strlen($messages)<255) {
return $messages;
- }
+ }
// see if it's already been compressed
if (strpos($messages, ':') !== false) {
@@ -3674,20 +3673,8 @@ class rcube_imap_generic
*/
static function strToTime($date)
{
- // Clean malformed data
- $date = preg_replace(
- array(
- '/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal
- '/[^a-z0-9\x20\x09:+-]/i', // remove any invalid characters
- '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names
- ),
- array(
- '\\1',
- '',
- '',
- ), $date);
-
- $date = trim($date);
+ // support non-standard "GMTXXXX" literal
+ $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
// if date parsing fails, we have a date in non-rfc format
// remove token from the end and try again
@@ -3712,10 +3699,6 @@ class rcube_imap_generic
$this->capability = explode(' ', strtoupper($str));
- if (!empty($this->prefs['disabled_caps'])) {
- $this->capability = array_diff($this->capability, $this->prefs['disabled_caps']);
- }
-
if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
$this->prefs['literal+'] = true;
}
@@ -3761,10 +3744,9 @@ class rcube_imap_generic
/**
* Set the value of the debugging flag.
*
- * @param boolean $debug New value for the debugging flag.
- * @param callback $handler Logging handler function
+ * @param boolean $debug New value for the debugging flag.
*
- * @since 0.5-stable
+ * @since 0.5-stable
*/
function setDebug($debug, $handler = null)
{
@@ -3775,18 +3757,12 @@ class rcube_imap_generic
/**
* Write the given debug text to the current debug output handler.
*
- * @param string $message Debug mesage text.
+ * @param string $message Debug mesage text.
*
- * @since 0.5-stable
+ * @since 0.5-stable
*/
private function debug($message)
{
- if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
- $diff = $len - self::DEBUG_LINE_LENGTH;
- $message = substr($message, 0, self::DEBUG_LINE_LENGTH)
- . "... [truncated $diff bytes]";
- }
-
if ($this->resourceid) {
$message = sprintf('[%s] %s', $this->resourceid, $message);
}
diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php
index 78573789b..7c4002337 100644
--- a/program/lib/Roundcube/rcube_ldap.php
+++ b/program/lib/Roundcube/rcube_ldap.php
@@ -3,8 +3,8 @@
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2013, The Roundcube Dev Team |
- | Copyright (C) 2011-2013, Kolab Systems AG |
+ | Copyright (C) 2006-2012, The Roundcube Dev Team |
+ | Copyright (C) 2011-2012, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
@@ -27,51 +27,38 @@
*/
class rcube_ldap extends rcube_addressbook
{
- // public properties
+ /** public properties */
public $primary_key = 'ID';
- public $groups = false;
- public $readonly = true;
- public $ready = false;
- public $group_id = 0;
- public $coltypes = array();
- public $export_groups = false;
-
- // private properties
- protected $ldap;
- protected $prop = array();
+ public $groups = false;
+ public $readonly = true;
+ public $ready = false;
+ public $group_id = 0;
+ public $coltypes = array();
+
+ /** private properties */
+ protected $conn;
+ protected $prop = array();
protected $fieldmap = array();
- protected $filter = '';
protected $sub_filter;
- protected $result;
- protected $ldap_result;
+ protected $filter = '';
+ protected $result = null;
+ protected $ldap_result = null;
protected $mail_domain = '';
protected $debug = false;
- /**
- * Group objectclass (lowercase) to member attribute mapping
- *
- * @var array
- */
- private static $group_types = array(
- 'group' => 'member',
- 'groupofnames' => 'member',
- 'kolabgroupofnames' => 'member',
- 'groupofuniquenames' => 'uniqueMember',
- 'kolabgroupofuniquenames' => 'uniqueMember',
- 'univentiongroup' => 'uniqueMember',
- 'groupofurls' => null,
- );
-
- private $base_dn = '';
+ private $base_dn = '';
private $groups_base_dn = '';
- private $group_url;
+ private $group_url = null;
private $cache;
+ private $vlv_active = false;
+ private $vlv_count = 0;
+
/**
* Object constructor
*
- * @param array $p LDAP connection properties
+ * @param array $p LDAP connection properties
* @param boolean $debug Enables debug mode
* @param string $mail_domain Current user mail domain name
*/
@@ -79,7 +66,8 @@ class rcube_ldap extends rcube_addressbook
{
$this->prop = $p;
- $fetch_attributes = array('objectClass');
+ if (isset($p['searchonly']))
+ $this->searchonly = $p['searchonly'];
// check if groups are configured
if (is_array($p['groups']) && count($p['groups'])) {
@@ -94,21 +82,6 @@ class rcube_ldap extends rcube_addressbook
$this->prop['groups']['name_attr'] = 'cn';
if (empty($this->prop['groups']['scope']))
$this->prop['groups']['scope'] = 'sub';
-
- // add group name attrib to the list of attributes to be fetched
- $fetch_attributes[] = $this->prop['groups']['name_attr'];
- }
- if (is_array($p['group_filters']) && count($p['group_filters'])) {
- $this->groups = true;
-
- foreach ($p['group_filters'] as $k => $group_filter) {
- // set default name attribute to cn
- if (empty($group_filter['name_attr']) && empty($this->prop['groups']['name_attr']))
- $this->prop['group_filters'][$k]['name_attr'] = $group_filter['name_attr'] = 'cn';
-
- if ($group_filter['name_attr'])
- $fetch_attributes[] = $group_filter['name_attr'];
- }
}
// fieldmap property is given
@@ -196,7 +169,7 @@ class rcube_ldap extends rcube_addressbook
// Build sub_fields filter
if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
$this->sub_filter = '';
- foreach ($this->prop['sub_fields'] as $class) {
+ foreach ($this->prop['sub_fields'] as $attr => $class) {
if (!empty($class)) {
$class = is_array($class) ? array_pop($class) : $class;
$this->sub_filter .= '(objectClass=' . $class . ')';
@@ -213,24 +186,7 @@ class rcube_ldap extends rcube_addressbook
// initialize cache
$rcube = rcube::get_instance();
- if ($cache_type = $rcube->config->get('ldap_cache', 'db')) {
- $cache_ttl = $rcube->config->get('ldap_cache_ttl', '10m');
- $cache_name = 'LDAP.' . asciiwords($this->prop['name']);
-
- $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl);
- }
-
- // determine which attributes to fetch
- $this->prop['list_attributes'] = array_unique($fetch_attributes);
- $this->prop['attributes'] = array_merge(array_values($this->fieldmap), $fetch_attributes);
- foreach ($rcube->config->get('contactlist_fields') as $col) {
- $this->prop['list_attributes'] = array_merge($this->prop['list_attributes'], $this->_map_field($col));
- }
-
- // initialize ldap wrapper object
- $this->ldap = new rcube_ldap_generic($this->prop);
- $this->ldap->set_cache($this->cache);
- $this->ldap->set_debug($this->debug);
+ $this->cache = $rcube->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600);
$this->_connect();
}
@@ -243,18 +199,49 @@ class rcube_ldap extends rcube_addressbook
{
$rcube = rcube::get_instance();
- if ($this->ready)
+ if (!function_exists('ldap_connect'))
+ rcube::raise_error(array('code' => 100, 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "No ldap support in this installation of PHP"),
+ true, true);
+
+ if (is_resource($this->conn))
return true;
if (!is_array($this->prop['hosts']))
$this->prop['hosts'] = array($this->prop['hosts']);
+ if (empty($this->prop['ldap_version']))
+ $this->prop['ldap_version'] = 3;
+
// try to connect + bind for every host configured
// with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
// see http://www.php.net/manual/en/function.ldap-connect.php
foreach ($this->prop['hosts'] as $host) {
- // skip host if connection failed
- if (!$this->ldap->connect($host)) {
+ $host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
+ $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : '');
+
+ $this->_debug("C: Connect [$hostname] [{$this->prop['name']}]");
+
+ if ($lc = @ldap_connect($host, $this->prop['port'])) {
+ if ($this->prop['use_tls'] === true)
+ if (!ldap_start_tls($lc))
+ continue;
+
+ $this->_debug("S: OK");
+
+ ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
+ $this->prop['host'] = $host;
+ $this->conn = $lc;
+
+ if (!empty($this->prop['network_timeout']))
+ ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->prop['network_timeout']);
+
+ if (isset($this->prop['referrals']))
+ ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']);
+ }
+ else {
+ $this->_debug("S: NOT OK");
continue;
}
@@ -269,7 +256,7 @@ class rcube_ldap extends rcube_addressbook
$this->base_dn = $this->prop['base_dn'];
$this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
- $this->prop['groups']['base_dn'] : $this->base_dn;
+ $this->prop['groups']['base_dn'] : $this->base_dn;
// User specific access, generate the proper values to use.
if ($this->prop['user_specific']) {
@@ -288,47 +275,30 @@ class rcube_ldap extends rcube_addressbook
$replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
- // Search for the dn to use to authenticate
if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
- $search_bind_dn = strtr($this->prop['search_bind_dn'], $replaces);
- $search_base_dn = strtr($this->prop['search_base_dn'], $replaces);
- $search_filter = strtr($this->prop['search_filter'], $replaces);
-
- $cache_key = 'DN.' . md5("$host:$search_bind_dn:$search_base_dn:$search_filter:"
- .$this->prop['search_bind_pw']);
-
- if ($this->cache && ($dn = $this->cache->get($cache_key))) {
- $replaces['%dn'] = $dn;
+ if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) {
+ $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']);
}
- else {
- $ldap = $this->ldap;
- if (!empty($search_bind_dn) && !empty($this->prop['search_bind_pw'])) {
- // To protect from "Critical extension is unavailable" error
- // we need to use a separate LDAP connection
- if (!empty($this->prop['vlv'])) {
- $ldap = new rcube_ldap_generic($this->prop);
- $ldap->set_debug($this->debug);
- $ldap->set_cache($this->cache);
- if (!$ldap->connect($host)) {
- continue;
- }
- }
-
- if (!$ldap->bind($search_bind_dn, $this->prop['search_bind_pw'])) {
- continue; // bind failed, try next host
- }
- }
- $res = $ldap->search($search_base_dn, $search_filter, 'sub', array('uid'));
- if ($res) {
- $res->rewind();
- $replaces['%dn'] = $res->get_dn();
- }
+ // Search for the dn to use to authenticate
+ $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
+ $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
+
+ $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
- if ($ldap != $this->ldap) {
- $ldap->close();
+ $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
+ if ($res) {
+ if (($entry = ldap_first_entry($this->conn, $res))
+ && ($bind_dn = ldap_get_dn($this->conn, $entry))
+ ) {
+ $this->_debug("S: search returned dn: $bind_dn");
+ $dn = ldap_explode_dn($bind_dn, 1);
+ $replaces['%dn'] = $dn[0];
}
}
+ else {
+ $this->_debug("S: ".ldap_error($this->conn));
+ }
// DN not found
if (empty($replaces['%dn'])) {
@@ -339,13 +309,9 @@ class rcube_ldap extends rcube_addressbook
'code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "DN not found using LDAP search."), true);
- continue;
+ return false;
}
}
-
- if ($this->cache && !empty($replaces['%dn'])) {
- $this->cache->set($cache_key, $replaces['%dn']);
- }
}
// Replace the bind_dn and base_dn variables.
@@ -363,13 +329,13 @@ class rcube_ldap extends rcube_addressbook
}
else {
if (!empty($bind_dn)) {
- $this->ready = $this->ldap->bind($bind_dn, $bind_pass);
+ $this->ready = $this->bind($bind_dn, $bind_pass);
}
else if (!empty($this->prop['auth_cid'])) {
- $this->ready = $this->ldap->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
+ $this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
}
else {
- $this->ready = $this->ldap->sasl_bind($bind_user, $bind_pass);
+ $this->ready = $this->sasl_bind($bind_user, $bind_pass);
}
}
@@ -380,10 +346,10 @@ class rcube_ldap extends rcube_addressbook
} // end foreach hosts
- if (!is_resource($this->ldap->conn)) {
+ if (!is_resource($this->conn)) {
rcube::raise_error(array('code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Could not connect to any LDAP server, last tried $host"), true);
+ 'message' => "Could not connect to any LDAP server, last tried $hostname"), true);
return false;
}
@@ -393,47 +359,112 @@ class rcube_ldap extends rcube_addressbook
/**
- * Close connection to LDAP server
+ * Bind connection with (SASL-) user and password
+ *
+ * @param string $authc Authentication user
+ * @param string $pass Bind password
+ * @param string $authz Autorization user
+ *
+ * @return boolean True on success, False on error
*/
- function close()
+ public function sasl_bind($authc, $pass, $authz=null)
{
- if ($this->ldap) {
- $this->ldap->close();
+ if (!$this->conn) {
+ return false;
+ }
+
+ if (!function_exists('ldap_sasl_bind')) {
+ rcube::raise_error(array('code' => 100, 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Unable to bind: ldap_sasl_bind() not exists"),
+ true, true);
}
+
+ if (!empty($authz)) {
+ $authz = 'u:' . $authz;
+ }
+
+ if (!empty($this->prop['auth_method'])) {
+ $method = $this->prop['auth_method'];
+ }
+ else {
+ $method = 'DIGEST-MD5';
+ }
+
+ $this->_debug("C: Bind [mech: $method, authc: $authc, authz: $authz] [pass: $pass]");
+
+ if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ $this->_debug("S: ".ldap_error($this->conn));
+
+ rcube::raise_error(array(
+ 'code' => ldap_errno($this->conn), 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Bind failed for authcid=$authc ".ldap_error($this->conn)),
+ true);
+
+ return false;
}
/**
- * Returns address book name
+ * Bind connection with DN and password
*
- * @return string Address book name
+ * @param string Bind DN
+ * @param string Bind password
+ *
+ * @return boolean True on success, False on error
*/
- function get_name()
+ public function bind($dn, $pass)
{
- return $this->prop['name'];
+ if (!$this->conn) {
+ return false;
+ }
+
+ $this->_debug("C: Bind [dn: $dn] [pass: $pass]");
+
+ if (@ldap_bind($this->conn, $dn, $pass)) {
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ $this->_debug("S: ".ldap_error($this->conn));
+
+ rcube::raise_error(array(
+ 'code' => ldap_errno($this->conn), 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
+ true);
+
+ return false;
}
/**
- * Set internal list page
- *
- * @param number Page number to list
+ * Close connection to LDAP server
*/
- function set_page($page)
+ function close()
{
- $this->list_page = (int)$page;
- $this->ldap->set_vlv_page($this->list_page, $this->page_size);
+ if ($this->conn)
+ {
+ $this->_debug("C: Close");
+ ldap_unbind($this->conn);
+ $this->conn = null;
+ }
}
+
/**
- * Set internal page size
+ * Returns address book name
*
- * @param number Number of records to display on one page
+ * @return string Address book name
*/
- function set_pagesize($size)
+ function get_name()
{
- $this->page_size = (int)$size;
- $this->ldap->set_vlv_page($this->list_page, $this->page_size);
+ return $this->prop['name'];
}
@@ -493,14 +524,16 @@ class rcube_ldap extends rcube_addressbook
*/
function list_records($cols=null, $subset=0)
{
- if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id) {
+ if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id)
+ {
$this->result = new rcube_result_set(0);
$this->result->searchonly = true;
return $this->result;
}
// fetch group members recursively
- if ($this->group_id && $this->group_data['dn']) {
+ if ($this->group_id && $this->group_data['dn'])
+ {
$entries = $this->list_group_members($this->group_data['dn']);
// make list of entries unique and sort it
@@ -514,35 +547,34 @@ class rcube_ldap extends rcube_addressbook
$entries['count'] = count($entries);
$this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
}
- else {
- $prop = $this->group_id ? $this->group_data : $this->prop;
- $base_dn = $this->group_id ? $this->group_base_dn : $this->base_dn;
-
- // use global search filter
- if (!empty($this->filter))
- $prop['filter'] = $this->filter;
+ else
+ {
+ // add general filter to query
+ if (!empty($this->prop['filter']) && empty($this->filter))
+ $this->set_search_set($this->prop['filter']);
// exec LDAP search if no result resource is stored
- if ($this->ready && !$this->ldap_result)
- $this->ldap_result = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], $this->prop['attributes'], $prop);
+ if ($this->conn && !$this->ldap_result)
+ $this->_exec_search();
// count contacts for this user
$this->result = $this->count();
// we have a search result resource
- if ($this->ldap_result && $this->result->count > 0) {
+ if ($this->ldap_result && $this->result->count > 0)
+ {
// sorting still on the ldap server
- if ($this->sort_col && $prop['scope'] !== 'base' && !$this->ldap->vlv_active)
- $this->ldap_result->sort($this->sort_col);
+ if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active)
+ ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
// get all entries from the ldap server
- $entries = $this->ldap_result->entries();
+ $entries = ldap_get_entries($this->conn, $this->ldap_result);
}
} // end else
// start and end of the page
- $start_row = $this->ldap->vlv_active ? 0 : $this->result->first;
+ $start_row = $this->vlv_active ? 0 : $this->result->first;
$start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
$last_row = $this->result->first + $this->page_size;
$last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
@@ -567,34 +599,43 @@ class rcube_ldap extends rcube_addressbook
// fetch group object
if (empty($entries)) {
- $attribs = array('dn','objectClass','member','uniqueMember','memberURL');
- $entries = $this->ldap->read_entries($dn, '(objectClass=*)', $attribs);
- if ($entries === false) {
+ $result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
+ if ($result === false)
+ {
+ $this->_debug("S: ".ldap_error($this->conn));
return $group_members;
}
+
+ $entries = @ldap_get_entries($this->conn, $result);
}
- for ($i=0; $i < $entries['count']; $i++) {
+ for ($i=0; $i < $entries['count']; $i++)
+ {
$entry = $entries[$i];
- $attrs = array();
- foreach ((array)$entry['objectclass'] as $objectclass) {
- if (strtolower($objectclass) == 'groupofurls') {
- $members = $this->_list_group_memberurl($dn, $entry, $count);
- $group_members = array_merge($group_members, $members);
- }
- else if (($member_attr = $this->get_group_member_attr(array($objectclass), ''))
- && ($member_attr = strtolower($member_attr)) && !in_array($member_attr, $attrs)
- ) {
- $members = $this->_list_group_members($dn, $entry, $member_attr, $count);
- $group_members = array_merge($group_members, $members);
- $attrs[] = $member_attr;
- }
+ if (empty($entry['objectclass']))
+ continue;
- if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit']) {
- break 2;
+ foreach ((array)$entry['objectclass'] as $objectclass)
+ {
+ switch (strtolower($objectclass)) {
+ case "group":
+ case "groupofnames":
+ case "kolabgroupofnames":
+ $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'member', $count));
+ break;
+ case "groupofuniquenames":
+ case "kolabgroupofuniquenames":
+ $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'uniquemember', $count));
+ break;
+ case "groupofurls":
+ $group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry, $count));
+ break;
}
}
+
+ if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit'])
+ break;
}
return array_filter($group_members);
@@ -613,24 +654,28 @@ class rcube_ldap extends rcube_addressbook
// Use the member attributes to return an array of member ldap objects
// NOTE that the member attribute is supposed to contain a DN
$group_members = array();
- if (empty($entry[$attr])) {
+ if (empty($entry[$attr]))
return $group_members;
- }
// read these attributes for all members
- $attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
+ $attrib = $count ? array('dn') : array_values($this->fieldmap);
+ $attrib[] = 'objectClass';
$attrib[] = 'member';
$attrib[] = 'uniqueMember';
$attrib[] = 'memberURL';
- $filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)';
-
- for ($i=0; $i < $entry[$attr]['count']; $i++) {
+ for ($i=0; $i < $entry[$attr]['count']; $i++)
+ {
if (empty($entry[$attr][$i]))
continue;
- $members = $this->ldap->read_entries($entry[$attr][$i], $filter, $attrib);
- if ($members == false) {
+ $result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)',
+ $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']);
+
+ $members = @ldap_get_entries($this->conn, $result);
+ if ($members == false)
+ {
+ $this->_debug("S: ".ldap_error($this->conn));
$members = array();
}
@@ -656,22 +701,34 @@ class rcube_ldap extends rcube_addressbook
{
$group_members = array();
- for ($i=0; $i < $entry['memberurl']['count']; $i++) {
+ for ($i=0; $i < $entry['memberurl']['count']; $i++)
+ {
// extract components from url
if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))
continue;
// add search filter if any
$filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3];
- $attrs = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
- if ($result = $this->ldap->search($m[1], $filter, $m[2], $attrs, $this->group_data)) {
- $entries = $result->entries();
- for ($j = 0; $j < $entries['count']; $j++) {
- if (self::is_group_entry($entries[$j]) && ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count)))
- $group_members = array_merge($group_members, $nested_group_members);
- else
- $group_members[] = $entries[$j];
- }
+ $func = $m[2] == 'sub' ? 'ldap_search' : ($m[2] == 'base' ? 'ldap_read' : 'ldap_list');
+
+ $attrib = $count ? array('dn') : array_values($this->fieldmap);
+ if ($result = @$func($this->conn, $m[1], $filter,
+ $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
+ ) {
+ $this->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]);
+ }
+ else {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return $group_members;
+ }
+
+ $entries = @ldap_get_entries($this->conn, $result);
+ for ($j = 0; $j < $entries['count']; $j++)
+ {
+ if ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count))
+ $group_members = array_merge($group_members, $nested_group_members);
+ else
+ $group_members[] = $entries[$j];
}
}
@@ -707,11 +764,14 @@ class rcube_ldap extends rcube_addressbook
$mode = intval($mode);
// special treatment for ID-based search
- if ($fields == 'ID' || $fields == $this->primary_key) {
+ if ($fields == 'ID' || $fields == $this->primary_key)
+ {
$ids = !is_array($value) ? explode(',', $value) : $value;
$result = new rcube_result_set();
- foreach ($ids as $id) {
- if ($rec = $this->get_record($id, true)) {
+ foreach ($ids as $id)
+ {
+ if ($rec = $this->get_record($id, true))
+ {
$result->add($rec);
$result->count++;
}
@@ -723,20 +783,34 @@ class rcube_ldap extends rcube_addressbook
$rcube = rcube::get_instance();
$list_fields = $rcube->config->get('contactlist_fields');
- if ($this->prop['vlv_search'] && $this->ready && join(',', (array)$fields) == join(',', $list_fields)) {
+ if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == join(',', $list_fields))
+ {
+ // add general filter to query
+ if (!empty($this->prop['filter']) && empty($this->filter))
+ $this->set_search_set($this->prop['filter']);
+
+ // set VLV controls with encoded search string
+ $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size, $value);
+
+ $function = $this->_scope2func($this->prop['scope']);
+ $this->ldap_result = @$function($this->conn, $this->base_dn, $this->filter ? $this->filter : '(objectclass=*)',
+ array_values($this->fieldmap), 0, $this->page_size, (int)$this->prop['timelimit']);
+
$this->result = new rcube_result_set(0);
- $search_suffix = $this->prop['fuzzy_search'] && $mode != 1 ? '*' : '';
- $ldap_data = $this->ldap->search($this->base_dn, $this->prop['filter'], $this->prop['scope'], $this->prop['attributes'],
- array('search' => $value . $search_suffix /*, 'sort' => $this->prop['sort'] */));
- if ($ldap_data === false) {
+ if (!$this->ldap_result) {
+ $this->_debug("S: ".ldap_error($this->conn));
return $this->result;
}
+ $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
+
// get all entries of this page and post-filter those that really match the query
- $search = mb_strtolower($value);
- foreach ($ldap_data as $i => $entry) {
- $rec = $this->_ldap2result($entry);
+ $search = mb_strtolower($value);
+ $entries = ldap_get_entries($this->conn, $this->ldap_result);
+
+ for ($i = 0; $i < $entries['count']; $i++) {
+ $rec = $this->_ldap2result($entries[$i]);
foreach ($fields as $f) {
foreach ((array)$rec[$f] as $val) {
if ($this->compare_search_value($f, $val, $search, $mode)) {
@@ -762,27 +836,31 @@ class rcube_ldap extends rcube_addressbook
}
}
- if ($fields == '*') {
+ if ($fields == '*')
+ {
// search_fields are required for fulltext search
- if (empty($this->prop['search_fields'])) {
+ if (empty($this->prop['search_fields']))
+ {
$this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
$this->result = new rcube_result_set();
return $this->result;
}
- if (is_array($this->prop['search_fields'])) {
+ if (is_array($this->prop['search_fields']))
+ {
foreach ($this->prop['search_fields'] as $field) {
- $filter .= "($field=$wp" . rcube_ldap_generic::quote_string($value) . "$ws)";
+ $filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)";
}
}
}
- else {
+ else
+ {
foreach ((array)$fields as $idx => $field) {
$val = is_array($value) ? $value[$idx] : $value;
if ($attrs = $this->_map_field($field)) {
if (count($attrs) > 1)
$filter .= '(|';
foreach ($attrs as $f)
- $filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
+ $filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)";
if (count($attrs) > 1)
$filter .= ')';
}
@@ -817,6 +895,7 @@ class rcube_ldap extends rcube_addressbook
// set filter string and execute search
$this->set_search_set($filter);
+ $this->_exec_search();
if ($select)
$this->list_records();
@@ -835,21 +914,20 @@ class rcube_ldap extends rcube_addressbook
function count()
{
$count = 0;
- if ($this->ldap_result) {
- $count = $this->ldap_result->count();
+ if ($this->conn && $this->ldap_result) {
+ $count = $this->vlv_active ? $this->vlv_count : ldap_count_entries($this->conn, $this->ldap_result);
}
else if ($this->group_id && $this->group_data['dn']) {
$count = count($this->list_group_members($this->group_data['dn'], true));
}
- // We have a connection but no result set, attempt to get one.
- else if ($this->ready) {
- $prop = $this->group_id ? $this->group_data : $this->prop;
- $base_dn = $this->group_id ? $this->group_base_dn : $this->base_dn;
-
- if (!empty($this->filter)) { // Use global search filter
- $prop['filter'] = $this->filter;
+ else if ($this->conn) {
+ // We have a connection but no result set, attempt to get one.
+ if (empty($this->filter)) {
+ // The filter is not set, set it.
+ $this->filter = $this->prop['filter'];
}
- $count = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], array('dn'), $prop, true);
+
+ $count = (int) $this->_exec_search(true);
}
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
@@ -879,16 +957,28 @@ class rcube_ldap extends rcube_addressbook
{
$res = $this->result = null;
- if ($this->ready && $dn) {
+ if ($this->conn && $dn)
+ {
$dn = self::dn_decode($dn);
- if ($rec = $this->ldap->get_entry($dn)) {
- $rec = array_change_key_case($rec, CASE_LOWER);
+ $this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
+
+ if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap))) {
+ $this->_debug("S: OK");
+
+ $entry = ldap_first_entry($this->conn, $ldap_result);
+
+ if ($entry && ($rec = ldap_get_attributes($this->conn, $entry))) {
+ $rec = array_change_key_case($rec, CASE_LOWER);
+ }
+ }
+ else {
+ $this->_debug("S: ".ldap_error($this->conn));
}
// Use ldap_list to get subentries like country (c) attribute (#1488123)
if (!empty($rec) && $this->sub_filter) {
- if ($entries = $this->ldap->list_entries($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
+ if ($entries = $this->ldap_list($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
foreach ($entries as $entry) {
$lrec = array_change_key_case($entry, CASE_LOWER);
$rec = array_merge($lrec, $rec);
@@ -900,7 +990,7 @@ class rcube_ldap extends rcube_addressbook
// Add in the dn for the entry.
$rec['dn'] = $dn;
$res = $this->_ldap2result($rec);
- $this->result = new rcube_result_set(1);
+ $this->result = new rcube_result_set();
$this->result->add($res);
}
}
@@ -947,6 +1037,7 @@ class rcube_ldap extends rcube_addressbook
$mail_field = $this->fieldmap['email'];
// try to extract surname and firstname from displayname
+ $reverse_map = array_flip($this->fieldmap);
$name_parts = preg_split('/[\s,.]+/', $save_data['name']);
if ($sn_field && $missing[$sn_field]) {
@@ -1013,12 +1104,12 @@ class rcube_ldap extends rcube_addressbook
}
// Build the new entries DN.
- $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap_generic::quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
+ $dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
// Remove attributes that need to be added separately (child objects)
$xfields = array();
if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
- foreach (array_keys($this->prop['sub_fields']) as $xf) {
+ foreach ($this->prop['sub_fields'] as $xf => $xclass) {
if (!empty($newentry[$xf])) {
$xfields[$xf] = $newentry[$xf];
unset($newentry[$xf]);
@@ -1026,19 +1117,19 @@ class rcube_ldap extends rcube_addressbook
}
}
- if (!$this->ldap->add($dn, $newentry)) {
+ if (!$this->ldap_add($dn, $newentry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
foreach ($xfields as $xidx => $xf) {
- $xdn = $xidx.'='.rcube_ldap_generic::quote_string($xf).','.$dn;
+ $xdn = $xidx.'='.$this->_quote_string($xf).','.$dn;
$xf = array(
$xidx => $xf,
'objectClass' => (array) $this->prop['sub_fields'][$xidx],
);
- $this->ldap->add($xdn, $xf);
+ $this->ldap_add($xdn, $xf);
}
$dn = self::dn_encode($dn);
@@ -1081,7 +1172,7 @@ class rcube_ldap extends rcube_addressbook
}
}
- foreach ($this->fieldmap as $fld) {
+ foreach ($this->fieldmap as $col => $fld) {
if ($fld) {
$val = $ldap_data[$fld];
$old = $old_data[$fld];
@@ -1144,7 +1235,7 @@ class rcube_ldap extends rcube_addressbook
// Update the entry as required.
if (!empty($deletedata)) {
// Delete the fields.
- if (!$this->ldap->mod_del($dn, $deletedata)) {
+ if (!$this->ldap_mod_del($dn, $deletedata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
@@ -1154,17 +1245,17 @@ class rcube_ldap extends rcube_addressbook
// Handle RDN change
if ($replacedata[$this->prop['LDAP_rdn']]) {
$newdn = $this->prop['LDAP_rdn'].'='
- .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
+ .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true)
.','.$this->base_dn;
if ($dn != $newdn) {
$newrdn = $this->prop['LDAP_rdn'].'='
- .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
+ .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true);
unset($replacedata[$this->prop['LDAP_rdn']]);
}
}
// Replace the fields.
if (!empty($replacedata)) {
- if (!$this->ldap->mod_replace($dn, $replacedata)) {
+ if (!$this->ldap_mod_replace($dn, $replacedata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
@@ -1180,8 +1271,8 @@ class rcube_ldap extends rcube_addressbook
// remove sub-entries
if (!empty($subdeldata)) {
foreach ($subdeldata as $fld => $val) {
- $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
- if (!$this->ldap->delete($subdn)) {
+ $subdn = $fld.'='.$this->_quote_string($val).','.$dn;
+ if (!$this->ldap_delete($subdn)) {
return false;
}
}
@@ -1189,7 +1280,7 @@ class rcube_ldap extends rcube_addressbook
if (!empty($newdata)) {
// Add the fields.
- if (!$this->ldap->mod_add($dn, $newdata)) {
+ if (!$this->ldap_mod_add($dn, $newdata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
@@ -1197,7 +1288,7 @@ class rcube_ldap extends rcube_addressbook
// Handle RDN change
if (!empty($newrdn)) {
- if (!$this->ldap->rename($dn, $newrdn, null, true)) {
+ if (!$this->ldap_rename($dn, $newrdn, null, true)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
@@ -1208,7 +1299,8 @@ class rcube_ldap extends rcube_addressbook
// change the group membership of the contact
if ($this->groups) {
$group_ids = $this->get_record_groups($dn);
- foreach (array_keys($group_ids) as $group_id) {
+ foreach ($group_ids as $group_id)
+ {
$this->remove_from_group($group_id, $dn);
$this->add_to_group($group_id, $newdn);
}
@@ -1220,12 +1312,12 @@ class rcube_ldap extends rcube_addressbook
// add sub-entries
if (!empty($subnewdata)) {
foreach ($subnewdata as $fld => $val) {
- $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
+ $subdn = $fld.'='.$this->_quote_string($val).','.$dn;
$xf = array(
$fld => $val,
'objectClass' => (array) $this->prop['sub_fields'][$fld],
);
- $this->ldap->add($subdn, $xf);
+ $this->ldap_add($subdn, $xf);
}
}
@@ -1253,9 +1345,9 @@ class rcube_ldap extends rcube_addressbook
// Need to delete all sub-entries first
if ($this->sub_filter) {
- if ($entries = $this->ldap->list_entries($dn, $this->sub_filter)) {
+ if ($entries = $this->ldap_list($dn, $this->sub_filter)) {
foreach ($entries as $entry) {
- if (!$this->ldap->delete($entry['dn'])) {
+ if (!$this->ldap_delete($entry['dn'])) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
@@ -1264,7 +1356,7 @@ class rcube_ldap extends rcube_addressbook
}
// Delete the record.
- if (!$this->ldap->delete($dn)) {
+ if (!$this->ldap_delete($dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
@@ -1273,7 +1365,7 @@ class rcube_ldap extends rcube_addressbook
if ($this->groups) {
$dn = self::dn_encode($dn);
$group_ids = $this->get_record_groups($dn);
- foreach (array_keys($group_ids) as $group_id) {
+ foreach ($group_ids as $group_id) {
$this->remove_from_group($group_id, $dn);
}
}
@@ -1288,8 +1380,8 @@ class rcube_ldap extends rcube_addressbook
*/
function delete_all()
{
- // searching for contact entries
- $dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
+ //searching for contact entries
+ $dn_list = $this->ldap_list($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
if (!empty($dn_list)) {
foreach ($dn_list as $idx => $entry) {
@@ -1306,10 +1398,6 @@ class rcube_ldap extends rcube_addressbook
*/
protected function add_autovalues(&$attrs)
{
- if (empty($this->prop['autovalues'])) {
- return;
- }
-
$attrvals = array();
foreach ($attrs as $k => $v) {
$attrvals['{'.$k.'}'] = is_array($v) ? $v[0] : $v;
@@ -1320,16 +1408,7 @@ class rcube_ldap extends rcube_addressbook
if (strpos($templ, '(') !== false) {
// replace {attr} placeholders with (escaped!) attribute values to be safely eval'd
$code = preg_replace('/\{\w+\}/', '', strtr($templ, array_map('addslashes', $attrvals)));
- $fn = create_function('', "return ($code);");
- if (!$fn) {
- rcube::raise_error(array(
- 'code' => 505, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Expression parse error on: ($code)"), true, false);
- continue;
- }
-
- $attrs[$lf] = $fn();
+ $attrs[$lf] = eval("return ($code);");
}
else {
// replace {attr} placeholders with concrete attribute values
@@ -1339,26 +1418,120 @@ class rcube_ldap extends rcube_addressbook
}
}
+ /**
+ * Execute the LDAP search based on the stored credentials
+ */
+ private function _exec_search($count = false)
+ {
+ if ($this->ready)
+ {
+ $filter = $this->filter ? $this->filter : '(objectclass=*)';
+ $function = $this->_scope2func($this->prop['scope'], $ns_function);
+
+ $this->_debug("C: Search [$filter][dn: $this->base_dn]");
+
+ // when using VLV, we get the total count by...
+ if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) {
+ // ...either reading numSubOrdinates attribute
+ if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
+ $counts = ldap_get_entries($this->conn, $result_count);
+ for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++)
+ $this->vlv_count += $counts[$j]['numsubordinates'][0];
+ $this->_debug("D: total numsubordinates = " . $this->vlv_count);
+ }
+ else if (!function_exists('ldap_parse_virtuallist_control')) // ...or by fetching all records dn and count them
+ $this->vlv_count = $this->_exec_search(true);
+
+ $this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size);
+ }
+
+ // only fetch dn for count (should keep the payload low)
+ $attrs = $count ? array('dn') : array_values($this->fieldmap);
+ if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter,
+ $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
+ ) {
+ // when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result
+ if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
+ if (ldap_parse_result($this->conn, $this->ldap_result,
+ $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)
+ && $serverctrls // can be null e.g. in case of adm. limit error
+ ) {
+ ldap_parse_virtuallist_control($this->conn, $serverctrls,
+ $last_offset, $this->vlv_count, $vresult);
+ $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$this->vlv_count");
+ }
+ else {
+ $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
+ }
+ }
+
+ $entries_count = ldap_count_entries($this->conn, $this->ldap_result);
+ $this->_debug("S: $entries_count record(s)");
+
+ return $count ? $entries_count : true;
+ }
+ else {
+ $this->_debug("S: ".ldap_error($this->conn));
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Choose the right PHP function according to scope property
+ */
+ private function _scope2func($scope, &$ns_function = null)
+ {
+ switch ($scope) {
+ case 'sub':
+ $function = $ns_function = 'ldap_search';
+ break;
+ case 'base':
+ $function = $ns_function = 'ldap_read';
+ break;
+ default:
+ $function = 'ldap_list';
+ $ns_function = 'ldap_read';
+ break;
+ }
+
+ return $function;
+ }
+
+ /**
+ * Set server controls for Virtual List View (paginated listing)
+ */
+ private function _vlv_set_controls($prop, $list_page, $page_size, $search = null)
+ {
+ $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473", 'value' => $this->_sort_ber_encode((array)$prop['sort']));
+ $vlv_ctrl = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
+
+ $sort = (array)$prop['sort'];
+ $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
+ . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size)");
+
+ if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
+ return false;
+ }
+
+ return true;
+ }
+
/**
* Converts LDAP entry into an array
*/
private function _ldap2result($rec)
{
- $out = array('_type' => 'person');
- $fieldmap = $this->fieldmap;
+ $out = array();
if ($rec['dn'])
$out[$this->primary_key] = self::dn_encode($rec['dn']);
- // determine record type
- if (self::is_group_entry($rec)) {
- $out['_type'] = 'group';
- $out['readonly'] = true;
- $fieldmap['name'] = $this->group_data['name_attr'] ? $this->group_data['name_attr'] : $this->prop['groups']['name_attr'];
- }
-
- foreach ($fieldmap as $rf => $lf)
+ foreach ($this->fieldmap as $rf => $lf)
{
for ($i=0; $i < $rec[$lf]['count']; $i++) {
if (!($value = $rec[$lf][$i]))
@@ -1420,10 +1593,8 @@ class rcube_ldap extends rcube_addressbook
if (is_array($colprop['serialized'])) {
foreach ($colprop['serialized'] as $subtype => $delim) {
$key = $col.':'.$subtype;
- foreach ((array)$save_cols[$key] as $i => $val) {
- $values = array($val['street'], $val['locality'], $val['zipcode'], $val['country']);
- $save_cols[$key][$i] = count(array_filter($values)) ? join($delim, $values) : null;
- }
+ foreach ((array)$save_cols[$key] as $i => $val)
+ $save_cols[$key][$i] = join($delim, array($val['street'], $val['locality'], $val['zipcode'], $val['country']));
}
}
}
@@ -1461,11 +1632,11 @@ class rcube_ldap extends rcube_addressbook
{
// list of known attribute aliases
static $aliases = array(
- 'gn' => 'givenname',
+ 'gn' => 'givenname',
'rfc822mailbox' => 'email',
- 'userid' => 'uid',
- 'emailaddress' => 'email',
- 'pkcs9email' => 'email',
+ 'userid' => 'uid',
+ 'emailaddress' => 'email',
+ 'pkcs9email' => 'email',
);
list($name, $limit) = explode(':', $namev, 2);
@@ -1474,15 +1645,6 @@ class rcube_ldap extends rcube_addressbook
return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
}
- /**
- * Determines whether the given LDAP entry is a group record
- */
- private static function is_group_entry($entry)
- {
- $classes = array_map('strtolower', (array)$entry['objectclass']);
-
- return count(array_intersect(array_keys(self::$group_types), $classes)) > 0;
- }
/**
* Prints debug info to the log
@@ -1499,27 +1661,55 @@ class rcube_ldap extends rcube_addressbook
* Activate/deactivate debug mode
*
* @param boolean $dbg True if LDAP commands should be logged
+ * @access public
*/
function set_debug($dbg = true)
{
$this->debug = $dbg;
+ }
- if ($this->ldap) {
- $this->ldap->set_debug($dbg);
- }
+
+ /**
+ * Quotes attribute value string
+ *
+ * @param string $str Attribute value
+ * @param bool $dn True if the attribute is a DN
+ *
+ * @return string Quoted string
+ */
+ private static function _quote_string($str, $dn=false)
+ {
+ // take firt entry if array given
+ if (is_array($str))
+ $str = reset($str);
+
+ if ($dn)
+ $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
+ '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
+ else
+ $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
+ '/'=>'\2f');
+
+ return strtr($str, $replace);
}
/**
* Setter for the current group
+ * (empty, has to be re-implemented by extending class)
*/
function set_group($group_id)
{
- if ($group_id) {
+ if ($group_id)
+ {
+ if (($group_cache = $this->cache->get('groups')) === null)
+ $group_cache = $this->_fetch_groups();
+
$this->group_id = $group_id;
- $this->group_data = $this->get_group_entry($group_id);
+ $this->group_data = $group_cache[$group_id];
}
- else {
+ else
+ {
$this->group_id = 0;
$this->group_data = null;
}
@@ -1538,13 +1728,15 @@ class rcube_ldap extends rcube_addressbook
*/
function list_groups($search = null, $mode = 0)
{
- if (!$this->groups) {
+ if (!$this->groups)
return array();
- }
- $group_cache = $this->_fetch_groups();
- $groups = array();
+ // use cached list for searching
+ $this->cache->expunge();
+ if (!$search || ($group_cache = $this->cache->get('groups')) === null)
+ $group_cache = $this->_fetch_groups();
+ $groups = array();
if ($search) {
foreach ($group_cache as $group) {
if ($this->compare_search_value('name', $group['name'], $search, $mode)) {
@@ -1552,9 +1744,8 @@ class rcube_ldap extends rcube_addressbook
}
}
}
- else {
+ else
$groups = $group_cache;
- }
return array_values($groups);
}
@@ -1562,140 +1753,80 @@ class rcube_ldap extends rcube_addressbook
/**
* Fetch groups from server
*/
- private function _fetch_groups($vlv_page = null)
+ private function _fetch_groups($vlv_page = 0)
{
- // special case: list groups from 'group_filters' config
- if ($vlv_page === null && !empty($this->prop['group_filters'])) {
- $groups = array();
-
- // list regular groups configuration as special filter
- if (!empty($this->prop['groups']['filter'])) {
- $id = '__groups__';
- $groups[$id] = array('ID' => $id, 'name' => rcube_label('groups'), 'virtual' => true) + $this->prop['groups'];
- }
-
- foreach ($this->prop['group_filters'] as $id => $prop) {
- $groups[$id] = $prop + array('ID' => $id, 'name' => ucfirst($id), 'virtual' => true, 'base_dn' => $this->base_dn);
- }
-
- return $groups;
- }
-
- if ($this->cache && $vlv_page === null && ($groups = $this->cache->get('groups')) !== null) {
- return $groups;
- }
-
- $base_dn = $this->groups_base_dn;
- $filter = $this->prop['groups']['filter'];
- $scope = $this->prop['groups']['scope'];
- $name_attr = $this->prop['groups']['name_attr'];
+ $base_dn = $this->groups_base_dn;
+ $filter = $this->prop['groups']['filter'];
+ $name_attr = $this->prop['groups']['name_attr'];
$email_attr = $this->prop['groups']['email_attr'] ? $this->prop['groups']['email_attr'] : 'mail';
$sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr);
- $sort_attr = $sort_attrs[0];
+ $sort_attr = $sort_attrs[0];
- $ldap = $this->ldap;
+ $this->_debug("C: Search [$filter][dn: $base_dn]");
// use vlv to list groups
if ($this->prop['groups']['vlv']) {
$page_size = 200;
- if (!$this->prop['groups']['sort']) {
+ if (!$this->prop['groups']['sort'])
$this->prop['groups']['sort'] = $sort_attrs;
- }
-
- $ldap = clone $this->ldap;
- $ldap->set_config($this->prop['groups']);
- $ldap->set_vlv_page($vlv_page+1, $page_size);
+ $vlv_active = $this->_vlv_set_controls($this->prop['groups'], $vlv_page+1, $page_size);
}
- $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr));
- $ldap_data = $ldap->search($base_dn, $filter, $scope, $attrs, $this->prop['groups']);
-
- if ($ldap_data === false) {
+ $function = $this->_scope2func($this->prop['groups']['scope'], $ns_function);
+ $res = @$function($this->conn, $base_dn, $filter, array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)));
+ if ($res === false)
+ {
+ $this->_debug("S: ".ldap_error($this->conn));
return array();
}
- $groups = array();
- $group_sortnames = array();
- $group_count = $ldap_data->count();
-
- foreach ($ldap_data as $entry) {
- if (!$entry['dn']) // DN is mandatory
- $entry['dn'] = $ldap_data->get_dn();
+ $ldap_data = ldap_get_entries($this->conn, $res);
+ $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
- $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
- $group_id = self::dn_encode($entry['dn']);
+ $groups = array();
+ $group_sortnames = array();
+ $group_count = $ldap_data["count"];
+ for ($i=0; $i < $group_count; $i++)
+ {
+ $group_name = is_array($ldap_data[$i][$name_attr]) ? $ldap_data[$i][$name_attr][0] : $ldap_data[$i][$name_attr];
+ $group_id = self::dn_encode($group_name);
$groups[$group_id]['ID'] = $group_id;
- $groups[$group_id]['dn'] = $entry['dn'];
+ $groups[$group_id]['dn'] = $ldap_data[$i]['dn'];
$groups[$group_id]['name'] = $group_name;
- $groups[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
+ $groups[$group_id]['member_attr'] = $this->get_group_member_attr($ldap_data[$i]['objectclass']);
// list email attributes of a group
- for ($j=0; $entry[$email_attr] && $j < $entry[$email_attr]['count']; $j++) {
- if (strpos($entry[$email_attr][$j], '@') > 0)
- $groups[$group_id]['email'][] = $entry[$email_attr][$j];
+ for ($j=0; $ldap_data[$i][$email_attr] && $j < $ldap_data[$i][$email_attr]['count']; $j++) {
+ if (strpos($ldap_data[$i][$email_attr][$j], '@') > 0)
+ $groups[$group_id]['email'][] = $ldap_data[$i][$email_attr][$j];
}
- $group_sortnames[] = mb_strtolower($entry[$sort_attr][0]);
+ $group_sortnames[] = mb_strtolower($ldap_data[$i][$sort_attr][0]);
}
// recursive call can exit here
- if ($vlv_page > 0) {
+ if ($vlv_page > 0)
return $groups;
- }
// call recursively until we have fetched all groups
- while ($this->prop['groups']['vlv'] && $group_count == $page_size) {
- $next_page = $this->_fetch_groups(++$vlv_page);
- $groups = array_merge($groups, $next_page);
+ while ($vlv_active && $group_count == $page_size)
+ {
+ $next_page = $this->_fetch_groups(++$vlv_page);
+ $groups = array_merge($groups, $next_page);
$group_count = count($next_page);
}
// when using VLV the list of groups is already sorted
- if (!$this->prop['groups']['vlv']) {
+ if (!$this->prop['groups']['vlv'])
array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
- }
// cache this
- if ($this->cache) {
- $this->cache->set('groups', $groups);
- }
+ $this->cache->set('groups', $groups);
return $groups;
}
/**
- * Fetch a group entry from LDAP and save in local cache
- */
- private function get_group_entry($group_id)
- {
- $group_cache = $this->_fetch_groups();
-
- // add group record to cache if it isn't yet there
- if (!isset($group_cache[$group_id])) {
- $name_attr = $this->prop['groups']['name_attr'];
- $dn = self::dn_decode($group_id);
-
- if ($list = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL',$name_attr,$this->fieldmap['email']))) {
- $entry = $list[0];
- $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
- $group_cache[$group_id]['ID'] = $group_id;
- $group_cache[$group_id]['dn'] = $dn;
- $group_cache[$group_id]['name'] = $group_name;
- $group_cache[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
- }
- else {
- $group_cache[$group_id] = false;
- }
-
- if ($this->cache) {
- $this->cache->set('groups', $group_cache);
- }
- }
-
- return $group_cache[$group_id];
- }
-
- /**
* Get group properties such as name and email address(es)
*
* @param string Group identifier
@@ -1703,7 +1834,10 @@ class rcube_ldap extends rcube_addressbook
*/
function get_group($group_id)
{
- $group_data = $this->get_group_entry($group_id);
+ if (($group_cache = $this->cache->get('groups')) === null)
+ $group_cache = $this->_fetch_groups();
+
+ $group_data = $group_cache[$group_id];
unset($group_data['dn'], $group_data['member_attr']);
return $group_data;
@@ -1717,24 +1851,24 @@ class rcube_ldap extends rcube_addressbook
*/
function create_group($group_name)
{
- $new_dn = 'cn=' . rcube_ldap_generic::quote_string($group_name, true) . ',' . $this->groups_base_dn;
- $new_gid = self::dn_encode($new_dn);
+ $base_dn = $this->groups_base_dn;
+ $new_dn = "cn=$group_name,$base_dn";
+ $new_gid = self::dn_encode($group_name);
$member_attr = $this->get_group_member_attr();
- $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
- $new_entry = array(
+ $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
+
+ $new_entry = array(
'objectClass' => $this->prop['groups']['object_classes'],
$name_attr => $group_name,
$member_attr => '',
);
- if (!$this->ldap->add($new_dn, $new_entry)) {
+ if (!$this->ldap_add($new_dn, $new_entry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
- if ($this->cache) {
- $this->cache->remove('groups');
- }
+ $this->cache->remove('groups');
return array('id' => $new_gid, 'name' => $group_name);
}
@@ -1747,18 +1881,19 @@ class rcube_ldap extends rcube_addressbook
*/
function delete_group($group_id)
{
- $group_cache = $this->_fetch_groups();
- $del_dn = $group_cache[$group_id]['dn'];
+ if (($group_cache = $this->cache->get('groups')) === null)
+ $group_cache = $this->_fetch_groups();
- if (!$this->ldap->delete($del_dn)) {
+ $base_dn = $this->groups_base_dn;
+ $group_name = $group_cache[$group_id]['name'];
+ $del_dn = "cn=$group_name,$base_dn";
+
+ if (!$this->ldap_delete($del_dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
- if ($this->cache) {
- unset($group_cache[$group_id]);
- $this->cache->set('groups', $group_cache);
- }
+ $this->cache->remove('groups');
return true;
}
@@ -1773,19 +1908,21 @@ class rcube_ldap extends rcube_addressbook
*/
function rename_group($group_id, $new_name, &$new_gid)
{
- $group_cache = $this->_fetch_groups();
- $old_dn = $group_cache[$group_id]['dn'];
- $new_rdn = "cn=" . rcube_ldap_generic::quote_string($new_name, true);
- $new_gid = self::dn_encode($new_rdn . ',' . $this->groups_base_dn);
+ if (($group_cache = $this->cache->get('groups')) === null)
+ $group_cache = $this->_fetch_groups();
+
+ $base_dn = $this->groups_base_dn;
+ $group_name = $group_cache[$group_id]['name'];
+ $old_dn = "cn=$group_name,$base_dn";
+ $new_rdn = "cn=$new_name";
+ $new_gid = self::dn_encode($new_name);
- if (!$this->ldap->rename($old_dn, $new_rdn, null, true)) {
+ if (!$this->ldap_rename($old_dn, $new_rdn, null, true)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
- if ($this->cache) {
- $this->cache->remove('groups');
- }
+ $this->cache->remove('groups');
return $new_name;
}
@@ -1800,27 +1937,27 @@ class rcube_ldap extends rcube_addressbook
*/
function add_to_group($group_id, $contact_ids)
{
- $group_cache = $this->_fetch_groups();
- $member_attr = $group_cache[$group_id]['member_attr'];
- $group_dn = $group_cache[$group_id]['dn'];
- $new_attrs = array();
+ if (($group_cache = $this->cache->get('groups')) === null)
+ $group_cache = $this->_fetch_groups();
- if (!is_array($contact_ids)) {
+ if (!is_array($contact_ids))
$contact_ids = explode(',', $contact_ids);
- }
- foreach ($contact_ids as $id) {
+ $base_dn = $this->groups_base_dn;
+ $group_name = $group_cache[$group_id]['name'];
+ $member_attr = $group_cache[$group_id]['member_attr'];
+ $group_dn = "cn=$group_name,$base_dn";
+ $new_attrs = array();
+
+ foreach ($contact_ids as $id)
$new_attrs[$member_attr][] = self::dn_decode($id);
- }
- if (!$this->ldap->mod_add($group_dn, $new_attrs)) {
+ if (!$this->ldap_mod_add($group_dn, $new_attrs)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return 0;
}
- if ($this->cache) {
- $this->cache->remove('groups');
- }
+ $this->cache->remove('groups');
return count($new_attrs[$member_attr]);
}
@@ -1835,27 +1972,27 @@ class rcube_ldap extends rcube_addressbook
*/
function remove_from_group($group_id, $contact_ids)
{
- $group_cache = $this->_fetch_groups();
- $member_attr = $group_cache[$group_id]['member_attr'];
- $group_dn = $group_cache[$group_id]['dn'];
- $del_attrs = array();
+ if (($group_cache = $this->cache->get('groups')) === null)
+ $group_cache = $this->_fetch_groups();
- if (!is_array($contact_ids)) {
+ if (!is_array($contact_ids))
$contact_ids = explode(',', $contact_ids);
- }
- foreach ($contact_ids as $id) {
+ $base_dn = $this->groups_base_dn;
+ $group_name = $group_cache[$group_id]['name'];
+ $member_attr = $group_cache[$group_id]['member_attr'];
+ $group_dn = "cn=$group_name,$base_dn";
+ $del_attrs = array();
+
+ foreach ($contact_ids as $id)
$del_attrs[$member_attr][] = self::dn_decode($id);
- }
- if (!$this->ldap->mod_del($group_dn, $del_attrs)) {
+ if (!$this->ldap_mod_del($group_dn, $del_attrs)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return 0;
}
- if ($this->cache) {
- $this->cache->remove('groups');
- }
+ $this->cache->remove('groups');
return count($del_attrs[$member_attr]);
}
@@ -1870,63 +2007,206 @@ class rcube_ldap extends rcube_addressbook
*/
function get_record_groups($contact_id)
{
- if (!$this->groups) {
+ if (!$this->groups)
return array();
- }
$base_dn = $this->groups_base_dn;
$contact_dn = self::dn_decode($contact_id);
$name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
$member_attr = $this->get_group_member_attr();
$add_filter = '';
-
if ($member_attr != 'member' && $member_attr != 'uniqueMember')
$add_filter = "($member_attr=$contact_dn)";
$filter = strtr("(|(member=$contact_dn)(uniqueMember=$contact_dn)$add_filter)", array('\\' => '\\\\'));
- $ldap_data = $this->ldap->search($base_dn, $filter, 'sub', array('dn', $name_attr));
- if ($res === false) {
+ $this->_debug("C: Search [$filter][dn: $base_dn]");
+
+ $res = @ldap_search($this->conn, $base_dn, $filter, array($name_attr));
+ if ($res === false)
+ {
+ $this->_debug("S: ".ldap_error($this->conn));
return array();
}
+ $ldap_data = ldap_get_entries($this->conn, $res);
+ $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
$groups = array();
- foreach ($ldap_data as $entry) {
- if (!$entry['dn'])
- $entry['dn'] = $ldap_data->get_dn();
- $group_name = $entry[$name_attr][0];
- $group_id = self::dn_encode($entry['dn']);
- $groups[$group_id] = $group_name;
+ for ($i=0; $i<$ldap_data["count"]; $i++)
+ {
+ $group_name = $ldap_data[$i][$name_attr][0];
+ $group_id = self::dn_encode($group_name);
+ $groups[$group_id] = $group_id;
}
-
return $groups;
}
/**
* Detects group member attribute name
*/
- private function get_group_member_attr($object_classes = array(), $default = 'member')
+ private function get_group_member_attr($object_classes = array())
{
if (empty($object_classes)) {
$object_classes = $this->prop['groups']['object_classes'];
}
-
if (!empty($object_classes)) {
foreach ((array)$object_classes as $oc) {
- if ($attr = self::$group_types[strtolower($oc)]) {
- return $attr;
+ switch (strtolower($oc)) {
+ case 'group':
+ case 'groupofnames':
+ case 'kolabgroupofnames':
+ $member_attr = 'member';
+ break;
+
+ case 'groupofuniquenames':
+ case 'kolabgroupofuniquenames':
+ $member_attr = 'uniqueMember';
+ break;
}
}
}
+ if (!empty($member_attr)) {
+ return $member_attr;
+ }
+
if (!empty($this->prop['groups']['member_attr'])) {
return $this->prop['groups']['member_attr'];
}
- return $default;
+ return 'member';
}
/**
+ * Generate BER encoded string for Virtual List View option
+ *
+ * @param integer List offset (first record)
+ * @param integer Records per page
+ * @return string BER encoded option value
+ */
+ private function _vlv_ber_encode($offset, $rpp, $search = '')
+ {
+ # this string is ber-encoded, php will prefix this value with:
+ # 04 (octet string) and 10 (length of 16 bytes)
+ # the code behind this string is broken down as follows:
+ # 30 = ber sequence with a length of 0e (14) bytes following
+ # 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
+ # 02 = type integer (in two's complement form) with 2 bytes following (afterCount): 01 18 (ie 25-1=24)
+ # a0 = type context-specific/constructed with a length of 06 (6) bytes following
+ # 02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
+ # 02 = type integer with 2 bytes following (contentCount): 01 00
+
+ # whith a search string present:
+ # 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)
+ # 81 indicates a user string is present where as a a0 indicates just a offset search
+ # 81 = type context-specific/constructed with a length of 06 (6) bytes following
+
+ # the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
+ # encoding of integer values (note: these values are in
+ # two-complement form so since offset will never be negative bit 8 of the
+ # leftmost octet should never by set to 1):
+ # 8.3.2: If the contents octets of an integer value encoding consist
+ # of more than one octet, then the bits of the first octet (rightmost) and bit 8
+ # of the second (to the left of first octet) octet:
+ # a) shall not all be ones; and
+ # b) shall not all be zero
+
+ if ($search)
+ {
+ $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search);
+ $ber_val = self::_string2hex($search);
+ $str = self::_ber_addseq($ber_val, '81');
+ }
+ else
+ {
+ # construct the string from right to left
+ $str = "020100"; # contentCount
+
+ $ber_val = self::_ber_encode_int($offset); // returns encoded integer value in hex format
+
+ // calculate octet length of $ber_val
+ $str = self::_ber_addseq($ber_val, '02') . $str;
+
+ // now compute length over $str
+ $str = self::_ber_addseq($str, 'a0');
+ }
+
+ // now tack on records per page
+ $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
+
+ // now tack on sequence identifier and length
+ $str = self::_ber_addseq($str, '30');
+
+ return pack('H'.strlen($str), $str);
+ }
+
+
+ /**
+ * create ber encoding for sort control
+ *
+ * @param array List of cols to sort by
+ * @return string BER encoded option value
+ */
+ private function _sort_ber_encode($sortcols)
+ {
+ $str = '';
+ foreach (array_reverse((array)$sortcols) as $col) {
+ $ber_val = self::_string2hex($col);
+
+ # 30 = ber sequence with a length of octet value
+ # 04 = octet string with a length of the ascii value
+ $oct = self::_ber_addseq($ber_val, '04');
+ $str = self::_ber_addseq($oct, '30') . $str;
+ }
+
+ // now tack on sequence identifier and length
+ $str = self::_ber_addseq($str, '30');
+
+ return pack('H'.strlen($str), $str);
+ }
+
+ /**
+ * Add BER sequence with correct length and the given identifier
+ */
+ private static function _ber_addseq($str, $identifier)
+ {
+ $len = dechex(strlen($str)/2);
+ if (strlen($len) % 2 != 0)
+ $len = '0'.$len;
+
+ return $identifier . $len . $str;
+ }
+
+ /**
+ * Returns BER encoded integer value in hex format
+ */
+ private static function _ber_encode_int($offset)
+ {
+ $val = dechex($offset);
+ $prefix = '';
+
+ // check if bit 8 of high byte is 1
+ if (preg_match('/^[89abcdef]/', $val))
+ $prefix = '00';
+
+ if (strlen($val)%2 != 0)
+ $prefix .= '0';
+
+ return $prefix . $val;
+ }
+
+ /**
+ * Returns ascii string encoded in hex
+ */
+ private static function _string2hex($str)
+ {
+ $hex = '';
+ for ($i=0; $i < strlen($str); $i++)
+ $hex .= dechex(ord($str[$i]));
+ return $hex;
+ }
+
+ /**
* HTML-safe DN string encoding
*
* @param string $str DN string
@@ -1953,4 +2233,130 @@ class rcube_ldap extends rcube_addressbook
return base64_decode($str);
}
+ /**
+ * Wrapper for ldap_add()
+ */
+ protected function ldap_add($dn, $entry)
+ {
+ $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
+
+ $res = ldap_add($this->conn, $dn, $entry);
+ if ($res === false) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_delete()
+ */
+ protected function ldap_delete($dn)
+ {
+ $this->_debug("C: Delete [dn: $dn]");
+
+ $res = ldap_delete($this->conn, $dn);
+ if ($res === false) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_mod_replace()
+ */
+ protected function ldap_mod_replace($dn, $entry)
+ {
+ $this->_debug("C: Replace [dn: $dn]: ".print_r($entry, true));
+
+ if (!ldap_mod_replace($this->conn, $dn, $entry)) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_mod_add()
+ */
+ protected function ldap_mod_add($dn, $entry)
+ {
+ $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
+
+ if (!ldap_mod_add($this->conn, $dn, $entry)) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_mod_del()
+ */
+ protected function ldap_mod_del($dn, $entry)
+ {
+ $this->_debug("C: Delete [dn: $dn]: ".print_r($entry, true));
+
+ if (!ldap_mod_del($this->conn, $dn, $entry)) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_rename()
+ */
+ protected function ldap_rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true)
+ {
+ $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
+
+ if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_list()
+ */
+ protected function ldap_list($dn, $filter, $attrs = array(''))
+ {
+ $list = array();
+ $this->_debug("C: List [dn: $dn] [{$filter}]");
+
+ if ($result = ldap_list($this->conn, $dn, $filter, $attrs)) {
+ $list = ldap_get_entries($this->conn, $result);
+
+ if ($list === false) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return array();
+ }
+
+ $count = $list['count'];
+ unset($list['count']);
+
+ $this->_debug("S: $count record(s)");
+ }
+ else {
+ $this->_debug("S: ".ldap_error($this->conn));
+ }
+
+ return $list;
+ }
+
}
diff --git a/program/lib/Roundcube/rcube_ldap_generic.php b/program/lib/Roundcube/rcube_ldap_generic.php
index 88378dc22..923a12a41 100644
--- a/program/lib/Roundcube/rcube_ldap_generic.php
+++ b/program/lib/Roundcube/rcube_ldap_generic.php
@@ -696,11 +696,17 @@ class rcube_ldap_generic
* Turn an LDAP entry into a regular PHP array with attributes as keys.
*
* @param array $entry Attributes array as retrieved from ldap_get_attributes() or ldap_get_entries()
+ *
* @return array Hash array with attributes as keys
*/
public static function normalize_entry($entry)
{
+ if (!isset($entry['count'])) {
+ return $entry;
+ }
+
$rec = array();
+
for ($i=0; $i < $entry['count']; $i++) {
$attr = $entry[$i];
if ($entry[$attr]['count'] == 1) {
diff --git a/program/lib/Roundcube/rcube_message.php b/program/lib/Roundcube/rcube_message.php
index 0d33ea44d..a8bcf6afc 100644
--- a/program/lib/Roundcube/rcube_message.php
+++ b/program/lib/Roundcube/rcube_message.php
@@ -168,11 +168,10 @@ class rcube_message
* @param resource $fp File pointer to save the message part
* @param boolean $skip_charset_conv Disables charset conversion
* @param int $max_bytes Only read this number of bytes
- * @param boolean $formatted Enables formatting of text/* parts bodies
*
* @return string Part content
*/
- public function get_part_content($mime_id, $fp = null, $skip_charset_conv = false, $max_bytes = 0, $formatted = true)
+ public function get_part_content($mime_id, $fp = null, $skip_charset_conv = false, $max_bytes = 0)
{
if ($part = $this->mime_parts[$mime_id]) {
// stored in message structure (winmail/inline-uuencode)
@@ -186,89 +185,47 @@ class rcube_message
// get from IMAP
$this->storage->set_folder($this->folder);
- return $this->storage->get_message_part($this->uid, $mime_id, $part,
- NULL, $fp, $skip_charset_conv, $max_bytes, $formatted);
+ return $this->storage->get_message_part($this->uid, $mime_id, $part, NULL, $fp, $skip_charset_conv, $max_bytes);
}
}
/**
- * Determine if the message contains a HTML part. This must to be
- * a real part not an attachment (or its part)
- * This must to be
- * a real part not an attachment (or its part)
+ * Determine if the message contains a HTML part
*
- * @param bool $enriched Enables checking for text/enriched parts too
+ * @param bool $recursive Enables checking in all levels of the structure
+ * @param bool $enriched Enables checking for text/enriched parts too
*
* @return bool True if a HTML is available, False if not
*/
- function has_html_part($enriched = false)
+ function has_html_part($recursive = true, $enriched = false)
{
// check all message parts
- foreach ($this->mime_parts as $part) {
+ foreach ($this->parts as $part) {
if ($part->mimetype == 'text/html' || ($enriched && $part->mimetype == 'text/enriched')) {
- // Skip if part is an attachment, don't use is_attachment() here
- if ($part->filename) {
- continue;
- }
-
- $level = explode('.', $part->mime_id);
-
- // Check if the part belongs to higher-level's alternative/related
- while (array_pop($level) !== null) {
- if (!count($level)) {
- return true;
- }
+ // Level check, we'll skip e.g. HTML attachments
+ if (!$recursive) {
+ $level = explode('.', $part->mime_id);
- $parent = $this->mime_parts[join('.', $level)];
- if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') {
- continue 2;
+ // Skip if part is an attachment
+ if ($this->is_attachment($part)) {
+ continue;
}
- }
- if ($part->size) {
- return true;
- }
- }
- }
-
- return false;
- }
-
-
- /**
- * Determine if the message contains a text/plain part. This must to be
- * a real part not an attachment (or its part)
- *
- * @return bool True if a plain text part is available, False if not
- */
- function has_text_part()
- {
- // check all message parts
- foreach ($this->mime_parts as $part) {
- if ($part->mimetype == 'text/plain') {
- // Skip if part is an attachment, don't use is_attachment() here
- if ($part->filename) {
- continue;
- }
-
- $level = explode('.', $part->mime_id);
-
- // Check if the part belongs to higher-level's alternative/related
- while (array_pop($level) !== null) {
- if (!count($level)) {
- return true;
- }
+ // Check if the part belongs to higher-level's alternative/related
+ while (array_pop($level) !== null) {
+ if (!count($level)) {
+ return true;
+ }
- $parent = $this->mime_parts[join('.', $level)];
- if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') {
- continue 2;
+ $parent = $this->mime_parts[join('.', $level)];
+ if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') {
+ continue 2;
+ }
}
}
- if ($part->size) {
- return true;
- }
+ return true;
}
}
@@ -363,8 +320,8 @@ class rcube_message
$mimetype = $structure->real_mimetype;
// parse headers from message/rfc822 part
- if (!isset($structure->headers['subject']) && !isset($structure->headers['from'])) {
- list($headers, ) = explode("\r\n\r\n", $this->get_part_content($structure->mime_id, null, true, 32768));
+ if (!isset($structure->headers['subject'])) {
+ list($headers, $dump) = explode("\r\n\r\n", $this->get_part_content($structure->mime_id, null, true, 32768));
$structure->headers = rcube_mime::parse_headers($headers);
}
}
@@ -373,7 +330,7 @@ class rcube_message
// show message headers
if ($recursive && is_array($structure->headers) &&
- (isset($structure->headers['subject']) || $structure->headers['from'] || $structure->headers['to'])) {
+ ($structure->headers['subject'] || $structure->headers['from'] || $structure->headers['to'])) {
$c = new stdClass;
$c->type = 'headers';
$c->headers = $structure->headers;
@@ -487,6 +444,14 @@ class rcube_message
$this->parts[] = $c;
}
+ // add html part as attachment
+ if ($html_part !== null && $structure->parts[$html_part] !== $print_part) {
+ $html_part = $structure->parts[$html_part];
+ $html_part->mimetype = 'text/html';
+
+ $this->attachments[] = $html_part;
+ }
+
// add unsupported/unrecognized parts to attachments list
if ($attach_part) {
$this->attachments[] = $structure->parts[$attach_part];
@@ -571,6 +536,10 @@ class rcube_message
if (!empty($mail_part->filename)) {
$this->attachments[] = $mail_part;
}
+ // list html part as attachment (here the part is most likely inside a multipart/related part)
+ else if ($this->parse_alternative && ($secondary_type == 'html' && !$this->opt['prefer_html'])) {
+ $this->attachments[] = $mail_part;
+ }
}
// part message/*
else if ($primary_type == 'message') {
diff --git a/program/lib/Roundcube/rcube_mime.php b/program/lib/Roundcube/rcube_mime.php
index 96a8eac61..323a5e900 100644
--- a/program/lib/Roundcube/rcube_mime.php
+++ b/program/lib/Roundcube/rcube_mime.php
@@ -637,7 +637,8 @@ class rcube_mime
if ($nextChar === ' ' || $nextChar === $separator) {
$afterNextChar = mb_substr($string, $width + 1, 1);
- if ($afterNextChar === false) {
+ // Note: mb_substr() does never return False
+ if ($afterNextChar === false || $afterNextChar === '') {
$subString .= $nextChar;
}
@@ -650,24 +651,23 @@ class rcube_mime
$subString = mb_substr($subString, 0, $spacePos);
$cutLength = $spacePos + 1;
}
- else if ($cut === false && $breakPos === false) {
- $subString = $string;
- $cutLength = null;
- }
else if ($cut === false) {
$spacePos = mb_strpos($string, ' ', 0);
- if ($spacePos !== false && $spacePos < $breakPos) {
+ if ($spacePos !== false && ($breakPos === false || $spacePos < $breakPos)) {
$subString = mb_substr($string, 0, $spacePos);
$cutLength = $spacePos + 1;
}
+ else if ($breakPos === false) {
+ $subString = $string;
+ $cutLength = null;
+ }
else {
$subString = mb_substr($string, 0, $breakPos);
$cutLength = $breakPos + 1;
}
}
else {
- $subString = mb_substr($subString, 0, $width);
$cutLength = $width;
}
}
@@ -708,20 +708,12 @@ class rcube_mime
*/
public static function file_content_type($path, $name, $failover = 'application/octet-stream', $is_stream = false, $skip_suffix = false)
{
- static $mime_ext = array();
-
$mime_type = null;
- $config = rcube::get_instance()->config;
- $mime_magic = $config->get('mime_magic');
-
- if (!$skip_suffix && empty($mime_ext)) {
- foreach ($config->resolve_paths('mimetypes.php') as $fpath) {
- $mime_ext = array_merge($mime_ext, (array) @include($fpath));
- }
- }
+ $mime_magic = rcube::get_instance()->config->get('mime_magic');
+ $mime_ext = $skip_suffix ? null : @include(RCUBE_CONFIG_DIR . '/mimetypes.php');
// use file name suffix with hard-coded mime-type map
- if (!$skip_suffix && is_array($mime_ext) && $name) {
+ if (is_array($mime_ext) && $name) {
if ($suffix = substr($name, strrpos($name, '.')+1)) {
$mime_type = $mime_ext[strtolower($suffix)];
}
@@ -826,9 +818,7 @@ class rcube_mime
// fallback to some well-known types most important for daily emails
if (empty($mime_types)) {
- foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) {
- $mime_extensions = array_merge($mime_extensions, (array) @include($fpath));
- }
+ $mime_extensions = (array) @include(RCUBE_CONFIG_DIR . '/mimetypes.php');
foreach ($mime_extensions as $ext => $mime) {
$mime_types[$mime][] = $ext;
diff --git a/program/lib/Roundcube/rcube_plugin.php b/program/lib/Roundcube/rcube_plugin.php
index 3153a8410..34720cfd7 100644
--- a/program/lib/Roundcube/rcube_plugin.php
+++ b/program/lib/Roundcube/rcube_plugin.php
@@ -92,16 +92,6 @@ abstract class rcube_plugin
abstract function init();
/**
- * Provide information about this
- *
- * @return array Meta information about a plugin or false if not implemented
- */
- public static function info()
- {
- return false;
- }
-
- /**
* Attempt to load the given plugin which is required for the current plugin
*
* @param string Plugin name
@@ -227,7 +217,7 @@ abstract class rcube_plugin
$rcube->load_language($lang, $add);
// add labels to client
- if ($add2client && method_exists($rcube->output, 'add_label')) {
+ if ($add2client) {
if (is_array($add2client)) {
$js_labels = array_map(array($this, 'label_map_callback'), $add2client);
}
@@ -240,24 +230,6 @@ abstract class rcube_plugin
}
/**
- * Wrapper for add_label() adding the plugin ID as domain
- */
- public function add_label()
- {
- $rcube = rcube::get_instance();
-
- if (method_exists($rcube->output, 'add_label')) {
- $args = func_get_args();
- if (count($args) == 1 && is_array($args[0])) {
- $args = $args[0];
- }
-
- $args = array_map(array($this, 'label_map_callback'), $args);
- $rcube->output->add_label($args);
- }
- }
-
- /**
* Wrapper for rcube::gettext() adding the plugin ID as domain
*
* @param string $p Message identifier
@@ -273,7 +245,7 @@ abstract class rcube_plugin
/**
* Register this plugin to be responsible for a specific task
*
- * @param string $task Task name (only characters [a-z0-9_-] are allowed)
+ * @param string $task Task name (only characters [a-z0-9_.-] are allowed)
*/
public function register_task($task)
{
@@ -408,10 +380,6 @@ abstract class rcube_plugin
*/
private function label_map_callback($key)
{
- if (strpos($key, $this->ID.'.') === 0) {
- return $key;
- }
-
return $this->ID.'.'.$key;
}
}
diff --git a/program/lib/Roundcube/rcube_plugin_api.php b/program/lib/Roundcube/rcube_plugin_api.php
index 33f04eaa5..c9602d912 100644
--- a/program/lib/Roundcube/rcube_plugin_api.php
+++ b/program/lib/Roundcube/rcube_plugin_api.php
@@ -35,8 +35,9 @@ class rcube_plugin_api
public $url = 'plugins/';
public $task = '';
public $output;
- public $handlers = array();
- public $allowed_prefs = array();
+ public $handlers = array();
+ public $allowed_prefs = array();
+ public $allowed_session_prefs = array();
protected $plugins = array();
protected $tasks = array();
@@ -228,119 +229,6 @@ class rcube_plugin_api
}
/**
- * Get information about a specific plugin.
- * This is either provided my a plugin's info() method or extracted from a package.xml or a composer.json file
- *
- * @param string Plugin name
- * @return array Meta information about a plugin or False if plugin was not found
- */
- public function get_info($plugin_name)
- {
- static $composer_lock, $license_uris = array(
- 'Apache' => 'http://www.apache.org/licenses/LICENSE-2.0.html',
- 'Apache-2' => 'http://www.apache.org/licenses/LICENSE-2.0.html',
- 'Apache-1' => 'http://www.apache.org/licenses/LICENSE-1.0',
- 'Apache-1.1' => 'http://www.apache.org/licenses/LICENSE-1.1',
- 'GPL' => 'http://www.gnu.org/licenses/gpl.html',
- 'GPLv2' => 'http://www.gnu.org/licenses/gpl-2.0.html',
- 'GPL-2.0' => 'http://www.gnu.org/licenses/gpl-2.0.html',
- 'GPLv3' => 'http://www.gnu.org/licenses/gpl-3.0.html',
- 'GPL-3.0' => 'http://www.gnu.org/licenses/gpl-3.0.html',
- 'GPL-3.0+' => 'http://www.gnu.org/licenses/gpl.html',
- 'GPL-2.0+' => 'http://www.gnu.org/licenses/gpl.html',
- 'LGPL' => 'http://www.gnu.org/licenses/lgpl.html',
- 'LGPLv2' => 'http://www.gnu.org/licenses/lgpl-2.0.html',
- 'LGPLv2.1' => 'http://www.gnu.org/licenses/lgpl-2.1.html',
- 'LGPLv3' => 'http://www.gnu.org/licenses/lgpl.html',
- 'LGPL-2.0' => 'http://www.gnu.org/licenses/lgpl-2.0.html',
- 'LGPL-2.1' => 'http://www.gnu.org/licenses/lgpl-2.1.html',
- 'LGPL-3.0' => 'http://www.gnu.org/licenses/lgpl.html',
- 'LGPL-3.0+' => 'http://www.gnu.org/licenses/lgpl.html',
- 'BSD' => 'http://opensource.org/licenses/bsd-license.html',
- 'BSD-2-Clause' => 'http://opensource.org/licenses/BSD-2-Clause',
- 'BSD-3-Clause' => 'http://opensource.org/licenses/BSD-3-Clause',
- 'FreeBSD' => 'http://opensource.org/licenses/BSD-2-Clause',
- 'MIT' => 'http://www.opensource.org/licenses/mit-license.php',
- 'PHP' => 'http://opensource.org/licenses/PHP-3.0',
- 'PHP-3' => 'http://www.php.net/license/3_01.txt',
- 'PHP-3.0' => 'http://www.php.net/license/3_0.txt',
- 'PHP-3.01' => 'http://www.php.net/license/3_01.txt',
- );
-
- $dir = dir($this->dir);
- $fn = unslashify($dir->path) . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php';
- $info = false;
-
- if (!class_exists($plugin_name))
- include($fn);
-
- if (class_exists($plugin_name))
- $info = $plugin_name::info();
-
- // fall back to composer.json file
- if (!$info) {
- $composer = INSTALL_PATH . "/plugins/$plugin_name/composer.json";
- if (file_exists($composer) && ($json = @json_decode(file_get_contents($composer), true))) {
- list($info['vendor'], $info['name']) = explode('/', $json['name']);
- $info['license'] = $json['license'];
- if ($license_uri = $license_uris[$info['license']])
- $info['license_uri'] = $license_uri;
- }
-
- // read local composer.lock file (once)
- if (!isset($composer_lock)) {
- $composer_lock = @json_decode(@file_get_contents(INSTALL_PATH . "/composer.lock"), true);
- if ($composer_lock['packages']) {
- foreach ($composer_lock['packages'] as $i => $package) {
- $composer_lock['installed'][$package['name']] = $package;
- }
- }
- }
-
- // load additional information from local composer.lock file
- if ($lock = $composer_lock['installed'][$json['name']]) {
- $info['version'] = $lock['version'];
- $info['uri'] = $lock['homepage'] ? $lock['homepage'] : $lock['source']['uri'];
- $info['src_uri'] = $lock['dist']['uri'] ? $lock['dist']['uri'] : $lock['source']['uri'];
- }
- }
-
- // fall back to package.xml file
- if (!$info) {
- $package = INSTALL_PATH . "/plugins/$plugin_name/package.xml";
- if (file_exists($package) && ($file = file_get_contents($package))) {
- $doc = new DOMDocument();
- $doc->loadXML($file);
- $xpath = new DOMXPath($doc);
- $xpath->registerNamespace('rc', "http://pear.php.net/dtd/package-2.0");
-
- // XPaths of plugin metadata elements
- $metadata = array(
- 'name' => 'string(//rc:package/rc:name)',
- 'version' => 'string(//rc:package/rc:version/rc:release)',
- 'license' => 'string(//rc:package/rc:license)',
- 'license_uri' => 'string(//rc:package/rc:license/@uri)',
- 'src_uri' => 'string(//rc:package/rc:srcuri)',
- 'uri' => 'string(//rc:package/rc:uri)',
- );
-
- foreach ($metadata as $key => $path) {
- $info[$key] = $xpath->evaluate($path);
- }
-
- // dependent required plugins (can be used, but not included in config)
- $deps = $xpath->evaluate('//rc:package/rc:dependencies/rc:required/rc:package/rc:name');
- for ($i = 0; $i < $deps->length; $i++) {
- $dn = $deps->item($i)->nodeValue;
- $info['requires'][] = $dn;
- }
- }
- }
-
- return $info;
- }
-
- /**
* Allows a plugin object to register a callback for a certain hook
*
* @param string $hook Hook name
@@ -491,7 +379,7 @@ class rcube_plugin_api
/**
* Register this plugin to be responsible for a specific task
*
- * @param string $task Task name (only characters [a-z0-9_-] are allowed)
+ * @param string $task Task name (only characters [a-z0-9_.-] are allowed)
* @param string $owner Plugin name that registers this action
*/
public function register_task($task, $owner)
@@ -501,7 +389,7 @@ class rcube_plugin_api
return true;
}
- if ($task != asciiwords($task, true)) {
+ if ($task != asciiwords($task)) {
rcube::raise_error(array('code' => 526, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid task name: $task."
diff --git a/program/lib/Roundcube/rcube_result_set.php b/program/lib/Roundcube/rcube_result_set.php
index a4b070e28..1391e5e4b 100644
--- a/program/lib/Roundcube/rcube_result_set.php
+++ b/program/lib/Roundcube/rcube_result_set.php
@@ -3,7 +3,7 @@
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2013, The Roundcube Dev Team |
+ | Copyright (C) 2006-2011, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
@@ -17,22 +17,20 @@
*/
/**
- * Roundcube result set class
- *
+ * Roundcube result set class.
* Representing an address directory result set.
- * Implenets Iterator and thus be used in foreach() loops.
*
* @package Framework
* @subpackage Addressbook
*/
-class rcube_result_set implements Iterator
+class rcube_result_set
{
- public $count = 0;
- public $first = 0;
- public $searchonly = false;
- public $records = array();
+ var $count = 0;
+ var $first = 0;
+ var $current = 0;
+ var $searchonly = false;
+ var $records = array();
- private $current = 0;
function __construct($c=0, $f=0)
{
@@ -53,39 +51,18 @@ class rcube_result_set implements Iterator
function first()
{
$this->current = 0;
- return $this->records[$this->current];
- }
-
- function seek($i)
- {
- $this->current = $i;
- }
-
- /*** PHP 5 Iterator interface ***/
-
- function rewind()
- {
- $this->current = 0;
- }
-
- function current()
- {
- return $this->records[$this->current];
- }
-
- function key()
- {
- return $this->current;
+ return $this->records[$this->current++];
}
+ // alias for iterate()
function next()
{
return $this->iterate();
}
- function valid()
+ function seek($i)
{
- return isset($this->records[$this->current]);
+ $this->current = $i;
}
}
diff --git a/program/lib/Roundcube/rcube_session.php b/program/lib/Roundcube/rcube_session.php
index 67072df41..ee4db6e86 100644
--- a/program/lib/Roundcube/rcube_session.php
+++ b/program/lib/Roundcube/rcube_session.php
@@ -32,7 +32,6 @@ class rcube_session
private $ip;
private $start;
private $changed;
- private $time_diff = 0;
private $reloaded = false;
private $unsets = array();
private $gc_handlers = array();
@@ -43,7 +42,6 @@ class rcube_session
private $secret = '';
private $ip_check = false;
private $logging = false;
- private $storage;
private $memcache;
@@ -54,21 +52,18 @@ class rcube_session
{
$this->db = $db;
$this->start = microtime(true);
- $this->ip = rcube_utils::remote_addr();
+ $this->ip = $_SERVER['REMOTE_ADDR'];
$this->logging = $config->get('log_session', false);
$lifetime = $config->get('session_lifetime', 1) * 60;
$this->set_lifetime($lifetime);
// use memcache backend
- $this->storage = $config->get('session_storage', 'db');
- if ($this->storage == 'memcache') {
+ if ($config->get('session_storage', 'db') == 'memcache') {
$this->memcache = rcube::get_instance()->get_memcache();
// set custom functions for PHP session management if memcache is available
if ($this->memcache) {
- ini_set('session.serialize_handler', 'php');
-
session_set_save_handler(
array($this, 'open'),
array($this, 'close'),
@@ -84,9 +79,7 @@ class rcube_session
true, true);
}
}
- else if ($this->storage != 'php') {
- ini_set('session.serialize_handler', 'php');
-
+ else {
// set custom functions for PHP session management
session_set_save_handler(
array($this, 'open'),
@@ -94,23 +87,7 @@ class rcube_session
array($this, 'db_read'),
array($this, 'db_write'),
array($this, 'db_destroy'),
- array($this, 'gc'));
- }
- }
-
-
- /**
- * Wrapper for session_start()
- */
- public function start()
- {
- session_start();
-
- // copy some session properties to object vars
- if ($this->storage == 'php') {
- $this->key = session_id();
- $this->ip = $_SESSION['__IP'];
- $this->changed = $_SESSION['__MTIME'];
+ array($this, 'db_gc'));
}
}
@@ -139,25 +116,6 @@ class rcube_session
/**
- * Wrapper for session_write_close()
- */
- public function write_close()
- {
- if ($this->storage == 'php') {
- $_SESSION['__IP'] = $this->ip;
- $_SESSION['__MTIME'] = time();
- }
-
- session_write_close();
-
- // write_close() is called on script shutdown, see rcube::shutdown()
- // execute cleanup functionality if enabled by session gc handler
- // we do this after closing the session for better performance
- $this->gc_shutdown();
- }
-
-
- /**
* Read session data from database
*
* @param string Session ID
@@ -167,16 +125,14 @@ class rcube_session
public function db_read($key)
{
$sql_result = $this->db->query(
- "SELECT vars, ip, changed, " . $this->db->now() . " AS ts"
- . " FROM " . $this->db->table_name('session')
- . " WHERE sess_id = ?", $key);
+ "SELECT vars, ip, changed FROM ".$this->db->table_name('session')
+ ." WHERE sess_id = ?", $key);
if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
- $this->time_diff = time() - strtotime($sql_arr['ts']);
- $this->changed = strtotime($sql_arr['changed']);
- $this->ip = $sql_arr['ip'];
- $this->vars = base64_decode($sql_arr['vars']);
- $this->key = $key;
+ $this->changed = strtotime($sql_arr['changed']);
+ $this->ip = $sql_arr['ip'];
+ $this->vars = base64_decode($sql_arr['vars']);
+ $this->key = $key;
return !empty($this->vars) ? (string) $this->vars : '';
}
@@ -196,9 +152,8 @@ class rcube_session
*/
public function db_write($key, $vars)
{
- $now = $this->db->now();
- $table = $this->db->table_name('session');
- $ts = microtime(true);
+ $ts = microtime(true);
+ $now = $this->db->fromunixtime((int)$ts);
// no session row in DB (db_read() returns false)
if (!$this->key) {
@@ -216,19 +171,22 @@ class rcube_session
$newvars = $this->_fixvars($vars, $oldvars);
if ($newvars !== $oldvars) {
- $this->db->query("UPDATE $table "
- . "SET changed = $now, vars = ? WHERE sess_id = ?",
- base64_encode($newvars), $key);
+ $this->db->query(
+ sprintf("UPDATE %s SET vars=?, changed=%s WHERE sess_id=?",
+ $this->db->table_name('session'), $now),
+ base64_encode($newvars), $key);
}
- else if ($ts - $this->changed + $this->time_diff > $this->lifetime / 2) {
- $this->db->query("UPDATE $table SET changed = $now"
- . " WHERE sess_id = ?", $key);
+ else if ($ts - $this->changed > $this->lifetime / 2) {
+ $this->db->query("UPDATE ".$this->db->table_name('session')
+ ." SET changed=$now WHERE sess_id=?", $key);
}
}
else {
- $this->db->query("INSERT INTO $table (sess_id, vars, ip, created, changed)"
- . " VALUES (?, ?, ?, $now, $now)",
- $key, base64_encode($vars), (string)$this->ip);
+ $this->db->query(
+ sprintf("INSERT INTO %s (sess_id, vars, ip, created, changed) ".
+ "VALUES (?, ?, ?, %s, %s)",
+ $this->db->table_name('session'), $now, $now),
+ $key, base64_encode($vars), (string)$this->ip);
}
return true;
@@ -288,6 +246,25 @@ class rcube_session
/**
+ * Garbage collecting function
+ *
+ * @param string Session lifetime in seconds
+ * @return boolean True on success
+ */
+ public function db_gc($maxlifetime)
+ {
+ // just delete all expired sessions
+ $this->db->query(
+ sprintf("DELETE FROM %s WHERE changed < %s",
+ $this->db->table_name('session'), $this->db->fromunixtime(time() - $maxlifetime)));
+
+ $this->gc();
+
+ return true;
+ }
+
+
+ /**
* Read session data from memcache
*
* @param string Session ID
@@ -363,11 +340,11 @@ class rcube_session
/**
* Execute registered garbage collector routines
*/
- public function gc($maxlifetime)
+ public function gc()
{
- // move gc execution to the script shutdown function
- // see rcube::shutdown() and rcube_session::write_close()
- return $this->gc_enabled = $maxlifetime;
+ foreach ($this->gc_handlers as $fct) {
+ call_user_func($fct);
+ }
}
@@ -389,25 +366,6 @@ class rcube_session
/**
- * Garbage collector handler to run on script shutdown
- */
- protected function gc_shutdown()
- {
- if ($this->gc_enabled) {
- // just delete all expired sessions
- if ($this->storage == 'db') {
- $this->db->query("DELETE FROM " . $this->db->table_name('session')
- . " WHERE changed < " . $this->db->now(-$this->gc_enabled));
- }
-
- foreach ($this->gc_handlers as $fct) {
- call_user_func($fct);
- }
- }
- }
-
-
- /**
* Generate and set new session id
*
* @param boolean $destroy If enabled the current session will be destroyed
@@ -480,7 +438,7 @@ class rcube_session
public function kill()
{
$this->vars = null;
- $this->ip = rcube_utils::remote_addr(); // update IP (might have changed)
+ $this->ip = $_SERVER['REMOTE_ADDR']; // update IP (might have changed)
$this->destroy(session_id());
rcube_utils::setcookie($this->cookiename, '-del-', time() - 60);
}
@@ -694,10 +652,10 @@ class rcube_session
function check_auth()
{
$this->cookie = $_COOKIE[$this->cookiename];
- $result = $this->ip_check ? rcube_utils::remote_addr() == $this->ip : true;
+ $result = $this->ip_check ? $_SERVER['REMOTE_ADDR'] == $this->ip : true;
if (!$result) {
- $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . rcube_utils::remote_addr());
+ $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . $_SERVER['REMOTE_ADDR']);
}
if ($result && $this->_mkcookie($this->now) != $this->cookie) {
diff --git a/program/lib/Roundcube/rcube_smtp.php b/program/lib/Roundcube/rcube_smtp.php
index 60b1389ea..201e8269e 100644
--- a/program/lib/Roundcube/rcube_smtp.php
+++ b/program/lib/Roundcube/rcube_smtp.php
@@ -33,8 +33,6 @@ class rcube_smtp
// define headers delimiter
const SMTP_MIME_CRLF = "\r\n";
- const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n
-
/**
* SMTP Connection and authentication
@@ -329,12 +327,6 @@ class rcube_smtp
*/
public function debug_handler(&$smtp, $message)
{
- if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
- $diff = $len - self::DEBUG_LINE_LENGTH;
- $message = substr($message, 0, self::DEBUG_LINE_LENGTH)
- . "... [truncated $diff bytes]";
- }
-
rcube::write_log('smtp', preg_replace('/\r\n$/', '', $message));
}
@@ -441,9 +433,9 @@ class rcube_smtp
$recipients = rcube_utils::explode_quoted_string(',', $recipients);
reset($recipients);
- foreach ($recipients as $recipient) {
+ while (list($k, $recipient) = each($recipients)) {
$a = rcube_utils::explode_quoted_string(' ', $recipient);
- foreach ($a as $word) {
+ while (list($k2, $word) = each($a)) {
if (strpos($word, "@") > 0 && $word[strlen($word)-1] != '"') {
$word = preg_replace('/^<|>$/', '', trim($word));
if (in_array($word, $addresses) === false) {
diff --git a/program/lib/Roundcube/rcube_spellcheck_atd.php b/program/lib/Roundcube/rcube_spellcheck_atd.php
new file mode 100644
index 000000000..9f073f56f
--- /dev/null
+++ b/program/lib/Roundcube/rcube_spellcheck_atd.php
@@ -0,0 +1,204 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) 2013, The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Spellchecking backend implementation for afterthedeadline services |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Spellchecking backend implementation to work with an After the Deadline service
+ * See http://www.afterthedeadline.com/ for more information
+ *
+ * @package Framework
+ * @subpackage Utils
+ */
+class rcube_spellcheck_atd extends rcube_spellcheck_engine
+{
+ const SERVICE_HOST = 'service.afterthedeadline.com';
+ const SERVICE_PORT = 80;
+
+ private $matches = array();
+ private $content;
+ private $langhosts = array(
+ 'fr' => 'fr.',
+ 'de' => 'de.',
+ 'pt' => 'pt.',
+ 'es' => 'es.',
+ );
+
+ /**
+ * Return a list of languages supported by this backend
+ *
+ * @see rcube_spellcheck_engine::languages()
+ */
+ function languages()
+ {
+ $langs = array_values($this->langhosts);
+ $langs[] = 'en';
+ return $langs;
+ }
+
+ /**
+ * Set content and check spelling
+ *
+ * @see rcube_spellcheck_engine::check()
+ */
+ function check($text)
+ {
+ $this->content = $text;
+
+ // spell check uri is configured
+ $rcube = rcube::get_instance();
+ $url = $rcube->config->get('spellcheck_uri');
+ $key = $rcube->config->get('spellcheck_atd_key');
+
+ if ($url) {
+ $a_uri = parse_url($url);
+ $ssl = ($a_uri['scheme'] == 'https' || $a_uri['scheme'] == 'ssl');
+ $port = $a_uri['port'] ? $a_uri['port'] : ($ssl ? 443 : 80);
+ $host = ($ssl ? 'ssl://' : '') . $a_uri['host'];
+ $path = $a_uri['path'] . ($a_uri['query'] ? '?'.$a_uri['query'] : '') . $this->lang;
+ }
+ else {
+ $host = self::SERVICE_HOST;
+ $port = self::SERVICE_PORT;
+ $path = '/checkDocument';
+
+ // prefix host for other languages than 'en'
+ $lang = substr($this->lang, 0, 2);
+ if ($this->langhosts[$lang])
+ $host = $this->langhosts[$lang] . $host;
+ }
+
+ $postdata = 'data=' . urlencode($text);
+
+ if (!empty($key))
+ $postdata .= '&key=' . urlencode($key);
+
+ $response = $headers = '';
+ $in_header = true;
+ if ($fp = fsockopen($host, $port, $errno, $errstr, 30)) {
+ $out = "POST $path HTTP/1.0\r\n";
+ $out .= "Host: " . str_replace('ssl://', '', $host) . "\r\n";
+ $out .= "Content-Length: " . strlen($postdata) . "\r\n";
+ $out .= "Content-Type: application/x-www-form-urlencoded\r\n";
+ $out .= "Connection: Close\r\n\r\n";
+ $out .= $postdata;
+ fwrite($fp, $out);
+
+ while (!feof($fp)) {
+ if ($in_header) {
+ $line = fgets($fp, 512);
+ $headers .= $line;
+ if (trim($line) == '')
+ $in_header = false;
+ }
+ else {
+ $response .= fgets($fp, 1024);
+ }
+ }
+ fclose($fp);
+ }
+
+ // parse HTTP response headers
+ if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $headers, $m)) {
+ $http_status = $m[1];
+ if ($http_status != '200')
+ $this->error = 'HTTP ' . $m[1] . $m[2];
+ }
+
+ if (!$response) {
+ $this->error = "Empty result from spelling engine";
+ }
+
+ try {
+ $result = new SimpleXMLElement($response);
+ }
+ catch (Exception $e) {
+ $thid->error = "Unexpected response from server: " . $store;
+ return array();
+ }
+
+ foreach ($result->error as $error) {
+ if (strval($error->type) == 'spelling') {
+ $word = strval($error->string);
+
+ // skip exceptions
+ if ($this->dictionary->is_exception($word)) {
+ continue;
+ }
+
+ $prefix = strval($error->precontext);
+ $start = $prefix ? mb_strpos($text, $prefix) : 0;
+ $pos = mb_strpos($text, $word, $start);
+ $len = mb_strlen($word);
+ $num = 0;
+
+ $match = array($word, $pos, $len, null, array());
+ foreach ($error->suggestions->option as $option) {
+ $match[4][] = strval($option);
+ if (++$num == self::MAX_SUGGESTIONS)
+ break;
+ }
+ $matches[] = $match;
+ }
+ }
+
+ $this->matches = $matches;
+ return $matches;
+ }
+
+ /**
+ * Returns suggestions for the specified word
+ *
+ * @see rcube_spellcheck_engine::get_words()
+ */
+ function get_suggestions($word)
+ {
+ $matches = $word ? $this->check($word) : $this->matches;
+
+ if ($matches[0][4]) {
+ return $matches[0][4];
+ }
+
+ return array();
+ }
+
+ /**
+ * Returns misspelled words
+ *
+ * @see rcube_spellcheck_engine::get_suggestions()
+ */
+ function get_words($text = null)
+ {
+ if ($text) {
+ $matches = $this->check($text);
+ }
+ else {
+ $matches = $this->matches;
+ $text = $this->content;
+ }
+
+ $result = array();
+
+ foreach ($matches as $m) {
+ $result[] = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
+ }
+
+ return $result;
+ }
+
+}
+
diff --git a/program/lib/Roundcube/rcube_spellcheck_enchant.php b/program/lib/Roundcube/rcube_spellcheck_enchant.php
new file mode 100644
index 000000000..14d6fff46
--- /dev/null
+++ b/program/lib/Roundcube/rcube_spellcheck_enchant.php
@@ -0,0 +1,182 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) 2011-2013, Kolab Systems AG |
+ | Copyright (C) 20011-2013, The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Spellchecking backend implementation to work with Enchant |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Spellchecking backend implementation to work with Pspell
+ *
+ * @package Framework
+ * @subpackage Utils
+ */
+class rcube_spellcheck_enchant extends rcube_spellcheck_engine
+{
+ private $enchant_broker;
+ private $enchant_dictionary;
+ private $matches = array();
+
+ /**
+ * Return a list of languages supported by this backend
+ *
+ * @see rcube_spellcheck_engine::languages()
+ */
+ function languages()
+ {
+ $this->init();
+
+ $langs = array();
+ $dicts = enchant_broker_list_dicts($this->enchant_broker);
+ foreach ($dicts as $dict) {
+ $langs[] = preg_replace('/-.*$/', '', $dict['lang_tag']);
+ }
+
+ return array_unique($langs);
+ }
+
+ /**
+ * Initializes Enchant dictionary
+ */
+ private function init()
+ {
+ if (!$this->enchant_broker) {
+ if (!extension_loaded('enchant')) {
+ $this->error = "Enchant extension not available";
+ return;
+ }
+
+ $this->enchant_broker = enchant_broker_init();
+ }
+
+ if (!enchant_broker_dict_exists($this->enchant_broker, $this->lang)) {
+ $this->error = "Unable to load dictionary for selected language using Enchant";
+ return;
+ }
+
+ $this->enchant_dictionary = enchant_broker_request_dict($this->enchant_broker, $this->lang);
+ }
+
+ /**
+ * Set content and check spelling
+ *
+ * @see rcube_spellcheck_engine::check()
+ */
+ function check($text)
+ {
+ $this->init();
+
+ if (!$this->enchant_dictionary) {
+ return array();
+ }
+
+ // tokenize
+ $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
+
+ $diff = 0;
+ $matches = array();
+
+ foreach ($text as $w) {
+ $word = trim($w[0]);
+ $pos = $w[1] - $diff;
+ $len = mb_strlen($word);
+
+ // skip exceptions
+ if ($this->dictionary->is_exception($word)) {
+ }
+ else if (!enchant_dict_check($this->enchant_dictionary, $word)) {
+ $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
+
+ if (sizeof($suggestions) > self::MAX_SUGGESTIONS) {
+ $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
+ }
+
+ $matches[] = array($word, $pos, $len, null, $suggestions);
+ }
+
+ $diff += (strlen($word) - $len);
+ }
+
+ $this->matches = $matches;
+ return $matches;
+ }
+
+ /**
+ * Returns suggestions for the specified word
+ *
+ * @see rcube_spellcheck_engine::get_words()
+ */
+ function get_suggestions($word)
+ {
+ $this->init();
+
+ if (!$this->enchant_dictionary) {
+ return array();
+ }
+
+ $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
+
+ if (sizeof($suggestions) > self::MAX_SUGGESTIONS)
+ $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
+
+ return is_array($suggestions) ? $suggestions : array();
+ }
+
+ /**
+ * Returns misspelled words
+ *
+ * @see rcube_spellcheck_engine::get_suggestions()
+ */
+ function get_words($text = null)
+ {
+ $result = array();
+
+ if ($text) {
+ // init spellchecker
+ $this->init();
+
+ if (!$this->enchant_dictionary) {
+ return array();
+ }
+
+ // With Enchant we don't need to get suggestions to return misspelled words
+ $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
+
+ foreach ($text as $w) {
+ $word = trim($w[0]);
+
+ // skip exceptions
+ if ($this->dictionary->is_exception($word)) {
+ continue;
+ }
+
+ if (!enchant_dict_check($this->enchant_dictionary, $word)) {
+ $result[] = $word;
+ }
+ }
+
+ return $result;
+ }
+
+ foreach ($this->matches as $m) {
+ $result[] = $m[0];
+ }
+
+ return $result;
+ }
+
+}
+
diff --git a/program/lib/Roundcube/rcube_spellcheck_engine.php b/program/lib/Roundcube/rcube_spellcheck_engine.php
new file mode 100644
index 000000000..3cb4ca3de
--- /dev/null
+++ b/program/lib/Roundcube/rcube_spellcheck_engine.php
@@ -0,0 +1,91 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) 2011-2013, Kolab Systems AG |
+ | Copyright (C) 2008-2013, The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Interface class for a spell-checking backend |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Interface class for a spell-checking backend
+ *
+ * @package Framework
+ * @subpackage Utils
+ */
+abstract class rcube_spellcheck_engine
+{
+ const MAX_SUGGESTIONS = 10;
+
+ protected $lang;
+ protected $error;
+ protected $dictionary;
+ protected $separator = '/[\s\r\n\t\(\)\/\[\]{}<>\\"]+|[:;?!,\.](?=\W|$)/';
+
+ /**
+ * Default constructor
+ */
+ public function __construct($dict, $lang)
+ {
+ $this->dictionary = $dict;
+ $this->lang = $lang;
+ }
+
+ /**
+ * Return a list of languages supported by this backend
+ *
+ * @return array Indexed list of language codes
+ */
+ abstract function languages();
+
+ /**
+ * Set content and check spelling
+ *
+ * @param string $text Text content for spellchecking
+ *
+ * @return bool True when no mispelling found, otherwise false
+ */
+ abstract function check($text);
+
+ /**
+ * Returns suggestions for the specified word
+ *
+ * @param string $word The word
+ *
+ * @return array Suggestions list
+ */
+ abstract function get_suggestions($word);
+
+ /**
+ * Returns misspelled words
+ *
+ * @param string $text The content for spellchecking. If empty content
+ * used for check() method will be used.
+ *
+ * @return array List of misspelled words
+ */
+ abstract function get_words($text = null);
+
+ /**
+ * Returns error message
+ *
+ * @return string Error message
+ */
+ public function error()
+ {
+ return $this->error;
+ }
+
+}
+
diff --git a/program/lib/Roundcube/rcube_spellcheck_googie.php b/program/lib/Roundcube/rcube_spellcheck_googie.php
new file mode 100644
index 000000000..3777942a6
--- /dev/null
+++ b/program/lib/Roundcube/rcube_spellcheck_googie.php
@@ -0,0 +1,176 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) 2008-2013, The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Spellchecking backend implementation to work with Googiespell |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Spellchecking backend implementation to work with a Googiespell service
+ *
+ * @package Framework
+ * @subpackage Utils
+ */
+class rcube_spellcheck_googie extends rcube_spellcheck_engine
+{
+ const GOOGIE_HOST = 'ssl://spell.roundcube.net';
+ const GOOGIE_PORT = 443;
+
+ private $matches = array();
+ private $content;
+
+ /**
+ * Return a list of languages supported by this backend
+ *
+ * @see rcube_spellcheck_engine::languages()
+ */
+ function languages()
+ {
+ return array('am','ar','ar','bg','br','ca','cs','cy','da',
+ 'de_CH','de_DE','el','en_GB','en_US',
+ 'eo','es','et','eu','fa','fi','fr_FR','ga','gl','gl',
+ 'he','hr','hu','hy','is','it','ku','lt','lv','nl',
+ 'pl','pt_BR','pt_PT','ro','ru',
+ 'sk','sl','sv','uk');
+ }
+
+ /**
+ * Set content and check spelling
+ *
+ * @see rcube_spellcheck_engine::check()
+ */
+ function check($text)
+ {
+ $this->content = $text;
+
+ // spell check uri is configured
+ $url = rcube::get_instance()->config->get('spellcheck_uri');
+
+ if ($url) {
+ $a_uri = parse_url($url);
+ $ssl = ($a_uri['scheme'] == 'https' || $a_uri['scheme'] == 'ssl');
+ $port = $a_uri['port'] ? $a_uri['port'] : ($ssl ? 443 : 80);
+ $host = ($ssl ? 'ssl://' : '') . $a_uri['host'];
+ $path = $a_uri['path'] . ($a_uri['query'] ? '?'.$a_uri['query'] : '') . $this->lang;
+ }
+ else {
+ $host = self::GOOGIE_HOST;
+ $port = self::GOOGIE_PORT;
+ $path = '/tbproxy/spell?lang=' . $this->lang;
+ }
+
+ $path .= sprintf('&key=%06d', $_SESSION['user_id']);
+
+ $gtext = '<?xml version="1.0" encoding="utf-8" ?>'
+ .'<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">'
+ .'<text>' . htmlspecialchars($text, ENT_QUOTES, RCUBE_CHARSET) . '</text>'
+ .'</spellrequest>';
+
+ $store = '';
+ if ($fp = fsockopen($host, $port, $errno, $errstr, 30)) {
+ $out = "POST $path HTTP/1.0\r\n";
+ $out .= "Host: " . str_replace('ssl://', '', $host) . "\r\n";
+ $out .= "User-Agent: Roundcube Webmail/" . RCMAIL_VERSION . " (Googiespell Wrapper)\r\n";
+ $out .= "Content-Length: " . strlen($gtext) . "\r\n";
+ $out .= "Content-Type: text/xml\r\n";
+ $out .= "Connection: Close\r\n\r\n";
+ $out .= $gtext;
+ fwrite($fp, $out);
+
+ while (!feof($fp))
+ $store .= fgets($fp, 128);
+ fclose($fp);
+ }
+
+ // parse HTTP response
+ if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $store, $m)) {
+ $http_status = $m[1];
+ if ($http_status != '200') {
+ $this->error = 'HTTP ' . $m[1] . $m[2];
+ $this->error .= "\n" . $store;
+ }
+ }
+
+ if (!$store) {
+ $this->error = "Empty result from spelling engine";
+ }
+ else if (preg_match('/<spellresult error="([^"]+)"/', $store, $m) && $m[1]) {
+ $this->error = "Error code $m[1] returned";
+ $this->error .= preg_match('/<errortext>([^<]+)/', $store, $m) ? ": " . html_entity_decode($m[1]) : '';
+ }
+
+ preg_match_all('/<c o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/c>/', $store, $matches, PREG_SET_ORDER);
+
+ // skip exceptions (if appropriate options are enabled)
+ foreach ($matches as $idx => $m) {
+ $word = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
+ // skip exceptions
+ if ($this->dictionary->is_exception($word)) {
+ unset($matches[$idx]);
+ }
+ }
+
+ $this->matches = $matches;
+ return $matches;
+ }
+
+ /**
+ * Returns suggestions for the specified word
+ *
+ * @see rcube_spellcheck_engine::get_words()
+ */
+ function get_suggestions($word)
+ {
+ $matches = $word ? $this->check($word) : $this->matches;
+
+ if ($matches[0][4]) {
+ $suggestions = explode("\t", $matches[0][4]);
+ if (sizeof($suggestions) > self::MAX_SUGGESTIONS) {
+ $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
+ }
+
+ return $suggestions;
+ }
+
+ return array();
+ }
+
+ /**
+ * Returns misspelled words
+ *
+ * @see rcube_spellcheck_engine::get_suggestions()
+ */
+ function get_words($text = null)
+ {
+ if ($text) {
+ $matches = $this->check($text);
+ }
+ else {
+ $matches = $this->matches;
+ $text = $this->content;
+ }
+
+ $result = array();
+
+ foreach ($matches as $m) {
+ $result[] = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
+ }
+
+ return $result;
+ }
+
+}
+
diff --git a/program/lib/Roundcube/rcube_spellcheck_pspell.php b/program/lib/Roundcube/rcube_spellcheck_pspell.php
new file mode 100644
index 000000000..b12684e43
--- /dev/null
+++ b/program/lib/Roundcube/rcube_spellcheck_pspell.php
@@ -0,0 +1,189 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) 2008-2013, The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Spellchecking backend implementation to work with Pspell |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Spellchecking backend implementation to work with Pspell
+ *
+ * @package Framework
+ * @subpackage Utils
+ */
+class rcube_spellcheck_pspell extends rcube_spellcheck_engine
+{
+ private $plink;
+ private $matches = array();
+
+ /**
+ * Return a list of languages supported by this backend
+ *
+ * @see rcube_spellcheck_engine::languages()
+ */
+ function languages()
+ {
+ $defaults = array('en');
+ $langs = array();
+
+ // get aspell dictionaries
+ exec('aspell dump dicts', $dicts);
+ if (!empty($dicts)) {
+ $seen = array();
+ foreach ($dicts as $lang) {
+ $lang = preg_replace('/-.*$/', '', $lang);
+ $langc = strlen($lang) == 2 ? $lang.'_'.strtoupper($lang) : $lang;
+ if (!$seen[$langc]++)
+ $langs[] = $lang;
+ }
+ $langs = array_unique($langs);
+ }
+ else {
+ $langs = $defaults;
+ }
+
+ return $langs;
+ }
+
+ /**
+ * Initializes PSpell dictionary
+ */
+ private function init()
+ {
+ if (!$this->plink) {
+ if (!extension_loaded('pspell')) {
+ $this->error = "Pspell extension not available";
+ return;
+ }
+
+ $this->plink = pspell_new($this->lang, null, null, RCUBE_CHARSET, PSPELL_FAST);
+ }
+
+ if (!$this->plink) {
+ $this->error = "Unable to load Pspell engine for selected language";
+ }
+ }
+
+ /**
+ * Set content and check spelling
+ *
+ * @see rcube_spellcheck_engine::check()
+ */
+ function check($text)
+ {
+ $this->init();
+
+ if (!$this->plink) {
+ return array();
+ }
+
+ // tokenize
+ $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
+
+ $diff = 0;
+ $matches = array();
+
+ foreach ($text as $w) {
+ $word = trim($w[0]);
+ $pos = $w[1] - $diff;
+ $len = mb_strlen($word);
+
+ // skip exceptions
+ if ($this->dictionary->is_exception($word)) {
+ }
+ else if (!pspell_check($this->plink, $word)) {
+ $suggestions = pspell_suggest($this->plink, $word);
+
+ if (sizeof($suggestions) > self::MAX_SUGGESTIONS) {
+ $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
+ }
+
+ $matches[] = array($word, $pos, $len, null, $suggestions);
+ }
+
+ $diff += (strlen($word) - $len);
+ }
+
+ $this->matches = $matches;
+ return $matches;
+ }
+
+ /**
+ * Returns suggestions for the specified word
+ *
+ * @see rcube_spellcheck_engine::get_words()
+ */
+ function get_suggestions($word)
+ {
+ $this->init();
+
+ if (!$this->plink) {
+ return array();
+ }
+
+ $suggestions = pspell_suggest($this->plink, $word);
+
+ if (sizeof($suggestions) > self::MAX_SUGGESTIONS)
+ $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
+
+ return is_array($suggestions) ? $suggestions : array();
+ }
+
+ /**
+ * Returns misspelled words
+ *
+ * @see rcube_spellcheck_engine::get_suggestions()
+ */
+ function get_words($text = null)
+ {
+ $result = array();
+
+ if ($text) {
+ // init spellchecker
+ $this->init();
+
+ if (!$this->plink) {
+ return array();
+ }
+
+ // With PSpell we don't need to get suggestions to return misspelled words
+ $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
+
+ foreach ($text as $w) {
+ $word = trim($w[0]);
+
+ // skip exceptions
+ if ($this->dictionary->is_exception($word)) {
+ continue;
+ }
+
+ if (!pspell_check($this->plink, $word)) {
+ $result[] = $word;
+ }
+ }
+
+ return $result;
+ }
+
+ foreach ($this->matches as $m) {
+ $result[] = $m[0];
+ }
+
+ return $result;
+ }
+
+}
+
diff --git a/program/lib/Roundcube/rcube_spellchecker.php b/program/lib/Roundcube/rcube_spellchecker.php
index df4365223..672515204 100644
--- a/program/lib/Roundcube/rcube_spellchecker.php
+++ b/program/lib/Roundcube/rcube_spellchecker.php
@@ -38,7 +38,7 @@ class rcube_spellchecker
// default settings
- const GOOGLE_HOST = 'ssl://www.google.com';
+ const GOOGLE_HOST = 'ssl://spell.roundcube.net';
const GOOGLE_PORT = 443;
const MAX_SUGGESTIONS = 10;
@@ -84,9 +84,6 @@ class rcube_spellchecker
if ($this->engine == 'pspell') {
$this->matches = $this->_pspell_check($this->content);
}
- else if ($this->engine == 'enchant') {
- $this->matches = $this->_enchant_check($this->content);
- }
else {
$this->matches = $this->_googie_check($this->content);
}
@@ -118,9 +115,6 @@ class rcube_spellchecker
if ($this->engine == 'pspell') {
return $this->_pspell_suggestions($word);
}
- else if ($this->engine == 'enchant') {
- return $this->_enchant_suggestions($word);
- }
return $this->_googie_suggestions($word);
}
@@ -139,9 +133,6 @@ class rcube_spellchecker
if ($this->engine == 'pspell') {
return $this->_pspell_words($text, $is_html);
}
- else if ($this->engine == 'enchant') {
- return $this->_enchant_words($text, $is_html);
- }
return $this->_googie_words($text, $is_html);
}
@@ -323,6 +314,11 @@ class rcube_spellchecker
if (!$this->plink) {
if (!extension_loaded('pspell')) {
$this->error = "Pspell extension not available";
+ rcube::raise_error(array(
+ 'code' => 500, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => $this->error), true, false);
+
return;
}
@@ -335,141 +331,6 @@ class rcube_spellchecker
}
- /**
- * Checks the text using enchant
- *
- * @param string $text Text content for spellchecking
- */
- private function _enchant_check($text)
- {
- // init spellchecker
- $this->_enchant_init();
-
- if (!$this->enchant_dictionary) {
- return array();
- }
-
- // tokenize
- $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
-
- $diff = 0;
- $matches = array();
-
- foreach ($text as $w) {
- $word = trim($w[0]);
- $pos = $w[1] - $diff;
- $len = mb_strlen($word);
-
- // skip exceptions
- if ($this->is_exception($word)) {
- }
- else if (!enchant_dict_check($this->enchant_dictionary, $word)) {
- $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
-
- if (sizeof($suggestions) > self::MAX_SUGGESTIONS) {
- $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
- }
-
- $matches[] = array($word, $pos, $len, null, $suggestions);
- }
-
- $diff += (strlen($word) - $len);
- }
-
- return $matches;
- }
-
-
- /**
- * Returns the misspelled words
- */
- private function _enchant_words($text = null, $is_html=false)
- {
- $result = array();
-
- if ($text) {
- // init spellchecker
- $this->_enchant_init();
-
- if (!$this->enchant_dictionary) {
- return array();
- }
-
- // With Enchant we don't need to get suggestions to return misspelled words
- if ($is_html) {
- $text = $this->html2text($text);
- }
-
- $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
-
- foreach ($text as $w) {
- $word = trim($w[0]);
-
- // skip exceptions
- if ($this->is_exception($word)) {
- continue;
- }
-
- if (!enchant_dict_check($this->enchant_dictionary, $word)) {
- $result[] = $word;
- }
- }
-
- return $result;
- }
-
- foreach ($this->matches as $m) {
- $result[] = $m[0];
- }
-
- return $result;
- }
-
-
- /**
- * Returns suggestions for misspelled word
- */
- private function _enchant_suggestions($word)
- {
- // init spellchecker
- $this->_enchant_init();
-
- if (!$this->enchant_dictionary) {
- return array();
- }
-
- $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
-
- if (sizeof($suggestions) > self::MAX_SUGGESTIONS)
- $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
-
- return is_array($suggestions) ? $suggestions : array();
- }
-
-
- /**
- * Initializes PSpell dictionary
- */
- private function _enchant_init()
- {
- if (!$this->enchant_broker) {
- if (!extension_loaded('enchant')) {
- $this->error = "Enchant extension not available";
- return;
- }
-
- $this->enchant_broker = enchant_broker_init();
- }
-
- if (!enchant_broker_dict_exists($this->enchant_broker, $this->lang)) {
- $this->error = "Unable to load dictionary for selected language using Enchant";
- return;
- }
-
- $this->enchant_dictionary = enchant_broker_request_dict($this->enchant_broker, $this->lang);
- }
-
-
private function _googie_check($text)
{
// spell check uri is configured
@@ -493,7 +354,7 @@ class rcube_spellchecker
$gtext = '<?xml version="1.0" encoding="utf-8" ?>'
.'<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">'
- .'<text>' . $gtext . '</text>'
+ .'<text>' . htmlspecialchars($gtext) . '</text>'
.'</spellrequest>';
$store = '';
@@ -511,19 +372,9 @@ class rcube_spellchecker
fclose($fp);
}
- // parse HTTP response
- if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $store, $m)) {
- $http_status = $m[1];
- if ($http_status != '200')
- $this->error = 'HTTP ' . $m[1] . $m[2];
- }
-
if (!$store) {
$this->error = "Empty result from spelling engine";
}
- else if (preg_match('/<spellresult error="([^"]+)"/', $store, $m) && $m[1]) {
- $this->error = "Error code $m[1] returned";
- }
preg_match_all('/<c o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/c>/', $store, $matches, PREG_SET_ORDER);
diff --git a/program/lib/Roundcube/rcube_storage.php b/program/lib/Roundcube/rcube_storage.php
index e697b2c73..8193e540c 100644
--- a/program/lib/Roundcube/rcube_storage.php
+++ b/program/lib/Roundcube/rcube_storage.php
@@ -61,6 +61,8 @@ abstract class rcube_storage
'MAIL-FOLLOWUP-TO',
'MAIL-REPLY-TO',
'RETURN-PATH',
+ 'DELIVERED-TO',
+ 'ENVELOPE-TO',
);
const UNKNOWN = 0;
@@ -538,13 +540,12 @@ abstract class rcube_storage
/**
* Append a mail message (source) to a specific folder.
*
- * @param string $folder Target folder
- * @param string|array $message The message source string or filename
- * or array (of strings and file pointers)
- * @param string $headers Headers string if $message contains only the body
- * @param boolean $is_file True if $message is a filename
- * @param array $flags Message flags
- * @param mixed $date Message internal date
+ * @param string $folder Target folder
+ * @param string $message The message source string or filename
+ * @param string $headers Headers string if $message contains only the body
+ * @param boolean $is_file True if $message is a filename
+ * @param array $flags Message flags
+ * @param mixed $date Message internal date
*
* @return int|bool Appended message UID or True on success, False on error
*/
@@ -806,14 +807,13 @@ abstract class rcube_storage
/**
- * Returns current status of a folder (compared to the last time use)
+ * Returns current status of a folder
*
* @param string $folder Folder name
- * @param array $diff Difference data
*
* @return int Folder status
*/
- abstract function folder_status($folder = null, &$diff = array());
+ abstract function folder_status($folder = null);
/**
@@ -985,6 +985,6 @@ abstract class rcube_storage
/**
* Delete outdated cache entries
*/
- abstract function cache_gc();
+ abstract function expunge_cache();
} // end class rcube_storage
diff --git a/program/lib/Roundcube/rcube_string_replacer.php b/program/lib/Roundcube/rcube_string_replacer.php
index 354b4596d..bd26f8e7d 100644
--- a/program/lib/Roundcube/rcube_string_replacer.php
+++ b/program/lib/Roundcube/rcube_string_replacer.php
@@ -28,10 +28,9 @@ class rcube_string_replacer
public $mailto_pattern;
public $link_pattern;
private $values = array();
- private $options = array();
- function __construct($options = array())
+ function __construct()
{
// Simplified domain expression for UTF8 characters handling
// Support unicode/punycode in top-level domain part
@@ -45,8 +44,6 @@ class rcube_string_replacer
."@$utf_domain" // domain-part
."(\?[$url1$url2]+)?" // e.g. ?subject=test...
.")/";
-
- $this->options = $options;
}
/**
@@ -92,10 +89,10 @@ class rcube_string_replacer
if ($url) {
$suffix = $this->parse_url_brackets($url);
- $attrib = (array)$this->options['link_attribs'];
- $attrib['href'] = $url_prefix . $url;
-
- $i = $this->add(html::a($attrib, rcube::Q($url)) . $suffix);
+ $i = $this->add(html::a(array(
+ 'href' => $url_prefix . $url,
+ 'target' => '_blank'
+ ), rcube::Q($url)) . $suffix);
}
// Return valid link for recognized schemes, otherwise
diff --git a/program/lib/Roundcube/rcube_user.php b/program/lib/Roundcube/rcube_user.php
index 5e9c9af80..505b190d1 100644
--- a/program/lib/Roundcube/rcube_user.php
+++ b/program/lib/Roundcube/rcube_user.php
@@ -495,9 +495,9 @@ class rcube_user
"INSERT INTO ".$dbh->table_name('users').
" (created, last_login, username, mail_host, language)".
" VALUES (".$dbh->now().", ".$dbh->now().", ?, ?, ?)",
- $data['user'],
- $data['host'],
- $data['language']);
+ strip_newlines($data['user']),
+ strip_newlines($data['host']),
+ strip_newlines($data['language']));
if ($user_id = $dbh->insert_id('users')) {
// create rcube_user instance to make plugin hooks work
@@ -517,7 +517,7 @@ class rcube_user
if (empty($user_email)) {
$user_email = strpos($data['user'], '@') ? $user : sprintf('%s@%s', $data['user'], $mail_domain);
}
- $email_list[] = $user_email;
+ $email_list[] = strip_newlines($user_email);
}
// identities_level check
else if (count($email_list) > 1 && $rcube->config->get('identities_level', 0) > 1) {
@@ -547,6 +547,7 @@ class rcube_user
$record['name'] = $user_name != $record['email'] ? $user_name : '';
}
+ $record['name'] = strip_newlines($record['name']);
$record['user_id'] = $user_id;
$record['standard'] = $standard;
diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php
index 1d76ae508..4dadbb8bd 100644
--- a/program/lib/Roundcube/rcube_utils.php
+++ b/program/lib/Roundcube/rcube_utils.php
@@ -400,7 +400,7 @@ class rcube_utils
$out = array();
$src = $mode == self::INPUT_GET ? $_GET : ($mode == self::INPUT_POST ? $_POST : $_REQUEST);
- foreach (array_keys($src) as $key) {
+ foreach ($src as $key => $value) {
$fname = $key[0] == '_' ? substr($key, 1) : $key;
if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname)) {
$out[$fname] = self::get_input_value($key, $mode);
@@ -476,9 +476,9 @@ class rcube_utils
// remove html comments and add #container to each tag selector.
// also replace body definition because we also stripped off the <body> tag
- $styles = preg_replace(
+ $source = preg_replace(
array(
- '/(^\s*<!--)|(-->\s*$)/',
+ '/(^\s*<\!--)|(-->\s*$)/m',
'/(^\s*|,\s*|\}\s*)([a-z0-9\._#\*][a-z0-9\.\-_]*)/im',
'/'.preg_quote($container_id, '/').'\s+body/i',
),
@@ -490,9 +490,9 @@ class rcube_utils
$source);
// put block contents back in
- $styles = $replacements->resolve($styles);
+ $source = $replacements->resolve($source);
- return $styles;
+ return $source;
}
@@ -506,24 +506,17 @@ class rcube_utils
*/
public static function file2class($mimetype, $filename)
{
- $mimetype = strtolower($mimetype);
- $filename = strtolower($filename);
-
list($primary, $secondary) = explode('/', $mimetype);
$classes = array($primary ? $primary : 'unknown');
-
if ($secondary) {
$classes[] = $secondary;
}
-
- if (preg_match('/\.([a-z0-9]+)$/', $filename, $m)) {
- if (!in_array($m[1], $classes)) {
- $classes[] = $m[1];
- }
+ if (preg_match('/\.([a-z0-9]+)$/i', $filename, $m)) {
+ $classes[] = $m[1];
}
- return join(" ", $classes);
+ return strtolower(join(" ", $classes));
}
@@ -666,21 +659,6 @@ class rcube_utils
/**
- * Returns the real remote IP address
- *
- * @return string Remote IP address
- */
- public static function remote_addr()
- {
- foreach (array('HTTP_X_FORWARDED_FOR','HTTP_X_REAL_IP','REMOTE_ADDR') as $prop) {
- if (!empty($_SERVER[$prop]))
- return $_SERVER[$prop];
- }
-
- return '';
- }
-
- /**
* Read a specific HTTP request header.
*
* @param string $name Header name
@@ -761,9 +739,9 @@ class rcube_utils
// Clean malformed data
$date = preg_replace(
array(
- '/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal
- '/[^a-z0-9\x20\x09:+-]/i', // remove any invalid characters
- '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names
+ '/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal
+ '/[^a-z0-9\x20\x09:+-]/i', // remove any invalid characters
+ '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names
),
array(
'\\1',
@@ -771,8 +749,6 @@ class rcube_utils
'',
), $date);
- $date = trim($date);
-
// if date parsing fails, we have a date in non-rfc format.
// remove token from the end and try again
while ((($ts = @strtotime($date)) === false) || ($ts < 0)) {
@@ -787,44 +763,6 @@ class rcube_utils
return (int) $ts;
}
- /**
- * Date parsing function that turns the given value into a DateTime object
- *
- * @param string $date Date string
- *
- * @return object DateTime instance or false on failure
- */
- public static function anytodatetime($date)
- {
- if (is_object($date) && is_a($date, 'DateTime')) {
- return $date;
- }
-
- $dt = false;
- $date = trim($date);
-
- // try to parse string with DateTime first
- if (!empty($date)) {
- try {
- $dt = new DateTime($date);
- }
- catch (Exception $e) {
- // ignore
- }
- }
-
- // try our advanced strtotime() method
- if (!$dt && ($timestamp = self::strtotime($date))) {
- try {
- $dt = new DateTime("@".$timestamp);
- }
- catch (Exception $e) {
- // ignore
- }
- }
-
- return $dt;
- }
/*
* Idn_to_ascii wrapper.
diff --git a/program/lib/Roundcube/rcube_vcard.php b/program/lib/Roundcube/rcube_vcard.php
index d54dc56ad..f76c4f08d 100644
--- a/program/lib/Roundcube/rcube_vcard.php
+++ b/program/lib/Roundcube/rcube_vcard.php
@@ -47,7 +47,6 @@ class rcube_vcard
'manager' => 'X-MANAGER',
'spouse' => 'X-SPOUSE',
'edit' => 'X-AB-EDIT',
- 'groups' => 'CATEGORIES',
);
private $typemap = array(
'IPHONE' => 'mobile',
@@ -358,8 +357,8 @@ class rcube_vcard
case 'birthday':
case 'anniversary':
- if (($val = rcube_utils::anytodatetime($value)) && ($fn = self::$fieldmap[$field])) {
- $this->raw[$fn][] = array(0 => $val->format('Y-m-d'), 'value' => array('date'));
+ if (($val = rcube_utils::strtotime($value)) && ($fn = self::$fieldmap[$field])) {
+ $this->raw[$fn][] = array(0 => date('Y-m-d', $val), 'value' => array('date'));
}
break;
@@ -482,7 +481,7 @@ class rcube_vcard
$vcard_block = '';
$in_vcard_block = false;
- foreach (preg_split("/[\r\n]+/", $data) as $line) {
+ foreach (preg_split("/[\r\n]+/", $data) as $i => $line) {
if ($in_vcard_block && !empty($line)) {
$vcard_block .= $line . "\n";
}
@@ -612,8 +611,8 @@ class rcube_vcard
$enc = null;
foreach($regs2[1] as $attrid => $attr) {
+ $attr = preg_replace('/[\s\t\n\r\0\x0B]/', '', $attr);
if ((list($key, $value) = explode('=', $attr)) && $value) {
- $value = trim($value);
if ($key == 'ENCODING') {
$value = strtoupper($value);
// add next line(s) to value string if QP line end detected
@@ -757,7 +756,7 @@ class rcube_vcard
*
* @return string Joined and quoted string
*/
- public static function vcard_quote($s, $sep = ';')
+ private static function vcard_quote($s, $sep = ';')
{
if (is_array($s)) {
foreach($s as $part) {
@@ -766,7 +765,7 @@ class rcube_vcard
return(implode($sep, (array)$r));
}
- return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', $sep => '\\'.$sep));
+ return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', ',' => '\,', ';' => '\;'));
}
/**
@@ -792,7 +791,7 @@ class rcube_vcard
return $result;
}
- $s = strtr($s, $rep2);
+ $s = trim(strtr($s, $rep2));
}
// some implementations (GMail) use non-standard backslash before colon (#1489085)
diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php
index 8f7fe9749..f964f8b35 100644
--- a/program/lib/Roundcube/rcube_washtml.php
+++ b/program/lib/Roundcube/rcube_washtml.php
@@ -410,25 +410,6 @@ class rcube_washtml
);
$html = preg_replace($html_search, $html_replace, trim($html));
- //-> Replace all of those weird MS Word quotes and other high characters
- $badwordchars=array(
- "\xe2\x80\x98", // left single quote
- "\xe2\x80\x99", // right single quote
- "\xe2\x80\x9c", // left double quote
- "\xe2\x80\x9d", // right double quote
- "\xe2\x80\x94", // em dash
- "\xe2\x80\xa6" // elipses
- );
- $fixedwordchars=array(
- "'",
- "'",
- '"',
- '"',
- '&mdash;',
- '...'
- );
- $html = str_replace($badwordchars,$fixedwordchars, $html);
-
// PCRE errors handling (#1486856), should we use something like for every preg_* use?
if ($html === null && ($preg_error = preg_last_error()) != PREG_NO_ERROR) {
$errstr = "Could not clean up HTML message! PCRE Error: $preg_error.";
@@ -448,7 +429,7 @@ class rcube_washtml
}
// fix (unknown/malformed) HTML tags before "wash"
- $html = preg_replace_callback('/(<[\/]*)([^\s>]+)/', array($this, 'html_tag_callback'), $html);
+ $html = preg_replace_callback('/(<(?!\!)[\/]*)([^\s>]+)/', array($this, 'html_tag_callback'), $html);
// Remove invalid HTML comments (#1487759)
// Don't remove valid conditional comments