<?php
/**
 * Copyright 2010-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 * http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

namespace Aws\Common\Credentials;

use Aws\Common\Enum\ClientOptions as Options;
use Aws\Common\Exception\InvalidArgumentException;
use Aws\Common\Exception\RequiredExtensionNotLoadedException;
use Aws\Common\Exception\RuntimeException;
use Guzzle\Common\FromConfigInterface;
use Guzzle\Cache\CacheAdapterInterface;
use Guzzle\Cache\DoctrineCacheAdapter;
use Guzzle\Common\Collection;

/**
 * Basic implementation of the AWSCredentials interface that allows callers to
 * pass in the AWS access key and secret access in the constructor.
 */
class Credentials implements CredentialsInterface, FromConfigInterface
{
    const ENV_KEY = 'AWS_ACCESS_KEY_ID';
    const ENV_SECRET = 'AWS_SECRET_KEY';
    const ENV_SECRET_ACCESS_KEY = 'AWS_SECRET_ACCESS_KEY';

    /** @var string AWS Access key ID */
    protected $key;

    /** @var string AWS Secret access key */
    protected $secret;

    /** @var string Security token */
    protected $token;

    /** @var int Time to die of token */
    protected $ttd;

    /**
     * Get the available keys for the factory method
     *
     * @return array
     */
    public static function getConfigDefaults()
    {
        return array(
            Options::KEY                   => null,
            Options::SECRET                => null,
            Options::TOKEN                 => null,
            Options::TOKEN_TTD             => null,
            Options::CREDENTIALS_CACHE     => null,
            Options::CREDENTIALS_CACHE_KEY => null,
            Options::CREDENTIALS_CLIENT    => null
        );
    }

    /**
     * Factory method for creating new credentials.  This factory method will
     * create the appropriate credentials object with appropriate decorators
     * based on the passed configuration options.
     *
     * @param array $config Options to use when instantiating the credentials
     *
     * @return CredentialsInterface
     * @throws InvalidArgumentException If the caching options are invalid
     * @throws RuntimeException         If using the default cache and APC is disabled
     */
    public static function factory($config = array())
    {
        // Add default key values
        foreach (self::getConfigDefaults() as $key => $value) {
            if (!isset($config[$key])) {
                $config[$key] = $value;
            }
        }

        // Start tracking the cache key
        $cacheKey = $config[Options::CREDENTIALS_CACHE_KEY];

        // Create the credentials object
        if (!$config[Options::KEY] || !$config[Options::SECRET]) {
            $credentials = self::createFromEnvironment($config);
            // If no cache key was set, use the crc32 hostname of the server
            $cacheKey = $cacheKey ?: 'credentials_' . crc32(gethostname());
        } else {
            // Instantiate using short or long term credentials
            $credentials = new static(
                $config[Options::KEY],
                $config[Options::SECRET],
                $config[Options::TOKEN],
                $config[Options::TOKEN_TTD]
            );
            // If no cache key was set, use the access key ID
            $cacheKey = $cacheKey ?: 'credentials_' . $config[Options::KEY];
        }

        // Check if the credentials are refreshable, and if so, configure caching
        $cache = $config[Options::CREDENTIALS_CACHE];
        if ($cacheKey && $cache) {
            $credentials = self::createCache($credentials, $cache, $cacheKey);
        }

        return $credentials;
    }

    /**
     * Constructs a new BasicAWSCredentials object, with the specified AWS
     * access key and AWS secret key
     *
     * @param string $accessKeyId     AWS access key ID
     * @param string $secretAccessKey AWS secret access key
     * @param string $token           Security token to use
     * @param int    $expiration      UNIX timestamp for when credentials expire
     */
    public function __construct($accessKeyId, $secretAccessKey, $token = null, $expiration = null)
    {
        $this->key = trim($accessKeyId);
        $this->secret = trim($secretAccessKey);
        $this->token = $token;
        $this->ttd = $expiration;
    }

