<?php

/**
 * Class PayPal_Button
 *
 * Needs to be compatible with PHP 5.2 - no namespacing, no late static binding, no shorthand arrays
 *
 * @author Georgi Boiko <georgi.boiko@mtcmedia.co.uk>
 * @date   2015.2.18
 *
 * @link   http://blog.scrobbld.com/paypal/protecting-your-payments-with-ewp/
 */
class PayPal_Button
{
    /** @var string $certificate Certificate resource */
    protected $certificate;
    /** @var string $certificate_file Path to the certificate file */
    protected $certificate_file;

    /** @var string $private_key Private key resource (matching certificate) */
    protected $private_key;
    /** @var string $private_key_file Path to the private key file */
    protected $private_key_file;
    /** @var string $paypal_certificate PayPal public certificate resource */
    protected $paypal_certificate;
    /** @var string $paypal_certificate_file Path to PayPal public certificate file */
    protected $paypal_certificate_file;
    /** @var string $certificate_id ID assigned by PayPal to the $certificate. */
    protected $certificate_id;
    /** @var string $temp_file_directory */
    protected $temp_file_directory;

    /**
     * Shortcut factory
     *
     * @param string $certificate_file
     * @param string $private_key_file
     * @param string $certificate_id
     * @param string $paypal_certificate
     *
     * @return PayPal_Button
     */
    public static function make($certificate_file, $private_key_file, $certificate_id, $paypal_certificate)
    {
        $instance = new self();
        $instance->setTempFileDirectory('/tmp');
        $instance->setCertificate($certificate_file, $private_key_file);
        $instance->setCertificateId($certificate_id);
        $instance->setPayPalCertificate($paypal_certificate);

        return $instance;
    }

    /**
     * setCertificate: set the client certificate and private key pair.
     *
     * @param string $certificate_filename - The path to the client certificate
     * @param string $private_key_filename - The path to the private key corresponding to the certificate
     *
     * @return $this
     * @throws Exception
     */
    public function setCertificate($certificate_filename, $private_key_filename)
    {

        if (is_readable($certificate_filename) && is_readable($private_key_filename)) {
            $certificate =
                openssl_x509_read(file_get_contents($certificate_filename));

            $private_key =
                openssl_get_privatekey(file_get_contents($private_key_filename));

            $cert_and_key_valid = ($certificate !== false) && ($private_key !== false);
            if ($cert_and_key_valid && openssl_x509_check_private_key($certificate, $private_key)) {
                $this->certificate = $certificate;
                $this->certificate_file = $certificate_filename;

                $this->private_key = $private_key;
                $this->private_key_file = $private_key_filename;

                return $this;
            }
        }

        throw new Exception('PayPal button certificate and key don\'t match!');
    }

    /**
     * setCertificateID: Sets the ID assigned by PayPal to the client certificate
     *
     * @param string $id
     */
    public function setCertificateId($id)
    {
        $this->certificate_id = $id;
    }

    /**
     * setPayPalCertificate: Sets the PayPal certificate
     *
     * @param string $file_name - The path to the PayPal certificate.
     *
     * @return $this
     * @throws Exception
     */
    public function setPayPalCertificate($file_name)
    {
        if (is_readable($file_name)) {
            $certificate = openssl_x509_read(file_get_contents($file_name));

            if ($certificate !== false) {
                $this->paypal_certificate = $certificate;
                $this->paypal_certificate_file = $file_name;

                return $this;
            }
        }

        throw new Exception('Could not read PayPal button certificate!');
    }

    /**
     * setTempFileDirectory: Sets the directory into which temporary files are written.
     *
     * @param string $directory - Directory in which to write temporary files.
     *
     * @return $this
     * @throws Exception
     */
    public function setTempFileDirectory($directory)
    {
        if (is_dir($directory) && is_writable($directory)) {
            $this->temp_file_directory = $directory;

            return $this;
        } else {
            throw new Exception('PayPal button temporary directory not accessible!');
        }
    }

