1<?php
2
3/**
4 * This file supplies a memcached store backend for OpenID servers and
5 * consumers.
6 *
7 * PHP versions 4 and 5
8 *
9 * LICENSE: See the COPYING file included in this distribution.
10 *
11 * @package OpenID
12 * @author Artemy Tregubenko <me@arty.name>
13 * @copyright 2008 JanRain, Inc.
14 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
15 * Contributed by Open Web Technologies <http://openwebtech.ru/>
16 */
17
18/**
19 * Import the interface for creating a new store class.
20 */
21require_once 'Auth/OpenID/Interface.php';
22
23/**
24 * This is a memcached-based store for OpenID associations and
25 * nonces.
26 *
27 * As memcache has limit of 250 chars for key length,
28 * server_url, handle and salt are hashed with sha1().
29 *
30 * Most of the methods of this class are implementation details.
31 * People wishing to just use this store need only pay attention to
32 * the constructor.
33 *
34 * @package OpenID
35 */
36class Auth_OpenID_MemcachedStore extends Auth_OpenID_OpenIDStore {
37    /** @var int */
38    private $compress = 0;
39
40    /** @var Memcache */
41    private $connection;
42
43    /**
44     * Initializes a new {@link Auth_OpenID_MemcachedStore} instance.
45     * Just saves memcached object as property.
46     *
47     * @param Memcache $connection Memcache connection resource
48     * @param bool $compress
49     */
50    function __construct($connection, $compress = false)
51    {
52        $this->connection = $connection;
53        $this->compress = $compress ? MEMCACHE_COMPRESSED : 0;
54    }
55
56    /**
57     * Store association until its expiration time in memcached.
58     * Overwrites any existing association with same server_url and
59     * handle. Handles list of associations for every server.
60     *
61     * @param string $server_url
62     * @param Auth_OpenID_Association $association
63     */
64    function storeAssociation($server_url, $association)
65    {
66        // create memcached keys for association itself
67        // and list of associations for this server
68        $associationKey = $this->associationKey($server_url,
69            $association->handle);
70        $serverKey = $this->associationServerKey($server_url);
71
72        // get list of associations
73        $serverAssociations = $this->connection->get($serverKey);
74
75        // if no such list, initialize it with empty array
76        if (!$serverAssociations) {
77            $serverAssociations = [];
78        }
79        // and store given association key in it
80        $serverAssociations[$association->issued] = $associationKey;
81
82        // save associations' keys list
83        $this->connection->set(
84            $serverKey,
85            $serverAssociations,
86            $this->compress
87        );
88        // save association itself
89        $this->connection->set(
90            $associationKey,
91            $association,
92            $this->compress,
93            $association->issued + $association->lifetime);
94    }
95
96    /**
97     * Read association from memcached. If no handle given
98     * and multiple associations found, returns latest issued
99     *
100     * @param string $server_url
101     * @param null $handle
102     * @return Auth_OpenID_Association|null
103     */
104    function getAssociation($server_url, $handle = null)
105    {
106        // simple case: handle given
107        if ($handle !== null) {
108            // get association, return null if failed
109            $association = $this->connection->get(
110                $this->associationKey($server_url, $handle));
111            return $association ? $association : null;
112        }
113
114        // no handle given, working with list
115        // create key for list of associations
116        $serverKey = $this->associationServerKey($server_url);
117
118        // get list of associations
119        $serverAssociations = $this->connection->get($serverKey);
120        // return null if failed or got empty list
121        if (!$serverAssociations) {
122            return null;
123        }
124
125        // get key of most recently issued association
126        $keys = array_keys($serverAssociations);
127        sort($keys);
128        $lastKey = $serverAssociations[array_pop($keys)];
129
130        // get association, return null if failed
131        $association = $this->connection->get($lastKey);
132        return $association ? $association : null;
133    }
134
135    /**
136     * Immediately delete association from memcache.
137     *
138     * @param string $server_url
139     * @param string $handle
140     * @return bool|mixed
141     */
142    function removeAssociation($server_url, $handle)
143    {
144        // create memcached keys for association itself
145        // and list of associations for this server
146        $serverKey = $this->associationServerKey($server_url);
147        $associationKey = $this->associationKey($server_url,
148            $handle);
149
150        // get list of associations
151        $serverAssociations = $this->connection->get($serverKey);
152        // return null if failed or got empty list
153        if (!$serverAssociations) {
154            return false;
155        }
156
157        // ensure that given association key exists in list
158        $serverAssociations = array_flip($serverAssociations);
159        if (!array_key_exists($associationKey, $serverAssociations)) {
160            return false;
161        }
162
163        // remove given association key from list
164        unset($serverAssociations[$associationKey]);
165        $serverAssociations = array_flip($serverAssociations);
166
167        // save updated list
168        $this->connection->set(
169            $serverKey,
170            $serverAssociations,
171            $this->compress
172        );
173
174        // delete association
175        return $this->connection->delete($associationKey);
176    }
177
178    /**
179     * Create nonce for server and salt, expiring after
180     * $Auth_OpenID_SKEW seconds.
181     *
182     * @param string $server_url
183     * @param int $timestamp
184     * @param string $salt
185     * @return bool
186     */
187    function useNonce($server_url, $timestamp, $salt)
188    {
189        global $Auth_OpenID_SKEW;
190
191        // save one request to memcache when nonce obviously expired
192        if (abs($timestamp - time()) > $Auth_OpenID_SKEW) {
193            return false;
194        }
195
196        // returns false when nonce already exists
197        // otherwise adds nonce
198        return $this->connection->add(
199            'openid_nonce_' . sha1($server_url) . '_' . sha1($salt),
200            1, // any value here
201            $this->compress,
202            $Auth_OpenID_SKEW);
203    }
204
205    /**
206     * Memcache key is prefixed with 'openid_association_' string.
207     *
208     * @param string $server_url
209     * @param null $handle
210     * @return string
211     */
212    function associationKey($server_url, $handle = null)
213    {
214        return 'openid_association_' . sha1($server_url) . '_' . sha1($handle);
215    }
216
217    /**
218     * Memcache key is prefixed with 'openid_association_' string.
219     *
220     * @param string $server_url
221     * @return string
222     */
223    function associationServerKey($server_url)
224    {
225        return 'openid_association_server_' . sha1($server_url);
226    }
227
228    /**
229     * Report that this storage doesn't support cleanup
230     */
231    function supportsCleanup()
232    {
233        return false;
234    }
235}
236
237