    public function serialize()
    {
        return json_encode(array(
            Options::KEY       => $this->key,
            Options::SECRET    => $this->secret,
            Options::TOKEN     => $this->token,
            Options::TOKEN_TTD => $this->ttd
        ));
    }

    public function unserialize($serialized)
    {
        $data = json_decode($serialized, true);
        $this->key    = $data[Options::KEY];
        $this->secret = $data[Options::SECRET];
        $this->token  = $data[Options::TOKEN];
        $this->ttd    = $data[Options::TOKEN_TTD];
    }

    public function getAccessKeyId()
    {
        return $this->key;
    }

    public function getSecretKey()
    {
        return $this->secret;
    }

    public function getSecurityToken()
    {
        return $this->token;
    }

    public function getExpiration()
    {
        return $this->ttd;
    }

    public function isExpired()
    {
        return $this->ttd !== null && time() >= $this->ttd;
    }

    public function setAccessKeyId($key)
    {
        $this->key = $key;

        return $this;
    }

    public function setSecretKey($secret)
    {
        $this->secret = $secret;

        return $this;
    }

    public function setSecurityToken($token)
    {
        $this->token = $token;

        return $this;
    }

    public function setExpiration($timestamp)
    {
        $this->ttd = $timestamp;

        return $this;
    }

    /**
     * When no keys are provided, attempt to create them based on the
     * environment or instance profile credentials.
     *
     * @param array|Collection $config
     *
     * @return CredentialsInterface
     */
    private static function createFromEnvironment($config)
    {
        $envKey = isset($_SERVER[self::ENV_KEY])
            ? $_SERVER[self::ENV_KEY]
            : getenv(self::ENV_KEY);

        $envSecret = isset($_SERVER[self::ENV_SECRET])
            ? $_SERVER[self::ENV_SECRET]
            : getenv(self::ENV_SECRET);

        // Use AWS_SECRET_ACCESS_KEY if AWS_SECRET_KEY was not set.
        if (!$envSecret) {
            $envSecret = isset($_SERVER[self::ENV_SECRET_ACCESS_KEY])
                ? $_SERVER[self::ENV_SECRET_ACCESS_KEY]
                : getenv(self::ENV_SECRET_ACCESS_KEY);
        }

        if ($envKey && $envSecret) {
            // Use credentials set in the environment variables
            $credentials = new static($envKey, $envSecret);
        } else {
            // Use instance profile credentials (available on EC2 instances)
            $credentials = new RefreshableInstanceProfileCredentials(
                new static('', '', '', 1),
                $config[Options::CREDENTIALS_CLIENT]
            );
        }

        return $credentials;
    }

    private static function createCache(CredentialsInterface $credentials, $cache, $cacheKey)
    {
        if ($cache === 'true' || $cache === true) {
            // If no cache adapter was provided, then create one for the user
            // @codeCoverageIgnoreStart
            if (!extension_loaded('apc')) {
                throw new RequiredExtensionNotLoadedException('PHP has not been compiled with APC. Unable to cache '
                    . 'the credentials.');
            } elseif (!class_exists('Doctrine\Common\Cache\ApcCache')) {
                throw new RuntimeException(
                    'Cannot set ' . Options::CREDENTIALS_CACHE . ' to true because the Doctrine cache component is '
                    . 'not installed. Either install doctrine/cache or pass in an instantiated '
                    . 'Guzzle\Cache\CacheAdapterInterface object'
                );
            }
            // @codeCoverageIgnoreEnd
            $cache = new DoctrineCacheAdapter(new \Doctrine\Common\Cache\ApcCache());
        } elseif (!($cache instanceof CacheAdapterInterface)) {
            throw new InvalidArgumentException('Unable to utilize caching with the specified options');
        }

        // Decorate the credentials with a cache
        return new CacheableCredentials($credentials, $cache, $cacheKey);
    }
}
