<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * PHPUnit AllTests suite for the Crypt_GPG package.
 *
 * These tests require the PHPUnit 3.6 or greater package to be installed.
 * PHPUnit is installable using PEAR. See the
 * {@link http://www.phpunit.de/manual/3.6/en/installation.html manual}
 * for detailed installation instructions.
 *
 * 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, see
 * <http://www.gnu.org/licenses/>
 *
 * @category  Encryption
 * @package   Crypt_GPG
 * @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
 */

/**
 * The Crypt_GPG class to test
 */
require_once 'Crypt/GPG.php';

/**
 * Key class definition
 */
require_once 'Crypt/GPG/Key.php';

/**
 * Signature class definition
 */
require_once 'Crypt/GPG/Signature.php';

/**
 * Abstract base class for testing Crypt_GPG.
 *
 * Test keyring contains:
 *
 * 1) first-keypair@example.com - passphrase 'test1'
 *    A public-private key pair that can be used to both encrypt and decrypt.
 *
 * 2) second-keypair@example.com - passphrase 'test2'
 *    A public-private key pair that can be used to both encrypt and decrypt.
 *
 * 3) public-only@example.com - passphrase 'test'
 *    A public key with no private key. Used for testing private key import.
 *
 * 4) no-passphrase@example.com - no passphrase
 *    A public-private key pair that can be used to both encrypt and decrypt
 *    with no passphrase.
 *
 * 5) external-public@example.com - passphrase 'test'
 *    A public key that does not initially exist in the keyring that can be
 *    imported.
 *
 * 6) multiple-subkeys@example.com - passphrases 'test1' and 'test2'
 *    A public-private key pair that has multiple encrypting subkeys. The
 *    first subkey is an ELG-E key. The second is an RSA key.
 */
abstract class Crypt_GPG_TestCase extends PHPUnit\Framework\TestCase
{
    const HOMEDIR = 'test-keychain';
    const TEMPDIR = 'temp-files';
    const DATADIR = 'data-files';

    protected $config;
    protected $gpg;

    protected function getOptions()
    {
        $debugFunction = function ($text) {
            file_put_contents(__DIR__ . '/debug.log', $text . "\n", FILE_APPEND);
        };

        $config = [
            'homedir' => __DIR__ . '/' . self::HOMEDIR,
            // 'binary' => '/usr/bin/gpg2',
            // 'agent'  => '/usr/bin/gpg-agent',
            // 'gpgconf'  => '/usr/local/bin/gpgconf',
            // 'cipher-algo' => 'AES256',
            // 'digest-algo' => 'SHA512',
            // 'compress-algo' => 'zip',
            'debug'  => $debugFunction,
            // 'options' => array(),
        ];

        if ($binary = getenv('TESTS_GPG_BINARY')) {
            $config['binary'] = $binary;
        }

        return $config;
    }

    public function setUp(): void
    {
        // load test configuration file if it exists
        $configFilename = __DIR__ . '/config.php';
        if (file_exists($configFilename)) {
            include $configFilename;

            if (!isset($GLOBALS['Crypt_GPG_Unittest_Config'])
                || !is_array($GLOBALS['Crypt_GPG_Unittest_Config'])
            ) {
                $this->markTestSkipped(
                    'Unit test configuration is incorrect. Please read the '
                    . 'documentation in TestCase.php and fix the '
                    . 'configuration file. See the configuration in '
                    . '\'config.php.dist\' for an example.'
                );
            }

            $this->config = $GLOBALS['Crypt_GPG_Unittest_Config'];
        } else {
            $this->config = [];
        }

        // default test config values
        if (!isset($this->config['enable-key-generation'])) {
            $this->config['enable-key-generation'] = false;
        }

        $this->_setUpKeyring();
        $this->_setUpTempdir();

        $this->gpg = new Crypt_GPG($this->getOptions());
    }

    private function _setUpKeyring()
    {
        // {{{ pubring data
        $pubringData = <<<TEXT
[encoded data, 8723 characters]
2SXgmRBMkfID6VoR87RDD/RHSbACAAM=

TEXT;
        // }}}
        // {{{ secring data
        $secringData = <<<TEXT
[encoded data, 8723 characters]
Q/q3S7ACAAA=

TEXT;
        // }}}
        // {{{ trustdb data
        $trustdbData = <<<TEXT
AWdwZwMDAQUBAAAASXgQ5QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQoAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAJAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAqAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
CgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAoAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAALwAAAAAA
AAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAmAAAAAAAAAAAAAAAAAAAAAAAA
CgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAoAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAxAAAAAAAAAAAA
AAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
CgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAiAAAAMwAAAAAAAAAAAAAKAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADACNIpnZxcIREosyu7DA
l9nslMBjYwYAAAAAAAAfAAAAAAAAAAAAAA0ASuRA053T81XR0YsL9GQz36zO
so8GAAAAAAAAAAAAAAAAAAAAAAAMAIgJItvqcz6QZpPkqQPMiQr6Ha1LBgAA
AAAAACEAAAAAAAAAAAAADQDkLyWSxvRVinJea5Y3Sc4dh3lKQQYAAAAAAAAA
AAAAAAAAAAAAAAwA+DEYy29YktwcPpNtq6ge9U6MDesGAAAAAAAAIwAAAAAA
AAAAAAANAMR3BlsMercywmS8VQx+WBRwzSxrBgAAAAAAAAAAAAAAAAAAAAAA
DAAeycXb8jndCjpPzQ2UVj+zmK2msgYAAAAAAAAlAAAAAAAAAAAAAA0A+Xih
okzxb5YYF2H8zt6XjTHnb5oGAAAAAAAAAAAAAAAAAAAAAAAMAJSPmDX/CfX5
HP8qwSaKtxA0NeZdAAAAAAAAACcAAAAAAAAAAAAADQB1TeCPc5cRDuwveD0X
xhsm64ZrYAAAAAAAAAAAAAAAAAAAAAAAAAwAxo3/oHXKbdbf3Wfe8oOFM8Yg
1NUAAAAAAAAAKQAAAAAAAAAAAAANAOQflEprmQfxXYoQ4ziqhrc8pjZDAAAA
AAAAAAAAAAAAAAAAAAAADAAlEjmrDfswlGd8hOOp7mR+yD1FcwAAAAAAAAAr
AAAAAAAAAAAAAA0A5B+USmuZB/FdihDjOKqGtzymNkMAAAAAAAAAAAAAAAAA
AAAAAAAMANk0YYDRuuh0Qo8Qx/Ye3VqwpH+AAAAAAAAAAC0AAAAAAAAAAAAA
DQDkH5RKa5kH8V2KEOM4qoa3PKY2QwAAAAAAAAAAAAAAAAAAAAAAAAwAjfos
sg2v7kfmnu1bCuJ9bDb23VAAAAAAAAAAMAAAAAAAAAAAAAALAAAAAAAAAAAe
AAAALgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQDkH5RKa5kH8V2KEOM4
qoa3PKY2QwAAAAAAAAAAAAAAAAAAAAAAAAwA3Z5wduQspEtWG5gn+7R5kNtT
j/cAAAAAAAAAMgAAAAAAAAAAAAANADPMTOwYcVpigI8JYuYIOyOWVB6sAAAA
AAAAAAAAAAAAAAAAAAAADAD53yG10t0C0992AnCwemIdySlXZQYAAAAAAAA0
AAAAAAAAAAAAAA0AM8xM7BhxWmKAjwli5gg7I5ZUHqwGAAAAAAAAAAAAAAAA
AAAAAAA=

TEXT;
        // }}}
        // {{{ random_seed data
        $randomSeedData = <<<TEXT
neyWWNvenulSaxVyMeIGkfDEfjYOk/fWdXdVziAlyCuTOaA3TQ3CKgFyUMCl
56z9k4gvoAVk4UKpspxvt0yvPh10wejwkb7MrFQ58f9mqSdhSxnP06dmxPaA
pmjTVaHQ74o7U/sitSM9DgCR/sKwmm7SFqP8LhELgHaIfx1AWbHNuTCSYdxH
z4SMMzTAiMq6cMKv86cpjAWMLABM5TFFKZzYtj+lpSowcuNkHWBU6qv7Y8Iw
ZMvcXX8cCvFWLVY9JNfQPvbb3WONrWmkqfExqAe2zKRyYUVP9mY9szAWwP0k
HqR1fICVuGVzJEzz//7AukOJ1TNiokgPoSW7KayJ1YjTuGyQc4JOOxvEdZAR
uXdEmD8V3yhQ/bN86OocZi/gT5+2Qorbk19hlHVAd4xw6HhFgBiqM0snhnQq
2YJOeVWuciAlnyetCV5DaOI5mUKhNOqT2EUo1qQ6vI9uZ8n+aJxD4DPH+AHo
wOzWzzsQujmaLmzulJtZnvHpwm/Sbi0qG9sohnxAD8INhnbRSviwROBgAR1r
HzticUERCJ6C+RGSUqa0YktAzNY5yZiyWEhPJfl16zC4vNFOdJmjWlf01ugJ
l7FlURvzHb3BkLJVlNCYNBRvW0yz2xXrhOzc1Nl5cpYGl3IWxopQDpHLHXYl
DVEKIlTgEQHq8FEU24xYX+GpvN1dZ9FjAuPjzNqAPEDzBQ4LB6JevUPL/hHN
ySy97NgZrauxTMC+SNrxl+Vab4JA9BgPCMXISmdQzzkZUFE5GV3Dq0O4OCiI
NfH1l4n9va0EcqKEwb25

TEXT;
        // }}}

        $directoryName = __DIR__ . '/' . self::HOMEDIR;
        if (!file_exists($directoryName)) {
            mkdir($directoryName);
        }

        $pubring = fopen($this->getKeyringFilename('pubring.gpg'), 'wb');
        fwrite($pubring, base64_decode(str_replace("\n", '', $pubringData)));
        fclose($pubring);

        $secring = fopen($this->getKeyringFilename('secring.gpg'), 'wb');
        fwrite($secring, base64_decode(str_replace("\n", '', $secringData)));
        fclose($secring);

        $trustdb = fopen($this->getKeyringFilename('trustdb.gpg'), 'wb');
        fwrite($trustdb, base64_decode(str_replace("\n", '', $trustdbData)));
        fclose($trustdb);

        $randomSeed = fopen($this->getKeyringFilename('random_seed'), 'wb');
        fwrite($randomSeed, base64_decode(str_replace("\n", '', $randomSeedData)));

        fclose($randomSeed);
    }

    private function _setUpTempdir()
    {
        $directoryName = __DIR__ . '/' . self::TEMPDIR;
        if (!file_exists($directoryName)) {
            mkdir($directoryName);
        }
    }

    public function tearDown(): void
    {
        unset($this->gpg);

        $this->_tearDownKeyring();
        $this->_tearDownTempdir();
    }

    private function _tearDownKeyring()
    {
        $dirnames = [
            $this->getKeyringFilename('private-keys-v1.d'),
            $this->getKeyringFilename('openpgp-revocs.d')
        ];

        foreach ($dirnames as $dirname) {
            if (file_exists($dirname)) {
                $iterator = new DirectoryIterator($dirname);
                foreach ($iterator as $file) {
                    if (!$file->isDot()) {
                        $filename = $dirname . '/' . $file->getFilename();
                        if (file_exists($filename)) {
                            unlink($filename);
                        }
                    }
                }
                rmdir($dirname);
            }
        }

        $homedir  = __DIR__ . '/' . self::HOMEDIR;
        $iterator = new DirectoryIterator($homedir);

        foreach ($iterator as $file) {
            if (!$file->isDot()) {
                $filename = $homedir . '/' . $file->getFilename();
                if (file_exists($filename)) {
                    unlink($filename);
                }
            }
        }

        rmdir($homedir);
    }

    private function _tearDownTempdir()
    {
        $directoryName = __DIR__ . '/' . self::TEMPDIR;

        // remove temporary files and temporary directory
        $iterator = new DirectoryIterator($directoryName);
        foreach ($iterator as $file) {
            if (!$file->isDot()) {
                $filename = $this->getTempFilename($file->getFilename());
                if (file_exists($filename)) {
                    if (is_dir($filename)) {
                        rmdir($filename);
                    } else {
                        unlink($filename);
                    }
                }
            }
        }

        rmdir($directoryName);
    }

    protected function getMd5Sum($filename)
    {
        return md5_file($filename);
    }

    protected function getKeyringFilename($filename)
    {
        return __DIR__ . '/'. self::HOMEDIR . '/' . $filename;
    }

    protected function getDataFilename($filename)
    {
        return __DIR__ . '/'. self::DATADIR . '/' . $filename;
    }

    protected function getTempFilename($filename)
    {
        return __DIR__ . '/' . self::TEMPDIR . '/' . $filename;
    }

    protected function assertDecryptAndVerifyResultsEquals(array $expected, array $actual, $csf = false)
    {
        $this->assertEquals(
            count($expected),
            count($actual),
            'Result counts are different.'
        );

        $this->assertArrayHasKey(
            'data',
            $expected,
            'Expected result does not include data.'
        );

        $this->assertArrayHasKey(
            'data',
            $actual,
            'Actual result does not include data.'
        );

        $this->assertArrayHasKey(
            'signatures',
            $expected,
            'Expected result does not include signatures.'
        );

        $this->assertArrayHasKey(
            'signatures',
            $actual,
            'Actual result does not include signatures.'
        );

        if ($csf && (substr($actual['data'], -1) != "\n")) {
            // see discussion around GnuPG's handling of trailing
            // newlines in CSF messages at https://dev.gnupg.org/T7106
            $actual['data'] = $actual['data']."\n";
        }
        $this->assertEquals(
            $expected['data'],
            $actual['data'],
            'Decrypted data does not match.'
        );

        $this->assertSignaturesEquals(
            $expected['signatures'],
            $actual['signatures']
        );
    }

    protected function assertSignaturesEquals(array $expected, array $actual)
    {
        $this->assertEquals(
            count($expected),
            count($actual),
            'Signature counts are different.'
        );

        for ($i = 0; $i < count($expected); $i++) {
            $this->assertSignatureEquals($expected[$i], $actual[$i]);
        }
    }

    protected function assertSignatureEquals(
        Crypt_GPG_Signature $expected,
        Crypt_GPG_Signature $actual
    ) {
        $expectedUserId = $expected->getUserId();
        $actualUserId   = $actual->getUserId();

        $this->assertEquals($expectedUserId, $actualUserId,
            'Signature user ids do not match.'
        );

        $expectedId = $expected->getId();
        $actualId = $actual->getId();

        $this->assertEquals(
            strlen($expectedId),
            strlen($actualId),
            'Signature IDs are of different length.'
        );

        $this->assertEquals(
            $expected->getKeyFingerprint(),
            $actual->getKeyFingerprint(),
            'Signature key fingerprints do not match.'
        );

        $this->assertEquals(
            $expected->getKeyId(),
            $actual->getKeyId(),
            'Signature key IDs do not match.'
        );

        $this->assertEquals(
            $expected->getCreationDate(),
            $actual->getCreationDate(),
            'Signature creation dates do not match.'
        );

        $this->assertEquals(
            $expected->getExpirationDate(),
            $actual->getExpirationDate(),
            'Signature expiration dates do not match.'
        );

        $this->assertEquals(
            $expected->isValid(),
            $actual->isValid(),
            'Signature validity does match.'
        );
    }

    protected function getPropertyValue($class, $object, $property)
    {
        $reflectionClass = new ReflectionClass($class);

        $prop = $reflectionClass->getProperty($property);

        $prop->setAccessible(true);

        return $prop->getValue($object);
    }

    /**
     * This method is used only when we run tests under PHPUnit 4.8 via Github Actions
     */
    protected function setExpectedExceptionMessage($message)
    {
        // do nothing
    }
}
