<?php

/**
 * This file supplies a memcached store backend for OpenID servers and
 * consumers.
 *
 * PHP versions 4 and 5
 *
 * LICENSE: See the COPYING file included in this distribution.
 *
 * @package OpenID
 * @author Artemy Tregubenko <me@arty.name>
 * @copyright 2008 JanRain, Inc.
 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
 * Contributed by Open Web Technologies <http://openwebtech.ru/>
 */

/**
 * Import the interface for creating a new store class.
 */
require_once 'Auth/OpenID/Interface.php';

/**
 * This is a memcached-based store for OpenID associations and
 * nonces.
 *
 * As memcache has limit of 250 chars for key length,
 * server_url, handle and salt are hashed with sha1().
 *
 * Most of the methods of this class are implementation details.
 * People wishing to just use this store need only pay attention to
 * the constructor.
 *
 * @package OpenID
 */
class Auth_OpenID_MemcachedStore extends Auth_OpenID_OpenIDStore {
    /** @var int */
    private $compress = 0;

    /** @var Memcache */
    private $connection;

    /**
     * Initializes a new {@link Auth_OpenID_MemcachedStore} instance.
     * Just saves memcached object as property.
     *
     * @param Memcache $connection Memcache connection resource
     * @param bool $compress
     */
    function __construct($connection, $compress = false)
    {
        $this->connection = $connection;
        $this->compress = $compress ? MEMCACHE_COMPRESSED : 0;
    }

    /**
     * Store association until its expiration time in memcached.
     * Overwrites any existing association with same server_url and
     * handle. Handles list of associations for every server.
     *
     * @param string $server_url
     * @param Auth_OpenID_Association $association
     */
    function storeAssociation($server_url, $association)
    {
        // create memcached keys for association itself
        // and list of associations for this server
        $associationKey = $this->associationKey($server_url,
            $association->handle);
        $serverKey = $this->associationServerKey($server_url);

        // get list of associations
        $serverAssociations = $this->connection->get($serverKey);

        // if no such list, initialize it with empty array
        if (!$serverAssociations) {
            $serverAssociations = [];
        }
        // and store given association key in it
        $serverAssociations[$association->issued] = $associationKey;

        // save associations' keys list
        $this->connection->set(
            $serverKey,
            $serverAssociations,
            $this->compress
        );
        // save association itself
        $this->connection->set(
            $associationKey,
            $association,
            $this->compress,
            $association->issued + $association->lifetime);
    }

    /**
     * Read association from memcached. If no handle given
     * and multiple associations found, returns latest issued
     *
     * @param string $server_url
     * @param null $handle
     * @return Auth_OpenID_Association|null
     */
    function getAssociation($server_url, $handle = null)
    {
        // simple case: handle given
        if ($handle !== null) {
            // get association, return null if failed
            $association = $this->connection->get(
                $this->associationKey($server_url, $handle));
            return $association ? $association : null;
        }

        // no handle given, working with list
        // create key for list of associations
        $serverKey = $this->associationServerKey($server_url);

        // get list of associations
        $serverAssociations = $this->connection->get($serverKey);
        // return null if failed or got empty list
        if (!$serverAssociations) {
            return null;
        }

        // get key of most recently issued association
        $keys = array_keys($serverAssociations);
        sort($keys);
        $lastKey = $serverAssociations[array_pop($keys)];

        // get association, return null if failed
        $association = $this->connection->get($lastKey);
        return $association ? $association : null;
    }

    /**
     * Immediately delete association from memcache.
     *
     * @param string $server_url
     * @param string $handle
     * @return bool|mixed
     */
    function removeAssociation($server_url, $handle)
    {
        // create memcached keys for association itself
        // and list of associations for this server
        $serverKey = $this->associationServerKey($server_url);
        $associationKey = $this->associationKey($server_url,
            $handle);

        // get list of associations
        $serverAssociations = $this->connection->get($serverKey);
        // return null if failed or got empty list
        if (!$serverAssociations) {
            return false;
        }

        // ensure that given association key exists in list
        $serverAssociations = array_flip($serverAssociations);
        if (!array_key_exists($associationKey, $serverAssociations)) {
            return false;
        }

        // remove given association key from list
        unset($serverAssociations[$associationKey]);
        $serverAssociations = array_flip($serverAssociations);

        // save updated list
        $this->connection->set(
            $serverKey,
            $serverAssociations,
            $this->compress
        );

        // delete association
        return $this->connection->delete($associationKey);
    }

    /**
     * Create nonce for server and salt, expiring after
     * $Auth_OpenID_SKEW seconds.
     *
     * @param string $server_url
     * @param int $timestamp
     * @param string $salt
     * @return bool
     */
    function useNonce($server_url, $timestamp, $salt)
    {
        global $Auth_OpenID_SKEW;

        // save one request to memcache when nonce obviously expired
        if (abs($timestamp - time()) > $Auth_OpenID_SKEW) {
            return false;
        }

        // returns false when nonce already exists
        // otherwise adds nonce
        return $this->connection->add(
            'openid_nonce_' . sha1($server_url) . '_' . sha1($salt),
            1, // any value here
            $this->compress,
            $Auth_OpenID_SKEW);
    }

    /**
     * Memcache key is prefixed with 'openid_association_' string.
     *
     * @param string $server_url
     * @param null $handle
     * @return string
     */
    function associationKey($server_url, $handle = null)
    {
        return 'openid_association_' . sha1($server_url) . '_' . sha1($handle);
    }

    /**
     * Memcache key is prefixed with 'openid_association_' string.
     *
     * @param string $server_url
     * @return string
     */
    function associationServerKey($server_url)
    {
        return 'openid_association_server_' . sha1($server_url);
    }

    /**
     * Report that this storage doesn't support cleanup
     */
    function supportsCleanup()
    {
        return false;
    }
}

