Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
46.88% covered (danger)
46.88%
30 / 64
69.23% covered (warning)
69.23%
9 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
TokenStoreKeyValue
46.88% covered (danger)
46.88%
30 / 64
69.23% covered (warning)
69.23%
9 / 13
175.09
0.00% covered (danger)
0.00%
0 / 1
 getToken
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clear
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAll
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 getPersistence
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
3.24
 getKey
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 clearAll
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 deleteAllExpiredByKey
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 getPersistenceManager
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSessionService
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContainer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2018-2023 (original work) Open Assessment Technologies SA.
19 */
20
21declare(strict_types=1);
22
23namespace oat\tao\model\security\xsrf;
24
25use common_exception_Error;
26use common_persistence_PhpRedisDriver;
27use Psr\Container\ContainerInterface;
28use oat\oatbox\session\SessionService;
29use oat\oatbox\service\ConfigurableService;
30use common_persistence_AdvKeyValuePersistence;
31use oat\generis\persistence\PersistenceManager;
32
33/**
34 * Class to store tokens in a key value storage
35 *
36 * @author Martijn Swinkels <m.swinkels@taotesting.com>
37 */
38class TokenStoreKeyValue extends ConfigurableService implements TokenStore
39{
40    public const OPTION_PERSISTENCE = 'persistence';
41    public const OPTION_TTL = 'ttl';
42
43    public const TOKENS_STORAGE_KEY = 'tao_tokens';
44
45    private common_persistence_AdvKeyValuePersistence $persistence;
46    private string $keyPrefix;
47
48    public function getToken(string $tokenId): ?Token
49    {
50        $tokenData = $this->getPersistence()->hGet($this->getKey(), $tokenId);
51
52        return is_string($tokenData) ? new Token(json_decode($tokenData, true)) : null;
53    }
54
55    public function setToken(string $tokenId, Token $token): void
56    {
57        $this->getPersistence()->hSet($this->getKey(), $tokenId, json_encode($token));
58    }
59
60    public function hasToken(string $tokenId): bool
61    {
62        return $this->getPersistence()->hExists($this->getKey(), $tokenId);
63    }
64
65    public function removeToken(string $tokenId): bool
66    {
67        return $this->getPersistence()->hDel($this->getKey(), $tokenId);
68    }
69
70    public function clear(): void
71    {
72        $this->getPersistence()->del($this->getKey());
73    }
74
75    /**
76     * @inheritDoc
77     */
78    public function getAll(): array
79    {
80        $tokensData = $this->getPersistence()->hGetAll($this->getKey());
81
82        if (!is_array($tokensData)) {
83            return [];
84        }
85
86        $tokens = [];
87
88        foreach ($tokensData as $tokenData) {
89            if (is_string($tokenData)) {
90                $tokens[] = new Token(json_decode($tokenData, true));
91            }
92        }
93
94        return $tokens;
95    }
96
97    /**
98     * @throws common_exception_Error
99     */
100    protected function getPersistence(): common_persistence_AdvKeyValuePersistence
101    {
102        if (!isset($this->persistence)) {
103            $persistence = $this->getPersistenceManager()->getPersistenceById(
104                $this->getOption(self::OPTION_PERSISTENCE)
105            );
106
107            if (!$persistence instanceof common_persistence_AdvKeyValuePersistence) {
108                throw new common_exception_Error(
109                    'TokenStoreKeyValue expects advanced key value persistence implementation.'
110                );
111            }
112
113            $this->persistence = $persistence;
114        }
115
116        return $this->persistence;
117    }
118
119    /**
120     * @throws common_exception_Error
121     */
122    protected function getKey(): string
123    {
124        if (!isset($this->keyPrefix)) {
125            $this->keyPrefix = sprintf(
126                '%s_%s',
127                $this->getSessionService()->getCurrentUser()->getIdentifier(),
128                self::TOKENS_STORAGE_KEY
129            );
130        }
131
132        return $this->keyPrefix;
133    }
134
135    /**
136     * @param int $uSleepInterval Microseconds interval between scan/keys to perform deletion
137     * @param int $timeLimit Expiration time for tokens, 0 means, no expiration
138     * @return int - The total deleted records
139     */
140    public function clearAll(int $uSleepInterval, int $timeLimit = 0): int
141    {
142        $persistence = $this->getPersistence();
143        $driver = $persistence->getDriver();
144        $pattern = sprintf('*%s*', self::TOKENS_STORAGE_KEY);
145        $countDeleted = 0;
146
147        if ($driver instanceof common_persistence_PhpRedisDriver) {
148            $iterator = null;
149
150            while ($iterator !== 0) {
151                foreach ($driver->scan($iterator, $pattern) as $key) {
152                    $countDeleted += $this->deleteAllExpiredByKey($key, $timeLimit);
153                }
154
155                usleep($uSleepInterval);
156            }
157
158            return $countDeleted;
159        }
160
161        $keys = $persistence->keys($pattern);
162        $keys = is_array($keys) ? $keys : [];
163
164        foreach ($keys as $key) {
165            $countDeleted += $this->deleteAllExpiredByKey($key, $timeLimit);
166        }
167
168        return $countDeleted;
169    }
170
171    private function deleteAllExpiredByKey(string $key, int $timeLimit): int
172    {
173        $persistence = $this->getPersistence();
174        $tokensData = $persistence->hGetAll($key);
175        $countDeleted = 0;
176
177        if (empty($tokensData)) {
178            if ($persistence->del($key)) {
179                return 1;
180            }
181        }
182
183        foreach ($tokensData as $tokenData) {
184            if (is_string($tokenData)) {
185                $token = new Token(json_decode($tokenData, true));
186
187                if (!$token->isExpired($timeLimit)) {
188                    continue;
189                }
190
191                if ($persistence->hDel($key, $token->getValue())) {
192                    $countDeleted++;
193                }
194            }
195        }
196
197        return $countDeleted;
198    }
199
200    private function getPersistenceManager(): PersistenceManager
201    {
202        return $this->getContainer()->get(PersistenceManager::SERVICE_ID);
203    }
204
205    private function getSessionService(): SessionService
206    {
207        return $this->getContainer()->get(SessionService::SERVICE_ID);
208    }
209
210    private function getContainer(): ContainerInterface
211    {
212        return $this->getServiceManager()->getContainer();
213    }
214}