    /**
     * encrypt: Using the previously set certificates and tempFileDirectory
     * encrypt the button information.
     *
     * @param array $parameters - Array with parameter names as keys.
     *
     * @return string The encrypted string for the _s_xclick button form field.
     * @throws Exception
     */
    public function encrypt($parameters)
    {
        // Check encryption data is available.
        if (!$this->isReady()) {
            throw new Exception('PayPal button encryption not ready!');
        }

        // Compose plain text data.
        $parameters['cert_id'] = $this->certificate_id;
        $plain_text = $this->makeString($parameters);

        $clear_file = tempnam($this->temp_file_directory, 'clear_');
        $signed_file = preg_replace('/clear/', 'signed', $clear_file);
        $encrypted_file = preg_replace('/clear/', 'encrypted', $clear_file);

        $out = fopen($clear_file, 'wb');
        fwrite($out, $plain_text);
        fclose($out);

        if (!openssl_pkcs7_sign(
            $clear_file,
            $signed_file,
            $this->certificate,
            $this->private_key,
            array(),
            PKCS7_BINARY
        )
        ) {
            throw new Exception('PayPal button could not be signed with provided credentials!');
        }

        $signed_data = explode("\n\n", file_get_contents($signed_file));

        $out = fopen($signed_file, 'wb');
        fwrite($out, base64_decode($signed_data[1]));
        fclose($out);

        if (!openssl_pkcs7_encrypt($signed_file, $encrypted_file, $this->paypal_certificate, array(), PKCS7_BINARY)) {
            throw new Exception('PayPal button could not be encrypted with provided credentials!');
        }

        $encryptedData = explode("\n\n", file_get_contents($encrypted_file));

        $cipher_text = $encryptedData[1];

        @unlink($clear_file);
        @unlink($signed_file);
        @unlink($encrypted_file);

        return $cipher_text;
    }

    /**
     * @param string $cipher_text
     *
     * @return string
     */
    public function wrapInPkcsHeader($cipher_text)
    {
        return '-----BEGIN PKCS7-----' . str_replace("\n", "", $cipher_text) . '-----END PKCS7-----';
    }

    /**
     * @return bool
     */
    protected function isReady()
    {
        return ($this->certificate_id != '') && isset($this->certificate) && isset($this->paypal_certificate);
    }

    /**
     * @param array $params
     *
     * @return string
     */
    protected function makeString($params)
    {
        $string = '';

        foreach ($params as $key => $value) {
            if ($value != '') {
                $string .= $key . '=' . $value . "\n";
            }
        }

        return $string;
    }

    /**
     * @param array $params
     * @param bool  $encrypt
     *
     * @return string
     */
    public function getFormFields($params, $encrypt = true)
    {
        if ($encrypt === true) {
            return $this->getEncryptedFormFields($params);
        } else {
            return $this->getPlaintextFormFields($params);
        }
    }

    /**
     * @param array $params
     *
     * @return string
     */
    protected function getEncryptedFormFields($params)
    {
        $fields = "\n";

        /**
         * For encrypted buttons you have to submit CMD as '_s-xclick' without crypto and
         * as whatever your normal value is inside ciphertext, which is '_xclick' on our form page
         */
        $fields .= $this->makeField('cmd', '_s-xclick');

        $fields .= $this->makeField('encrypted', $this->wrapInPkcsHeader($this->encrypt($params)));

        return $fields;
    }

    /**
     * @param array $params
     *
     * @return string
     */
    protected function getPlaintextFormFields($params)
    {
        $fields = "\n";

        foreach ($params as $name => $value) {
            $fields .= $this->makeField($name, $value);
            $fields .= "\n";
        }

        return $fields;
    }

    /**
     * @param string $name
     * @param string $value
     *
     * @return string
     */
    protected function makeField($name, $value)
    {
        $field = '<input type="hidden" name="' . $name . '" value="' . $value . '" />';

        return $field;
    }
}