diff options
Diffstat (limited to 'program/lib')
50 files changed, 4482 insertions, 941 deletions
diff --git a/program/lib/Crypt/GPG.php b/program/lib/Crypt/GPG.php index 6e8e717e8..5c2231289 100644 --- a/program/lib/Crypt/GPG.php +++ b/program/lib/Crypt/GPG.php @@ -47,15 +47,20 @@ * @package Crypt_GPG * @author Nathan Fredrickson <nathan@silverorange.com> * @author Michael Gauthier <mike@silverorange.com> - * @copyright 2005-2010 silverorange + * @copyright 2005-2013 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 $ + * @version CVS: $Id$ * @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/ */ /** + * Base class for GPG methods + */ +require_once 'Crypt/GPGAbstract.php'; + +/** * Signature handler class */ require_once 'Crypt/GPG/VerifyStatusHandler.php'; @@ -65,31 +70,6 @@ require_once 'Crypt/GPG/VerifyStatusHandler.php'; */ 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 /** @@ -104,82 +84,13 @@ require_once 'Crypt/GPG/Exceptions.php'; * @package Crypt_GPG * @author Nathan Fredrickson <nathan@silverorange.com> * @author Michael Gauthier <mike@silverorange.com> - * @copyright 2005-2010 silverorange + * @copyright 2005-2013 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 Crypt_GPG extends Crypt_GPGAbstract { - // {{{ 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 /** @@ -249,12 +160,27 @@ class Crypt_GPG const FORMAT_X509 = 3; // }}} - // {{{ other class constants + // {{{ class constants for boolean options + + /** + * Use to specify ASCII armored mode for returned data + */ + const ARMOR_ASCII = true; + + /** + * Use to specify binary mode for returned data + */ + const ARMOR_BINARY = false; + + /** + * Use to specify that line breaks in signed text should be normalized + */ + const TEXT_NORMALIZED = true; /** - * URI at which package bugs may be reported. + * Use to specify that line breaks in signed text should not be normalized */ - const BUG_URI = 'http://pear.php.net/bugs/report.php?package=Crypt_GPG'; + const TEXT_RAW = false; // }}} // {{{ protected class properties @@ -326,88 +252,6 @@ class Crypt_GPG 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() /** @@ -520,7 +364,9 @@ class Crypt_GPG if ($fingerprint === null) { throw new Crypt_GPG_KeyNotFoundException( 'Public key not found: ' . $keyId, - Crypt_GPG::ERROR_KEY_NOT_FOUND, $keyId); + self::ERROR_KEY_NOT_FOUND, + $keyId + ); } $keyData = ''; @@ -534,11 +380,13 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); - if ($code !== Crypt_GPG::ERROR_NONE) { + if ($code !== self::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); + 'file a bug report at ' . self::BUG_URI, + $code + ); } return $keyData; @@ -583,7 +431,9 @@ class Crypt_GPG if ($fingerprint === null) { throw new Crypt_GPG_KeyNotFoundException( 'Public key not found: ' . $keyId, - Crypt_GPG::ERROR_KEY_NOT_FOUND, $keyId); + self::ERROR_KEY_NOT_FOUND, + $keyId + ); } $operation = '--delete-key ' . escapeshellarg($fingerprint); @@ -599,17 +449,22 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_NONE: + case self::ERROR_NONE: break; - case Crypt_GPG::ERROR_DELETE_PRIVATE_KEY: + case self::ERROR_DELETE_PRIVATE_KEY: throw new Crypt_GPG_DeletePrivateKeyException( 'Private key must be deleted before public key can be ' . - 'deleted.', $code, $keyId); + '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); + 'file a bug report at ' . self::BUG_URI, + $code + ); } } @@ -647,7 +502,9 @@ class Crypt_GPG if ($fingerprint === null) { throw new Crypt_GPG_KeyNotFoundException( 'Private key not found: ' . $keyId, - Crypt_GPG::ERROR_KEY_NOT_FOUND, $keyId); + self::ERROR_KEY_NOT_FOUND, + $keyId + ); } $operation = '--delete-secret-key ' . escapeshellarg($fingerprint); @@ -663,17 +520,21 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_NONE: + case self::ERROR_NONE: break; - case Crypt_GPG::ERROR_KEY_NOT_FOUND: + case self::ERROR_KEY_NOT_FOUND: throw new Crypt_GPG_KeyNotFoundException( 'Private key not found: ' . $keyId, - $code, $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); + 'file a bug report at ' . self::BUG_URI, + $code + ); } } @@ -705,161 +566,7 @@ class Crypt_GPG */ 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; + return parent::_getKeys($keyId); } // }}} @@ -895,7 +602,7 @@ class Crypt_GPG * Use the <kbd>debug</kbd> option and file a bug report if these * exceptions occur. */ - public function getFingerprint($keyId, $format = Crypt_GPG::FORMAT_NONE) + public function getFingerprint($keyId, $format = self::FORMAT_NONE) { $output = ''; $operation = '--list-keys ' . escapeshellarg($keyId); @@ -912,15 +619,17 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_NONE: - case Crypt_GPG::ERROR_KEY_NOT_FOUND: + case self::ERROR_NONE: + case self::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); + 'file a bug report at ' . self::BUG_URI, + $code + ); } $fingerprint = null; @@ -932,13 +641,13 @@ class Crypt_GPG $fingerprint = $lineExp[9]; switch ($format) { - case Crypt_GPG::FORMAT_CANONICAL: + case self::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: + case self::FORMAT_X509: $fingerprintExp = str_split($fingerprint, 2); $fingerprint = implode(':', $fingerprintExp); break; @@ -976,7 +685,7 @@ class Crypt_GPG * * @sensitive $data */ - public function encrypt($data, $armor = true) + public function encrypt($data, $armor = self::ARMOR_ASCII) { return $this->_encrypt($data, false, null, $armor); } @@ -1012,8 +721,11 @@ class Crypt_GPG * Use the <kbd>debug</kbd> option and file a bug report if these * exceptions occur. */ - public function encryptFile($filename, $encryptedFile = null, $armor = true) - { + public function encryptFile( + $filename, + $encryptedFile = null, + $armor = self::ARMOR_ASCII + ) { return $this->_encrypt($filename, true, $encryptedFile, $armor); } @@ -1052,7 +764,7 @@ class Crypt_GPG * * @see Crypt_GPG::decryptAndVerify() */ - public function encryptAndSign($data, $armor = true) + public function encryptAndSign($data, $armor = self::ARMOR_ASCII) { return $this->_encryptAndSign($data, false, null, $armor); } @@ -1103,8 +815,10 @@ class Crypt_GPG * * @see Crypt_GPG::decryptAndVerifyFile() */ - public function encryptAndSignFile($filename, $signedFile = null, - $armor = true + public function encryptAndSignFile( + $filename, + $signedFile = null, + $armor = self::ARMOR_ASCII ) { return $this->_encryptAndSign($filename, true, $signedFile, $armor); } @@ -1315,8 +1029,11 @@ class Crypt_GPG * 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 + public function sign( + $data, + $mode = self::SIGN_MODE_NORMAL, + $armor = self::ARMOR_ASCII, + $textmode = self::TEXT_RAW ) { return $this->_sign($data, false, null, $mode, $armor, $textmode); } @@ -1376,8 +1093,12 @@ class Crypt_GPG * 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 + public function signFile( + $filename, + $signedFile = null, + $mode = self::SIGN_MODE_NORMAL, + $armor = self::ARMOR_ASCII, + $textmode = self::TEXT_RAW ) { return $this->_sign( $filename, @@ -1472,7 +1193,7 @@ class Crypt_GPG * @param string $passphrase optional. The passphrase of the key required * for decryption. * - * @return void + * @return Crypt_GPG the current object, for fluent interface. * * @see Crypt_GPG::decrypt() * @see Crypt_GPG::decryptFile() @@ -1485,6 +1206,7 @@ class Crypt_GPG public function addDecryptKey($key, $passphrase = null) { $this->_addKey($this->decryptKeys, true, false, $key, $passphrase); + return $this; } // }}} @@ -1498,7 +1220,7 @@ class Crypt_GPG * {@link Crypt_GPG_SubKey}. The key must be able to * encrypt. * - * @return void + * @return Crypt_GPG the current object, for fluent interface. * * @see Crypt_GPG::encrypt() * @see Crypt_GPG::encryptFile() @@ -1508,6 +1230,7 @@ class Crypt_GPG public function addEncryptKey($key) { $this->_addKey($this->encryptKeys, true, false, $key); + return $this; } // }}} @@ -1523,7 +1246,7 @@ class Crypt_GPG * @param string $passphrase optional. The passphrase of the key required * for signing. * - * @return void + * @return Crypt_GPG the current object, for fluent interface. * * @see Crypt_GPG::sign() * @see Crypt_GPG::signFile() @@ -1536,6 +1259,7 @@ class Crypt_GPG public function addSignKey($key, $passphrase = null) { $this->_addKey($this->signKeys, false, true, $key, $passphrase); + return $this; } // }}} @@ -1544,7 +1268,7 @@ class Crypt_GPG /** * Clears all decryption keys * - * @return void + * @return Crypt_GPG the current object, for fluent interface. * * @see Crypt_GPG::decrypt() * @see Crypt_GPG::addDecryptKey() @@ -1552,6 +1276,7 @@ class Crypt_GPG public function clearDecryptKeys() { $this->decryptKeys = array(); + return $this; } // }}} @@ -1560,7 +1285,7 @@ class Crypt_GPG /** * Clears all encryption keys * - * @return void + * @return Crypt_GPG the current object, for fluent interface. * * @see Crypt_GPG::encrypt() * @see Crypt_GPG::addEncryptKey() @@ -1568,6 +1293,7 @@ class Crypt_GPG public function clearEncryptKeys() { $this->encryptKeys = array(); + return $this; } // }}} @@ -1576,7 +1302,7 @@ class Crypt_GPG /** * Clears all signing keys * - * @return void + * @return Crypt_GPG the current object, for fluent interface. * * @see Crypt_GPG::sign() * @see Crypt_GPG::addSignKey() @@ -1584,6 +1310,7 @@ class Crypt_GPG public function clearSignKeys() { $this->signKeys = array(); + return $this; } // }}} @@ -1658,24 +1385,6 @@ class Crypt_GPG } // }}} - // {{{ 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() /** @@ -1698,7 +1407,7 @@ class Crypt_GPG * * @sensitive $passphrase */ - private function _addKey(array &$array, $encrypt, $sign, $key, + protected function _addKey(array &$array, $encrypt, $sign, $key, $passphrase = null ) { $subKeys = array(); @@ -1707,7 +1416,10 @@ class Crypt_GPG $keys = $this->getKeys($key); if (count($keys) == 0) { throw new Crypt_GPG_KeyNotFoundException( - 'Key "' . $key . '" not found.', 0, $key); + 'Key "' . $key . '" not found.', + 0, + $key + ); } $key = $keys[0]; } @@ -1715,12 +1427,14 @@ class Crypt_GPG if ($key instanceof Crypt_GPG_Key) { if ($encrypt && !$key->canEncrypt()) { throw new InvalidArgumentException( - 'Key "' . $key . '" cannot encrypt.'); + 'Key "' . $key . '" cannot encrypt.' + ); } if ($sign && !$key->canSign()) { throw new InvalidArgumentException( - 'Key "' . $key . '" cannot sign.'); + 'Key "' . $key . '" cannot sign.' + ); } foreach ($key->getSubKeys() as $subKey) { @@ -1741,18 +1455,21 @@ class Crypt_GPG if (count($subKeys) === 0) { throw new InvalidArgumentException( - 'Key "' . $key . '" is not in a recognized format.'); + 'Key "' . $key . '" is not in a recognized format.' + ); } foreach ($subKeys as $subKey) { if ($encrypt && !$subKey->canEncrypt()) { throw new InvalidArgumentException( - 'Key "' . $key . '" cannot encrypt.'); + 'Key "' . $key . '" cannot encrypt.' + ); } if ($sign && !$subKey->canSign()) { throw new InvalidArgumentException( - 'Key "' . $key . '" cannot sign.'); + 'Key "' . $key . '" cannot sign.' + ); } $array[$subKey->getId()] = array( @@ -1763,6 +1480,37 @@ class Crypt_GPG } // }}} + // {{{ _setPinEntryEnv() + + /** + * Sets the PINENTRY_USER_DATA environment variable with the currently + * added keys and passphrases + * + * Keys and pasphrases are stored as an indexed array of associative + * arrays that is JSON encoded to a flat string. + * + * For GnuPG 2.x this is how passphrases are passed. For GnuPG 1.x the + * environment variable is set but not used. + * + * @param array $keys the internal key array to use. + * + * @return void + */ + protected function _setPinEntryEnv(array $keys) + { + $envKeys = array(); + foreach ($keys as $id => $key) { + $envKeys[] = array( + 'keyId' => $id, + 'fingerprint' => $key['fingerprint'], + 'passphrase' => $key['passphrase'] + ); + } + $envKeys = json_encode($envKeys); + $_ENV['PINENTRY_USER_DATA'] = $envKeys; + } + + // }}} // {{{ _importKey() /** @@ -1792,21 +1540,26 @@ class Crypt_GPG * Use the <kbd>debug</kbd> option and file a bug report if these * exceptions occur. */ - private function _importKey($key, $isFile) + protected 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); + 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); + 'No valid GPG key data found.', + self::ERROR_NO_DATA + ); } } @@ -1836,18 +1589,22 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_DUPLICATE_KEY: - case Crypt_GPG::ERROR_NONE: + case self::ERROR_DUPLICATE_KEY: + case self::ERROR_NONE: // ignore duplicate key import errors break; - case Crypt_GPG::ERROR_NO_DATA: + case self::ERROR_NO_DATA: throw new Crypt_GPG_NoDataException( - 'No valid GPG key data found.', $code); + '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); + 'report at ' . self::BUG_URI, + $code + ); } return $result; @@ -1880,18 +1637,23 @@ class Crypt_GPG * Use the <kbd>debug</kbd> option and file a bug report if these * exceptions occur. */ - private function _encrypt($data, $isFile, $outputFile, $armor) + protected function _encrypt($data, $isFile, $outputFile, $armor) { if (count($this->encryptKeys) === 0) { throw new Crypt_GPG_KeyNotFoundException( - 'No encryption keys specified.'); + '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); + throw new Crypt_GPG_FileException( + 'Could not open input file "' . $data . + '" for encryption.', + 0, + $data + ); } } else { $input = strval($data); @@ -1905,9 +1667,12 @@ class Crypt_GPG if ($isFile) { fclose($input); } - throw new Crypt_GPG_FileException('Could not open output ' . - 'file "' . $outputFile . '" for storing encrypted data.', - 0, $outputFile); + throw new Crypt_GPG_FileException( + 'Could not open output file "' . $outputFile . + '" for storing encrypted data.', + 0, + $outputFile + ); } } @@ -1932,11 +1697,13 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); - if ($code !== Crypt_GPG::ERROR_NONE) { + if ($code !== self::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); + 'report at ' . self::BUG_URI, + $code + ); } if ($outputFile === null) { @@ -1976,20 +1743,26 @@ class Crypt_GPG * Use the <kbd>debug</kbd> option and file a bug report if these * exceptions occur. */ - private function _decrypt($data, $isFile, $outputFile) + protected 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); + 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); + 'the provided data.', + self::ERROR_NO_DATA + ); } } @@ -2001,14 +1774,22 @@ class Crypt_GPG if ($isFile) { fclose($input); } - throw new Crypt_GPG_FileException('Could not open output ' . - 'file "' . $outputFile . '" for storing decrypted data.', - 0, $outputFile); + 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); + $handler = new Crypt_GPG_DecryptStatusHandler( + $this->engine, + $this->decryptKeys + ); + + // If using gpg-agent, set the decrypt pins used by the pinentry + $this->_setPinEntryEnv($this->decryptKeys); $this->engine->reset(); $this->engine->addStatusHandler(array($handler, 'handle')); @@ -2080,19 +1861,23 @@ class Crypt_GPG * Use the <kbd>debug</kbd> option and file a bug report if these * exceptions occur. */ - private function _sign($data, $isFile, $outputFile, $mode, $armor, + protected function _sign($data, $isFile, $outputFile, $mode, $armor, $textmode ) { if (count($this->signKeys) === 0) { throw new Crypt_GPG_KeyNotFoundException( - 'No signing keys specified.'); + '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); + throw new Crypt_GPG_FileException( + 'Could not open input file "' . $data . '" for signing.', + 0, + $data + ); } } else { $input = strval($data); @@ -2106,20 +1891,23 @@ class Crypt_GPG if ($isFile) { fclose($input); } - throw new Crypt_GPG_FileException('Could not open output ' . - 'file "' . $outputFile . '" for storing signed ' . - 'data.', 0, $outputFile); + 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: + case self::SIGN_MODE_DETACHED: $operation = '--detach-sign'; break; - case Crypt_GPG::SIGN_MODE_CLEAR: + case self::SIGN_MODE_CLEAR: $operation = '--clearsign'; break; - case Crypt_GPG::SIGN_MODE_NORMAL: + case self::SIGN_MODE_NORMAL: default: $operation = '--sign'; break; @@ -2139,6 +1927,9 @@ class Crypt_GPG escapeshellarg($key['fingerprint']); } + // If using gpg-agent, set the sign pins used by the pinentry + $this->_setPinEntryEnv($this->signKeys); + $this->engine->reset(); $this->engine->addStatusHandler(array($this, 'handleSignStatus')); $this->engine->setInput($input); @@ -2157,24 +1948,32 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_NONE: + case self::ERROR_NONE: break; - case Crypt_GPG::ERROR_KEY_NOT_FOUND: + case self::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: + 'private key before trying to sign data.', + $code, + $this->engine->getErrorKeyId() + ); + case self::ERROR_BAD_PASSPHRASE: throw new Crypt_GPG_BadPassphraseException( - 'Cannot sign data. Incorrect passphrase provided.', $code); - case Crypt_GPG::ERROR_MISSING_PASSPHRASE: + 'Cannot sign data. Incorrect passphrase provided.', + $code + ); + case self::ERROR_MISSING_PASSPHRASE: throw new Crypt_GPG_BadPassphraseException( - 'Cannot sign data. No passphrase provided.', $code); + '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); + 'at ' . self::BUG_URI, + $code + ); } if ($outputFile === null) { @@ -2216,25 +2015,30 @@ class Crypt_GPG * Use the <kbd>debug</kbd> option and file a bug report if these * exceptions occur. */ - private function _encryptAndSign($data, $isFile, $outputFile, $armor) + protected function _encryptAndSign($data, $isFile, $outputFile, $armor) { if (count($this->signKeys) === 0) { throw new Crypt_GPG_KeyNotFoundException( - 'No signing keys specified.'); + 'No signing keys specified.' + ); } if (count($this->encryptKeys) === 0) { throw new Crypt_GPG_KeyNotFoundException( - 'No encryption keys specified.'); + '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); + throw new Crypt_GPG_FileException( + 'Could not open input file "' . $data . + '" for encrypting and signing.', + 0, + $data + ); } } else { $input = strval($data); @@ -2248,9 +2052,12 @@ class Crypt_GPG if ($isFile) { fclose($input); } - throw new Crypt_GPG_FileException('Could not open output ' . - 'file "' . $outputFile . '" for storing encrypted, ' . - 'signed data.', 0, $outputFile); + throw new Crypt_GPG_FileException( + 'Could not open output file "' . $outputFile . + '" for storing encrypted, signed data.', + 0, + $outputFile + ); } } @@ -2261,6 +2068,9 @@ class Crypt_GPG escapeshellarg($key['fingerprint']); } + // If using gpg-agent, set the sign pins used by the pinentry + $this->_setPinEntryEnv($this->signKeys); + foreach ($this->encryptKeys as $key) { $arguments[] = '--recipient ' . escapeshellarg($key['fingerprint']); } @@ -2283,25 +2093,32 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_NONE: + case self::ERROR_NONE: break; - case Crypt_GPG::ERROR_KEY_NOT_FOUND: + case self::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: + $code, + $this->engine->getErrorKeyId() + ); + case self::ERROR_BAD_PASSPHRASE: throw new Crypt_GPG_BadPassphraseException( 'Cannot sign encrypted data. Incorrect passphrase provided.', - $code); - case Crypt_GPG::ERROR_MISSING_PASSPHRASE: + $code + ); + case self::ERROR_MISSING_PASSPHRASE: throw new Crypt_GPG_BadPassphraseException( - 'Cannot sign encrypted data. No passphrase provided.', $code); + '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); + 'file a bug report at ' . self::BUG_URI, + $code + ); } if ($outputFile === null) { @@ -2335,7 +2152,7 @@ class Crypt_GPG * * @see Crypt_GPG_Signature */ - private function _verify($data, $isFile, $signature) + protected function _verify($data, $isFile, $signature) { if ($signature == '') { $operation = '--verify'; @@ -2352,14 +2169,19 @@ class Crypt_GPG if ($isFile) { $input = @fopen($data, 'rb'); if ($input === false) { - throw new Crypt_GPG_FileException('Could not open input ' . - 'file "' . $data . '" for verifying.', 0, $data); + 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); + 'No valid signature data found.', + self::ERROR_NO_DATA + ); } } @@ -2385,21 +2207,27 @@ class Crypt_GPG $code = $this->engine->getErrorCode(); switch ($code) { - case Crypt_GPG::ERROR_NONE: - case Crypt_GPG::ERROR_BAD_SIGNATURE: + case self::ERROR_NONE: + case self::ERROR_BAD_SIGNATURE: break; - case Crypt_GPG::ERROR_NO_DATA: + case self::ERROR_NO_DATA: throw new Crypt_GPG_NoDataException( - 'No valid signature data found.', $code); - case Crypt_GPG::ERROR_KEY_NOT_FOUND: + 'No valid signature data found.', + $code + ); + case self::ERROR_KEY_NOT_FOUND: throw new Crypt_GPG_KeyNotFoundException( 'Public key required for data verification not in keyring.', - $code, $this->engine->getErrorKeyId()); + $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); + 'file a bug report at ' . self::BUG_URI, + $code + ); } return $handler->getSignatures(); @@ -2445,21 +2273,25 @@ class Crypt_GPG * * @see Crypt_GPG_Signature */ - private function _decryptAndVerify($data, $isFile, $outputFile) + protected 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); + 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); + self::ERROR_NO_DATA + ); } } @@ -2471,16 +2303,24 @@ class Crypt_GPG if ($isFile) { fclose($input); } - throw new Crypt_GPG_FileException('Could not open output ' . - 'file "' . $outputFile . '" for storing decrypted data.', - 0, $outputFile); + 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); + $decryptHandler = new Crypt_GPG_DecryptStatusHandler( + $this->engine, + $this->decryptKeys + ); + + // If using gpg-agent, set the decrypt pins used by the pinentry + $this->_setPinEntryEnv($this->decryptKeys); $this->engine->reset(); $this->engine->addStatusHandler(array($verifyHandler, 'handle')); @@ -2515,13 +2355,17 @@ class Crypt_GPG '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()); + 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); + 'was found in the provided data.', + self::ERROR_NO_DATA + ); } throw $e; diff --git a/program/lib/Crypt/GPG/ByteUtils.php b/program/lib/Crypt/GPG/ByteUtils.php new file mode 100644 index 000000000..342905471 --- /dev/null +++ b/program/lib/Crypt/GPG/ByteUtils.php @@ -0,0 +1,105 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * A class for performing byte-wise string operations + * + * GPG I/O streams are managed using bytes rather than characters. + * + * 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 2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id$ + * @link http://pear.php.net/package/Crypt_GPG + */ + +// {{{ class Crypt_GPG_ByteUtils + +/** + * A class for performing byte-wise string operations + * + * GPG I/O streams are managed using bytes rather than characters. This class + * requires the mbstring extension to be available. + * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier <mike@silverorange.com> + * @copyright 2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @link http://pear.php.net/package/Crypt_GPG + * @link http://php.net/mbstring + */ +class Crypt_GPG_ByteUtils +{ + // {{{ strlen() + + /** + * Gets the length of a string in bytes + * + * 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. + */ + public static function strlen($string) + { + return mb_strlen($string, '8bit'); + } + + // }}} + // {{{ substr() + + /** + * Gets the substring of a string in bytes + * + * 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. + */ + public static function substr($string, $start, $length = null) + { + if ($length === null) { + return mb_substr( + $string, + $start, + self::strlen($string) - $start, '8bit' + ); + } + + return mb_substr($string, $start, $length, '8bit'); + } + + // }}} +} + +// }}} + +?> diff --git a/program/lib/Crypt/GPG/DecryptStatusHandler.php b/program/lib/Crypt/GPG/DecryptStatusHandler.php index 40e8d50ed..67c0dd74b 100644 --- a/program/lib/Crypt/GPG/DecryptStatusHandler.php +++ b/program/lib/Crypt/GPG/DecryptStatusHandler.php @@ -3,9 +3,9 @@ /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ /** - * Crypt_GPG is a package to use GPG from PHP + * Crypt_GPG is a package to use GnuPG from PHP * - * This file contains an object that handles GPG's status output for the + * This file contains an object that handles GnuPG's status output for the * decrypt operation. * * PHP version 5 @@ -29,9 +29,9 @@ * @category Encryption * @package Crypt_GPG * @author Michael Gauthier <mike@silverorange.com> - * @copyright 2008-2009 silverorange + * @copyright 2008-2013 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 $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG * @link http://www.gnupg.org/ */ @@ -42,7 +42,7 @@ require_once 'Crypt/GPG.php'; /** - * GPG exception classes + * Crypt_GPG exception classes */ require_once 'Crypt/GPG/Exceptions.php'; @@ -55,8 +55,8 @@ require_once 'Crypt/GPG/Exceptions.php'; * * 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. + * {@link http://www.gnupg.org/download/ GnuPG distribution} for detailed + * information on GnuPG'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. @@ -64,7 +64,7 @@ require_once 'Crypt/GPG/Exceptions.php'; * @category Encryption * @package Crypt_GPG * @author Michael Gauthier <mike@silverorange.com> - * @copyright 2008 silverorange + * @copyright 2008-2013 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/ @@ -293,8 +293,10 @@ class Crypt_GPG_DecryptStatusHandler 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); - + 'decrypt this data.', + $code, + $keyId + ); case Crypt_GPG::ERROR_BAD_PASSPHRASE: $badPassphrases = array_diff_key( $this->badPassphrases, @@ -316,17 +318,23 @@ class Crypt_GPG_DecryptStatusHandler implode('", "', $badPassphrases) . '".'; } - throw new Crypt_GPG_BadPassphraseException($message, $code, - $badPassphrases, $missingPassphrases); - + 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); - + 'the provided data.', + $code + ); default: throw new Crypt_GPG_Exception( - 'Unknown error decrypting data.', $code); + 'Unknown error decrypting data.', + $code + ); } } diff --git a/program/lib/Crypt/GPG/Engine.php b/program/lib/Crypt/GPG/Engine.php index 081be8e21..601541443 100644 --- a/program/lib/Crypt/GPG/Engine.php +++ b/program/lib/Crypt/GPG/Engine.php @@ -30,9 +30,9 @@ * @package Crypt_GPG * @author Nathan Fredrickson <nathan@silverorange.com> * @author Michael Gauthier <mike@silverorange.com> - * @copyright 2005-2010 silverorange + * @copyright 2005-2013 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 $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG * @link http://www.gnupg.org/ */ @@ -48,6 +48,16 @@ require_once 'Crypt/GPG.php'; require_once 'Crypt/GPG/Exceptions.php'; /** + * Byte string operations. + */ +require_once 'Crypt/GPG/ByteUtils.php'; + +/** + * Process control methods. + */ +require_once 'Crypt/GPG/ProcessControl.php'; + +/** * Standard PEAR exception is used if GPG binary is not found. */ require_once 'PEAR/Exception.php'; @@ -70,7 +80,7 @@ require_once 'PEAR/Exception.php'; * @package Crypt_GPG * @author Nathan Fredrickson <nathan@silverorange.com> * @author Michael Gauthier <mike@silverorange.com> - * @copyright 2005-2010 silverorange + * @copyright 2005-2013 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/ @@ -163,6 +173,17 @@ class Crypt_GPG_Engine private $_binary = ''; /** + * Location of GnuPG agent binary + * + * Only used for GnuPG 2.x + * + * @var string + * @see Crypt_GPG_Engine::__construct() + * @see Crypt_GPG_Engine::_getAgent() + */ + private $_agent = ''; + + /** * Directory containing the GPG key files * * This property only contains the path when the <i>homedir</i> option @@ -228,6 +249,15 @@ class Crypt_GPG_Engine private $_pipes = array(); /** + * Array of pipes used for communication with the gpg-agent binary + * + * This is an array of file descriptor resources. + * + * @var array + */ + private $_agentPipes = array(); + + /** * Array of currently opened pipes * * This array is used to keep track of remaining opened pipes so they can @@ -248,6 +278,20 @@ class Crypt_GPG_Engine private $_process = null; /** + * A handle for the gpg-agent process + * + * @var resource + */ + private $_agentProcess = null; + + /** + * GPG agent daemon socket and PID for running gpg-agent + * + * @var string + */ + private $_agentInfo = null; + + /** * Whether or not the operating system is Darwin (OS X) * * @var boolean @@ -367,18 +411,6 @@ class Crypt_GPG_Engine */ 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() @@ -432,6 +464,14 @@ class Crypt_GPG_Engine * operating system. The option * <kbd>gpgBinary</kbd> is a * deprecated alias for this option. + * - <kbd>string agent</kbd> - the location of the GnuPG agent + * binary. The gpg-agent is only + * used for GnuPG 2.x. If not + * specified, the engine attempts + * to auto-detect the gpg-agent + * binary location using a list of + * know default locations for the + * current operating system. * - <kbd>boolean debug</kbd> - whether or not to use debug mode. * When debug mode is on, all * communication to and from the GPG @@ -457,24 +497,38 @@ class Crypt_GPG_Engine * @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. + * + * @throws PEAR_Exception if the provided <kbd>agent</kbd> is invalid, or + * if no <kbd>agent</kbd> is provided and no suitable gpg-agent + * cound 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'; + if (extension_loaded('posix')) { + // note: this requires the package OS dep exclude 'windows' + $info = posix_getpwuid(posix_getuid()); + $this->_homedir = $info['dir'].'/.gnupg'; + } else { + if (isset($_SERVER['HOME'])) { + $this->_homedir = $_SERVER['HOME']; + } else { + $this->_homedir = getenv('HOME'); + } + } + + if ($this->_homedir === false) { + throw new Crypt_GPG_FileException( + 'Could not locate homedir. Please specify the homedir ' . + 'to use with the \'homedir\' option when instantiating ' . + 'the Crypt_GPG object.' + ); + } } // attempt to create homedir if it does not exist @@ -484,16 +538,40 @@ class Crypt_GPG_Engine // 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); + 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 + ); } } + // check homedir permissions (See Bug #19833) + if (!is_executable($this->_homedir)) { + throw new Crypt_GPG_FileException( + 'The \'homedir\' "' . $this->_homedir . '" is not enterable ' . + 'by the current user. Please check the permissions on your ' . + 'homedir and make sure the current user can both enter and ' . + 'write to the directory.', + 0, + $this->_homedir + ); + } + if (!is_writeable($this->_homedir)) { + throw new Crypt_GPG_FileException( + 'The \'homedir\' "' . $this->_homedir . '" is not writable ' . + 'by the current user. Please check the permissions on your ' . + 'homedir and make sure the current user can both enter and ' . + 'write to the directory.', + 0, + $this->_homedir + ); + } + // get binary if (array_key_exists('binary', $options)) { $this->_binary = (string)$options['binary']; @@ -505,9 +583,26 @@ class Crypt_GPG_Engine } 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.'); + 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.' + ); + } + + // get agent + if (array_key_exists('agent', $options)) { + $this->_agent = (string)$options['agent']; + } else { + $this->_agent = $this->_getAgent(); + } + + if ($this->_agent == '' || !is_executable($this->_agent)) { + throw new PEAR_Exception( + 'gpg-agent binary not found. If you are sure the gpg-agent ' . + 'is installed, please specify the location of the gpg-agent ' . + 'binary using the \'agent\' driver option.' + ); } /* @@ -891,7 +986,7 @@ class Crypt_GPG_Engine } $matches = array(); - $expression = '/gpg \(GnuPG\) (\S+)/'; + $expression = '#gpg \(GnuPG[A-Za-z0-9/]*?\) (\S+)#'; if (preg_match($expression, $info, $matches) === 1) { $this->_version = $matches[1]; @@ -1114,6 +1209,9 @@ class Crypt_GPG_Engine $fdCommand = $this->_pipes[self::FD_COMMAND]; $fdMessage = $this->_pipes[self::FD_MESSAGE]; + // select loop delay in milliseconds + $delay = 0; + while (true) { $inputStreams = array(); @@ -1166,15 +1264,15 @@ class Crypt_GPG_Engine $outputStreams[] = $this->_output; } - if ($this->_commandBuffer != '') { + if ($this->_commandBuffer != '' && is_resource($fdCommand)) { $outputStreams[] = $fdCommand; } - if ($messageBuffer != '') { + if ($messageBuffer != '' && is_resource($fdMessage)) { $outputStreams[] = $fdMessage; } - if ($inputBuffer != '') { + if ($inputBuffer != '' && is_resource($fdInput)) { $outputStreams[] = $fdInput; } @@ -1209,33 +1307,41 @@ class Crypt_GPG_Engine } // write input (to GPG) - if (in_array($fdInput, $outputStreams)) { + if (in_array($fdInput, $outputStreams, true)) { $this->_debug('GPG is ready for input'); - $chunk = self::_byteSubstring( + $chunk = Crypt_GPG_ByteUtils::substr( $inputBuffer, 0, self::CHUNK_SIZE ); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($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 - ); + if ($length === 0) { + // If we wrote 0 bytes it was either EAGAIN or EPIPE. Since + // the pipe was seleted for writing, we assume it was EPIPE. + // There's no way to get the actual erorr code in PHP. See + // PHP Bug #39598. https://bugs.php.net/bug.php?id=39598 + $this->_debug('=> broken pipe on GPG input'); + $this->_debug('=> closing pipe GPG input'); + $this->_closePipe(self::FD_INPUT); + } else { + $this->_debug('=> wrote ' . $length . ' bytes'); + $inputBuffer = Crypt_GPG_ByteUtils::substr( + $inputBuffer, + $length + ); + } } // read input (from PHP stream) - if (in_array($this->_input, $inputStreams)) { + if (in_array($this->_input, $inputStreams, true)) { $this->_debug('input stream is ready for reading'); $this->_debug( '=> about to read ' . self::CHUNK_SIZE . @@ -1243,36 +1349,48 @@ class Crypt_GPG_Engine ); $chunk = fread($this->_input, self::CHUNK_SIZE); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $inputBuffer .= $chunk; $this->_debug('=> read ' . $length . ' bytes'); } // write message (to GPG) - if (in_array($fdMessage, $outputStreams)) { + if (in_array($fdMessage, $outputStreams, true)) { $this->_debug('GPG is ready for message data'); - $chunk = self::_byteSubstring( + $chunk = Crypt_GPG_ByteUtils::substr( $messageBuffer, 0, self::CHUNK_SIZE ); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($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); + if ($length === 0) { + // If we wrote 0 bytes it was either EAGAIN or EPIPE. Since + // the pipe was seleted for writing, we assume it was EPIPE. + // There's no way to get the actual erorr code in PHP. See + // PHP Bug #39598. https://bugs.php.net/bug.php?id=39598 + $this->_debug('=> broken pipe on GPG message'); + $this->_debug('=> closing pipe GPG message'); + $this->_closePipe(self::FD_MESSAGE); + } else { + $this->_debug('=> wrote ' . $length . ' bytes'); + $messageBuffer = Crypt_GPG_ByteUtils::substr( + $messageBuffer, + $length + ); + } } // read message (from PHP stream) - if (in_array($this->_message, $inputStreams)) { + if (in_array($this->_message, $inputStreams, true)) { $this->_debug('message stream is ready for reading'); $this->_debug( '=> about to read ' . self::CHUNK_SIZE . @@ -1280,14 +1398,14 @@ class Crypt_GPG_Engine ); $chunk = fread($this->_message, self::CHUNK_SIZE); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $messageBuffer .= $chunk; $this->_debug('=> read ' . $length . ' bytes'); } // read output (from GPG) - if (in_array($fdOutput, $inputStreams)) { + if (in_array($fdOutput, $inputStreams, true)) { $this->_debug('GPG output stream ready for reading'); $this->_debug( '=> about to read ' . self::CHUNK_SIZE . @@ -1295,23 +1413,23 @@ class Crypt_GPG_Engine ); $chunk = fread($fdOutput, self::CHUNK_SIZE); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $outputBuffer .= $chunk; $this->_debug('=> read ' . $length . ' bytes'); } // write output (to PHP stream) - if (in_array($this->_output, $outputStreams)) { + if (in_array($this->_output, $outputStreams, true)) { $this->_debug('output stream is ready for data'); - $chunk = self::_byteSubstring( + $chunk = Crypt_GPG_ByteUtils::substr( $outputBuffer, 0, self::CHUNK_SIZE ); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $this->_debug( '=> about to write ' . $length . ' bytes to output stream' @@ -1321,11 +1439,14 @@ class Crypt_GPG_Engine $this->_debug('=> wrote ' . $length . ' bytes'); - $outputBuffer = self::_byteSubstring($outputBuffer, $length); + $outputBuffer = Crypt_GPG_ByteUtils::substr( + $outputBuffer, + $length + ); } // read error (from GPG) - if (in_array($fdError, $inputStreams)) { + if (in_array($fdError, $inputStreams, true)) { $this->_debug('GPG error stream ready for reading'); $this->_debug( '=> about to read ' . self::CHUNK_SIZE . @@ -1333,14 +1454,14 @@ class Crypt_GPG_Engine ); $chunk = fread($fdError, self::CHUNK_SIZE); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($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); + $line = Crypt_GPG_ByteUtils::substr($errorBuffer, 0, $pos); foreach ($this->_errorHandlers as $handler) { array_unshift($handler['args'], $line); call_user_func_array( @@ -1350,15 +1471,15 @@ class Crypt_GPG_Engine array_shift($handler['args']); } - $errorBuffer = self::_byteSubString( + $errorBuffer = Crypt_GPG_ByteUtils::substr( $errorBuffer, - $pos + self::_byteLength(PHP_EOL) + $pos + Crypt_GPG_ByteUtils::strlen(PHP_EOL) ); } } // read status (from GPG) - if (in_array($fdStatus, $inputStreams)) { + if (in_array($fdStatus, $inputStreams, true)) { $this->_debug('GPG status stream ready for reading'); $this->_debug( '=> about to read ' . self::CHUNK_SIZE . @@ -1366,17 +1487,17 @@ class Crypt_GPG_Engine ); $chunk = fread($fdStatus, self::CHUNK_SIZE); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($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); + $line = Crypt_GPG_ByteUtils::substr($statusBuffer, 0, $pos); // only pass lines beginning with magic prefix - if (self::_byteSubstring($line, 0, 9) == '[GNUPG:] ') { - $line = self::_byteSubstring($line, 9); + if (Crypt_GPG_ByteUtils::substr($line, 0, 9) == '[GNUPG:] ') { + $line = Crypt_GPG_ByteUtils::substr($line, 9); foreach ($this->_statusHandlers as $handler) { array_unshift($handler['args'], $line); call_user_func_array( @@ -1387,38 +1508,60 @@ class Crypt_GPG_Engine array_shift($handler['args']); } } - $statusBuffer = self::_byteSubString( + $statusBuffer = Crypt_GPG_ByteUtils::substr( $statusBuffer, - $pos + self::_byteLength(PHP_EOL) + $pos + Crypt_GPG_ByteUtils::strlen(PHP_EOL) ); } } // write command (to GPG) - if (in_array($fdCommand, $outputStreams)) { + if (in_array($fdCommand, $outputStreams, true)) { $this->_debug('GPG is ready for command data'); // send commands - $chunk = self::_byteSubstring( + $chunk = Crypt_GPG_ByteUtils::substr( $this->_commandBuffer, 0, self::CHUNK_SIZE ); - $length = self::_byteLength($chunk); + $length = Crypt_GPG_ByteUtils::strlen($chunk); $this->_debug( '=> about to write ' . $length . ' bytes to GPG command' ); $length = fwrite($fdCommand, $chunk, $length); + if ($length === 0) { + // If we wrote 0 bytes it was either EAGAIN or EPIPE. Since + // the pipe was seleted for writing, we assume it was EPIPE. + // There's no way to get the actual erorr code in PHP. See + // PHP Bug #39598. https://bugs.php.net/bug.php?id=39598 + $this->_debug('=> broken pipe on GPG command'); + $this->_debug('=> closing pipe GPG command'); + $this->_closePipe(self::FD_COMMAND); + } else { + $this->_debug('=> wrote ' . $length); + $this->_commandBuffer = Crypt_GPG_ByteUtils::substr( + $this->_commandBuffer, + $length + ); + } + } - $this->_debug('=> wrote ' . $length); + if (count($outputStreams) === 0 || count($inputStreams) === 0) { + // we have an I/O imbalance, increase the select loop delay + // to smooth things out + $delay += 10; + } else { + // things are running smoothly, decrease the delay + $delay -= 8; + $delay = max(0, $delay); + } - $this->_commandBuffer = self::_byteSubstring( - $this->_commandBuffer, - $length - ); + if ($delay > 0) { + usleep($delay); } } // end loop while streams are open @@ -1449,12 +1592,83 @@ class Crypt_GPG_Engine { $version = $this->getVersion(); + // 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'; + $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'; + // If using GnuPG 2.x start the gpg-agent + if (version_compare($version, '2.0.0', 'ge')) { + $agentCommandLine = $this->_agent; + + $agentArguments = array( + '--options /dev/null', // ignore any saved options + '--csh', // output is easier to parse + '--keep-display', // prevent passing --display to pinentry + '--no-grab', + '--ignore-cache-for-signing', + '--pinentry-touch-file /dev/null', + '--disable-scdaemon', + '--no-use-standard-socket', + '--pinentry-program ' . escapeshellarg($this->_getPinEntry()) + ); + + if ($this->_homedir) { + $agentArguments[] = '--homedir ' . + escapeshellarg($this->_homedir); + } + + + $agentCommandLine .= ' ' . implode(' ', $agentArguments) + . ' --daemon'; + + $agentDescriptorSpec = array( + self::FD_INPUT => array('pipe', $rb), // stdin + self::FD_OUTPUT => array('pipe', $wb), // stdout + self::FD_ERROR => array('pipe', $wb) // stderr + ); + + $this->_debug('OPENING GPG-AGENT SUBPROCESS WITH THE FOLLOWING COMMAND:'); + $this->_debug($agentCommandLine); + + $this->_agentProcess = proc_open( + $agentCommandLine, + $agentDescriptorSpec, + $this->_agentPipes, + null, + $env, + array('binary_pipes' => true) + ); + + if (!is_resource($this->_agentProcess)) { + throw new Crypt_GPG_OpenSubprocessException( + 'Unable to open gpg-agent subprocess.', + 0, + $agentCommandLine + ); + } + + // Get GPG_AGENT_INFO and set environment variable for gpg process. + // This is a blocking read, but is only 1 line. + $agentInfo = fread( + $this->_agentPipes[self::FD_OUTPUT], + self::CHUNK_SIZE + ); + + $agentInfo = explode(' ', $agentInfo, 3); + $this->_agentInfo = $agentInfo[2]; + $env['GPG_AGENT_INFO'] = $this->_agentInfo; + + // gpg-agent daemon is started, we can close the launching process + $this->_closeAgentLaunchProcess(); + } + $commandLine = $this->_binary; $defaultArguments = array( @@ -1511,11 +1725,6 @@ class Crypt_GPG_Engine $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 @@ -1525,7 +1734,7 @@ class Crypt_GPG_Engine self::FD_MESSAGE => array('pipe', $rb) // message ); - $this->_debug('OPENING SUBPROCESS WITH THE FOLLOWING COMMAND:'); + $this->_debug('OPENING GPG SUBPROCESS WITH THE FOLLOWING COMMAND:'); $this->_debug($commandLine); $this->_process = proc_open( @@ -1542,6 +1751,11 @@ class Crypt_GPG_Engine 'Unable to open GPG subprocess.', 0, $commandLine); } + // Set streams as non-blocking. See Bug #18618. + foreach ($this->_pipes as $pipe) { + stream_set_blocking($pipe, 0); + } + $this->_openPipes = $this->_pipes; $this->_errorCode = Crypt_GPG::ERROR_NONE; } @@ -1562,8 +1776,11 @@ class Crypt_GPG_Engine */ private function _closeSubprocess() { + // clear PINs from environment if they were set + $_ENV['PINENTRY_USER_DATA'] = null; + if (is_resource($this->_process)) { - $this->_debug('CLOSING SUBPROCESS'); + $this->_debug('CLOSING GPG SUBPROCESS'); // close remaining open pipes foreach (array_keys($this->_openPipes) as $pipeNumber) { @@ -1590,9 +1807,55 @@ class Crypt_GPG_Engine $this->_process = null; $this->_pipes = array(); } + + $this->_closeAgentLaunchProcess(); + + if ($this->_agentInfo !== null) { + $this->_debug('STOPPING GPG-AGENT DAEMON'); + + $parts = explode(':', $this->_agentInfo, 3); + $pid = $parts[1]; + $process = new Crypt_GPG_ProcessControl($pid); + + // terminate agent daemon + $process->terminate(); + + while ($process->isRunning()) { + usleep(10000); // 10 ms + $process->terminate(); + } + + $this->_agentInfo = null; + + $this->_debug('GPG-AGENT DAEMON STOPPED'); + } } // }}} + // {{ _closeAgentLaunchProcess() + + private function _closeAgentLaunchProcess() + { + if (is_resource($this->_agentProcess)) { + $this->_debug('CLOSING GPG-AGENT LAUNCH PROCESS'); + + // close agent pipes + foreach ($this->_agentPipes as $pipe) { + fflush($pipe); + fclose($pipe); + } + + // close agent launching process + proc_close($this->_agentProcess); + + $this->_agentProcess = null; + $this->_agentPipes = array(); + + $this->_debug('GPG-AGENT LAUNCH PROCESS CLOSED'); + } + } + + // }} // {{{ _closePipe() /** @@ -1658,6 +1921,55 @@ class Crypt_GPG_Engine } // }}} + // {{ _getAgent() + + private function _getAgent() + { + $agent = ''; + + if ($this->_isDarwin) { + $agentFiles = array( + '/opt/local/bin/gpg-agent', // MacPorts + '/usr/local/bin/gpg-agent', // Mac GPG + '/sw/bin/gpg-agent', // Fink + '/usr/bin/gpg-agent' + ); + } else { + $agentFiles = array( + '/usr/bin/gpg-agent', + '/usr/local/bin/gpg-agent' + ); + } + + foreach ($agentFiles as $agentFile) { + if (is_executable($agentFile)) { + $agent = $agentFile; + break; + } + } + + return $agent; + } + + // }} + // {{ _getPinEntry() + + private function _getPinEntry() + { + // Check if we're running directly from git or if we're using a + // PEAR-packaged version + $pinEntry = '@bin-dir@' . DIRECTORY_SEPARATOR . 'crypt-gpg-pinentry'; + + if ($pinEntry[0] === '@') { + $pinEntry = dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' + . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'scripts' + . DIRECTORY_SEPARATOR . 'crypt-gpg-pinentry'; + } + + return $pinEntry; + } + + // }} // {{{ _debug() /** @@ -1672,7 +1984,7 @@ class Crypt_GPG_Engine private function _debug($text) { if ($this->_debug) { - if (array_key_exists('SHELL', $_ENV)) { + if (php_sapi_name() === 'cli') { foreach (explode(PHP_EOL, $text) as $line) { echo "Crypt_GPG DEBUG: ", $line, PHP_EOL; } @@ -1687,70 +1999,6 @@ class Crypt_GPG_Engine } // }}} - // {{{ _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 index 744acf5d4..0ca917db6 100644 --- a/program/lib/Crypt/GPG/Exceptions.php +++ b/program/lib/Crypt/GPG/Exceptions.php @@ -32,9 +32,9 @@ * @package Crypt_GPG * @author Nathan Fredrickson <nathan@silverorange.com> * @author Michael Gauthier <mike@silverorange.com> - * @copyright 2005 silverorange + * @copyright 2005-2011 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 $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG */ @@ -469,5 +469,130 @@ class Crypt_GPG_DeletePrivateKeyException extends Crypt_GPG_Exception } // }}} +// {{{ class Crypt_GPG_KeyNotCreatedException + +/** + * An exception thrown when an attempt is made to generate a key and the + * attempt fails + * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier <mike@silverorange.com> + * @copyright 2011 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @link http://pear.php.net/package/Crypt_GPG + */ +class Crypt_GPG_KeyNotCreatedException extends Crypt_GPG_Exception +{ +} + +// }}} +// {{{ class Crypt_GPG_InvalidKeyParamsException + +/** + * An exception thrown when an attempt is made to generate a key and the + * key parameters set on the key generator are invalid + * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier <mike@silverorange.com> + * @copyright 2011 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @link http://pear.php.net/package/Crypt_GPG + */ +class Crypt_GPG_InvalidKeyParamsException extends Crypt_GPG_Exception +{ + // {{{ private class properties + + /** + * The key algorithm + * + * @var integer + */ + private $_algorithm = 0; + + /** + * The key size + * + * @var integer + */ + private $_size = 0; + + /** + * The key usage + * + * @var integer + */ + private $_usage = 0; + + // }}} + // {{{ __construct() + + /** + * Creates a new Crypt_GPG_InvalidKeyParamsException + * + * @param string $message an error message. + * @param integer $code a user defined error code. + * @param string $algorithm the key algorithm. + * @param string $size the key size. + * @param string $usage the key usage. + */ + public function __construct( + $message, + $code = 0, + $algorithm = 0, + $size = 0, + $usage = 0 + ) { + parent::__construct($message, $code); + + $this->_algorithm = $algorithm; + $this->_size = $size; + $this->_usage = $usage; + } + + // }}} + // {{{ getAlgorithm() + + /** + * Gets the key algorithm + * + * @return integer the key algorithm. + */ + public function getAlgorithm() + { + return $this->_algorithm; + } + + // }}} + // {{{ getSize() + + /** + * Gets the key size + * + * @return integer the key size. + */ + public function getSize() + { + return $this->_size; + } + + // }}} + // {{{ getUsage() + + /** + * Gets the key usage + * + * @return integer the key usage. + */ + public function getUsage() + { + return $this->_usage; + } + + // }}} +} + +// }}} ?> diff --git a/program/lib/Crypt/GPG/Key.php b/program/lib/Crypt/GPG/Key.php index 67a4b9c7d..6ecb538bc 100644 --- a/program/lib/Crypt/GPG/Key.php +++ b/program/lib/Crypt/GPG/Key.php @@ -28,7 +28,7 @@ * @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 $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG */ diff --git a/program/lib/Crypt/GPG/KeyGenerator.php b/program/lib/Crypt/GPG/KeyGenerator.php new file mode 100644 index 000000000..f59c0ee3a --- /dev/null +++ b/program/lib/Crypt/GPG/KeyGenerator.php @@ -0,0 +1,790 @@ +<?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 GnuPG key generation. + * + * 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 2011-2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id:$ + * @link http://pear.php.net/package/Crypt_GPG + * @link http://www.gnupg.org/ + */ + +/** + * Base class for GPG methods + */ +require_once 'Crypt/GPGAbstract.php'; + +/** + * Status output handler for key generation + */ +require_once 'Crypt/GPG/KeyGeneratorStatusHandler.php'; + +/** + * Error output handler for key generation + */ +require_once 'Crypt/GPG/KeyGeneratorErrorHandler.php'; + +// {{{ class Crypt_GPG_KeyGenerator + +/** + * GnuPG key generator + * + * This class provides an object oriented interface for generating keys with + * the GNU Privacy Guard (GPG). + * + * Secure key generation requires true random numbers, and as such can be slow. + * If the operating system runs out of entropy, key generation will block until + * more entropy is available. + * + * If quick key generation is important, a hardware entropy generator, or an + * entropy gathering daemon may be installed. For example, administrators of + * Debian systems may want to install the 'randomsound' package. + * + * This class uses the experimental automated key generation support available + * in GnuPG. See <b>doc/DETAILS</b> in the + * {@link http://www.gnupg.org/download/ GPG distribution} for detailed + * information on the key generation format. + * + * @category Encryption + * @package Crypt_GPG + * @author Nathan Fredrickson <nathan@silverorange.com> + * @author Michael Gauthier <mike@silverorange.com> + * @copyright 2005-2013 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_KeyGenerator extends Crypt_GPGAbstract +{ + // {{{ protected properties + + /** + * The expiration date of generated keys + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setExpirationDate() + */ + protected $expirationDate = 0; + + /** + * The passphrase of generated keys + * + * @var string + * + * @see Crypt_GPG_KeyGenerator::setPassphrase() + */ + protected $passphrase = ''; + + /** + * The algorithm for generated primary keys + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setKeyParams() + */ + protected $keyAlgorithm = Crypt_GPG_SubKey::ALGORITHM_DSA; + + /** + * The size of generated primary keys + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setKeyParams() + */ + protected $keySize = 1024; + + /** + * The usages of generated primary keys + * + * This is a bitwise combination of the usage constants in + * {@link Crypt_GPG_SubKey}. + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setKeyParams() + */ + protected $keyUsage = 6; // USAGE_SIGN | USAGE_CERTIFY + + /** + * The algorithm for generated sub-keys + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setSubKeyParams() + */ + protected $subKeyAlgorithm = Crypt_GPG_SubKey::ALGORITHM_ELGAMAL_ENC; + + /** + * The size of generated sub-keys + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setSubKeyParams() + */ + protected $subKeySize = 2048; + + /** + * The usages of generated sub-keys + * + * This is a bitwise combination of the usage constants in + * {@link Crypt_GPG_SubKey}. + * + * @var integer + * + * @see Crypt_GPG_KeyGenerator::setSubKeyParams() + */ + protected $subKeyUsage = Crypt_GPG_SubKey::USAGE_ENCRYPT; + + /** + * The GnuPG status handler to use for key generation + * + * @var Crypt_GPG_KeyGeneratorStatusHandler + * + * @see Crypt_GPG_KeyGenerator::setStatusHandler() + */ + protected $statusHandler = null; + + /** + * The GnuPG error handler to use for key generation + * + * @var Crypt_GPG_KeyGeneratorErrorHandler + * + * @see Crypt_GPG_KeyGenerator::setErrorHandler() + */ + protected $errorHandler = null; + + // }}} + // {{{ __construct() + + /** + * Creates a new GnuPG key generator + * + * 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>string agent</kbd> - the location of the GnuPG agent + * binary. The gpg-agent is only + * used for GnuPG 2.x. If not + * specified, the engine attempts + * to auto-detect the gpg-agent + * binary location using a list of + * know default locations for the + * current operating system. + * - <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. + * + * @throws PEAR_Exception if the provided <kbd>agent</kbd> is invalid, or + * if no <kbd>agent</kbd> is provided and no suitable gpg-agent + * cound be found. + */ + public function __construct(array $options = array()) + { + parent::__construct($options); + + $this->statusHandler = new Crypt_GPG_KeyGeneratorStatusHandler(); + $this->errorHandler = new Crypt_GPG_KeyGeneratorErrorHandler(); + } + + // }}} + // {{{ setExpirationDate() + + /** + * Sets the expiration date of generated keys + * + * @param string|integer $date either a string that may be parsed by + * PHP's strtotime() function, or an integer + * timestamp representing the number of seconds + * since the UNIX epoch. This date must be at + * least one date in the future. Keys that + * expire in the past may not be generated. Use + * an expiration date of 0 for keys that do not + * expire. + * + * @throws InvalidArgumentException if the date is not a valid format, or + * if the date is not at least one day in + * the future, or if the date is greater + * than 2038-01-19T03:14:07. + * + * @return Crypt_GPG_KeyGenerator the current object, for fluent interface. + */ + public function setExpirationDate($date) + { + if (is_int($date) || ctype_digit(strval($date))) { + $expirationDate = intval($date); + } else { + $expirationDate = strtotime($date); + } + + if ($expirationDate === false) { + throw new InvalidArgumentException( + sprintf( + 'Invalid expiration date format: "%s". Please use a ' . + 'format compatible with PHP\'s strtotime().', + $date + ) + ); + } + + if ($expirationDate !== 0 && $expirationDate < time() + 86400) { + throw new InvalidArgumentException( + 'Expiration date must be at least a day in the future.' + ); + } + + // GnuPG suffers from the 2038 bug + if ($expirationDate > 2147483647) { + throw new InvalidArgumentException( + 'Expiration date must not be greater than 2038-01-19T03:14:07.' + ); + } + + $this->expirationDate = $expirationDate; + + return $this; + } + + // }}} + // {{{ setPassphrase() + + /** + * Sets the passphrase of generated keys + * + * @param string $passphrase the passphrase to use for generated keys. Use + * null or an empty string for no passphrase. + * + * @return Crypt_GPG_KeyGenerator the current object, for fluent interface. + */ + public function setPassphrase($passphrase) + { + $this->passphrase = strval($passphrase); + return $this; + } + + // }}} + // {{{ setKeyParams() + + /** + * Sets the parameters for the primary key of generated key-pairs + * + * @param integer $algorithm the algorithm used by the key. This should be + * one of the Crypt_GPG_SubKey::ALGORITHM_* + * constants. + * @param integer $size optional. The size of the key. Different + * algorithms have different size requirements. + * If not specified, the default size for the + * specified algorithm will be used. If an + * invalid key size is used, GnuPG will do its + * best to round it to a valid size. + * @param integer $usage optional. A bitwise combination of key usages. + * If not specified, the primary key will be used + * only to sign and certify. This is the default + * behavior of GnuPG in interactive mode. Use + * the Crypt_GPG_SubKey::USAGE_* constants here. + * The primary key may be used to certify even + * if the certify usage is not specified. + * + * @return Crypt_GPG_KeyGenerator the current object, for fluent interface. + */ + public function setKeyParams($algorithm, $size = 0, $usage = 0) + { + $apgorithm = intval($algorithm); + + if ($algorithm === Crypt_GPG_SubKey::ALGORITHM_ELGAMAL_ENC) { + throw new Crypt_GPG_InvalidKeyParamsException( + 'Primary key algorithm must be capable of signing. The ' . + 'Elgamal algorithm can only encrypt.', + 0, + $algorithm, + $size, + $usage + ); + } + + if ($size != 0) { + $size = intval($size); + } + + if ($usage != 0) { + $usage = intval($usage); + } + + $usageEncrypt = Crypt_GPG_SubKey::USAGE_ENCRYPT; + + if ( $algorithm === Crypt_GPG_SubKey::ALGORITHM_DSA + && ($usage & $usageEncrypt) === $usageEncrypt + ) { + throw new Crypt_GPG_InvalidKeyParamsException( + 'The DSA algorithm is not capable of encrypting. Please ' . + 'specify a different algorithm or do not include encryption ' . + 'as a usage for the primary key.', + 0, + $algorithm, + $size, + $usage + ); + } + + $this->keyAlgorithm = $algorithm; + + if ($size != 0) { + $this->keySize = $size; + } + + if ($usage != 0) { + $this->keyUsage = $usage; + } + + return $this; + } + + // }}} + // {{{ setSubKeyParams() + + /** + * Sets the parameters for the sub-key of generated key-pairs + * + * @param integer $algorithm the algorithm used by the key. This should be + * one of the Crypt_GPG_SubKey::ALGORITHM_* + * constants. + * @param integer $size optional. The size of the key. Different + * algorithms have different size requirements. + * If not specified, the default size for the + * specified algorithm will be used. If an + * invalid key size is used, GnuPG will do its + * best to round it to a valid size. + * @param integer $usage optional. A bitwise combination of key usages. + * If not specified, the sub-key will be used + * only to encrypt. This is the default behavior + * of GnuPG in interactive mode. Use the + * Crypt_GPG_SubKey::USAGE_* constants here. + * + * @return Crypt_GPG_KeyGenerator the current object, for fluent interface. + */ + public function setSubKeyParams($algorithm, $size = '', $usage = 0) + { + $apgorithm = intval($algorithm); + + if ($size != 0) { + $size = intval($size); + } + + if ($usage != 0) { + $usage = intval($usage); + } + + $usageSign = Crypt_GPG_SubKey::USAGE_SIGN; + + if ( $algorithm === Crypt_GPG_SubKey::ALGORITHM_ELGAMAL_ENC + && ($usage & $usageSign) === $usageSign + ) { + throw new Crypt_GPG_InvalidKeyParamsException( + 'The Elgamal algorithm is not capable of signing. Please ' . + 'specify a different algorithm or do not include signing ' . + 'as a usage for the sub-key.', + 0, + $algorithm, + $size, + $usage + ); + } + + $usageEncrypt = Crypt_GPG_SubKey::USAGE_ENCRYPT; + + if ( $algorithm === Crypt_GPG_SubKey::ALGORITHM_DSA + && ($usage & $usageEncrypt) === $usageEncrypt + ) { + throw new Crypt_GPG_InvalidKeyParamsException( + 'The DSA algorithm is not capable of encrypting. Please ' . + 'specify a different algorithm or do not include encryption ' . + 'as a usage for the sub-key.', + 0, + $algorithm, + $size, + $usage + ); + } + + $this->subKeyAlgorithm = $algorithm; + + if ($size != 0) { + $this->subKeySize = $size; + } + + if ($usage != 0) { + $this->subKeyUsage = $usage; + } + + return $this; + } + + // }}} + // {{{ setStatusHandler() + + /** + * Sets the status handler to use for key generation + * + * Normally this method does not need to be used. It provides a means for + * dependency injection. + * + * @param Crypt_GPG_KeyStatusHandler $handler the key status handler to + * use. + * + * @return Crypt_GPG_KeyGenerator the current object, for fluent interface. + */ + public function setStatusHandler( + Crypt_GPG_KeyGeneratorStatusHandler $handler + ) { + $this->statusHandler = $handler; + return $this; + } + + // }}} + // {{{ setErrorHandler() + + /** + * Sets the error handler to use for key generation + * + * Normally this method does not need to be used. It provides a means for + * dependency injection. + * + * @param Crypt_GPG_KeyErrorHandler $handler the key error handler to + * use. + * + * @return Crypt_GPG_KeyGenerator the current object, for fluent interface. + */ + public function setErrorHandler( + Crypt_GPG_KeyGeneratorErrorHandler $handler + ) { + $this->errorHandler = $handler; + return $this; + } + + // }}} + // {{{ generateKey() + + /** + * Generates a new key-pair in the current keyring + * + * Secure key generation requires true random numbers, and as such can be + * solw. If the operating system runs out of entropy, key generation will + * block until more entropy is available. + * + * If quick key generation is important, a hardware entropy generator, or + * an entropy gathering daemon may be installed. For example, + * administrators of Debian systems may want to install the 'randomsound' + * package. + * + * @param string|Crypt_GPG_UserId $name either a {@link Crypt_GPG_UserId} + * object, or a string containing + * the name of the user id. + * @param string $email optional. If <i>$name</i> is + * specified as a string, this is + * the email address of the user id. + * @param string $comment optional. If <i>$name</i> is + * specified as a string, this is + * the comment of the user id. + * + * @return Crypt_GPG_Key the newly generated key. + * + * @throws Crypt_GPG_KeyNotCreatedException if the key parameters are + * incorrect, if an unknown error occurs during key generation, or + * if the newly generated key is not found in the keyring. + * + * @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 generateKey($name, $email = '', $comment = '') + { + $handle = uniqid('key', true); + + $userId = $this->getUserId($name, $email, $comment); + + $keyParams = array( + 'Key-Type' => $this->keyAlgorithm, + 'Key-Length' => $this->keySize, + 'Key-Usage' => $this->getUsage($this->keyUsage), + 'Subkey-Type' => $this->subKeyAlgorithm, + 'Subkey-Length' => $this->subKeySize, + 'Subkey-Usage' => $this->getUsage($this->subKeyUsage), + 'Name-Real' => $userId->getName(), + 'Handle' => $handle, + ); + + if ($this->expirationDate != 0) { + // GnuPG only accepts granularity of days + $expirationDate = date('Y-m-d', $this->expirationDate); + $keyParams['Expire-Date'] = $expirationDate; + } + + if ($this->passphrase != '') { + $keyParams['Passphrase'] = $this->passphrase; + } + + if ($userId->getEmail() != '') { + $keyParams['Name-Email'] = $userId->getEmail(); + } + + if ($userId->getComment() != '') { + $keyParams['Name-Comment'] = $userId->getComment(); + } + + + $keyParamsFormatted = array(); + foreach ($keyParams as $name => $value) { + $keyParamsFormatted[] = $name . ': ' . $value; + } + + $input = implode("\n", $keyParamsFormatted) . "\n%commit\n"; + + $statusHandler = clone $this->statusHandler; + $statusHandler->setHandle($handle); + + $errorHandler = clone $this->errorHandler; + + $this->engine->reset(); + $this->engine->addStatusHandler(array($statusHandler, 'handle')); + $this->engine->addErrorHandler(array($errorHandler, 'handle')); + $this->engine->setInput($input); + $this->engine->setOutput($output); + $this->engine->setOperation('--gen-key', array('--batch')); + $this->engine->run(); + + $code = $errorHandler->getErrorCode(); + switch ($code) { + case self::ERROR_BAD_KEY_PARAMS: + switch ($errorHandler->getLineNumber()) { + case 1: + throw new Crypt_GPG_InvalidKeyParamsException( + 'Invalid primary key algorithm specified.', + 0, + $this->keyAlgorithm, + $this->keySize, + $this->keyUsage + ); + case 4: + throw new Crypt_GPG_InvalidKeyParamsException( + 'Invalid sub-key algorithm specified.', + 0, + $this->subKeyAlgorithm, + $this->subKeySize, + $this->subKeyUsage + ); + default: + throw new Crypt_GPG_InvalidKeyParamsException( + 'Invalid key algorithm specified.' + ); + } + } + + $code = $this->engine->getErrorCode(); + + switch ($code) { + case self::ERROR_NONE: + break; + default: + throw new Crypt_GPG_Exception( + 'Unknown error generating key-pair. Please use the \'debug\' ' . + 'option when creating the Crypt_GPG object, and file a bug ' . + 'report at ' . self::BUG_URI, + $code + ); + } + + $code = $statusHandler->getErrorCode(); + + switch ($code) { + case self::ERROR_NONE: + break; + case self::ERROR_KEY_NOT_CREATED: + throw new Crypt_GPG_KeyNotCreatedException( + 'Unable to create new key-pair. Invalid key parameters. ' . + 'Make sure the specified key algorithms and sizes are ' . + 'correct.', + $code + ); + } + + $fingerprint = $statusHandler->getKeyFingerprint(); + $keys = $this->_getKeys($fingerprint); + + if (count($keys) === 0) { + throw new Crypt_GPG_KeyNotCreatedException( + sprintf( + 'Newly created key "%s" not found in keyring.', + $fingerprint + ) + ); + } + + return $keys[0]; + } + + // }}} + // {{{ getUsage() + + /** + * Builds a GnuPG key usage string suitable for key generation + * + * See <b>doc/DETAILS</b> in the + * {@link http://www.gnupg.org/download/ GPG distribution} for detailed + * information on the key usage format. + * + * @param integer $usage a bitwise combination of the key usages. This is + * a combination of the Crypt_GPG_SubKey::USAGE_* + * constants. + * + * @return string the key usage string. + */ + protected function getUsage($usage) + { + $map = array( + Crypt_GPG_SubKey::USAGE_ENCRYPT => 'encrypt', + Crypt_GPG_SubKey::USAGE_SIGN => 'sign', + Crypt_GPG_SubKey::USAGE_CERTIFY => 'cert', + Crypt_GPG_SubKey::USAGE_AUTHENTICATION => 'auth', + ); + + // cert is always used for primary keys and does not need to be + // specified + $usage &= ~Crypt_GPG_SubKey::USAGE_CERTIFY; + + $usageArray = array(); + + foreach ($map as $key => $value) { + if (($usage & $key) === $key) { + $usageArray[] = $value; + } + } + + return implode(',', $usageArray); + } + + // }}} + // {{{ getUserId() + + /** + * Gets a user id object from parameters + * + * @param string|Crypt_GPG_UserId $name either a {@link Crypt_GPG_UserId} + * object, or a string containing + * the name of the user id. + * @param string $email optional. If <i>$name</i> is + * specified as a string, this is + * the email address of the user id. + * @param string $comment optional. If <i>$name</i> is + * specified as a string, this is + * the comment of the user id. + * + * @return Crypt_GPG_UserId a user id object for the specified parameters. + */ + protected function getUserId($name, $email = '', $comment = '') + { + if ($name instanceof Crypt_GPG_UserId) { + $userId = $name; + } else { + $userId = new Crypt_GPG_UserId(); + $userId->setName($name)->setEmail($email)->setComment($comment); + } + + return $userId; + } + + // }}} +} + +// }}} + +?> diff --git a/program/lib/Crypt/GPG/KeyGeneratorErrorHandler.php b/program/lib/Crypt/GPG/KeyGeneratorErrorHandler.php new file mode 100644 index 000000000..ad9ebf395 --- /dev/null +++ b/program/lib/Crypt/GPG/KeyGeneratorErrorHandler.php @@ -0,0 +1,121 @@ +<?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 error output for the + * key generation 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 2011-2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id:$ + * @link http://pear.php.net/package/Crypt_GPG + * @link http://www.gnupg.org/ + */ + +/** + * Error line handler for the key generation 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. + * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier <mike@silverorange.com> + * @copyright 2011-2013 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_KeyGeneratorErrorHandler +{ + // {{{ protected properties + + /** + * Error code (if any) caused by key generation + * + * @var integer + */ + protected $errorCode = Crypt_GPG::ERROR_NONE; + + /** + * Line number at which the error occurred + * + * @var integer + */ + protected $lineNumber = null; + + // }}} + // {{{ handle() + + /** + * Handles an error line + * + * @param string $line the error line to handle. + * + * @return void + */ + public function handle($line) + { + $matches = array(); + $pattern = '/:([0-9]+): invalid algorithm$/'; + if (preg_match($pattern, $line, $matches) === 1) { + $this->errorCode = Crypt_GPG::ERROR_BAD_KEY_PARAMS; + $this->lineNumber = intval($matches[1]); + } + } + + // }}} + // {{{ getErrorCode() + + /** + * Gets the error code resulting from key gneration + * + * @return integer the error code resulting from key generation. + */ + public function getErrorCode() + { + return $this->errorCode; + } + + // }}} + // {{{ getLineNumber() + + /** + * Gets the line number at which the error occurred + * + * @return integer the line number at which the error occurred. Null if + * no error occurred. + */ + public function getLineNumber() + { + return $this->lineNumber; + } + + // }}} +} + +?> diff --git a/program/lib/Crypt/GPG/KeyGeneratorStatusHandler.php b/program/lib/Crypt/GPG/KeyGeneratorStatusHandler.php new file mode 100644 index 000000000..8b4c85c7a --- /dev/null +++ b/program/lib/Crypt/GPG/KeyGeneratorStatusHandler.php @@ -0,0 +1,173 @@ +<?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 + * key generation 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 2011-2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id:$ + * @link http://pear.php.net/package/Crypt_GPG + * @link http://www.gnupg.org/ + */ + +/** + * Status line handler for the key generation 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 parsing the final key fingerprint from the + * status output and for updating the key generation progress file. 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 batch key generation operation. + * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier <mike@silverorange.com> + * @copyright 2011-2013 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_KeyGeneratorStatusHandler +{ + // {{{ protected properties + + /** + * The key fingerprint + * + * Ths key fingerprint is emitted by GPG after the key generation is + * complete. + * + * @var string + */ + protected $keyFingerprint = ''; + + /** + * The unique key handle used by this handler + * + * The key handle is used to track GPG status output for a particular key + * before the key has its own identifier. + * + * @var string + * + * @see Crypt_GPG_KeyGeneratorStatusHandler::setHandle() + */ + protected $handle = ''; + + /** + * Error code (if any) caused by key generation + * + * @var integer + */ + protected $errorCode = Crypt_GPG::ERROR_NONE; + + // }}} + // {{{ setHandle() + + /** + * Sets the unique key handle used by this handler + * + * The key handle is used to track GPG status output for a particular key + * before the key has its own identifier. + * + * @param string $handle the key handle this status handle will use. + * + * @return Crypt_GPG_KeyGeneratorStatusHandler the current object, for + * fluent interface. + */ + public function setHandle($handle) + { + $this->handle = strval($handle); + return $this; + } + + // }}} + // {{{ 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 'KEY_CREATED': + if ($tokens[3] == $this->handle) { + $this->keyFingerprint = $tokens[2]; + } + break; + + case 'KEY_NOT_CREATED': + if ($tokens[1] == $this->handle) { + $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_CREATED; + } + break; + + case 'PROGRESS': + // todo: at some point, support reporting status async + break; + } + } + + // }}} + // {{{ getKeyFingerprint() + + /** + * Gets the key fingerprint parsed by this handler + * + * @return array the key fingerprint parsed by this handler. + */ + public function getKeyFingerprint() + { + return $this->keyFingerprint; + } + + // }}} + // {{{ getErrorCode() + + /** + * Gets the error code resulting from key gneration + * + * @return integer the error code resulting from key generation. + */ + public function getErrorCode() + { + return $this->errorCode; + } + + // }}} +} + +?> diff --git a/program/lib/Crypt/GPG/PinEntry.php b/program/lib/Crypt/GPG/PinEntry.php new file mode 100644 index 000000000..c09703617 --- /dev/null +++ b/program/lib/Crypt/GPG/PinEntry.php @@ -0,0 +1,875 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * Contains a class implementing automatic pinentry for gpg-agent + * + * 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 2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id$ + * @link http://pear.php.net/package/Crypt_GPG + */ + +/** + * CLI user-interface and parser. + */ +require_once 'Console/CommandLine.php'; + +// {{{ class Crypt_GPG_PinEntry + +/** + * A command-line dummy pinentry program for use with gpg-agent and Crypt_GPG + * + * This pinentry receives passphrases through en environment variable and + * automatically enters the PIN in response to gpg-agent requests. No user- + * interaction required. + * + * Thie pinentry can be run independently for testing and debugging with the + * following syntax: + * + * <pre> + * Usage: + * crypt-gpg-pinentry [options] + * + * Options: + * -l log, --log=log Optional location to log pinentry activity. + * -v, --verbose Sets verbosity level. Use multiples for more detail + * (e.g. "-vv"). + * -h, --help show this help message and exit + * --version show the program version and exit + * </pre> + * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier <mike@silverorange.com> + * @copyright 2013 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_PinEntry +{ + // {{{ class constants + + /** + * Verbosity level for showing no output. + */ + const VERBOSITY_NONE = 0; + + /** + * Verbosity level for showing error output. + */ + const VERBOSITY_ERRORS = 1; + + /** + * Verbosity level for showing all output, including Assuan protocol + * messages. + */ + const VERBOSITY_ALL = 2; + + /** + * Length of buffer for reading lines from the Assuan server. + * + * 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; + + // }}} + // {{{ protected properties + + /** + * File handle for the input stream + * + * @var resource + */ + protected $stdin = null; + + /** + * File handle for the output stream + * + * @var resource + */ + protected $stdout = null; + + /** + * File handle for the log file if a log file is used + * + * @var resource + */ + protected $logFile = null; + + /** + * Whether or not this pinentry is finished and is exiting + * + * @var boolean + */ + protected $moribund = false; + + /** + * Verbosity level + * + * One of: + * - {@link Crypt_GPG_PinEntry::VERBOSITY_NONE}, + * - {@link Crypt_GPG_PinEntry::VERBOSITY_ERRORS}, or + * - {@link Crypt_GPG_PinEntry::VERBOSITY_ALL} + * + * @var integer + */ + protected $verbosity = self::VERBOSITY_NONE; + + /** + * The command-line interface parser for this pinentry + * + * @var Console_CommandLine + * + * @see Crypt_GPG_PinEntry::getParser() + */ + protected $parser = null; + + /** + * PINs to be entered by this pinentry + * + * An indexed array of associative arrays in the form: + * <code> + * <?php + * array( + * array( + * 'keyId' => $keyId, + * 'passphrase' => $passphrase + * ), + * ... + * ); + * ?> + * </code> + * + * This array is parsed from the environment variable + * <kbd>PINENTRY_USER_DATA</kbd>. + * + * @var array + * + * @see Crypt_GPG_PinEntry::initPinsFromENV() + */ + protected $pins = array(); + + /** + * PINs that have been tried for the current PIN + * + * This is an associative array indexed by the key identifier with + * values being the same as elements in the {@link Crypt_GPG_PinEntry::$pins} + * array. + * + * @var array + */ + protected $triedPins = array(); + + /** + * The PIN currently being requested by the Assuan server + * + * If set, this is an associative array in the form: + * <code> + * <?php + * array( + * 'keyId' => $shortKeyId, + * 'userId' => $userIdString + * ); + * ?> + * </code> + * + * @var array|null + */ + protected $currentPin = null; + + // }}} + // {{{ __invoke() + + /** + * Runs this pinentry + * + * @return void + */ + public function __invoke() + { + $this->parser = $this->getCommandLineParser(); + + try { + $result = $this->parser->parse(); + + $this->setVerbosity($result->options['verbose']); + $this->setLogFilename($result->options['log']); + + $this->connect(); + $this->initPinsFromENV(); + + while (($line = fgets($this->stdin, self::CHUNK_SIZE)) !== false) { + $this->parseCommand(mb_substr($line, 0, -1, '8bit')); + if ($this->moribund) { + break; + } + } + + $this->disconnect(); + + } catch (Console_CommandLineException $e) { + $this->log($e->getMessage() . PHP_EOL, slf::VERBOSITY_ERRORS); + exit(1); + } catch (Exception $e) { + $this->log($e->getMessage() . PHP_EOL, self::VERBOSITY_ERRORS); + $this->log($e->getTraceAsString() . PHP_EOL, self::VERBOSITY_ERRORS); + exit(1); + } + } + + // }}} + // {{{ setVerbosity() + + /** + * Sets the verbosity of logging for this pinentry + * + * Verbosity levels are: + * + * - {@link Crypt_GPG_PinEntry::VERBOSITY_NONE} - no logging. + * - {@link Crypt_GPG_PinEntry::VERBOSITY_ERRORS} - log errors only. + * - {@link Crypt_GPG_PinEntry::VERBOSITY_ALL} - log everything, including + * the assuan protocol. + * + * @param integer $verbosity the level of verbosity of this pinentry. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + public function setVerbosity($verbosity) + { + $this->verbosity = (integer)$verbosity; + return $this; + } + + // }}} + // {{{ setLogFilename() + + /** + * Sets the log file location + * + * @param string $filename the new log filename to use. If an empty string + * is used, file-based logging is disabled. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + public function setLogFilename($filename) + { + if (is_resource($this->logFile)) { + fflush($this->logFile); + fclose($this->logFile); + $this->logFile = null; + } + + if ($filename != '') { + if (($this->logFile = fopen($filename, 'w')) === false) { + $this->log( + 'Unable to open log file "' . $filename . '" ' + . 'for writing.' . PHP_EOL, + self::VERBOSITY_ERRORS + ); + exit(1); + } else { + stream_set_write_buffer($this->logFile, 0); + } + } + + return $this; + } + + // }}} + // {{{ getUIXML() + + /** + * Gets the CLI user-interface definition for this pinentry + * + * Detects whether or not this package is PEAR-installed and appropriately + * locates the XML UI definition. + * + * @return string the location of the CLI user-interface definition XML. + */ + protected function getUIXML() + { + $dir = '@data-dir@' . DIRECTORY_SEPARATOR + . '@package-name@' . DIRECTORY_SEPARATOR . 'data'; + + // Check if we're running directly from a git checkout or if we're + // running from a PEAR-packaged version. + if ($dir[0] == '@') { + $dir = dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' + . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'data'; + } + + return $dir . DIRECTORY_SEPARATOR . 'pinentry-cli.xml'; + } + + // }}} + // {{{ getCommandLineParser() + + /** + * Gets the CLI parser for this pinentry + * + * @return Console_CommandLine the CLI parser for this pinentry. + */ + protected function getCommandLineParser() + { + return Console_CommandLine::fromXmlFile($this->getUIXML()); + } + + // }}} + // {{{ log() + + /** + * Logs a message at the specified verbosity level + * + * If a log file is used, the message is written to the log. Otherwise, + * the message is sent to STDERR. + * + * @param string $data the message to log. + * @param integer $level the verbosity level above which the message should + * be logged. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function log($data, $level) + { + if ($this->verbosity >= $level) { + if (is_resource($this->logFile)) { + fwrite($this->logFile, $data); + fflush($this->logFile); + } else { + $this->parser->outputter->stderr($data); + } + } + + return $this; + } + + // }}} + // {{{ connect() + + /** + * Connects this pinentry to the assuan server + * + * Opens I/O streams and sends initial handshake. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function connect() + { + // Binary operations will not work on Windows with PHP < 5.2.6. + $rb = (version_compare(PHP_VERSION, '5.2.6') < 0) ? 'r' : 'rb'; + $wb = (version_compare(PHP_VERSION, '5.2.6') < 0) ? 'w' : 'wb'; + + $this->stdin = fopen('php://stdin', $rb); + $this->stdout = fopen('php://stdout', $wb); + + if (function_exists('stream_set_read_buffer')) { + stream_set_read_buffer($this->stdin, 0); + } + stream_set_write_buffer($this->stdout, 0); + + // initial handshake + $this->send($this->getOK('Crypt_GPG pinentry ready and waiting')); + + return $this; + } + + // }}} + // {{{ parseCommand() + + /** + * Parses an assuan command and performs the appropriate action + * + * Documentation of the assuan commands for pinentry is limited to + * non-existent. Most of these commands were taken from the C source code + * to gpg-agent and pinentry. + * + * Additional context was provided by using strace -f when calling the + * gpg-agent. + * + * @param string $line the assuan command line to parse + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function parseCommand($line) + { + $this->log('<- ' . $line . PHP_EOL, self::VERBOSITY_ALL); + + $parts = explode(' ', $line, 2); + + $command = $parts[0]; + + if (count($parts) === 2) { + $data = $parts[1]; + } else { + $data = null; + } + + switch ($command) { + case 'SETDESC': + return $this->sendSetDescription($data); + + case 'SETPROMPT': + case 'SETERROR': + case 'SETOK': + case 'SETNOTOK': + case 'SETCANCEL': + case 'SETQUALITYBAR': + case 'SETQUALITYBAR_TT': + case 'OPTION': + return $this->sendNotImplementedOK(); + + case 'MESSAGE': + return $this->sendMessage(); + + case 'CONFIRM': + return $this->sendConfirm(); + + case 'GETINFO': + return $this->sendGetInfo($data); + + case 'GETPIN': + return $this->sendGetPin($data); + + case 'RESET': + return $this->sendReset(); + + case 'BYE': + return $this->sendBye(); + } + } + + // }}} + // {{{ initPinsFromENV() + + /** + * Initializes the PINs to be entered by this pinentry from the environment + * variable PINENTRY_USER_DATA + * + * The PINs are parsed from a JSON-encoded string. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function initPinsFromENV() + { + if (($userData = getenv('PINENTRY_USER_DATA')) !== false) { + $pins = json_decode($userData, true); + if ($pins === null) { + $this->log( + '-- failed to parse user data' . PHP_EOL, + self::VERBOSITY_ERRORS + ); + } else { + $this->pins = $pins; + $this->log( + '-- got user data [not showing passphrases]' . PHP_EOL, + self::VERBOSITY_ALL + ); + } + } + + return $this; + } + + // }}} + // {{{ disconnect() + + /** + * Disconnects this pinentry from the Assuan server + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function disconnect() + { + $this->log('-- disconnecting' . PHP_EOL, self::VERBOSITY_ALL); + + fflush($this->stdout); + fclose($this->stdout); + fclose($this->stdin); + + $this->stdin = null; + $this->stdout = null; + + $this->log('-- disconnected' . PHP_EOL, self::VERBOSITY_ALL); + + if (is_resource($this->logFile)) { + fflush($this->logFile); + fclose($this->logFile); + $this->logFile = null; + } + + return $this; + } + + // }}} + // {{{ sendNotImplementedOK() + + /** + * Sends an OK response for a not implemented feature + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendNotImplementedOK() + { + return $this->send($this->getOK()); + } + + // }}} + // {{{ sendSetDescription() + + /** + * Parses the currently requested key identifier and user identifier from + * the description passed to this pinentry + * + * @param string $text the raw description sent from gpg-agent. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendSetDescription($text) + { + $text = rawurldecode($text); + $matches = array(); + // TODO: handle user id with quotation marks + $exp = '/\n"(.+)"\n.*\sID ([A-Z0-9]+),\n/mu'; + if (preg_match($exp, $text, $matches) === 1) { + $userId = $matches[1]; + $keyId = $matches[2]; + + // only reset tried pins for new requested pin + if ( $this->currentPin === null + || $this->currentPin['keyId'] !== $keyId + ) { + $this->currentPin = array( + 'userId' => $userId, + 'keyId' => $keyId + ); + $this->triedPins = array(); + $this->log( + '-- looking for PIN for ' . $keyId . PHP_EOL, + self::VERBOSITY_ALL + ); + } + } + + return $this->send($this->getOK()); + } + + // }}} + // {{{ sendConfirm() + + /** + * Tells the assuan server the PIN entry was confirmed (not cancelled) + * by pressing the fake 'close' button + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendConfirm() + { + return $this->sendButtonInfo('close'); + } + + // }}} + // {{{ sendMessage() + + /** + * Tells the assuan server that any requested pop-up messages were confirmed + * by pressing the fake 'close' button + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendMessage() + { + return $this->sendButtonInfo('close'); + } + + // }}} + // {{{ sendButtonInfo() + + /** + * Sends information about pressed buttons to the assuan server + * + * This is used to fake a user-interface for this pinentry. + * + * @param string $text the button status to send. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendButtonInfo($text) + { + return $this->send('BUTTON_INFO ' . $text . "\n"); + } + + // }}} + // {{{ sendGetPin() + + /** + * Sends the PIN value for the currently requested key + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendGetPin() + { + $foundPin = ''; + + if (is_array($this->currentPin)) { + $keyIdLength = mb_strlen($this->currentPin['keyId'], '8bit'); + + // search for the pin + foreach ($this->pins as $pin) { + // only check pins we haven't tried + if (!isset($this->triedPins[$pin['keyId']])) { + + // get last X characters of key identifier to compare + $keyId = mb_substr( + $pin['keyId'], + -$keyIdLength, + mb_strlen($pin['keyId'], '8bit'), + '8bit' + ); + + if ($keyId === $this->currentPin['keyId']) { + $foundPin = $pin['passphrase']; + $this->triedPins[$pin['keyId']] = $pin; + break; + } + } + } + } + + return $this + ->send($this->getData($foundPin)) + ->send($this->getOK()); + } + + // }}} + // {{{ sendGetInfo() + + /** + * Sends information about this pinentry + * + * @param string $data the information requested by the assuan server. + * Currently only 'pid' is supported. Other requests + * return no information. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendGetInfo($data) + { + $parts = explode(' ', $data, 2); + $command = reset($parts); + + switch ($command) { + case 'pid': + return $this->sendGetInfoPID(); + default: + return $this->send($this->getOK()); + } + + return $this; + } + // }}} + // {{{ sendGetInfoPID() + + /** + * Sends the PID of this pinentry to the assuan server + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendGetInfoPID() + { + return $this + ->send($this->getData(getmypid())) + ->send($this->getOK()); + } + + // }}} + // {{{ sendBye() + + /** + * Flags this pinentry for disconnection and sends an OK response + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendBye() + { + $return = $this->send($this->getOK('closing connection')); + $this->moribund = true; + return $return; + } + + // }}} + // {{{ sendReset() + + /** + * Resets this pinentry and sends an OK response + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function sendReset() + { + $this->currentPin = null; + $this->triedPins = array(); + return $this->send($this->getOK()); + } + + // }}} + // {{{ getOK() + + /** + * Gets an OK response to send to the assuan server + * + * @param string $data an optional message to include with the OK response. + * + * @return string the OK response. + */ + protected function getOK($data = null) + { + $return = 'OK'; + + if ($data) { + $return .= ' ' . $data; + } + + return $return . "\n"; + } + + // }}} + // {{{ getData() + + /** + * Gets data ready to send to the assuan server + * + * Data is appropriately escaped and long lines are wrapped. + * + * @param string $data the data to send to the assuan server. + * + * @return string the properly escaped, formatted data. + * + * @see http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html + */ + protected function getData($data) + { + // Escape data. Only %, \n and \r need to be escaped but other + // values are allowed to be escaped. See + // http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html + $data = rawurlencode($data); + $data = $this->getWordWrappedData($data, 'D'); + return $data; + } + + // }}} + // {{{ getComment() + + /** + * Gets a comment ready to send to the assuan server + * + * @param string $data the comment to send to the assuan server. + * + * @return string the properly formatted comment. + * + * @see http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html + */ + protected function getComment($data) + { + return $this->getWordWrappedData($data, '#'); + } + + // }}} + // {{{ getWordWrappedData() + + /** + * Wraps strings at 1,000 bytes without splitting UTF-8 multibyte + * characters + * + * Each line is prepended with the specified line prefix. Wrapped lines + * are automatically appended with \ characters. + * + * Protocol strings are UTF-8 but maximum line length is 1,000 bytes. + * <kbd>mb_strcut()</kbd> is used so we can limit line length by bytes + * and not split characters across multiple lines. + * + * @param string $data the data to wrap. + * @param string $prefix a single character to use as the line prefix. For + * example, 'D' or '#'. + * + * @return string the word-wrapped, prefixed string. + * + * @see http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html + */ + protected function getWordWrappedData($data, $prefix) + { + $lines = array(); + + do { + if (mb_strlen($data, '8bit') > 997) { + $line = $prefix . ' ' . mb_strcut($data, 0, 996, 'utf-8') . "\\\n"; + $lines[] = $line; + $lineLength = mb_strlen($line, '8bit') - 1; + $dataLength = mb_substr($data, '8bit'); + $data = mb_substr( + $data, + $lineLength, + $dataLength - $lineLength, + '8bit' + ); + } else { + $lines[] = $prefix . ' ' . $data . "\n"; + $data = ''; + } + } while ($data != ''); + + return implode('', $lines); + } + + // }}} + // {{{ send() + + /** + * Sends raw data to the assuan server + * + * @param string $data the data to send. + * + * @return Crypt_GPG_PinEntry the current object, for fluent interface. + */ + protected function send($data) + { + $this->log('-> ' . $data, self::VERBOSITY_ALL); + fwrite($this->stdout, $data); + fflush($this->stdout); + return $this; + } + + // }}} +} + +// }}} + +?> diff --git a/program/lib/Crypt/GPG/ProcessControl.php b/program/lib/Crypt/GPG/ProcessControl.php new file mode 100644 index 000000000..d6dae0325 --- /dev/null +++ b/program/lib/Crypt/GPG/ProcessControl.php @@ -0,0 +1,150 @@ +<?php + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** + * A class for monitoring and terminating processes + * + * 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 2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id$ + * @link http://pear.php.net/package/Crypt_GPG + */ + +// {{{ class Crypt_GPG_ProcessControl + +/** + * A class for monitoring and terminating processes by PID + * + * This is used to safely terminate the gpg-agent for GnuPG 2.x. This class + * is limited in its abilities and can only check if a PID is running and + * send a PID SIGTERM. + * + * @category Encryption + * @package Crypt_GPG + * @author Michael Gauthier <mike@silverorange.com> + * @copyright 2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @link http://pear.php.net/package/Crypt_GPG + */ +class Crypt_GPG_ProcessControl +{ + // {{{ protected properties + + /** + * The PID (process identifier) being monitored + * + * @var integer + */ + protected $pid; + + // }}} + // {{{ __construct() + + /** + * Creates a new process controller from the given PID (process identifier) + * + * @param integer $pid the PID (process identifier). + */ + public function __construct($pid) + { + $this->pid = $pid; + } + + // }}} + // {{{ public function getPid() + + /** + * Gets the PID (process identifier) being controlled + * + * @return integer the PID being controlled. + */ + public function getPid() + { + return $this->pid; + } + + // }}} + // {{{ isRunning() + + /** + * Checks if the process is running + * + * Uses <kbd>ps</kbd> on UNIX-like systems and <kbd>tasklist</kbd> on + * Windows. + * + * @return boolean true if the process is running, false if not. + */ + public function isRunning() + { + $running = false; + + if (PHP_OS === 'WINNT') { + $command = 'tasklist /fo csv /nh /fi ' + . escapeshellarg('PID eq ' . $this->pid); + + $result = exec($command); + $parts = explode(',', $result); + $running = (count($parts) > 1 && trim($parts[1], '"') == $this->pid); + } else { + $result = exec('ps -p ' . escapeshellarg($this->pid) . ' -o pid='); + $running = (trim($result) == $this->pid); + } + + return $running; + } + + // }}} + // {{{ terminate() + + /** + * Ends the process gracefully + * + * The signal SIGTERM is sent to the process. The gpg-agent process will + * end gracefully upon receiving the SIGTERM signal. Upon 3 consecutive + * SIGTERM signals the gpg-agent will forcefully shut down. + * + * If the <kbd>posix</kbd> extension is available, <kbd>posix_kill()</kbd> + * is used. Otherwise <kbd>kill</kbd> is used on UNIX-like systems and + * <kbd>taskkill</kbd> is used in Windows. + * + * @return void + */ + public function terminate() + { + if (function_exists('posix_kill')) { + posix_kill($this->pid, 15); + } elseif (PHP_OS === 'WINNT') { + exec('taskkill /PID ' . escapeshellarg($this->pid)); + } else { + exec('kill -15 ' . escapeshellarg($this->pid)); + } + } + + // }}} +} + +// }}} + +?> diff --git a/program/lib/Crypt/GPG/Signature.php b/program/lib/Crypt/GPG/Signature.php index 03ab44c53..1d28a1188 100644 --- a/program/lib/Crypt/GPG/Signature.php +++ b/program/lib/Crypt/GPG/Signature.php @@ -28,9 +28,10 @@ * @category Encryption * @package Crypt_GPG * @author Nathan Fredrickson <nathan@silverorange.com> - * @copyright 2005-2010 silverorange + * @author Michael Gauthier <mike@silverorange.com> + * @copyright 2005-2013 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 $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG */ @@ -50,7 +51,7 @@ require_once 'Crypt/GPG/UserId.php'; * @package Crypt_GPG * @author Nathan Fredrickson <nathan@silverorange.com> * @author Michael Gauthier <mike@silverorange.com> - * @copyright 2005-2010 silverorange + * @copyright 2005-2013 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() @@ -159,8 +160,6 @@ class Crypt_GPG_Signature if ($signature->_userId instanceof Crypt_GPG_UserId) { $this->_userId = clone $signature->_userId; - } else { - $this->_userId = $signature->_userId; } } diff --git a/program/lib/Crypt/GPG/SubKey.php b/program/lib/Crypt/GPG/SubKey.php index b6316e99f..59245cac1 100644 --- a/program/lib/Crypt/GPG/SubKey.php +++ b/program/lib/Crypt/GPG/SubKey.php @@ -29,7 +29,7 @@ * @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 $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG */ @@ -53,7 +53,7 @@ */ class Crypt_GPG_SubKey { - // {{{ class constants + // {{{ algorithm class constants /** * RSA encryption algorithm. @@ -77,6 +77,29 @@ class Crypt_GPG_SubKey const ALGORITHM_ELGAMAL_ENC_SGN = 20; // }}} + // {{{ usage class constants + + /** + * Key can be used to encrypt + */ + const USAGE_ENCRYPT = 1; + + /** + * Key can be used to sign + */ + const USAGE_SIGN = 2; + + /** + * Key can be used to certify other keys + */ + const USAGE_CERTIFY = 4; + + /** + * Key can be used for authentication + */ + const USAGE_AUTHENTICATION = 8; + + // }}} // {{{ class properties /** diff --git a/program/lib/Crypt/GPG/UserId.php b/program/lib/Crypt/GPG/UserId.php index 04435708c..a367bceb3 100644 --- a/program/lib/Crypt/GPG/UserId.php +++ b/program/lib/Crypt/GPG/UserId.php @@ -28,7 +28,7 @@ * @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 $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG */ diff --git a/program/lib/Crypt/GPG/VerifyStatusHandler.php b/program/lib/Crypt/GPG/VerifyStatusHandler.php index 083bd3012..8904be149 100644 --- a/program/lib/Crypt/GPG/VerifyStatusHandler.php +++ b/program/lib/Crypt/GPG/VerifyStatusHandler.php @@ -31,7 +31,7 @@ * @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 $ + * @version CVS: $Id$ * @link http://pear.php.net/package/Crypt_GPG * @link http://www.gnupg.org/ */ diff --git a/program/lib/Crypt/GPGAbstract.php b/program/lib/Crypt/GPGAbstract.php new file mode 100644 index 000000000..214133936 --- /dev/null +++ b/program/lib/Crypt/GPGAbstract.php @@ -0,0 +1,508 @@ +<?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 an abstract implementation of a user of the + * {@link Crypt_GPG_Engine} class. + * + * 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-2013 silverorange + * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1 + * @version CVS: $Id: GPG.php 305428 2010-11-17 02:47:56Z 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/ + */ + +/** + * 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_GPGAbstract + +/** + * Base class for implementing a user of {@link Crypt_GPG_Engine} + * + * @category Encryption + * @package Crypt_GPG + * @author Nathan Fredrickson <nathan@silverorange.com> + * @author Michael Gauthier <mike@silverorange.com> + * @copyright 2005-2013 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/ + */ +abstract class Crypt_GPGAbstract +{ + // {{{ 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; + + /** + * Error code returned when a key could not be created. + */ + const ERROR_KEY_NOT_CREATED = 12; + + /** + * Error code returned when bad key parameters are used during key + * generation. + */ + const ERROR_BAD_KEY_PARAMS = 13; + + // }}} + // {{{ 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_GPGAbstract::setEngine() + */ + protected $engine = null; + + // }}} + // {{{ __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>string agent</kbd> - the location of the GnuPG agent + * binary. The gpg-agent is only + * used for GnuPG 2.x. If not + * specified, the engine attempts + * to auto-detect the gpg-agent + * binary location using a list of + * know default locations for the + * current operating system. + * - <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. + * + * @throws PEAR_Exception if the provided <kbd>agent</kbd> is invalid, or + * if no <kbd>agent</kbd> is provided and no suitable gpg-agent + * cound be found. + */ + public function __construct(array $options = array()) + { + $this->setEngine(new Crypt_GPG_Engine($options)); + } + + // }}} + // {{{ 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 Crypt_GPGAbstract the current object, for fluent interface. + */ + public function setEngine(Crypt_GPG_Engine $engine) + { + $this->engine = $engine; + return $this; + } + + // }}} + // {{{ _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 + */ + protected 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 self::ERROR_NONE: + case self::ERROR_KEY_NOT_FOUND: + // ignore not found key errors + break; + case self::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 self::ERROR_NONE: + case self::ERROR_KEY_NOT_FOUND: + // ignore not found key errors + break; + case self::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; + } + + // }}} +} + +// }}} + +?> diff --git a/program/lib/Roundcube/html.php b/program/lib/Roundcube/html.php index 5911c04d7..33517fbcd 100644 --- a/program/lib/Roundcube/html.php +++ b/program/lib/Roundcube/html.php @@ -3,7 +3,7 @@ /* +-----------------------------------------------------------------------+ | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2011, The Roundcube Dev Team | + | Copyright (C) 2005-2013, The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -32,8 +32,8 @@ class html public static $doctype = 'xhtml'; public static $lc_tags = true; - public static $common_attrib = array('id','class','style','title','align'); - public static $containers = array('iframe','div','span','p','h1','h2','h3','form','textarea','table','thead','tbody','tr','th','td','style','script'); + public static $common_attrib = array('id','class','style','title','align','unselectable'); + public static $containers = array('iframe','div','span','p','h1','h2','h3','ul','form','textarea','table','thead','tbody','tr','th','td','style','script'); /** @@ -645,7 +645,7 @@ class html_select extends html $option_content = self::quote($option_content); } - $this->content .= self::tag('option', $attr + $option, $option_content, array('class','style','title','disabled')); + $this->content .= self::tag('option', $attr + $option, $option_content, array('value','label','class','style','title','disabled','selected')); } return parent::show(); @@ -677,8 +677,8 @@ class html_table extends html */ public function __construct($attrib = array()) { - $default_attrib = self::$doctype == 'xhtml' ? array('summary' => '', 'border' => 0) : array(); - $this->attrib = array_merge($attrib, $default_attrib); + $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']; @@ -880,7 +880,7 @@ class html_table extends html private function _row_tagname() { static $row_tagnames = array('table' => 'tr', 'ul' => 'li', '*' => 'div'); - return $row_tagnames[$this->tagname] ?: $row_tagnames['*']; + return $row_tagnames[$this->tagname] ? $row_tagnames[$this->tagname] : $row_tagnames['*']; } /** @@ -889,7 +889,7 @@ class html_table extends html private function _col_tagname() { static $col_tagnames = array('table' => 'td', '*' => 'span'); - return $col_tagnames[$this->tagname] ?: $col_tagnames['*']; + return $col_tagnames[$this->tagname] ? $col_tagnames[$this->tagname] : $col_tagnames['*']; } } diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php index 399f84fd8..69d95f023 100644 --- a/program/lib/Roundcube/rcube.php +++ b/program/lib/Roundcube/rcube.php @@ -3,8 +3,8 @@ /* +-----------------------------------------------------------------------+ | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2012, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | + | Copyright (C) 2008-2014, The Roundcube Dev Team | + | Copyright (C) 2011-2014, Kolab Systems AG | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -94,6 +94,13 @@ class rcube */ public $plugins; + /** + * Instance of rcube_user class. + * + * @var rcube_user + */ + public $user; + /* private/protected vars */ protected $texts; @@ -642,10 +649,11 @@ class rcube /** * Load a localization package * - * @param string Language ID - * @param array Additional text labels/messages + * @param string $lang Language ID + * @param array $add Additional text labels/messages + * @param array $merge Additional text labels/messages to merge */ - public function load_language($lang = null, $add = array()) + public function load_language($lang = null, $add = array(), $merge = array()) { $lang = $this->language_prop(($lang ? $lang : $_SESSION['language'])); @@ -685,6 +693,11 @@ class rcube if (is_array($add) && !empty($add)) { $this->texts += $add; } + + // merge additional texts (from plugin) + if (is_array($merge) && !empty($merge)) { + $this->texts = array_merge($this->texts, $merge); + } } @@ -1108,7 +1121,20 @@ class rcube // log_driver == 'file' is assumed here $line = sprintf("[%s]: %s\n", $date, $line); - $log_dir = self::$instance ? self::$instance->config->get('log_dir') : null; + $log_dir = null; + + // per-user logging is activated + if (self::$instance && self::$instance->config->get('per_user_logging', false) && self::$instance->get_user_id()) { + $log_dir = self::$instance->get_user_log_dir(); + if (empty($log_dir)) + return false; + } + else if (!empty($log['dir'])) { + $log_dir = $log['dir']; + } + else if (self::$instance) { + $log_dir = self::$instance->config->get('log_dir'); + } if (empty($log_dir)) { $log_dir = RCUBE_INSTALL_PATH . 'logs'; @@ -1146,7 +1172,6 @@ class rcube // handle PHP exceptions if (is_object($arg) && is_a($arg, 'Exception')) { $arg = array( - 'type' => 'php', 'code' => $arg->getCode(), 'line' => $arg->getLine(), 'file' => $arg->getFile(), @@ -1154,7 +1179,7 @@ class rcube ); } else if (is_string($arg)) { - $arg = array('message' => $arg, 'type' => 'php'); + $arg = array('message' => $arg); } if (empty($arg['code'])) { @@ -1170,7 +1195,7 @@ class rcube $cli = php_sapi_name() == 'cli'; - if (($log || $terminate) && !$cli && $arg['type'] && $arg['message']) { + if (($log || $terminate) && !$cli && $arg['message']) { $arg['fatal'] = $terminate; self::log_bug($arg); } @@ -1198,7 +1223,7 @@ class rcube */ public static function log_bug($arg_arr) { - $program = strtoupper($arg_arr['type']); + $program = strtoupper(!empty($arg_arr['type']) ? $arg_arr['type'] : 'php'); $level = self::get_instance()->config->get('debug_level'); // disable errors for ajax requests, write to log instead (#1487831) @@ -1284,6 +1309,20 @@ class rcube self::write_log($dest, sprintf("%s: %0.4f sec", $label, $diff)); } + /** + * Setter for system user object + * + * @param rcube_user Current user instance + */ + public function set_user($user) + { + if (is_object($user)) { + $this->user = $user; + + // overwrite config with user preferences + $this->config->set_user_prefs((array)$this->user->get_prefs()); + } + } /** * Getter for logged user ID. @@ -1347,6 +1386,17 @@ class rcube } } + /** + * Get the per-user log directory + */ + protected function get_user_log_dir() + { + $log_dir = $this->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs'); + $user_name = $this->get_user_name(); + $user_log_dir = $log_dir . '/' . $user_name; + + return !empty($user_name) && is_writable($user_log_dir) ? $user_log_dir : false; + } /** * Getter for logged user language code. @@ -1537,6 +1587,10 @@ class rcube !empty($response) ? join('; ', $response) : '')); } } + else { + // allow plugins to catch sending errors with the same parameters as in 'message_before_send' + $this->plugins->exec_hook('message_send_error', $plugin + array('error' => $error)); + } if (is_resource($msg_body)) { fclose($msg_body); diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php index 6e2b439d8..4d9fa3db1 100644 --- a/program/lib/Roundcube/rcube_addressbook.php +++ b/program/lib/Roundcube/rcube_addressbook.php @@ -209,6 +209,7 @@ abstract class rcube_addressbook public function validate(&$save_data, $autofix = false) { $rcube = rcube::get_instance(); + $valid = true; // check validity of email addresses foreach ($this->get_col_values('email', $save_data, true) as $email) { @@ -216,12 +217,28 @@ abstract class rcube_addressbook if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) { $error = $rcube->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email))); $this->set_error(self::ERROR_VALIDATE, $error); - return false; + $valid = false; + break; } } } - return true; + // allow plugins to do contact validation and auto-fixing + $plugin = $rcube->plugins->exec_hook('contact_validate', array( + 'record' => $save_data, + 'autofix' => $autofix, + 'valid' => $valid, + )); + + if ($valid && !$plugin['valid']) { + $this->set_error(self::ERROR_VALIDATE, $plugin['error']); + } + + if (is_array($plugin['record'])) { + $save_data = $plugin['record']; + } + + return $plugin['valid']; } /** @@ -264,7 +281,8 @@ abstract class rcube_addressbook * @param array Assoziative array with save data * Keys: Field name with optional section in the form FIELD:SECTION * Values: Field value. Can be either a string or an array of strings for multiple values - * @return boolean True on success, False on error + * + * @return mixed On success if ID has been changed returns ID, otherwise True, False on error */ function update($id, $save_cols) { @@ -294,8 +312,10 @@ abstract class rcube_addressbook /** * Mark all records in database as deleted + * + * @param bool $with_groups Remove also groups */ - function delete_all() + function delete_all($with_groups = false) { /* empty for read-only address books */ } @@ -515,8 +535,12 @@ abstract class rcube_addressbook $fn = join(' ', array($contact['surname'], $contact['firstname'], $contact['middlename'])); else if ($compose_mode == 1) $fn = join(' ', array($contact['firstname'], $contact['middlename'], $contact['surname'])); - else + else if ($compose_mode == 0) $fn = !empty($contact['name']) ? $contact['name'] : join(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix'])); + else { + $plugin = rcube::get_instance()->plugins->exec_hook('contact_listname', array('contact' => $contact)); + $fn = $plugin['fn']; + } $fn = trim($fn, ', '); diff --git a/program/lib/Roundcube/rcube_browser.php b/program/lib/Roundcube/rcube_browser.php index 34128291b..b9642d8f9 100644 --- a/program/lib/Roundcube/rcube_browser.php +++ b/program/lib/Roundcube/rcube_browser.php @@ -28,32 +28,30 @@ class rcube_browser { $HTTP_USER_AGENT = strtolower($_SERVER['HTTP_USER_AGENT']); - $this->ver = 0; - $this->win = strpos($HTTP_USER_AGENT, 'win') != false; - $this->mac = strpos($HTTP_USER_AGENT, 'mac') != false; + $this->ver = 0; + $this->win = strpos($HTTP_USER_AGENT, 'win') != false; + $this->mac = strpos($HTTP_USER_AGENT, 'mac') != false; $this->linux = strpos($HTTP_USER_AGENT, 'linux') != false; $this->unix = strpos($HTTP_USER_AGENT, 'unix') != false; - $this->opera = strpos($HTTP_USER_AGENT, 'opera') !== false; - $this->ns4 = strpos($HTTP_USER_AGENT, 'mozilla/4') !== false && strpos($HTTP_USER_AGENT, 'msie') === false; - $this->ns = ($this->ns4 || strpos($HTTP_USER_AGENT, 'netscape') !== false); - $this->ie = !$this->opera && strpos($HTTP_USER_AGENT, 'compatible; msie') !== false; - $this->khtml = strpos($HTTP_USER_AGENT, 'khtml') !== false; - $this->mz = !$this->ie && !$this->khtml && strpos($HTTP_USER_AGENT, 'mozilla/5') !== false; - $this->chrome = strpos($HTTP_USER_AGENT, 'chrome') !== false; - $this->safari = !$this->chrome && ($this->khtml || strpos($HTTP_USER_AGENT, 'safari') !== false); + $this->webkit = strpos($HTTP_USER_AGENT, 'applewebkit') !== false; + $this->opera = strpos($HTTP_USER_AGENT, 'opera') !== false || ($this->webkit && strpos($HTTP_USER_AGENT, 'opr/') !== false); + $this->ns = strpos($HTTP_USER_AGENT, 'netscape') !== false; + $this->chrome = !$this->opera && strpos($HTTP_USER_AGENT, 'chrome') !== false; + $this->ie = !$this->opera && (strpos($HTTP_USER_AGENT, 'compatible; msie') !== false || strpos($HTTP_USER_AGENT, 'trident/') !== false); + $this->safari = !$this->opera && !$this->chrome && ($this->webkit || strpos($HTTP_USER_AGENT, 'safari') !== false); + $this->mz = !$this->ie && !$this->safari && !$this->chrome && !$this->ns && !$this->opera && strpos($HTTP_USER_AGENT, 'mozilla') !== false; - if ($this->ns || $this->chrome) { - $test = preg_match('/(mozilla|chrome)\/([0-9.]+)/', $HTTP_USER_AGENT, $regs); - $this->ver = $test ? (float)$regs[2] : 0; + if ($this->opera) { + if (preg_match('/(opera|opr)\/([0-9.]+)/', $HTTP_USER_AGENT, $regs)) { + $this->ver = (float) $regs[2]; + } } - else if ($this->mz) { - $test = preg_match('/rv:([0-9.]+)/', $HTTP_USER_AGENT, $regs); - $this->ver = $test ? (float)$regs[1] : 0; + else if (preg_match('/(chrome|msie|version|khtml)(\s*|\/)([0-9.]+)/', $HTTP_USER_AGENT, $regs)) { + $this->ver = (float) $regs[3]; } - else if ($this->ie || $this->opera) { - $test = preg_match('/(msie|opera) ([0-9.]+)/', $HTTP_USER_AGENT, $regs); - $this->ver = $test ? (float)$regs[2] : 0; + else if (preg_match('/rv:([0-9.]+)/', $HTTP_USER_AGENT, $regs)) { + $this->ver = (float) $regs[1]; } if (preg_match('/ ([a-z]{2})-([a-z]{2})/', $HTTP_USER_AGENT, $regs)) @@ -61,10 +59,10 @@ class rcube_browser else $this->lang = 'en'; - $this->dom = ($this->mz || $this->safari || ($this->ie && $this->ver>=5) || ($this->opera && $this->ver>=7)); + $this->dom = $this->mz || $this->safari || ($this->ie && $this->ver>=5) || ($this->opera && $this->ver>=7); $this->pngalpha = $this->mz || $this->safari || ($this->ie && $this->ver>=5.5) || ($this->ie && $this->ver>=5 && $this->mac) || ($this->opera && $this->ver>=7) ? true : false; - $this->imgdata = !$this->ie; + $this->imgdata = !$this->ie; } } diff --git a/program/lib/Roundcube/rcube_charset.php b/program/lib/Roundcube/rcube_charset.php index 19dbf6cbc..8612e7fca 100644 --- a/program/lib/Roundcube/rcube_charset.php +++ b/program/lib/Roundcube/rcube_charset.php @@ -199,10 +199,13 @@ class rcube_charset $iconv_options = ''; } } + else { + $iconv_options = false; + } } // convert charset using iconv module - if ($iconv_options !== null && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') { + if ($iconv_options !== false && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') { // throw an exception if iconv reports an illegal character in input // it means that input string has been truncated set_error_handler(array('rcube_charset', 'error_handler'), E_NOTICE); @@ -224,10 +227,13 @@ class rcube_charset $mbstring_list = mb_list_encodings(); $mbstring_list = array_map('strtoupper', $mbstring_list); } + else { + $mbstring_list = false; + } } // convert charset using mbstring module - if ($mbstring_list !== null) { + if ($mbstring_list !== false) { $aliases['WINDOWS-1257'] = 'ISO-8859-13'; // it happens that mbstring supports ASCII but not US-ASCII if (($from == 'US-ASCII' || $to == 'US-ASCII') && !in_array('US-ASCII', $mbstring_list)) { diff --git a/program/lib/Roundcube/rcube_config.php b/program/lib/Roundcube/rcube_config.php index 04b914c3d..afe13e879 100644 --- a/program/lib/Roundcube/rcube_config.php +++ b/program/lib/Roundcube/rcube_config.php @@ -63,7 +63,7 @@ class rcube_config $this->paths = explode(PATH_SEPARATOR, $paths); // make all paths absolute foreach ($this->paths as $i => $path) { - if (!$this->_is_absolute($path)) { + if (!rcube_utils::is_absolute_path($path)) { if ($realpath = realpath(RCUBE_INSTALL_PATH . $path)) { $this->paths[$i] = unslashify($realpath) . '/'; } @@ -243,8 +243,8 @@ class rcube_config */ public function resolve_paths($file, $use_env = true) { - $files = array(); - $abs_path = $this->_is_absolute($file); + $files = array(); + $abs_path = rcube_utils::is_absolute_path($file); foreach ($this->paths as $basepath) { $realpath = $abs_path ? $file : realpath($basepath . '/' . $file); @@ -270,14 +270,6 @@ class rcube_config } /** - * 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 * * @param string $name Parameter name @@ -373,7 +365,11 @@ class rcube_config */ public function all() { - return $this->prop; + $rcube = rcube::get_instance(); + $plugin = $rcube->plugins->exec_hook('config_get', array( + 'name' => '*', 'result' => $this->prop)); + + return $plugin['result']; } /** diff --git a/program/lib/Roundcube/rcube_contacts.php b/program/lib/Roundcube/rcube_contacts.php index 6d01368a1..d215760cf 100644 --- a/program/lib/Roundcube/rcube_contacts.php +++ b/program/lib/Roundcube/rcube_contacts.php @@ -350,7 +350,7 @@ class rcube_contacts extends rcube_addressbook if (in_array($col, $this->table_cols)) { switch ($mode) { case 1: // strict - $where[] = '(' . $this->db->quoteIdentifier($col) . ' = ' . $this->db->quote($val) + $where[] = '(' . $this->db->quote_identifier($col) . ' = ' . $this->db->quote($val) . ' OR ' . $this->db->ilike($col, $val . $AS . '%') . ' OR ' . $this->db->ilike($col, '%' . $AS . $val . $AS . '%') . ' OR ' . $this->db->ilike($col, '%' . $AS . $val) . ')'; @@ -390,7 +390,7 @@ class rcube_contacts extends rcube_addressbook } foreach (array_intersect($required, $this->table_cols) as $col) { - $and_where[] = $this->db->quoteIdentifier($col).' <> '.$this->db->quote(''); + $and_where[] = $this->db->quote_identifier($col).' <> '.$this->db->quote(''); } if (!empty($where)) { @@ -592,8 +592,8 @@ class rcube_contacts extends rcube_addressbook // validate e-mail addresses $valid = parent::validate($save_data, $autofix); - // require at least one e-mail address (syntax check is already done) - if ($valid && !array_filter($this->get_col_values('email', $save_data, true))) { + // require at least one email address or a name + if ($valid && !strlen($save_data['firstname'].$save_data['surname'].$save_data['name']) && !array_filter($this->get_col_values('email', $save_data, true))) { $this->set_error(self::ERROR_VALIDATE, 'noemailwarning'); $valid = false; } @@ -626,11 +626,11 @@ class rcube_contacts extends rcube_addressbook } } - $save_data = $this->convert_save_data($save_data); + $save_data = $this->convert_save_data($save_data); $a_insert_cols = $a_insert_values = array(); foreach ($save_data as $col => $value) { - $a_insert_cols[] = $this->db->quoteIdentifier($col); + $a_insert_cols[] = $this->db->quote_identifier($col); $a_insert_values[] = $this->db->quote($value); } @@ -655,17 +655,18 @@ class rcube_contacts extends rcube_addressbook * * @param mixed Record identifier * @param array Assoziative array with save data + * * @return boolean True on success, False on error */ function update($id, $save_cols) { - $updated = false; + $updated = false; $write_sql = array(); - $record = $this->get_record($id, true); + $record = $this->get_record($id, true); $save_cols = $this->convert_save_data($save_cols, $record); foreach ($save_cols as $col => $value) { - $write_sql[] = sprintf("%s=%s", $this->db->quoteIdentifier($col), $this->db->quote($value)); + $write_sql[] = sprintf("%s=%s", $this->db->quote_identifier($col), $this->db->quote($value)); } if (!empty($write_sql)) { @@ -683,7 +684,7 @@ class rcube_contacts extends rcube_addressbook $this->result = null; // clear current result (from get_record()) } - return $updated; + return $updated ? true : false; } @@ -812,16 +813,30 @@ class rcube_contacts extends rcube_addressbook /** * Remove all records from the database + * + * @param bool $with_groups Remove also groups + * + * @return int Number of removed records */ - function delete_all() + function delete_all($with_groups = false) { $this->cache = null; - $this->db->query("UPDATE ".$this->db->table_name($this->db_name). - " SET del=1, changed=".$this->db->now(). - " WHERE user_id = ?", $this->user_id); + $this->db->query("UPDATE " . $this->db->table_name($this->db_name) + . " SET del = 1, changed = " . $this->db->now() + . " WHERE user_id = ?", $this->user_id); - return $this->db->affected_rows(); + $count = $this->db->affected_rows(); + + if ($with_groups) { + $this->db->query("UPDATE " . $this->db->table_name($this->db_groups) + . " SET del = 1, changed = " . $this->db->now() + . " WHERE user_id = ?", $this->user_id); + + $count += $this->db->affected_rows(); + } + + return $count; } @@ -860,11 +875,11 @@ class rcube_contacts extends rcube_addressbook function delete_group($gid) { // flag group record as deleted - $sql_result = $this->db->query( - "UPDATE ".$this->db->table_name($this->db_groups). - " SET del=1, changed=".$this->db->now(). - " WHERE contactgroup_id=?". - " AND user_id=?", + $this->db->query( + "UPDATE " . $this->db->table_name($this->db_groups) + . " SET del = 1, changed = " . $this->db->now() + . " WHERE contactgroup_id = ?" + . " AND user_id = ?", $gid, $this->user_id ); @@ -873,7 +888,6 @@ class rcube_contacts extends rcube_addressbook return $this->db->affected_rows(); } - /** * Rename a specific contact group * diff --git a/program/lib/Roundcube/rcube_csv2vcard.php b/program/lib/Roundcube/rcube_csv2vcard.php index 00e6d4e20..aa385dce4 100644 --- a/program/lib/Roundcube/rcube_csv2vcard.php +++ b/program/lib/Roundcube/rcube_csv2vcard.php @@ -47,7 +47,7 @@ class rcube_csv2vcard //'business_street_2' => '', //'business_street_3' => '', 'car_phone' => 'phone:car', - 'categories' => 'categories', + 'categories' => 'groups', //'children' => '', 'company' => 'organization', //'company_main_phone' => '', @@ -146,6 +146,9 @@ class rcube_csv2vcard 'work_title' => 'jobtitle', 'work_zip' => 'zipcode:work', 'group' => 'groups', + + // GMail + 'groups' => 'groups', ); /** @@ -427,6 +430,11 @@ class rcube_csv2vcard $contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d']; } + // categories/groups separator in vCard is ',' not ';' + if (!empty($contact['groups'])) { + $contact['groups'] = str_replace(';', ',', $contact['groups']); + } + // Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00" foreach (array('birthday', 'anniversary') as $key) { if (!empty($contact[$key])) { diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php index aaba28172..2828f26ee 100644 --- a/program/lib/Roundcube/rcube_db.php +++ b/program/lib/Roundcube/rcube_db.php @@ -392,7 +392,7 @@ class rcube_db */ protected function _query($query, $offset, $numrows, $params) { - $query = trim($query); + $query = ltrim($query); $this->db_connect($this->dsn_select($query), true); @@ -405,27 +405,28 @@ class rcube_db $query = $this->set_limit($query, $numrows, $offset); } - $params = (array) $params; - // Because in Roundcube we mostly use queries that are // executed only once, we will not use prepared queries $pos = 0; $idx = 0; - while ($pos = strpos($query, '?', $pos)) { - if ($query[$pos+1] == '?') { // skip escaped ? - $pos += 2; - } - else { - $val = $this->quote($params[$idx++]); - unset($params[$idx-1]); - $query = substr_replace($query, $val, $pos, 1); - $pos += strlen($val); + if (count($params)) { + while ($pos = strpos($query, '?', $pos)) { + if ($query[$pos+1] == '?') { // skip escaped '?' + $pos += 2; + } + else { + $val = $this->quote($params[$idx++]); + unset($params[$idx-1]); + $query = substr_replace($query, $val, $pos, 1); + $pos += strlen($val); + } } } - // replace escaped ? back to normal - $query = rtrim(strtr($query, array('??' => '?')), ';'); + // replace escaped '?' back to normal, see self::quote() + $query = str_replace('??', '?', $query); + $query = rtrim($query, " \t\n\r\0\x0B;"); $this->debug($query); diff --git a/program/lib/Roundcube/rcube_html2text.php b/program/lib/Roundcube/rcube_html2text.php index 6f79e2f8e..3b4508da9 100644 --- a/program/lib/Roundcube/rcube_html2text.php +++ b/program/lib/Roundcube/rcube_html2text.php @@ -608,7 +608,7 @@ class rcube_html2text $this->width = $p_width; // Add citation markers and create <pre> block - $body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_ballback'), trim($body)); + $body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_callback'), trim($body)); $body = '<pre>' . htmlspecialchars($body) . '</pre>'; $text = substr_replace($text, $body . "\n", $start, $end + 13 - $start); @@ -616,6 +616,10 @@ class rcube_html2text break; } + // abort on invalid tag structure (e.g. no closing tag found) + else { + break; + } } while ($end || $next); } @@ -624,7 +628,7 @@ class rcube_html2text /** * Callback function to correctly add citation markers for blockquote contents */ - public function blockquote_citation_ballback($m) + public function blockquote_citation_callback($m) { $line = ltrim($m[2]); $space = $line[0] == '>' ? '' : ' '; diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php index 9faf1bbc6..432227091 100644 --- a/program/lib/Roundcube/rcube_imap.php +++ b/program/lib/Roundcube/rcube_imap.php @@ -680,6 +680,41 @@ class rcube_imap extends rcube_storage /** + * Public method for listing message flags + * + * @param string $folder Folder name + * @param array $uids Message UIDs + * @param int $mod_seq Optional MODSEQ value (of last flag update) + * + * @return array Indexed array with message flags + */ + public function list_flags($folder, $uids, $mod_seq = null) + { + if (!strlen($folder)) { + $folder = $this->folder; + } + + if (!$this->check_connection()) { + return array(); + } + + // @TODO: when cache was synchronized in this request + // we might already have asked for flag updates, use it. + + $flags = $this->conn->fetch($folder, $uids, true, array('FLAGS'), $mod_seq); + $result = array(); + + if (!empty($flags)) { + foreach ($flags as $message) { + $result[$message->uid] = $message->flags; + } + } + + return $result; + } + + + /** * Public method for listing headers * * @param string $folder Folder name @@ -1409,7 +1444,7 @@ class rcube_imap extends rcube_storage public function search_once($folder = null, $str = 'ALL') { if (!$str) { - return 'ALL'; + $str = 'ALL'; } if (!strlen($folder)) { @@ -2121,7 +2156,7 @@ class rcube_imap extends rcube_storage // convert charset (if text or message part) if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) { // Remove NULL characters if any (#1486189) - if (strpos($body, "\x00") !== false) { + if ($formatted && strpos($body, "\x00") !== false) { $body = str_replace("\x00", '', $body); } @@ -2843,12 +2878,21 @@ class rcube_imap extends rcube_storage /** * Filter the given list of folders according to access rights + * + * For performance reasons we assume user has full rights + * on all personal folders. */ protected function filter_rights($a_folders, $rights) { $regex = '/('.$rights.')/'; + foreach ($a_folders as $idx => $folder) { + if ($this->folder_namespace($folder) == 'personal') { + continue; + } + $myrights = join('', (array)$this->my_rights($folder)); + if ($myrights !== null && !preg_match($regex, $myrights)) { unset($a_folders[$idx]); } @@ -3848,9 +3892,12 @@ class rcube_imap extends rcube_storage /** * Sort folders first by default folders and then in alphabethical order * - * @param array $a_folders Folders list + * @param array $a_folders Folders list + * @param bool $skip_default Skip default folders handling + * + * @return array Sorted list */ - protected function sort_folder_list($a_folders) + public function sort_folder_list($a_folders, $skip_default = false) { $a_out = $a_defaults = $folders = array(); @@ -3862,7 +3909,7 @@ class rcube_imap extends rcube_storage continue; } - if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) { + if (!$skip_default && ($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) { $a_defaults[$p] = $folder; } else { diff --git a/program/lib/Roundcube/rcube_imap_cache.php b/program/lib/Roundcube/rcube_imap_cache.php index a8166545e..0c3edeaad 100644 --- a/program/lib/Roundcube/rcube_imap_cache.php +++ b/program/lib/Roundcube/rcube_imap_cache.php @@ -1250,10 +1250,8 @@ class rcube_imap_cache unset($msg->replaces); - if (is_array($msg->structure->parts)) { - foreach ($msg->structure->parts as $part) { - $this->message_object_prepare($part, $size); - } + if (is_object($msg->structure)) { + $this->message_object_prepare($msg->structure, $size); } if (is_array($msg->parts)) { diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php index f9a62f010..9035840a8 100644 --- a/program/lib/Roundcube/rcube_imap_generic.php +++ b/program/lib/Roundcube/rcube_imap_generic.php @@ -73,6 +73,7 @@ class rcube_imap_generic const COMMAND_NORESPONSE = 1; const COMMAND_CAPABILITY = 2; const COMMAND_LASTLINE = 4; + const COMMAND_ANONYMIZED = 8; const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n @@ -88,16 +89,28 @@ class rcube_imap_generic * * @param string $string Command string * @param bool $endln True if CRLF need to be added at the end of command + * @param bool $anonymized Don't write the given data to log but a placeholder * * @param int Number of bytes sent, False on error */ - function putLine($string, $endln=true) + function putLine($string, $endln=true, $anonymized=false) { if (!$this->fp) return false; if ($this->_debug) { - $this->debug('C: '. rtrim($string)); + // anonymize the sent command for logging + $cut = $endln ? 2 : 0; + if ($anonymized && preg_match('/^(A\d+ (?:[A-Z]+ )+)(.+)/', $string, $m)) { + $log = $m[1] . sprintf('****** [%d]', strlen($m[2]) - $cut); + } + else if ($anonymized) { + $log = sprintf('****** [%d]', strlen($string) - $cut); + } + else { + $log = rtrim($string); + } + $this->debug('C: ' . $log); } $res = fwrite($this->fp, $string . ($endln ? "\r\n" : '')); @@ -116,10 +129,11 @@ class rcube_imap_generic * * @param string $string Command string * @param bool $endln True if CRLF need to be added at the end of command + * @param bool $anonymized Don't write the given data to log but a placeholder * * @return int|bool Number of bytes sent, False on error */ - function putLineC($string, $endln=true) + function putLineC($string, $endln=true, $anonymized=false) { if (!$this->fp) { return false; @@ -138,7 +152,7 @@ class rcube_imap_generic $parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]); } - $bytes = $this->putLine($parts[$i].$parts[$i+1], false); + $bytes = $this->putLine($parts[$i].$parts[$i+1], false, $anonymized); if ($bytes === false) return false; $res += $bytes; @@ -153,7 +167,7 @@ class rcube_imap_generic $i++; } else { - $bytes = $this->putLine($parts[$i], false); + $bytes = $this->putLine($parts[$i], false, $anonymized); if ($bytes === false) return false; $res += $bytes; @@ -519,7 +533,7 @@ class rcube_imap_generic $reply = base64_encode($user . ' ' . $hash); // send result - $this->putLine($reply); + $this->putLine($reply, true, true); } else { // RFC2831: DIGEST-MD5 @@ -537,7 +551,7 @@ class rcube_imap_generic base64_decode($challenge), $this->host, 'imap', $user)); // send result - $this->putLine($reply); + $this->putLine($reply, true, true); $line = trim($this->readReply()); if ($line[0] == '+') { @@ -577,7 +591,7 @@ class rcube_imap_generic // RFC 4959 (SASL-IR): save one round trip if ($this->getCapability('SASL-IR')) { list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply), - self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY); + self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED); } else { $this->putLine($this->nextTag() . " AUTHENTICATE PLAIN"); @@ -588,7 +602,7 @@ class rcube_imap_generic } // send result, get reply and process it - $this->putLine($reply); + $this->putLine($reply, true, true); $line = $this->readReply(); $result = $this->parseResult($line); } @@ -3419,7 +3433,7 @@ class rcube_imap_generic } // Send command - if (!$this->putLineC($query)) { + if (!$this->putLineC($query, true, ($options & self::COMMAND_ANONYMIZED))) { $this->setError(self::ERROR_COMMAND, "Unable to send command: $query"); return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, ''); } diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php index 64288f973..de3790e5c 100644 --- a/program/lib/Roundcube/rcube_ldap.php +++ b/program/lib/Roundcube/rcube_ldap.php @@ -52,7 +52,7 @@ class rcube_ldap extends rcube_addressbook * * @var array */ - private static $group_types = array( + private $group_types = array( 'group' => 'member', 'groupofnames' => 'member', 'kolabgroupofnames' => 'member', @@ -94,6 +94,9 @@ class rcube_ldap extends rcube_addressbook $this->prop['groups']['name_attr'] = 'cn'; if (empty($this->prop['groups']['scope'])) $this->prop['groups']['scope'] = 'sub'; + // extend group objectclass => member attribute mapping + if (!empty($this->prop['groups']['class_member_attr'])) + $this->group_types = array_merge($this->group_types, $this->prop['groups']['class_member_attr']); // add group name attrib to the list of attributes to be fetched $fetch_attributes[] = $this->prop['groups']['name_attr']; @@ -292,6 +295,14 @@ class rcube_ldap extends rcube_addressbook if ($this->prop['search_base_dn'] && $this->prop['search_filter'] && (strstr($bind_dn, '%dn') || strstr($this->base_dn, '%dn') || strstr($this->groups_base_dn, '%dn')) ) { + $search_attribs = array('uid'); + if ($search_bind_attrib = (array)$this->prop['search_bind_attrib']) { + foreach ($search_bind_attrib as $r => $attr) { + $search_attribs[] = $attr; + $replaces[$r] = ''; + } + } + $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); @@ -321,10 +332,18 @@ class rcube_ldap extends rcube_addressbook } } - $res = $ldap->search($search_base_dn, $search_filter, 'sub', array('uid')); + $res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs); if ($res) { $res->rewind(); $replaces['%dn'] = $res->get_dn(); + + // add more replacements from 'search_bind_attrib' config + if ($search_bind_attrib) { + $res = $res->current(); + foreach ($search_bind_attrib as $r => $attr) { + $replaces[$r] = $res[$attr][0]; + } + } } if ($ldap != $this->ldap) { @@ -355,6 +374,23 @@ class rcube_ldap extends rcube_addressbook $this->base_dn = strtr($this->base_dn, $replaces); $this->groups_base_dn = strtr($this->groups_base_dn, $replaces); + // replace placeholders in filter settings + if (!empty($this->prop['filter'])) + $this->prop['filter'] = strtr($this->prop['filter'], $replaces); + if (!empty($this->prop['groups']['filter'])) + $this->prop['groups']['filter'] = strtr($this->prop['groups']['filter'], $replaces); + if (!empty($this->prop['groups']['member_filter'])) + $this->prop['groups']['member_filter'] = strtr($this->prop['groups']['member_filter'], $replaces); + + if (!empty($this->prop['group_filters'])) { + foreach ($this->prop['group_filters'] as $i => $gf) { + if (!empty($gf['base_dn'])) + $this->prop['group_filters'][$i]['base_dn'] = strtr($gf['base_dn'], $replaces); + if (!empty($gf['filter'])) + $this->prop['group_filters'][$i]['filter'] = strtr($gf['filter'], $replaces); + } + } + if (empty($bind_user)) { $bind_user = $u; } @@ -518,7 +554,7 @@ class rcube_ldap extends rcube_addressbook } else { $prop = $this->group_id ? $this->group_data : $this->prop; - $base_dn = $this->group_id ? $this->group_base_dn : $this->base_dn; + $base_dn = $this->group_id ? $prop['base_dn'] : $this->base_dn; // use global search filter if (!empty($this->filter)) @@ -559,9 +595,10 @@ class rcube_ldap extends rcube_addressbook /** * Get all members of the given group * - * @param string Group DN - * @param array Group entries (if called recursively) - * @return array Accumulated group members + * @param string Group DN + * @param boolean Count only + * @param array Group entries (if called recursively) + * @return array Accumulated group members */ function list_group_members($dn, $count = false, $entries = null) { @@ -569,7 +606,7 @@ class rcube_ldap extends rcube_addressbook // fetch group object if (empty($entries)) { - $attribs = array('dn','objectClass','member','uniqueMember','memberURL'); + $attribs = array_merge(array('dn','objectClass','memberURL'), array_values($this->group_types)); $entries = $this->ldap->read_entries($dn, '(objectClass=*)', $attribs); if ($entries === false) { return $group_members; @@ -581,17 +618,17 @@ class rcube_ldap extends rcube_addressbook $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), '')) + 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; } + else if (!empty($entry['memberurl'])) { + $members = $this->_list_group_memberurl($dn, $entry, $count); + $group_members = array_merge($group_members, $members); + } if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit']) { break 2; @@ -608,6 +645,7 @@ class rcube_ldap extends rcube_addressbook * @param string Group DN * @param array Group entry * @param string Member attribute to use + * @param boolean Count only * @return array Accumulated group members */ private function _list_group_members($dn, $entry, $attr, $count) @@ -621,8 +659,7 @@ class rcube_ldap extends rcube_addressbook // read these attributes for all members $attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes']; - $attrib[] = 'member'; - $attrib[] = 'uniqueMember'; + $attrib = array_merge($attrib, array_values($this->group_types)); $attrib[] = 'memberURL'; $filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)'; @@ -669,7 +706,7 @@ class rcube_ldap extends rcube_addressbook 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))) + if ($this->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]; @@ -1287,8 +1324,10 @@ class rcube_ldap extends rcube_addressbook /** * Remove all contact records + * + * @param bool $with_groups Delete also groups if enabled */ - function delete_all() + function delete_all($with_groups = false) { // searching for contact entries $dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)'); @@ -1299,6 +1338,16 @@ class rcube_ldap extends rcube_addressbook } $this->delete($dn_list); } + + if ($with_groups && $this->groups && ($groups = $this->_fetch_groups()) && count($groups)) { + foreach ($groups as $group) { + $this->ldap->delete($group['dn']); + } + + if ($this->cache) { + $this->cache->remove('groups'); + } + } } /** @@ -1354,7 +1403,7 @@ class rcube_ldap extends rcube_addressbook $out[$this->primary_key] = self::dn_encode($rec['dn']); // determine record type - if (self::is_group_entry($rec)) { + if ($this->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']; @@ -1479,11 +1528,11 @@ class rcube_ldap extends rcube_addressbook /** * Determines whether the given LDAP entry is a group record */ - private static function is_group_entry($entry) + private function is_group_entry($entry) { $classes = array_map('strtolower', (array)$entry['objectclass']); - return count(array_intersect(array_keys(self::$group_types), $classes)) > 0; + return count(array_intersect(array_keys($this->group_types), $classes)) > 0; } /** @@ -1569,11 +1618,12 @@ class rcube_ldap extends rcube_addressbook // special case: list groups from 'group_filters' config if ($vlv_page === null && !empty($this->prop['group_filters'])) { $groups = array(); + $rcube = rcube::get_instance(); // 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']; + $groups[$id] = array('ID' => $id, 'name' => $rcube->gettext('groups'), 'virtual' => true) + $this->prop['groups']; } foreach ($this->prop['group_filters'] as $id => $prop) { @@ -1914,7 +1964,7 @@ class rcube_ldap extends rcube_addressbook if (!empty($object_classes)) { foreach ((array)$object_classes as $oc) { - if ($attr = self::$group_types[strtolower($oc)]) { + if ($attr = $this->group_types[strtolower($oc)]) { return $attr; } } diff --git a/program/lib/Roundcube/rcube_ldap_generic.php b/program/lib/Roundcube/rcube_ldap_generic.php index 923a12a41..b85afe4ce 100644 --- a/program/lib/Roundcube/rcube_ldap_generic.php +++ b/program/lib/Roundcube/rcube_ldap_generic.php @@ -240,7 +240,7 @@ class rcube_ldap_generic $method = 'DIGEST-MD5'; } - $this->_debug("C: SASL Bind [mech: $method, authc: $authc, authz: $authz, pass: $pass]"); + $this->_debug("C: SASL Bind [mech: $method, authc: $authc, authz: $authz, pass: **** [" . strlen($pass) . "]"); if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) { $this->_debug("S: OK"); @@ -271,7 +271,7 @@ class rcube_ldap_generic return false; } - $this->_debug("C: Bind $dn [pass: $pass]"); + $this->_debug("C: Bind $dn, pass: **** [" . strlen($pass) . "]"); if (@ldap_bind($this->conn, $dn, $pass)) { $this->_debug("S: OK"); diff --git a/program/lib/Roundcube/rcube_message.php b/program/lib/Roundcube/rcube_message.php index 9b662a286..f24ec3ed8 100644 --- a/program/lib/Roundcube/rcube_message.php +++ b/program/lib/Roundcube/rcube_message.php @@ -211,16 +211,19 @@ class rcube_message } $level = explode('.', $part->mime_id); + $depth = count($level); // Check if the part belongs to higher-level's multipart part - // this can be alternative/related/signed/encrypted, but not mixed + // this can be alternative/related/signed/encrypted or mixed while (array_pop($level) !== null) { - if (!count($level)) { + $parent_depth = count($level); + if (!$parent_depth) { return true; } $parent = $this->mime_parts[join('.', $level)]; - if (!preg_match('/^multipart\/(alternative|related|signed|encrypted)$/', $parent->mimetype)) { + if (!preg_match('/^multipart\/(alternative|related|signed|encrypted|mixed)$/', $parent->mimetype) + || ($parent->mimetype == 'multipart/mixed' && $parent_depth < $depth - 1)) { continue 2; } } @@ -529,8 +532,9 @@ class rcube_message $part_mimetype = $mail_part->real_mimetype; list($primary_type, $secondary_type) = explode('/', $part_mimetype); } - else - $part_mimetype = $mail_part->mimetype; + else { + $part_mimetype = $part_orig_mimetype = $mail_part->mimetype; + } // multipart/alternative if ($primary_type == 'multipart') { diff --git a/program/lib/Roundcube/rcube_mime.php b/program/lib/Roundcube/rcube_mime.php index 9c2220328..55b70f67c 100644 --- a/program/lib/Roundcube/rcube_mime.php +++ b/program/lib/Roundcube/rcube_mime.php @@ -378,6 +378,10 @@ class rcube_mime } if ($decode) { $name = self::decode_header($name, $fallback); + // some clients encode addressee name with quotes around it + if ($name[0] == '"' && $name[strlen($name)-1] == '"') { + $name = substr($name, 1, -1); + } } } @@ -810,7 +814,7 @@ class rcube_mime } $mime_types = $mime_extensions = array(); - $regex = "/([\w\+\-\.\/]+)\t+([\w\s]+)/i"; + $regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i"; foreach((array)$lines as $line) { // skip comments or mime types w/o any extensions if ($line[0] == '#' || !preg_match($regex, $line, $matches)) diff --git a/program/lib/Roundcube/rcube_plugin.php b/program/lib/Roundcube/rcube_plugin.php index 3153a8410..f0af95332 100644 --- a/program/lib/Roundcube/rcube_plugin.php +++ b/program/lib/Roundcube/rcube_plugin.php @@ -109,7 +109,7 @@ abstract class rcube_plugin */ public function require_plugin($plugin_name) { - return $this->api->load_plugin($plugin_name); + return $this->api->load_plugin($plugin_name, true); } /** @@ -125,13 +125,17 @@ abstract class rcube_plugin $fpath = $this->home.'/'.$fname; $rcube = rcube::get_instance(); - if (is_file($fpath) && !$rcube->config->load_from_file($fpath)) { + if (($is_local = is_file($fpath)) && !$rcube->config->load_from_file($fpath)) { rcube::raise_error(array( 'code' => 527, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to load config from $fpath"), true, false); return false; } + else if (!$is_local) { + // Search plugin_name.inc.php file in any configured path + return $rcube->config->load_from_file($this->ID . '.inc.php'); + } return true; } diff --git a/program/lib/Roundcube/rcube_plugin_api.php b/program/lib/Roundcube/rcube_plugin_api.php index 5a25ada02..461c3cc07 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(); @@ -167,10 +168,11 @@ class rcube_plugin_api * Load the specified plugin * * @param string Plugin name + * @param boolean Force loading of the plugin even if it doesn't match the filter * * @return boolean True on success, false if not loaded or failure */ - public function load_plugin($plugin_name) + public function load_plugin($plugin_name, $force = false) { static $plugins_dir; @@ -196,7 +198,7 @@ class rcube_plugin_api // check inheritance... if (is_subclass_of($plugin, 'rcube_plugin')) { // ... task, request type and framed mode - if ((!$plugin->task || preg_match('/^('.$plugin->task.')$/i', $this->task)) + if (($force || !$plugin->task || preg_match('/^('.$plugin->task.')$/i', $this->task)) && (!$plugin->noajax || (is_object($this->output) && $this->output->type == 'html')) && (!$plugin->noframe || empty($_REQUEST['_framed'])) ) { @@ -282,6 +284,7 @@ class rcube_plugin_api $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['version'] = $json['version']; $info['license'] = $json['license']; if ($license_uri = $license_uris[$info['license']]) $info['license_uri'] = $license_uri; diff --git a/program/lib/Roundcube/rcube_result_index.php b/program/lib/Roundcube/rcube_result_index.php index 5f592c54f..058f25c6f 100644 --- a/program/lib/Roundcube/rcube_result_index.php +++ b/program/lib/Roundcube/rcube_result_index.php @@ -231,29 +231,13 @@ class rcube_result_index /** - * Filters data set. Removes elements listed in $ids list. + * Filters data set. Removes elements not listed in $ids list. * * @param array $ids List of IDs to remove. */ public function filter($ids = array()) { $data = $this->get(); - $data = array_diff($data, $ids); - - $this->meta = array(); - $this->meta['count'] = count($data); - $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); - } - - - /** - * Filters data set. Removes elements not listed in $ids list. - * - * @param array $ids List of IDs to keep. - */ - public function intersect($ids = array()) - { - $data = $this->get(); $data = array_intersect($data, $ids); $this->meta = array(); @@ -332,6 +316,7 @@ class rcube_result_index if (empty($this->raw_data)) { return array(); } + return explode(self::SEPARATOR_ELEMENT, $this->raw_data); } diff --git a/program/lib/Roundcube/rcube_result_thread.php b/program/lib/Roundcube/rcube_result_thread.php index 7657550be..ceaaf59a6 100644 --- a/program/lib/Roundcube/rcube_result_thread.php +++ b/program/lib/Roundcube/rcube_result_thread.php @@ -453,7 +453,7 @@ class rcube_result_thread // when sorting search result it's good to make the index smaller if ($index->count() != $this->count_messages()) { - $index->intersect($this->get()); + $index->filter($this->get()); } $result = array_fill_keys($index->get(), null); @@ -606,33 +606,39 @@ class rcube_result_thread // arrays handling is much more expensive // For the following structure: THREAD (2)(3 6 (4 23)(44 7 96)) // -- 2 - // // -- 3 // \-- 6 // |-- 4 // | \-- 23 // | // \-- 44 - // \-- 7 - // \-- 96 + // \-- 7 + // \-- 96 // // The output will be: 2,3^1:6^2:4^3:23^2:44^3:7^4:96 if ($str[$begin] != '(') { - $stop = $begin + strspn($str, '1234567890', $begin, $end - $begin); - $msg = substr($str, $begin, $stop - $begin); - if (!$msg) { + // find next bracket + $stop = $begin + strcspn($str, '()', $begin, $end - $begin); + $messages = explode(' ', trim(substr($str, $begin, $stop - $begin))); + + if (empty($messages)) { return $node; } - $this->meta['messages']++; - - $node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg; + foreach ($messages as $msg) { + if ($msg) { + $node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg; + $this->meta['messages']++; + $depth++; + } + } - if ($stop + 1 < $end) { - $node .= $this->parse_thread($str, $stop + 1, $end, $depth + 1); + if ($stop < $end) { + $node .= $this->parse_thread($str, $stop, $end, $depth); } - } else { + } + else { $off = $begin; while ($off < $end) { $start = $off; @@ -649,7 +655,8 @@ class rcube_result_thread if ($p1 !== false && $p1 < $p) { $off = $p1 + 1; $n++; - } else { + } + else { $off = $p + 1; $n--; } diff --git a/program/lib/Roundcube/rcube_session.php b/program/lib/Roundcube/rcube_session.php index 67072df41..caca262c6 100644 --- a/program/lib/Roundcube/rcube_session.php +++ b/program/lib/Roundcube/rcube_session.php @@ -34,6 +34,7 @@ class rcube_session private $changed; private $time_diff = 0; private $reloaded = false; + private $appends = array(); private $unsets = array(); private $gc_handlers = array(); private $cookiename = 'roundcube_sessauth'; @@ -441,8 +442,19 @@ class rcube_session $node = &$this->get_node(explode('.', $path), $_SESSION); - if ($key !== null) $node[$key] = $value; - else $node[] = $value; + if ($key !== null) { + $node[$key] = $value; + $path .= '.' . $key; + } + else { + $node[] = $value; + } + + $this->appends[] = $path; + + // when overwriting a previously unset variable + if ($this->unsets[$path]) + unset($this->unsets[$path]); } @@ -491,13 +503,40 @@ class rcube_session */ public function reload() { + // collect updated data from previous appends + $merge_data = array(); + foreach ((array)$this->appends as $var) { + $path = explode('.', $var); + $value = $this->get_node($path, $_SESSION); + $k = array_pop($path); + $node = &$this->get_node($path, $merge_data); + $node[$k] = $value; + } + if ($this->key && $this->memcache) $data = $this->mc_read($this->key); else if ($this->key) $data = $this->db_read($this->key); - if ($data) + if ($data) { session_decode($data); + + // apply appends and unsets to reloaded data + $_SESSION = array_merge_recursive($_SESSION, $merge_data); + + foreach ((array)$this->unsets as $var) { + if (isset($_SESSION[$var])) { + unset($_SESSION[$var]); + } + else { + $path = explode('.', $var); + $k = array_pop($path); + $node = &$this->get_node($path, $_SESSION); + unset($node[$k]); + } + } + } + } /** diff --git a/program/lib/Roundcube/rcube_smtp.php b/program/lib/Roundcube/rcube_smtp.php index 60b1389ea..70f15dc7b 100644 --- a/program/lib/Roundcube/rcube_smtp.php +++ b/program/lib/Roundcube/rcube_smtp.php @@ -29,6 +29,7 @@ class rcube_smtp private $conn = null; private $response; private $error; + private $anonymize_log = 0; // define headers delimiter const SMTP_MIME_CRLF = "\r\n"; @@ -67,6 +68,7 @@ class rcube_smtp 'smtp_auth_type' => $rcube->config->get('smtp_auth_type'), 'smtp_helo_host' => $rcube->config->get('smtp_helo_host'), 'smtp_timeout' => $rcube->config->get('smtp_timeout'), + 'smtp_conn_options' => $rcube->config->get('smtp_conn_options'), 'smtp_auth_callbacks' => array(), )); @@ -106,10 +108,11 @@ class rcube_smtp // IDNA Support $smtp_host = rcube_utils::idn_to_ascii($smtp_host); - $this->conn = new Net_SMTP($smtp_host, $smtp_port, $helo_host); + $this->conn = new Net_SMTP($smtp_host, $smtp_port, $helo_host, false, 0, $CONFIG['smtp_conn_options']); if ($rcube->config->get('smtp_debug')) { $this->conn->setDebug(true, array($this, 'debug_handler')); + $this->anonymize_log = 0; } // register authentication methods @@ -329,6 +332,15 @@ class rcube_smtp */ public function debug_handler(&$smtp, $message) { + // catch AUTH commands and set anonymization flag for subsequent sends + if (preg_match('/^Send: AUTH ([A-Z]+)/', $message, $m)) { + $this->anonymize_log = $m[1] == 'LOGIN' ? 2 : 1; + } + // anonymize this log entry + else if ($this->anonymize_log > 0 && strpos($message, 'Send:') === 0 && --$this->anonymize_log == 0) { + $message = sprintf('Send: ****** [%d]', strlen($message) - 8); + } + if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) { $diff = $len - self::DEBUG_LINE_LENGTH; $message = substr($message, 0, self::DEBUG_LINE_LENGTH) diff --git a/program/lib/Roundcube/rcube_spellcheck_atd.php b/program/lib/Roundcube/rcube_spellcheck_atd.php index 68e8b7cb8..9f073f56f 100644 --- a/program/lib/Roundcube/rcube_spellcheck_atd.php +++ b/program/lib/Roundcube/rcube_spellcheck_atd.php @@ -39,6 +39,18 @@ class rcube_spellcheck_atd extends rcube_spellcheck_engine ); /** + * 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() diff --git a/program/lib/Roundcube/rcube_spellcheck_enchant.php b/program/lib/Roundcube/rcube_spellcheck_enchant.php index a22251e00..14d6fff46 100644 --- a/program/lib/Roundcube/rcube_spellcheck_enchant.php +++ b/program/lib/Roundcube/rcube_spellcheck_enchant.php @@ -31,6 +31,24 @@ class rcube_spellcheck_enchant extends rcube_spellcheck_engine 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() diff --git a/program/lib/Roundcube/rcube_spellcheck_engine.php b/program/lib/Roundcube/rcube_spellcheck_engine.php index 88e10ac05..3cb4ca3de 100644 --- a/program/lib/Roundcube/rcube_spellcheck_engine.php +++ b/program/lib/Roundcube/rcube_spellcheck_engine.php @@ -43,6 +43,13 @@ abstract class rcube_spellcheck_engine } /** + * 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 diff --git a/program/lib/Roundcube/rcube_spellcheck_googie.php b/program/lib/Roundcube/rcube_spellcheck_googie.php index 70507dc23..3777942a6 100644 --- a/program/lib/Roundcube/rcube_spellcheck_googie.php +++ b/program/lib/Roundcube/rcube_spellcheck_googie.php @@ -26,13 +26,28 @@ */ class rcube_spellcheck_googie extends rcube_spellcheck_engine { - const GOOGLE_HOST = 'ssl://www.google.com'; - const GOOGLE_PORT = 443; + 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() @@ -52,25 +67,25 @@ class rcube_spellcheck_googie extends rcube_spellcheck_engine $path = $a_uri['path'] . ($a_uri['query'] ? '?'.$a_uri['query'] : '') . $this->lang; } else { - $host = self::GOOGLE_HOST; - $port = self::GOOGLE_PORT; + $host = self::GOOGIE_HOST; + $port = self::GOOGIE_PORT; $path = '/tbproxy/spell?lang=' . $this->lang; } - // Google has some problem with spaces, use \n instead - $gtext = str_replace(' ', "\n", $text); + $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>' . $gtext . '</text>' + .'<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: application/x-www-form-urlencoded\r\n"; + $out .= "Content-Type: text/xml\r\n"; $out .= "Connection: Close\r\n\r\n"; $out .= $gtext; fwrite($fp, $out); @@ -83,8 +98,10 @@ class rcube_spellcheck_googie extends rcube_spellcheck_engine // parse HTTP response if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $store, $m)) { $http_status = $m[1]; - if ($http_status != '200') + if ($http_status != '200') { $this->error = 'HTTP ' . $m[1] . $m[2]; + $this->error .= "\n" . $store; + } } if (!$store) { @@ -92,6 +109,7 @@ class rcube_spellcheck_googie extends rcube_spellcheck_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); diff --git a/program/lib/Roundcube/rcube_spellcheck_pspell.php b/program/lib/Roundcube/rcube_spellcheck_pspell.php index ce089ed8f..b12684e43 100644 --- a/program/lib/Roundcube/rcube_spellcheck_pspell.php +++ b/program/lib/Roundcube/rcube_spellcheck_pspell.php @@ -30,6 +30,35 @@ class rcube_spellcheck_pspell extends rcube_spellcheck_engine 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() diff --git a/program/lib/Roundcube/rcube_spellchecker.php b/program/lib/Roundcube/rcube_spellchecker.php index 31835dbb5..5b77bda02 100644 --- a/program/lib/Roundcube/rcube_spellchecker.php +++ b/program/lib/Roundcube/rcube_spellchecker.php @@ -65,6 +65,52 @@ class rcube_spellchecker } } + /** + * Return a list of supported languages + */ + function languages() + { + // trust configuration + $configured = $this->rc->config->get('spellcheck_languages'); + if (!empty($configured) && is_array($configured) && !$configured[0]) { + return $configured; + } + else if (!empty($configured)) { + $langs = (array)$configured; + } + else if ($this->backend) { + $langs = $this->backend->languages(); + } + + // load index + @include(RCUBE_LOCALIZATION_DIR . 'index.inc'); + + // add correct labels + $languages = array(); + foreach ($langs as $lang) { + $langc = strtolower(substr($lang, 0, 2)); + $alias = $rcube_language_aliases[$langc]; + if (!$alias) { + $alias = $langc.'_'.strtoupper($langc); + } + if ($rcube_languages[$lang]) { + $languages[$lang] = $rcube_languages[$lang]; + } + else if ($rcube_languages[$alias]) { + $languages[$lang] = $rcube_languages[$alias]; + } + else { + $languages[$lang] = ucfirst($lang); + } + } + + // remove possible duplicates (#1489395) + $languages = array_unique($languages); + + asort($languages); + + return $languages; + } /** * Set content and check spelling @@ -152,7 +198,7 @@ class rcube_spellchecker // send output $out = '<?xml version="1.0" encoding="'.RCUBE_CHARSET.'"?><spellresult charschecked="'.mb_strlen($this->content).'">'; - foreach ($this->matches as $item) { + foreach ((array)$this->matches as $item) { $out .= '<c o="'.$item[1].'" l="'.$item[2].'">'; $out .= is_array($item[4]) ? implode("\t", $item[4]) : $item[4]; $out .= '</c>'; @@ -173,7 +219,7 @@ class rcube_spellchecker { $result = array(); - foreach ($this->matches as $item) { + foreach ((array)$this->matches as $item) { if ($this->engine == 'pspell') { $word = $item[0]; } @@ -306,7 +352,7 @@ class rcube_spellchecker "UPDATE ".$this->rc->db->table_name('dictionary') ." SET data = ?" ." WHERE user_id " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL") - ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?", + ." AND " . $this->rc->db->quote_identifier('language') . " = ?", implode(' ', $plugin['dictionary']), $plugin['language']); } // don't store empty dict @@ -314,14 +360,14 @@ class rcube_spellchecker $this->rc->db->query( "DELETE FROM " . $this->rc->db->table_name('dictionary') ." WHERE user_id " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL") - ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?", + ." AND " . $this->rc->db->quote_identifier('language') . " = ?", $plugin['language']); } } else if (!empty($this->dict)) { $this->rc->db->query( "INSERT INTO " .$this->rc->db->table_name('dictionary') - ." (user_id, " . $this->rc->db->quoteIdentifier('language') . ", data) VALUES (?, ?, ?)", + ." (user_id, " . $this->rc->db->quote_identifier('language') . ", data) VALUES (?, ?, ?)", $plugin['userid'], $plugin['language'], implode(' ', $plugin['dictionary'])); } } @@ -348,7 +394,7 @@ class rcube_spellchecker $sql_result = $this->rc->db->query( "SELECT data FROM ".$this->rc->db->table_name('dictionary') ." WHERE user_id ". ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL") - ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?", + ." AND " . $this->rc->db->quote_identifier('language') . " = ?", $plugin['language']); if ($sql_arr = $this->rc->db->fetch_assoc($sql_result)) { diff --git a/program/lib/Roundcube/rcube_storage.php b/program/lib/Roundcube/rcube_storage.php index e697b2c73..ca65af1cb 100644 --- a/program/lib/Roundcube/rcube_storage.php +++ b/program/lib/Roundcube/rcube_storage.php @@ -360,6 +360,18 @@ abstract class rcube_storage /** + * Public method for listing message flags + * + * @param string $folder Folder name + * @param array $uids Message UIDs + * @param int $mod_seq Optional MODSEQ value + * + * @return array Indexed array with message flags + */ + abstract function list_flags($folder, $uids, $mod_seq = null); + + + /** * Public method for listing headers. * * @param string $folder Folder name diff --git a/program/lib/Roundcube/rcube_user.php b/program/lib/Roundcube/rcube_user.php index 5e9c9af80..e232736c9 100644 --- a/program/lib/Roundcube/rcube_user.php +++ b/program/lib/Roundcube/rcube_user.php @@ -125,8 +125,10 @@ class rcube_user */ function get_prefs() { + $prefs = array(); + if (!empty($this->language)) - $prefs = array('language' => $this->language); + $prefs['language'] = $this->language; if ($this->ID) { // Preferences from session (write-master is unavailable) @@ -163,8 +165,16 @@ class rcube_user if (!$this->ID) return false; - $config = $this->rc->config; - $old_prefs = (array)$this->get_prefs(); + $plugin = $this->rc->plugins->exec_hook('preferences_update', array( + 'userid' => $this->ID, 'prefs' => $a_user_prefs, 'old' => (array)$this->get_prefs())); + + if (!empty($plugin['abort'])) { + return; + } + + $a_user_prefs = $plugin['prefs']; + $old_prefs = $plugin['old']; + $config = $this->rc->config; // merge (partial) prefs array with existing settings $save_prefs = $a_user_prefs + $old_prefs; @@ -213,6 +223,14 @@ class rcube_user return false; } + /** + * Generate a unique hash to identify this user which + */ + function get_hash() + { + $key = substr($this->rc->config->get('des_key'), 1, 4); + return md5($this->data['user_id'] . $key . $this->data['username'] . '@' . $this->data['mail_host']); + } /** * Get default identity of this user @@ -249,7 +267,7 @@ class rcube_user "SELECT * FROM ".$this->db->table_name('identities'). " WHERE del <> 1 AND user_id = ?". ($sql_add ? " ".$sql_add : ""). - " ORDER BY ".$this->db->quoteIdentifier('standard')." DESC, name ASC, identity_id ASC", + " ORDER BY ".$this->db->quote_identifier('standard')." DESC, name ASC, identity_id ASC", $this->ID); while ($sql_arr = $this->db->fetch_assoc($sql_result)) { @@ -284,7 +302,7 @@ class rcube_user $query_cols = $query_params = array(); foreach ((array)$data as $col => $value) { - $query_cols[] = $this->db->quoteIdentifier($col) . ' = ?'; + $query_cols[] = $this->db->quote_identifier($col) . ' = ?'; $query_params[] = $value; } $query_params[] = $iid; @@ -320,7 +338,7 @@ class rcube_user $insert_cols = $insert_values = array(); foreach ((array)$data as $col => $value) { - $insert_cols[] = $this->db->quoteIdentifier($col); + $insert_cols[] = $this->db->quote_identifier($col); $insert_values[] = $value; } $insert_cols[] = 'user_id'; @@ -385,7 +403,7 @@ class rcube_user if ($this->ID && $iid) { $this->db->query( "UPDATE ".$this->db->table_name('identities'). - " SET ".$this->db->quoteIdentifier('standard')." = '0'". + " SET ".$this->db->quote_identifier('standard')." = '0'". " WHERE user_id = ?". " AND identity_id <> ?". " AND del <> 1", @@ -625,11 +643,11 @@ class rcube_user $result = array(); $sql_result = $this->db->query( - "SELECT search_id AS id, ".$this->db->quoteIdentifier('name') + "SELECT search_id AS id, ".$this->db->quote_identifier('name') ." FROM ".$this->db->table_name('searches') ." WHERE user_id = ?" - ." AND ".$this->db->quoteIdentifier('type')." = ?" - ." ORDER BY ".$this->db->quoteIdentifier('name'), + ." AND ".$this->db->quote_identifier('type')." = ?" + ." ORDER BY ".$this->db->quote_identifier('name'), (int) $this->ID, (int) $type); while ($sql_arr = $this->db->fetch_assoc($sql_result)) { @@ -657,9 +675,9 @@ class rcube_user } $sql_result = $this->db->query( - "SELECT ".$this->db->quoteIdentifier('name') - .", ".$this->db->quoteIdentifier('data') - .", ".$this->db->quoteIdentifier('type') + "SELECT ".$this->db->quote_identifier('name') + .", ".$this->db->quote_identifier('data') + .", ".$this->db->quote_identifier('type') ." FROM ".$this->db->table_name('searches') ." WHERE user_id = ?" ." AND search_id = ?", @@ -714,11 +732,11 @@ class rcube_user $insert_cols[] = 'user_id'; $insert_values[] = (int) $this->ID; - $insert_cols[] = $this->db->quoteIdentifier('type'); + $insert_cols[] = $this->db->quote_identifier('type'); $insert_values[] = (int) $data['type']; - $insert_cols[] = $this->db->quoteIdentifier('name'); + $insert_cols[] = $this->db->quote_identifier('name'); $insert_values[] = $data['name']; - $insert_cols[] = $this->db->quoteIdentifier('data'); + $insert_cols[] = $this->db->quote_identifier('data'); $insert_values[] = serialize($data['data']); $sql = "INSERT INTO ".$this->db->table_name('searches') diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php index b73bc0812..46d53ac91 100644 --- a/program/lib/Roundcube/rcube_utils.php +++ b/program/lib/Roundcube/rcube_utils.php @@ -454,6 +454,9 @@ class rcube_utils // cut out all contents between { and } while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) { + $nested = strpos($source, '{', $pos+1); + if ($nested && $nested < $pos2) // when dealing with nested blocks (e.g. @media), take the inner one + $pos = $nested; $length = $pos2 - $pos - 1; $styles = substr($source, $pos+1, $length); @@ -619,6 +622,10 @@ class rcube_utils */ public static function parse_host($name, $host = '') { + if (!is_string($name)) { + return $name; + } + // %n - host $n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']); // %t - host name without first part, e.g. %n=mail.domain.tld, %t=domain.tld @@ -639,8 +646,7 @@ class rcube_utils } } - $name = str_replace(array('%n', '%t', '%d', '%h', '%z', '%s'), array($n, $t, $d, $h, $z, $s[2]), $name); - return $name; + return str_replace(array('%n', '%t', '%d', '%h', '%z', '%s'), array($n, $t, $d, $h, $z, $s[2]), $name); } @@ -677,9 +683,17 @@ class rcube_utils */ 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]; + if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2); + return $hosts[0]; + } + + if (!empty($_SERVER['HTTP_X_REAL_IP'])) { + return $_SERVER['HTTP_X_REAL_IP']; + } + + if (!empty($_SERVER['REMOTE_ADDR'])) { + return $_SERVER['REMOTE_ADDR']; } return ''; @@ -744,40 +758,13 @@ class rcube_utils */ public static function strtotime($date) { - $date = trim($date); - - // check for MS Outlook vCard date format YYYYMMDD - if (preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) { - return mktime(0,0,0, intval($m[2]), intval($m[3]), intval($m[1])); - } - - // common little-endian formats, e.g. dd/mm/yyyy (not all are supported by strtotime) - if (preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})$/', $date, $m) - && $m[1] > 0 && $m[1] <= 31 && $m[2] > 0 && $m[2] <= 12 && $m[3] >= 1970 - ) { - return mktime(0,0,0, intval($m[2]), intval($m[1]), intval($m[3])); - } + $date = self::clean_datestr($date); // unix timestamp if (is_numeric($date)) { return (int) $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); - // 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)) { @@ -805,8 +792,8 @@ class rcube_utils return $date; } - $dt = false; - $date = trim($date); + $dt = false; + $date = self::clean_datestr($date); // try to parse string with DateTime first if (!empty($date)) { @@ -831,6 +818,52 @@ class rcube_utils return $dt; } + /** + * Clean up date string for strtotime() input + * + * @param string $date Date string + * + * @return string Date string + */ + public static function clean_datestr($date) + { + $date = trim($date); + + // check for MS Outlook vCard date format YYYYMMDD + if (preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) { + return sprintf('%04d-%02d-%02d 00:00:00', intval($m[1]), intval($m[2]), intval($m[3])); + } + + // 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); + + // try to fix dd/mm vs. mm/dd discrepancy, we can't do more here + if (preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})$/', $date, $m)) { + $mdy = $m[2] > 12 && $m[1] <= 12; + $day = $mdy ? $m[2] : $m[1]; + $month = $mdy ? $m[1] : $m[2]; + $date = sprintf('%04d-%02d-%02d 00:00:00', intval($m[3]), $month, $day); + } + // I've found that YYYY.MM.DD is recognized wrong, so here's a fix + else if (preg_match('/^(\d{4})\.(\d{1,2})\.(\d{1,2})$/', $date)) { + $date = str_replace('.', '-', $date) . ' 00:00:00'; + } + + return $date; + } + /* * Idn_to_ascii wrapper. * Intl/Idn modules version of this function doesn't work with e-mail address @@ -890,10 +923,20 @@ class rcube_utils * * @param string Input string (UTF-8) * @param boolean True to return list of words as array + * * @return mixed Normalized string or a list of normalized tokens */ public static function normalize_string($str, $as_array = false) { + // replace 4-byte unicode characters with '?' character, + // these are not supported in default utf-8 charset on mysql, + // the chance we'd need them in searching is very low + $str = preg_replace('/(' + . '\xF0[\x90-\xBF][\x80-\xBF]{2}' + . '|[\xF1-\xF3][\x80-\xBF]{3}' + . '|\xF4[\x80-\x8F][\x80-\xBF]{2}' + . ')/', '?', $str); + // split by words $arr = self::tokenize_string($str); @@ -1002,4 +1045,16 @@ class rcube_utils return !in_array($str, array('false', '0', 'no', 'off', 'nein', ''), true); } + /** + * OS-dependent absolute path detection + */ + public static function is_absolute_path($path) + { + if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { + return (bool) preg_match('!^[a-z]:[\\\\/]!i', $path); + } + else { + return $path[0] == DIRECTORY_SEPARATOR; + } + } } diff --git a/program/lib/Roundcube/rcube_vcard.php b/program/lib/Roundcube/rcube_vcard.php index d54dc56ad..a54ee7e11 100644 --- a/program/lib/Roundcube/rcube_vcard.php +++ b/program/lib/Roundcube/rcube_vcard.php @@ -378,7 +378,7 @@ class rcube_vcard default: if ($field == 'phone' && $this->phonetypemap[$type_uc]) { $type = $this->phonetypemap[$type_uc]; - } + } if (($tag = self::$fieldmap[$field]) && (is_array($value) || strlen($value))) { $index = count($this->raw[$tag]); @@ -518,20 +518,28 @@ class rcube_vcard */ public static function cleanup($vcard) { - // Convert special types (like Skype) to normal type='skype' classes with this simple regex ;) - $vcard = preg_replace( - '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s', - '\2;type=\5\3:\4', - $vcard); - // convert Apple X-ABRELATEDNAMES into X-* fields for better compatibility $vcard = preg_replace_callback( '/item(\d+)\.(X-ABRELATEDNAMES)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s', array('self', 'x_abrelatednames_callback'), $vcard); - // Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines - $vcard = preg_replace(array('/^item\d*\.X-AB.*$/m', '/^item\d*\./m', "/\n+/"), array('', '', "\n"), $vcard); + // Cleanup + $vcard = preg_replace(array( + // convert special types (like Skype) to normal type='skype' classes with this simple regex ;) + '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s', + '/^item\d*\.X-AB.*$/m', // remove cruft like item1.X-AB* + '/^item\d*\./m', // remove item1.ADR instead of ADR + '/\n+/', // remove empty lines + '/^(N:[^;\R]*)$/m', // if N doesn't have any semicolons, add some + ), + array( + '\2;type=\5\3:\4', + '', + '', + "\n", + '\1;;;;', + ), $vcard); // convert X-WAB-GENDER to X-GENDER if (preg_match('/X-WAB-GENDER:(\d)/', $vcard, $matches)) { @@ -539,9 +547,6 @@ class rcube_vcard $vcard = preg_replace('/X-WAB-GENDER:\d/', 'X-GENDER:' . $value, $vcard); } - // if N doesn't have any semicolons, add some - $vcard = preg_replace('/^(N:[^;\R]*)$/m', '\1;;;;', $vcard); - return $vcard; } @@ -612,8 +617,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 @@ -792,7 +797,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 e7467545f..51f7930aa 100644 --- a/program/lib/Roundcube/rcube_washtml.php +++ b/program/lib/Roundcube/rcube_washtml.php @@ -184,7 +184,7 @@ class rcube_washtml '|rgb\(\s*[0-9]+\s*,\s*[0-9]+\s*,\s*[0-9]+\s*\)'. '|-?[0-9.]+\s*(em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|%)?'. '|#[0-9a-f]{3,6}'. - '|[a-z0-9", -]+'. + '|[a-z0-9"\', -]+'. ')\s*/i', $str, $match) ) { if ($match[2]) { @@ -418,7 +418,7 @@ 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( + $badwordchars = array( "\xe2\x80\x98", // left single quote "\xe2\x80\x99", // right single quote "\xe2\x80\x9c", // left double quote @@ -426,7 +426,7 @@ class rcube_washtml "\xe2\x80\x94", // em dash "\xe2\x80\xa6" // elipses ); - $fixedwordchars=array( + $fixedwordchars = array( "'", "'", '"', @@ -434,7 +434,7 @@ class rcube_washtml '—', '...' ); - $html = str_replace($badwordchars,$fixedwordchars, $html); + $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) { @@ -455,13 +455,16 @@ 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 // Don't remove MSOutlook (<!-->) conditional comments (#1489004) $html = preg_replace('/<!--[^->\[\n]+>/', '', $html); + // fix broken nested lists + self::fix_broken_lists($html); + // turn relative into absolute urls $html = self::resolve_base($html); @@ -479,7 +482,12 @@ class rcube_washtml '/[^a-z0-9_\[\]\!-]/i', // forbidden characters ), '', $tagname); - return $matches[1] . $tagname; + // fix invalid closing tags - remove any attributes (#1489446) + if ($matches[1] == '</') { + $matches[3] = ''; + } + + return $matches[1] . $tagname . $matches[3]; } /** @@ -495,5 +503,77 @@ class rcube_washtml return $body; } -} + /** + * Fix broken nested lists, they are not handled properly by DOMDocument (#1488768) + */ + public static function fix_broken_lists(&$html) + { + // do two rounds, one for <ol>, one for <ul> + foreach (array('ol', 'ul') as $tag) { + $pos = 0; + while (($pos = stripos($html, '<' . $tag, $pos)) !== false) { + $pos++; + + // make sure this is an ol/ul tag + if (!in_array($html[$pos+2], array(' ', '>'))) { + continue; + } + + $p = $pos; + $in_li = false; + $li_pos = 0; + + while (($p = strpos($html, '<', $p)) !== false) { + $tt = strtolower(substr($html, $p, 4)); + + // li open tag + if ($tt == '<li>' || $tt == '<li ') { + $in_li = true; + $p += 4; + } + // li close tag + else if ($tt == '</li' && in_array($html[$p+4], array(' ', '>'))) { + $li_pos = $p; + $p += 4; + $in_li = false; + } + // ul/ol closing tag + else if ($tt == '</' . $tag && in_array($html[$p+4], array(' ', '>'))) { + break; + } + // nested ol/ul element out of li + else if (!$in_li && $li_pos && ($tt == '<ol>' || $tt == '<ol ' || $tt == '<ul>' || $tt == '<ul ')) { + // find closing tag of this ul/ol element + $element = substr($tt, 1, 2); + $cpos = $p; + do { + $tpos = stripos($html, '<' . $element, $cpos+1); + $cpos = stripos($html, '</' . $element, $cpos+1); + } + while ($tpos !== false && $cpos !== false && $cpos > $tpos); + + // not found, this is invalid HTML, skip it + if ($cpos === false) { + break; + } + + // get element content + $end = strpos($html, '>', $cpos); + $len = $end - $p + 1; + $element = substr($html, $p, $len); + + // move element to the end of the last li + $html = substr_replace($html, '', $p, $len); + $html = substr_replace($html, $element, $li_pos, 0); + + $p = $end; + } + else { + $p++; + } + } + } + } + } +} |