diff options
Diffstat (limited to 'program/lib')
50 files changed, 5251 insertions, 1336 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 a36711281..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'); /** @@ -604,16 +604,17 @@ class html_select extends html * * @param mixed $names Option name or array with option names * @param mixed $values Option value or array with option values + * @param array $attrib Additional attributes for the option entry */ - public function add($names, $values = null) + public function add($names, $values = null, $attrib = array()) { if (is_array($names)) { foreach ($names as $i => $text) { - $this->options[] = array('text' => $text, 'value' => $values[$i]); + $this->options[] = array('text' => $text, 'value' => $values[$i]) + $attrib; } } else { - $this->options[] = array('text' => $names, 'value' => $values); + $this->options[] = array('text' => $names, 'value' => $values) + $attrib; } } @@ -644,7 +645,7 @@ class html_select extends html $option_content = self::quote($option_content); } - $this->content .= self::tag('option', $attr, $option_content); + $this->content .= self::tag('option', $attr + $option, $option_content, array('value','label','class','style','title','disabled','selected')); } return parent::show(); @@ -676,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']; @@ -879,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['*']; } /** @@ -888,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..d58eb087b 100644 --- a/program/lib/Roundcube/rcube.php +++ b/program/lib/Roundcube/rcube.php @@ -642,10 +642,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 +686,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 +1114,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 +1165,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 +1172,7 @@ class rcube ); } else if (is_string($arg)) { - $arg = array('message' => $arg, 'type' => 'php'); + $arg = array('message' => $arg); } if (empty($arg['code'])) { @@ -1170,7 +1188,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 +1216,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) @@ -1347,6 +1365,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 +1566,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_base_replacer.php b/program/lib/Roundcube/rcube_base_replacer.php index a59bba926..fa6764753 100644 --- a/program/lib/Roundcube/rcube_base_replacer.php +++ b/program/lib/Roundcube/rcube_base_replacer.php @@ -90,8 +90,8 @@ class rcube_base_replacer if (preg_match_all('/\.\.\//', $path, $matches, PREG_SET_ORDER)) { foreach ($matches as $a_match) { - if (strrpos($base_url, '/')) { - $base_url = substr($base_url, 0, strrpos($base_url, '/')); + if ($pos = strrpos($base_url, '/')) { + $base_url = substr($base_url, 0, $pos); } $path = substr($path, 3); } diff --git a/program/lib/Roundcube/rcube_browser.php b/program/lib/Roundcube/rcube_browser.php index 34128291b..e53e31200 100644 --- a/program/lib/Roundcube/rcube_browser.php +++ b/program/lib/Roundcube/rcube_browser.php @@ -28,32 +28,24 @@ 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->opera = strpos($HTTP_USER_AGENT, 'opera') !== false; + $this->ns = strpos($HTTP_USER_AGENT, 'netscape') !== false; $this->chrome = strpos($HTTP_USER_AGENT, 'chrome') !== false; - $this->safari = !$this->chrome && ($this->khtml || strpos($HTTP_USER_AGENT, 'safari') !== false); + $this->ie = !$this->opera && (strpos($HTTP_USER_AGENT, 'compatible; msie') !== false || strpos($HTTP_USER_AGENT, 'trident/') !== false); + $this->safari = !$this->chrome && (strpos($HTTP_USER_AGENT, 'safari') !== false || strpos($HTTP_USER_AGENT, 'applewebkit') !== false); + $this->mz = !$this->ie && !$this->safari && !$this->chrome && !$this->ns && 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 (preg_match('/(chrome|msie|opera|version|khtml)(\s*|\/)([0-9.]+)/', $HTTP_USER_AGENT, $regs)) { + $this->ver = (float) $regs[3]; } - else if ($this->mz) { - $test = preg_match('/rv:([0-9.]+)/', $HTTP_USER_AGENT, $regs); - $this->ver = $test ? (float)$regs[1] : 0; - } - 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 +53,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..0352e4772 100644 --- a/program/lib/Roundcube/rcube_config.php +++ b/program/lib/Roundcube/rcube_config.php @@ -373,7 +373,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 08935853a..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); @@ -456,7 +457,7 @@ class rcube_db { $error = $this->dbh->errorInfo(); - if (empty($this->options['ignore_key_errors']) || $error[0] != '23000') { + if (empty($this->options['ignore_key_errors']) || !in_array($error[0], array('23000', '23505'))) { $this->db_error = true; $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]); diff --git a/program/lib/Roundcube/rcube_html2text.php b/program/lib/Roundcube/rcube_html2text.php index 9b248a3a8..01362e6fb 100644 --- a/program/lib/Roundcube/rcube_html2text.php +++ b/program/lib/Roundcube/rcube_html2text.php @@ -608,24 +608,27 @@ 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($text, 0, $start) . $body . "\n" . substr($text, $end + 13); + $text = substr_replace($text, $body . "\n", $start, $end + 13 - $start); $offset = 0; + break; } - } while ($end || $next); + } + while ($end || $next); } } /** * 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]); + $line = ltrim($m[2]); $space = $line[0] == '>' ? '' : ' '; + return $m[1] . '>' . $space . $line; } diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php index ff88bdccb..0cf34b2ca 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 @@ -1474,7 +1509,7 @@ class rcube_imap extends rcube_storage public function search_once($folder = null, $str = 'ALL') { if (!$str) { - return 'ALL'; + $str = 'ALL'; } if (!strlen($folder)) { @@ -2186,7 +2221,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); } @@ -2908,12 +2943,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]); } @@ -3913,9 +3957,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(); @@ -3927,7 +3974,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 445d46360..0c3edeaad 100644 --- a/program/lib/Roundcube/rcube_imap_cache.php +++ b/program/lib/Roundcube/rcube_imap_cache.php @@ -327,7 +327,6 @@ class rcube_imap_cache return array(); } - $msgs = array_flip($msgs); $result = array(); if ($this->mode & self::MODE_MESSAGE) { @@ -340,6 +339,8 @@ class rcube_imap_cache ." AND uid IN (".$this->db->array2list($msgs, 'integer').")", $this->userid, $mailbox); + $msgs = array_flip($msgs); + while ($sql_arr = $this->db->fetch_assoc($sql_result)) { $uid = intval($sql_arr['uid']); $result[$uid] = $this->build_message($sql_arr); @@ -351,11 +352,13 @@ class rcube_imap_cache unset($msgs[$uid]); } } + + $msgs = array_flip($msgs); } // Fetch not found messages from IMAP server if (!empty($msgs)) { - $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), false, true); + $messages = $this->imap->fetch_headers($mailbox, $msgs, false, true); // Insert to DB and add to result list if (!empty($messages)) { @@ -1247,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 bce4cd4e2..f9a62f010 100644 --- a/program/lib/Roundcube/rcube_imap_generic.php +++ b/program/lib/Roundcube/rcube_imap_generic.php @@ -48,6 +48,8 @@ class rcube_imap_generic '*' => '\\*', ); + public static $mupdate; + private $fp; private $host; private $logged = false; @@ -3156,6 +3158,11 @@ class rcube_imap_generic } foreach ($data as $entry) { + // Workaround cyrus-murder bug, the entry[2] string needs to be escaped + if (self::$mupdate) { + $entry[2] = addcslashes($entry[2], '\\"'); + } + // ANNOTATEMORE drafts before version 08 require quoted parameters $entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true), $this->escape($entry[1], true), $this->escape($entry[2], true)); @@ -3720,6 +3727,10 @@ class rcube_imap_generic $this->prefs['literal+'] = true; } + if (preg_match('/(\[| )MUPDATE=.*/', $str)) { + self::$mupdate = true; + } + if ($trusted) { $this->capability_readed = true; } diff --git a/program/lib/Roundcube/rcube_imap_search.php b/program/lib/Roundcube/rcube_imap_search.php index d82ec8a24..70a11bc1c 100644 --- a/program/lib/Roundcube/rcube_imap_search.php +++ b/program/lib/Roundcube/rcube_imap_search.php @@ -235,7 +235,7 @@ class rcube_imap_search_job extends Stackable } } - if (!$messages || !$messages->is_error()) { + if (!$messages || $messages->is_error()) { $messages = $imap->search($this->folder, ($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true); @@ -272,7 +272,7 @@ class rcube_imap_search_job extends Stackable /** - * Wrker thread to run search jobs while maintaining a common context + * Worker thread to run search jobs while maintaining a common context */ class rcube_imap_search_worker extends Worker { 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_message.php b/program/lib/Roundcube/rcube_message.php index 0cc0766f5..f24ec3ed8 100644 --- a/program/lib/Roundcube/rcube_message.php +++ b/program/lib/Roundcube/rcube_message.php @@ -195,8 +195,6 @@ class rcube_message /** * Determine if the message contains a HTML part. This must to be * a real part not an attachment (or its part) - * This must to be - * a real part not an attachment (or its part) * * @param bool $enriched Enables checking for text/enriched parts too * @@ -213,15 +211,19 @@ class rcube_message } $level = explode('.', $part->mime_id); + $depth = count($level); - // Check if the part belongs to higher-level's alternative/related + // Check if the part belongs to higher-level's multipart part + // 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 ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') { + if (!preg_match('/^multipart\/(alternative|related|signed|encrypted|mixed)$/', $parent->mimetype) + || ($parent->mimetype == 'multipart/mixed' && $parent_depth < $depth - 1)) { continue 2; } } @@ -530,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 96a8eac61..a931c27c1 100644 --- a/program/lib/Roundcube/rcube_mime.php +++ b/program/lib/Roundcube/rcube_mime.php @@ -637,7 +637,8 @@ class rcube_mime if ($nextChar === ' ' || $nextChar === $separator) { $afterNextChar = mb_substr($string, $width + 1, 1); - if ($afterNextChar === false) { + // Note: mb_substr() does never return False + if ($afterNextChar === false || $afterNextChar === '') { $subString .= $nextChar; } @@ -650,24 +651,23 @@ class rcube_mime $subString = mb_substr($subString, 0, $spacePos); $cutLength = $spacePos + 1; } - else if ($cut === false && $breakPos === false) { - $subString = $string; - $cutLength = null; - } else if ($cut === false) { $spacePos = mb_strpos($string, ' ', 0); - if ($spacePos !== false && $spacePos < $breakPos) { + if ($spacePos !== false && ($breakPos === false || $spacePos < $breakPos)) { $subString = mb_substr($string, 0, $spacePos); $cutLength = $spacePos + 1; } + else if ($breakPos === false) { + $subString = $string; + $cutLength = null; + } else { $subString = mb_substr($string, 0, $breakPos); $cutLength = $breakPos + 1; } } else { - $subString = mb_substr($subString, 0, $width); $cutLength = $width; } } @@ -810,7 +810,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 33f04eaa5..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; @@ -403,7 +406,7 @@ class rcube_plugin_api $args = $ret + $args; } - if ($args['abort']) { + if ($args['break']) { break; } } diff --git a/program/lib/Roundcube/rcube_result_thread.php b/program/lib/Roundcube/rcube_result_thread.php index 7657550be..c7f21db53 100644 --- a/program/lib/Roundcube/rcube_result_thread.php +++ b/program/lib/Roundcube/rcube_result_thread.php @@ -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_spellcheck_atd.php b/program/lib/Roundcube/rcube_spellcheck_atd.php new file mode 100644 index 000000000..9f073f56f --- /dev/null +++ b/program/lib/Roundcube/rcube_spellcheck_atd.php @@ -0,0 +1,204 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | This file is part of the Roundcube Webmail client | + | | + | Copyright (C) 2013, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Spellchecking backend implementation for afterthedeadline services | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ +*/ + +/** + * Spellchecking backend implementation to work with an After the Deadline service + * See http://www.afterthedeadline.com/ for more information + * + * @package Framework + * @subpackage Utils + */ +class rcube_spellcheck_atd extends rcube_spellcheck_engine +{ + const SERVICE_HOST = 'service.afterthedeadline.com'; + const SERVICE_PORT = 80; + + private $matches = array(); + private $content; + private $langhosts = array( + 'fr' => 'fr.', + 'de' => 'de.', + 'pt' => 'pt.', + 'es' => 'es.', + ); + + /** + * Return a list of languages supported by this backend + * + * @see rcube_spellcheck_engine::languages() + */ + function languages() + { + $langs = array_values($this->langhosts); + $langs[] = 'en'; + return $langs; + } + + /** + * Set content and check spelling + * + * @see rcube_spellcheck_engine::check() + */ + function check($text) + { + $this->content = $text; + + // spell check uri is configured + $rcube = rcube::get_instance(); + $url = $rcube->config->get('spellcheck_uri'); + $key = $rcube->config->get('spellcheck_atd_key'); + + if ($url) { + $a_uri = parse_url($url); + $ssl = ($a_uri['scheme'] == 'https' || $a_uri['scheme'] == 'ssl'); + $port = $a_uri['port'] ? $a_uri['port'] : ($ssl ? 443 : 80); + $host = ($ssl ? 'ssl://' : '') . $a_uri['host']; + $path = $a_uri['path'] . ($a_uri['query'] ? '?'.$a_uri['query'] : '') . $this->lang; + } + else { + $host = self::SERVICE_HOST; + $port = self::SERVICE_PORT; + $path = '/checkDocument'; + + // prefix host for other languages than 'en' + $lang = substr($this->lang, 0, 2); + if ($this->langhosts[$lang]) + $host = $this->langhosts[$lang] . $host; + } + + $postdata = 'data=' . urlencode($text); + + if (!empty($key)) + $postdata .= '&key=' . urlencode($key); + + $response = $headers = ''; + $in_header = true; + if ($fp = fsockopen($host, $port, $errno, $errstr, 30)) { + $out = "POST $path HTTP/1.0\r\n"; + $out .= "Host: " . str_replace('ssl://', '', $host) . "\r\n"; + $out .= "Content-Length: " . strlen($postdata) . "\r\n"; + $out .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $out .= "Connection: Close\r\n\r\n"; + $out .= $postdata; + fwrite($fp, $out); + + while (!feof($fp)) { + if ($in_header) { + $line = fgets($fp, 512); + $headers .= $line; + if (trim($line) == '') + $in_header = false; + } + else { + $response .= fgets($fp, 1024); + } + } + fclose($fp); + } + + // parse HTTP response headers + if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $headers, $m)) { + $http_status = $m[1]; + if ($http_status != '200') + $this->error = 'HTTP ' . $m[1] . $m[2]; + } + + if (!$response) { + $this->error = "Empty result from spelling engine"; + } + + try { + $result = new SimpleXMLElement($response); + } + catch (Exception $e) { + $thid->error = "Unexpected response from server: " . $store; + return array(); + } + + foreach ($result->error as $error) { + if (strval($error->type) == 'spelling') { + $word = strval($error->string); + + // skip exceptions + if ($this->dictionary->is_exception($word)) { + continue; + } + + $prefix = strval($error->precontext); + $start = $prefix ? mb_strpos($text, $prefix) : 0; + $pos = mb_strpos($text, $word, $start); + $len = mb_strlen($word); + $num = 0; + + $match = array($word, $pos, $len, null, array()); + foreach ($error->suggestions->option as $option) { + $match[4][] = strval($option); + if (++$num == self::MAX_SUGGESTIONS) + break; + } + $matches[] = $match; + } + } + + $this->matches = $matches; + return $matches; + } + + /** + * Returns suggestions for the specified word + * + * @see rcube_spellcheck_engine::get_words() + */ + function get_suggestions($word) + { + $matches = $word ? $this->check($word) : $this->matches; + + if ($matches[0][4]) { + return $matches[0][4]; + } + + return array(); + } + + /** + * Returns misspelled words + * + * @see rcube_spellcheck_engine::get_suggestions() + */ + function get_words($text = null) + { + if ($text) { + $matches = $this->check($text); + } + else { + $matches = $this->matches; + $text = $this->content; + } + + $result = array(); + + foreach ($matches as $m) { + $result[] = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET); + } + + return $result; + } + +} + diff --git a/program/lib/Roundcube/rcube_spellcheck_enchant.php b/program/lib/Roundcube/rcube_spellcheck_enchant.php new file mode 100644 index 000000000..14d6fff46 --- /dev/null +++ b/program/lib/Roundcube/rcube_spellcheck_enchant.php @@ -0,0 +1,182 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | This file is part of the Roundcube Webmail client | + | | + | Copyright (C) 2011-2013, Kolab Systems AG | + | Copyright (C) 20011-2013, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Spellchecking backend implementation to work with Enchant | + +-----------------------------------------------------------------------+ + | Author: Aleksander Machniak <machniak@kolabsys.com> | + +-----------------------------------------------------------------------+ +*/ + +/** + * Spellchecking backend implementation to work with Pspell + * + * @package Framework + * @subpackage Utils + */ +class rcube_spellcheck_enchant extends rcube_spellcheck_engine +{ + private $enchant_broker; + private $enchant_dictionary; + private $matches = array(); + + /** + * Return a list of languages supported by this backend + * + * @see rcube_spellcheck_engine::languages() + */ + function languages() + { + $this->init(); + + $langs = array(); + $dicts = enchant_broker_list_dicts($this->enchant_broker); + foreach ($dicts as $dict) { + $langs[] = preg_replace('/-.*$/', '', $dict['lang_tag']); + } + + return array_unique($langs); + } + + /** + * Initializes Enchant dictionary + */ + private function init() + { + if (!$this->enchant_broker) { + if (!extension_loaded('enchant')) { + $this->error = "Enchant extension not available"; + return; + } + + $this->enchant_broker = enchant_broker_init(); + } + + if (!enchant_broker_dict_exists($this->enchant_broker, $this->lang)) { + $this->error = "Unable to load dictionary for selected language using Enchant"; + return; + } + + $this->enchant_dictionary = enchant_broker_request_dict($this->enchant_broker, $this->lang); + } + + /** + * Set content and check spelling + * + * @see rcube_spellcheck_engine::check() + */ + function check($text) + { + $this->init(); + + if (!$this->enchant_dictionary) { + return array(); + } + + // tokenize + $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); + + $diff = 0; + $matches = array(); + + foreach ($text as $w) { + $word = trim($w[0]); + $pos = $w[1] - $diff; + $len = mb_strlen($word); + + // skip exceptions + if ($this->dictionary->is_exception($word)) { + } + else if (!enchant_dict_check($this->enchant_dictionary, $word)) { + $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word); + + if (sizeof($suggestions) > self::MAX_SUGGESTIONS) { + $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS); + } + + $matches[] = array($word, $pos, $len, null, $suggestions); + } + + $diff += (strlen($word) - $len); + } + + $this->matches = $matches; + return $matches; + } + + /** + * Returns suggestions for the specified word + * + * @see rcube_spellcheck_engine::get_words() + */ + function get_suggestions($word) + { + $this->init(); + + if (!$this->enchant_dictionary) { + return array(); + } + + $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word); + + if (sizeof($suggestions) > self::MAX_SUGGESTIONS) + $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS); + + return is_array($suggestions) ? $suggestions : array(); + } + + /** + * Returns misspelled words + * + * @see rcube_spellcheck_engine::get_suggestions() + */ + function get_words($text = null) + { + $result = array(); + + if ($text) { + // init spellchecker + $this->init(); + + if (!$this->enchant_dictionary) { + return array(); + } + + // With Enchant we don't need to get suggestions to return misspelled words + $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); + + foreach ($text as $w) { + $word = trim($w[0]); + + // skip exceptions + if ($this->dictionary->is_exception($word)) { + continue; + } + + if (!enchant_dict_check($this->enchant_dictionary, $word)) { + $result[] = $word; + } + } + + return $result; + } + + foreach ($this->matches as $m) { + $result[] = $m[0]; + } + + return $result; + } + +} + diff --git a/program/lib/Roundcube/rcube_spellcheck_engine.php b/program/lib/Roundcube/rcube_spellcheck_engine.php new file mode 100644 index 000000000..3cb4ca3de --- /dev/null +++ b/program/lib/Roundcube/rcube_spellcheck_engine.php @@ -0,0 +1,91 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | This file is part of the Roundcube Webmail client | + | | + | Copyright (C) 2011-2013, Kolab Systems AG | + | Copyright (C) 2008-2013, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Interface class for a spell-checking backend | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ +*/ + +/** + * Interface class for a spell-checking backend + * + * @package Framework + * @subpackage Utils + */ +abstract class rcube_spellcheck_engine +{ + const MAX_SUGGESTIONS = 10; + + protected $lang; + protected $error; + protected $dictionary; + protected $separator = '/[\s\r\n\t\(\)\/\[\]{}<>\\"]+|[:;?!,\.](?=\W|$)/'; + + /** + * Default constructor + */ + public function __construct($dict, $lang) + { + $this->dictionary = $dict; + $this->lang = $lang; + } + + /** + * Return a list of languages supported by this backend + * + * @return array Indexed list of language codes + */ + abstract function languages(); + + /** + * Set content and check spelling + * + * @param string $text Text content for spellchecking + * + * @return bool True when no mispelling found, otherwise false + */ + abstract function check($text); + + /** + * Returns suggestions for the specified word + * + * @param string $word The word + * + * @return array Suggestions list + */ + abstract function get_suggestions($word); + + /** + * Returns misspelled words + * + * @param string $text The content for spellchecking. If empty content + * used for check() method will be used. + * + * @return array List of misspelled words + */ + abstract function get_words($text = null); + + /** + * Returns error message + * + * @return string Error message + */ + public function error() + { + return $this->error; + } + +} + diff --git a/program/lib/Roundcube/rcube_spellcheck_googie.php b/program/lib/Roundcube/rcube_spellcheck_googie.php new file mode 100644 index 000000000..3777942a6 --- /dev/null +++ b/program/lib/Roundcube/rcube_spellcheck_googie.php @@ -0,0 +1,176 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | This file is part of the Roundcube Webmail client | + | | + | Copyright (C) 2008-2013, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Spellchecking backend implementation to work with Googiespell | + +-----------------------------------------------------------------------+ + | Author: Aleksander Machniak <machniak@kolabsys.com> | + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ +*/ + +/** + * Spellchecking backend implementation to work with a Googiespell service + * + * @package Framework + * @subpackage Utils + */ +class rcube_spellcheck_googie extends rcube_spellcheck_engine +{ + const GOOGIE_HOST = 'ssl://spell.roundcube.net'; + const GOOGIE_PORT = 443; + + private $matches = array(); + private $content; + + /** + * Return a list of languages supported by this backend + * + * @see rcube_spellcheck_engine::languages() + */ + function languages() + { + return array('am','ar','ar','bg','br','ca','cs','cy','da', + 'de_CH','de_DE','el','en_GB','en_US', + 'eo','es','et','eu','fa','fi','fr_FR','ga','gl','gl', + 'he','hr','hu','hy','is','it','ku','lt','lv','nl', + 'pl','pt_BR','pt_PT','ro','ru', + 'sk','sl','sv','uk'); + } + + /** + * Set content and check spelling + * + * @see rcube_spellcheck_engine::check() + */ + function check($text) + { + $this->content = $text; + + // spell check uri is configured + $url = rcube::get_instance()->config->get('spellcheck_uri'); + + if ($url) { + $a_uri = parse_url($url); + $ssl = ($a_uri['scheme'] == 'https' || $a_uri['scheme'] == 'ssl'); + $port = $a_uri['port'] ? $a_uri['port'] : ($ssl ? 443 : 80); + $host = ($ssl ? 'ssl://' : '') . $a_uri['host']; + $path = $a_uri['path'] . ($a_uri['query'] ? '?'.$a_uri['query'] : '') . $this->lang; + } + else { + $host = self::GOOGIE_HOST; + $port = self::GOOGIE_PORT; + $path = '/tbproxy/spell?lang=' . $this->lang; + } + + $path .= sprintf('&key=%06d', $_SESSION['user_id']); + + $gtext = '<?xml version="1.0" encoding="utf-8" ?>' + .'<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">' + .'<text>' . htmlspecialchars($text, ENT_QUOTES, RCUBE_CHARSET) . '</text>' + .'</spellrequest>'; + + $store = ''; + if ($fp = fsockopen($host, $port, $errno, $errstr, 30)) { + $out = "POST $path HTTP/1.0\r\n"; + $out .= "Host: " . str_replace('ssl://', '', $host) . "\r\n"; + $out .= "User-Agent: Roundcube Webmail/" . RCMAIL_VERSION . " (Googiespell Wrapper)\r\n"; + $out .= "Content-Length: " . strlen($gtext) . "\r\n"; + $out .= "Content-Type: text/xml\r\n"; + $out .= "Connection: Close\r\n\r\n"; + $out .= $gtext; + fwrite($fp, $out); + + while (!feof($fp)) + $store .= fgets($fp, 128); + fclose($fp); + } + + // parse HTTP response + if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $store, $m)) { + $http_status = $m[1]; + if ($http_status != '200') { + $this->error = 'HTTP ' . $m[1] . $m[2]; + $this->error .= "\n" . $store; + } + } + + if (!$store) { + $this->error = "Empty result from spelling engine"; + } + else if (preg_match('/<spellresult error="([^"]+)"/', $store, $m) && $m[1]) { + $this->error = "Error code $m[1] returned"; + $this->error .= preg_match('/<errortext>([^<]+)/', $store, $m) ? ": " . html_entity_decode($m[1]) : ''; + } + + preg_match_all('/<c o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/c>/', $store, $matches, PREG_SET_ORDER); + + // skip exceptions (if appropriate options are enabled) + foreach ($matches as $idx => $m) { + $word = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET); + // skip exceptions + if ($this->dictionary->is_exception($word)) { + unset($matches[$idx]); + } + } + + $this->matches = $matches; + return $matches; + } + + /** + * Returns suggestions for the specified word + * + * @see rcube_spellcheck_engine::get_words() + */ + function get_suggestions($word) + { + $matches = $word ? $this->check($word) : $this->matches; + + if ($matches[0][4]) { + $suggestions = explode("\t", $matches[0][4]); + if (sizeof($suggestions) > self::MAX_SUGGESTIONS) { + $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS); + } + + return $suggestions; + } + + return array(); + } + + /** + * Returns misspelled words + * + * @see rcube_spellcheck_engine::get_suggestions() + */ + function get_words($text = null) + { + if ($text) { + $matches = $this->check($text); + } + else { + $matches = $this->matches; + $text = $this->content; + } + + $result = array(); + + foreach ($matches as $m) { + $result[] = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET); + } + + return $result; + } + +} + diff --git a/program/lib/Roundcube/rcube_spellcheck_pspell.php b/program/lib/Roundcube/rcube_spellcheck_pspell.php new file mode 100644 index 000000000..b12684e43 --- /dev/null +++ b/program/lib/Roundcube/rcube_spellcheck_pspell.php @@ -0,0 +1,189 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | This file is part of the Roundcube Webmail client | + | | + | Copyright (C) 2008-2013, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Spellchecking backend implementation to work with Pspell | + +-----------------------------------------------------------------------+ + | Author: Aleksander Machniak <machniak@kolabsys.com> | + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ +*/ + +/** + * Spellchecking backend implementation to work with Pspell + * + * @package Framework + * @subpackage Utils + */ +class rcube_spellcheck_pspell extends rcube_spellcheck_engine +{ + private $plink; + private $matches = array(); + + /** + * Return a list of languages supported by this backend + * + * @see rcube_spellcheck_engine::languages() + */ + function languages() + { + $defaults = array('en'); + $langs = array(); + + // get aspell dictionaries + exec('aspell dump dicts', $dicts); + if (!empty($dicts)) { + $seen = array(); + foreach ($dicts as $lang) { + $lang = preg_replace('/-.*$/', '', $lang); + $langc = strlen($lang) == 2 ? $lang.'_'.strtoupper($lang) : $lang; + if (!$seen[$langc]++) + $langs[] = $lang; + } + $langs = array_unique($langs); + } + else { + $langs = $defaults; + } + + return $langs; + } + + /** + * Initializes PSpell dictionary + */ + private function init() + { + if (!$this->plink) { + if (!extension_loaded('pspell')) { + $this->error = "Pspell extension not available"; + return; + } + + $this->plink = pspell_new($this->lang, null, null, RCUBE_CHARSET, PSPELL_FAST); + } + + if (!$this->plink) { + $this->error = "Unable to load Pspell engine for selected language"; + } + } + + /** + * Set content and check spelling + * + * @see rcube_spellcheck_engine::check() + */ + function check($text) + { + $this->init(); + + if (!$this->plink) { + return array(); + } + + // tokenize + $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); + + $diff = 0; + $matches = array(); + + foreach ($text as $w) { + $word = trim($w[0]); + $pos = $w[1] - $diff; + $len = mb_strlen($word); + + // skip exceptions + if ($this->dictionary->is_exception($word)) { + } + else if (!pspell_check($this->plink, $word)) { + $suggestions = pspell_suggest($this->plink, $word); + + if (sizeof($suggestions) > self::MAX_SUGGESTIONS) { + $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS); + } + + $matches[] = array($word, $pos, $len, null, $suggestions); + } + + $diff += (strlen($word) - $len); + } + + $this->matches = $matches; + return $matches; + } + + /** + * Returns suggestions for the specified word + * + * @see rcube_spellcheck_engine::get_words() + */ + function get_suggestions($word) + { + $this->init(); + + if (!$this->plink) { + return array(); + } + + $suggestions = pspell_suggest($this->plink, $word); + + if (sizeof($suggestions) > self::MAX_SUGGESTIONS) + $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS); + + return is_array($suggestions) ? $suggestions : array(); + } + + /** + * Returns misspelled words + * + * @see rcube_spellcheck_engine::get_suggestions() + */ + function get_words($text = null) + { + $result = array(); + + if ($text) { + // init spellchecker + $this->init(); + + if (!$this->plink) { + return array(); + } + + // With PSpell we don't need to get suggestions to return misspelled words + $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); + + foreach ($text as $w) { + $word = trim($w[0]); + + // skip exceptions + if ($this->dictionary->is_exception($word)) { + continue; + } + + if (!pspell_check($this->plink, $word)) { + $result[] = $word; + } + } + + return $result; + } + + foreach ($this->matches as $m) { + $result[] = $m[0]; + } + + return $result; + } + +} + diff --git a/program/lib/Roundcube/rcube_spellchecker.php b/program/lib/Roundcube/rcube_spellchecker.php index df4365223..5b77bda02 100644 --- a/program/lib/Roundcube/rcube_spellchecker.php +++ b/program/lib/Roundcube/rcube_spellchecker.php @@ -3,8 +3,8 @@ /* +-----------------------------------------------------------------------+ | This file is part of the Roundcube Webmail client | - | Copyright (C) 2011, Kolab Systems AG | - | Copyright (C) 2008-2011, The Roundcube Dev Team | + | Copyright (C) 2011-2013, Kolab Systems AG | + | Copyright (C) 2008-2013, The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -28,21 +28,15 @@ class rcube_spellchecker { private $matches = array(); private $engine; + private $backend; private $lang; private $rc; private $error; - private $separator = '/[\s\r\n\t\(\)\/\[\]{}<>\\"]+|[:;?!,\.](?=\W|$)/'; private $options = array(); private $dict; private $have_dict; - // default settings - const GOOGLE_HOST = 'ssl://www.google.com'; - const GOOGLE_PORT = 443; - const MAX_SUGGESTIONS = 10; - - /** * Constructor * @@ -60,8 +54,63 @@ class rcube_spellchecker 'ignore_caps' => $this->rc->config->get('spellcheck_ignore_caps'), 'dictionary' => $this->rc->config->get('spellcheck_dictionary'), ); + + $cls = 'rcube_spellcheck_' . $this->engine; + if (class_exists($cls)) { + $this->backend = new $cls($this, $this->lang); + $this->backend->options = $this->options; + } + else { + $this->error = "Unknown spellcheck engine '$this->engine'"; + } } + /** + * 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 @@ -81,14 +130,8 @@ class rcube_spellchecker $this->content = $text; } - if ($this->engine == 'pspell') { - $this->matches = $this->_pspell_check($this->content); - } - else if ($this->engine == 'enchant') { - $this->matches = $this->_enchant_check($this->content); - } - else { - $this->matches = $this->_googie_check($this->content); + if ($this->backend) { + $this->matches = $this->backend->check($this->content); } return $this->found() == 0; @@ -115,14 +158,11 @@ class rcube_spellchecker */ function get_suggestions($word) { - if ($this->engine == 'pspell') { - return $this->_pspell_suggestions($word); - } - else if ($this->engine == 'enchant') { - return $this->_enchant_suggestions($word); + if ($this->backend) { + return $this->backend->get_suggestions($word); } - return $this->_googie_suggestions($word); + return array(); } @@ -136,14 +176,15 @@ class rcube_spellchecker */ function get_words($text = null, $is_html=false) { - if ($this->engine == 'pspell') { - return $this->_pspell_words($text, $is_html); + if ($is_html) { + $text = $this->html2text($text); } - else if ($this->engine == 'enchant') { - return $this->_enchant_words($text, $is_html); + + if ($this->backend) { + return $this->backend->get_words($text); } - return $this->_googie_words($text, $is_html); + return array(); } @@ -157,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>'; @@ -178,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]; } @@ -199,394 +240,7 @@ class rcube_spellchecker */ function error() { - return $this->error; - } - - - /** - * Checks the text using pspell - * - * @param string $text Text content for spellchecking - */ - private function _pspell_check($text) - { - // init spellchecker - $this->_pspell_init(); - - if (!$this->plink) { - return array(); - } - - // tokenize - $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); - - $diff = 0; - $matches = array(); - - foreach ($text as $w) { - $word = trim($w[0]); - $pos = $w[1] - $diff; - $len = mb_strlen($word); - - // skip exceptions - if ($this->is_exception($word)) { - } - else if (!pspell_check($this->plink, $word)) { - $suggestions = pspell_suggest($this->plink, $word); - - if (sizeof($suggestions) > self::MAX_SUGGESTIONS) { - $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS); - } - - $matches[] = array($word, $pos, $len, null, $suggestions); - } - - $diff += (strlen($word) - $len); - } - - return $matches; - } - - - /** - * Returns the misspelled words - */ - private function _pspell_words($text = null, $is_html=false) - { - $result = array(); - - if ($text) { - // init spellchecker - $this->_pspell_init(); - - if (!$this->plink) { - return array(); - } - - // With PSpell we don't need to get suggestions to return misspelled words - if ($is_html) { - $text = $this->html2text($text); - } - - $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); - - foreach ($text as $w) { - $word = trim($w[0]); - - // skip exceptions - if ($this->is_exception($word)) { - continue; - } - - if (!pspell_check($this->plink, $word)) { - $result[] = $word; - } - } - - return $result; - } - - foreach ($this->matches as $m) { - $result[] = $m[0]; - } - - return $result; - } - - - /** - * Returns suggestions for misspelled word - */ - private function _pspell_suggestions($word) - { - // init spellchecker - $this->_pspell_init(); - - if (!$this->plink) { - return array(); - } - - $suggestions = pspell_suggest($this->plink, $word); - - if (sizeof($suggestions) > self::MAX_SUGGESTIONS) - $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS); - - return is_array($suggestions) ? $suggestions : array(); - } - - - /** - * Initializes PSpell dictionary - */ - private function _pspell_init() - { - if (!$this->plink) { - if (!extension_loaded('pspell')) { - $this->error = "Pspell extension not available"; - return; - } - - $this->plink = pspell_new($this->lang, null, null, RCUBE_CHARSET, PSPELL_FAST); - } - - if (!$this->plink) { - $this->error = "Unable to load Pspell engine for selected language"; - } - } - - - /** - * Checks the text using enchant - * - * @param string $text Text content for spellchecking - */ - private function _enchant_check($text) - { - // init spellchecker - $this->_enchant_init(); - - if (!$this->enchant_dictionary) { - return array(); - } - - // tokenize - $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); - - $diff = 0; - $matches = array(); - - foreach ($text as $w) { - $word = trim($w[0]); - $pos = $w[1] - $diff; - $len = mb_strlen($word); - - // skip exceptions - if ($this->is_exception($word)) { - } - else if (!enchant_dict_check($this->enchant_dictionary, $word)) { - $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word); - - if (sizeof($suggestions) > self::MAX_SUGGESTIONS) { - $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS); - } - - $matches[] = array($word, $pos, $len, null, $suggestions); - } - - $diff += (strlen($word) - $len); - } - - return $matches; - } - - - /** - * Returns the misspelled words - */ - private function _enchant_words($text = null, $is_html=false) - { - $result = array(); - - if ($text) { - // init spellchecker - $this->_enchant_init(); - - if (!$this->enchant_dictionary) { - return array(); - } - - // With Enchant we don't need to get suggestions to return misspelled words - if ($is_html) { - $text = $this->html2text($text); - } - - $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); - - foreach ($text as $w) { - $word = trim($w[0]); - - // skip exceptions - if ($this->is_exception($word)) { - continue; - } - - if (!enchant_dict_check($this->enchant_dictionary, $word)) { - $result[] = $word; - } - } - - return $result; - } - - foreach ($this->matches as $m) { - $result[] = $m[0]; - } - - return $result; - } - - - /** - * Returns suggestions for misspelled word - */ - private function _enchant_suggestions($word) - { - // init spellchecker - $this->_enchant_init(); - - if (!$this->enchant_dictionary) { - return array(); - } - - $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word); - - if (sizeof($suggestions) > self::MAX_SUGGESTIONS) - $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS); - - return is_array($suggestions) ? $suggestions : array(); - } - - - /** - * Initializes PSpell dictionary - */ - private function _enchant_init() - { - if (!$this->enchant_broker) { - if (!extension_loaded('enchant')) { - $this->error = "Enchant extension not available"; - return; - } - - $this->enchant_broker = enchant_broker_init(); - } - - if (!enchant_broker_dict_exists($this->enchant_broker, $this->lang)) { - $this->error = "Unable to load dictionary for selected language using Enchant"; - return; - } - - $this->enchant_dictionary = enchant_broker_request_dict($this->enchant_broker, $this->lang); - } - - - private function _googie_check($text) - { - // spell check uri is configured - $url = $this->rc->config->get('spellcheck_uri'); - - if ($url) { - $a_uri = parse_url($url); - $ssl = ($a_uri['scheme'] == 'https' || $a_uri['scheme'] == 'ssl'); - $port = $a_uri['port'] ? $a_uri['port'] : ($ssl ? 443 : 80); - $host = ($ssl ? 'ssl://' : '') . $a_uri['host']; - $path = $a_uri['path'] . ($a_uri['query'] ? '?'.$a_uri['query'] : '') . $this->lang; - } - else { - $host = self::GOOGLE_HOST; - $port = self::GOOGLE_PORT; - $path = '/tbproxy/spell?lang=' . $this->lang; - } - - // Google has some problem with spaces, use \n instead - $gtext = str_replace(' ', "\n", $text); - - $gtext = '<?xml version="1.0" encoding="utf-8" ?>' - .'<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">' - .'<text>' . $gtext . '</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 .= "Content-Length: " . strlen($gtext) . "\r\n"; - $out .= "Content-Type: application/x-www-form-urlencoded\r\n"; - $out .= "Connection: Close\r\n\r\n"; - $out .= $gtext; - fwrite($fp, $out); - - while (!feof($fp)) - $store .= fgets($fp, 128); - fclose($fp); - } - - // parse HTTP response - if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $store, $m)) { - $http_status = $m[1]; - if ($http_status != '200') - $this->error = 'HTTP ' . $m[1] . $m[2]; - } - - if (!$store) { - $this->error = "Empty result from spelling engine"; - } - else if (preg_match('/<spellresult error="([^"]+)"/', $store, $m) && $m[1]) { - $this->error = "Error code $m[1] returned"; - } - - preg_match_all('/<c o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/c>/', $store, $matches, PREG_SET_ORDER); - - // skip exceptions (if appropriate options are enabled) - if (!empty($this->options['ignore_syms']) || !empty($this->options['ignore_nums']) - || !empty($this->options['ignore_caps']) || !empty($this->options['dictionary']) - ) { - foreach ($matches as $idx => $m) { - $word = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET); - // skip exceptions - if ($this->is_exception($word)) { - unset($matches[$idx]); - } - } - } - - return $matches; - } - - - private function _googie_words($text = null, $is_html=false) - { - if ($text) { - if ($is_html) { - $text = $this->html2text($text); - } - - $matches = $this->_googie_check($text); - } - else { - $matches = $this->matches; - $text = $this->content; - } - - $result = array(); - - foreach ($matches as $m) { - $result[] = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET); - } - - return $result; - } - - - private function _googie_suggestions($word) - { - if ($word) { - $matches = $this->_googie_check($word); - } - else { - $matches = $this->matches; - } - - if ($matches[0][4]) { - $suggestions = explode("\t", $matches[0][4]); - if (sizeof($suggestions) > self::MAX_SUGGESTIONS) { - $suggestions = array_slice($suggestions, 0, MAX_SUGGESTIONS); - } - - return $suggestions; - } - - return array(); + return $this->error ? $this->error : ($this->backend ? $this->backend->error() : false); } @@ -698,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 @@ -706,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'])); } } @@ -740,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_string_replacer.php b/program/lib/Roundcube/rcube_string_replacer.php index 354b4596d..77b91d18b 100644 --- a/program/lib/Roundcube/rcube_string_replacer.php +++ b/program/lib/Roundcube/rcube_string_replacer.php @@ -24,11 +24,16 @@ */ class rcube_string_replacer { - public static $pattern = '/##str_replacement\[([0-9]+)\]##/'; + public static $pattern = '/##str_replacement_(\d+)##/'; public $mailto_pattern; public $link_pattern; + public $linkref_index; + public $linkref_pattern; + private $values = array(); private $options = array(); + private $linkrefs = array(); + private $urls = array(); function __construct($options = array()) @@ -45,6 +50,8 @@ class rcube_string_replacer ."@$utf_domain" // domain-part ."(\?[$url1$url2]+)?" // e.g. ?subject=test... .")/"; + $this->linkref_index = '/\[([^\]#]+)\](:?\s*##str_replacement_(\d+)##)/'; + $this->linkref_pattern = '/\[([^\]#]+)\]/'; $this->options = $options; } @@ -67,7 +74,7 @@ class rcube_string_replacer */ public function get_replacement($i) { - return '##str_replacement['.$i.']##'; + return '##str_replacement_' . $i . '##'; } /** @@ -96,6 +103,7 @@ class rcube_string_replacer $attrib['href'] = $url_prefix . $url; $i = $this->add(html::a($attrib, rcube::Q($url)) . $suffix); + $this->urls[$i] = $attrib['href']; } // Return valid link for recognized schemes, otherwise @@ -104,6 +112,32 @@ class rcube_string_replacer } /** + * Callback to add an entry to the link index + */ + public function linkref_addindex($matches) + { + $key = $matches[1]; + $this->linkrefs[$key] = $this->urls[$matches[3]]; + + return $this->get_replacement($this->add('['.$key.']')) . $matches[2]; + } + + /** + * Callback to replace link references with real links + */ + public function linkref_callback($matches) + { + $i = 0; + if ($url = $this->linkrefs[$matches[1]]) { + $attrib = (array)$this->options['link_attribs']; + $attrib['href'] = $url; + $i = $this->add(html::a($attrib, rcube::Q($matches[1]))); + } + + return $i > 0 ? '['.$this->get_replacement($i).']' : $matches[0]; + } + + /** * Callback function used to build mailto: links around e-mail strings * * @param array Matches result from preg_replace_callback @@ -142,6 +176,9 @@ class rcube_string_replacer // search for patterns like links and e-mail addresses $str = preg_replace_callback($this->link_pattern, array($this, 'link_callback'), $str); $str = preg_replace_callback($this->mailto_pattern, array($this, 'mailto_callback'), $str); + // resolve link references + $str = preg_replace_callback($this->linkref_index, array($this, 'linkref_addindex'), $str); + $str = preg_replace_callback($this->linkref_pattern, array($this, 'linkref_callback'), $str); return $str; } 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 c1ad3823b..c48cd80e8 100644 --- a/program/lib/Roundcube/rcube_utils.php +++ b/program/lib/Roundcube/rcube_utils.php @@ -445,34 +445,41 @@ class rcube_utils $source = self::xss_entity_decode($source); $stripped = preg_replace('/[^a-z\(:;]/i', '', $source); $evilexpr = 'expression|behavior|javascript:|import[^a]' . (!$allow_remote ? '|url\(' : ''); + if (preg_match("/$evilexpr/i", $stripped)) { return '/* evil! */'; } + $strict_url_regexp = '!url\s*\([ "\'](https?:)//[a-z0-9/._+-]+["\' ]\)!Uims'; + // cut out all contents between { and } while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) { - $styles = substr($source, $pos+1, $pos2-($pos+1)); + $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); // check every line of a style block... if ($allow_remote) { $a_styles = preg_split('/;[\r\n]*/', $styles, -1, PREG_SPLIT_NO_EMPTY); + foreach ($a_styles as $line) { $stripped = preg_replace('/[^a-z\(:;]/i', '', $line); // ... and only allow strict url() values - $regexp = '!url\s*\([ "\'](https?:)//[a-z0-9/._+-]+["\' ]\)!Uims'; - if (stripos($stripped, 'url(') && !preg_match($regexp, $line)) { + if (stripos($stripped, 'url(') && !preg_match($strict_url_regexp, $line)) { $a_styles = array('/* evil! */'); break; } } + $styles = join(";\n", $a_styles); } - $key = $replacements->add($styles); - $source = substr($source, 0, $pos+1) - . $replacements->get_replacement($key) - . substr($source, $pos2, strlen($source)-$pos2); - $last_pos = $pos+2; + $key = $replacements->add($styles); + $repl = $replacements->get_replacement($key); + $source = substr_replace($source, $repl, $pos+1, $length); + $last_pos = $pos2 - ($length - strlen($repl)); } // remove html comments and add #container to each tag selector. @@ -615,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 @@ -635,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); } @@ -673,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 ''; @@ -740,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)) { @@ -801,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)) { @@ -827,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 @@ -886,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); 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..5a5b3dc55 100644 --- a/program/lib/Roundcube/rcube_washtml.php +++ b/program/lib/Roundcube/rcube_washtml.php @@ -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++; + } + } + } + } + } +} |