Completed
Pull Request — devel (#13)
by Philippe
02:33
created

ClientService::hasUser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
c 0
b 0
f 0
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 4
nc 1
nop 2
crap 1
1
<?php
2
/**
3
 * ClientService.php
4
 *
5
 * PHP version 5.6+
6
 *
7
 * @author Philippe Gaultier <[email protected]>
8
 * @copyright 2010-2017 Philippe Gaultier
9
 * @license http://www.sweelix.net/license license
10
 * @version 1.1.0
11
 * @link http://www.sweelix.net
12
 * @package sweelix\oauth2\server\services\redis
13
 */
14
15
namespace sweelix\oauth2\server\services\redis;
16
17
use sweelix\oauth2\server\exceptions\DuplicateIndexException;
18
use sweelix\oauth2\server\exceptions\DuplicateKeyException;
19
use sweelix\oauth2\server\interfaces\ClientModelInterface;
20
use sweelix\oauth2\server\interfaces\ClientServiceInterface;
21
use yii\db\Exception as DatabaseException;
22
use Yii;
23
24
/**
25
 * This is the client service for redis
26
 *  database structure
27
 *    * oauth2:clients:<cid> : hash (Client)
28
 *    * oauth2:clients:<cid>:users : set
29
 *    * oauth2:users:<uid>:clients : set
30
 *
31
 * @author Philippe Gaultier <[email protected]>
32
 * @copyright 2010-2017 Philippe Gaultier
33
 * @license http://www.sweelix.net/license license
34
 * @version 1.1.0
35
 * @link http://www.sweelix.net
36
 * @package sweelix\oauth2\server\services\redis
37
 * @since 1.0.0
38
 */
39
class ClientService extends BaseService implements ClientServiceInterface
40
{
41
42
    /**
43
     * @var string user namespace (collection for clients)
44
     */
45
    public $userNamespace = '';
46
47
    /**
48
     * @param string $cid client ID
49
     * @return string client Key
50
     * @since 1.0.0
51
     */
52 25
    protected function getClientKey($cid)
53
    {
54 25
        return $this->namespace . ':' . $cid;
55
    }
56
57
    /**
58
     * @param string $cid client ID
59
     * @return string clientUsers Key
60
     * @since 1.0.0
61
     */
62 6
    protected function getClientUsersListKey($cid)
63
    {
64 6
        return $this->namespace . ':' . $cid . ':users';
65
    }
66
67
    /**
68
     * @param string $uid user ID
69
     * @return string user clients collection Key
70
     * @since XXX
71
     */
72 5
    protected function getUserClientsListKey($uid)
73
    {
74 5
        return $this->userNamespace . ':' . $uid . ':clients';
75
    }
76
77
    /**
78
     * @inheritdoc
79
     */
80 21
    public function save(ClientModelInterface $client, $attributes)
81
    {
82 21
        if ($client->getIsNewRecord()) {
83 21
            $result = $this->insert($client, $attributes);
84 21
        } else {
85 2
            $result = $this->update($client, $attributes);
86
        }
87 21
        return $result;
88
    }
89
90
    /**
91
     * Save Client
92
     * @param ClientModelInterface $client
93
     * @param null|array $attributes attributes to save
94
     * @return bool
95
     * @throws DatabaseException
96
     * @throws DuplicateIndexException
97
     * @throws DuplicateKeyException
98
     * @since 1.0.0
99
     */
100 21
    protected function insert(ClientModelInterface $client, $attributes)
101
    {
102 21
        $result = false;
103 21
        if (!$client->beforeSave(true)) {
104
            return $result;
105
        }
106 21
        $clientKey = $this->getClientKey($client->getKey());
107
        //check if record exists
108 21
        $entityStatus = (int)$this->db->executeCommand('EXISTS', [$clientKey]);
109 21
        if ($entityStatus === 1) {
110 1
            throw new DuplicateKeyException('Duplicate key "'.$clientKey.'"');
111
        }
112
113 21
        $values = $client->getDirtyAttributes($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 100 can also be of type array; however, sweelix\oauth2\server\in...e::getDirtyAttributes() does only seem to accept array<integer,string>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
114 21
        $redisParameters = [$clientKey];
115 21
        $this->setAttributesDefinitions($client->attributesDefinition());
116 21
        foreach ($values as $key => $value)
117
        {
118 21
            if ($value !== null) {
119 21
                $redisParameters[] = $key;
120 21
                $redisParameters[] = $this->convertToDatabase($key, $value);
121 21
            }
122 21
        }
123
        //TODO: use EXEC/MULTI to avoid errors
124 21
        $transaction = $this->db->executeCommand('MULTI');
125 21
        if ($transaction === true) {
126
            try {
127 21
                $this->db->executeCommand('HMSET', $redisParameters);
128 21
                $this->db->executeCommand('EXEC');
129 21
            } catch (DatabaseException $e) {
130
                // @codeCoverageIgnoreStart
131
                // we have a REDIS exception, we should not discard
132
                Yii::trace('Error while inserting entity', __METHOD__);
133
                throw $e;
134
                // @codeCoverageIgnoreEnd
135
            }
136 21
        }
137 21
        $changedAttributes = array_fill_keys(array_keys($values), null);
138 21
        $client->setOldAttributes($values);
139 21
        $client->afterSave(true, $changedAttributes);
140 21
        $result = true;
141 21
        return $result;
142
    }
143
144
145
    /**
146
     * Update Client
147
     * @param ClientModelInterface $client
148
     * @param null|array $attributes attributes to save
149
     * @return bool
150
     * @throws DatabaseException
151
     * @throws DuplicateIndexException
152
     * @throws DuplicateKeyException
153
     */
154 2
    protected function update(ClientModelInterface $client, $attributes)
155
    {
156 2
        if (!$client->beforeSave(false)) {
157
            return false;
158
        }
159
160 2
        $values = $client->getDirtyAttributes($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 154 can also be of type array; however, sweelix\oauth2\server\in...e::getDirtyAttributes() does only seem to accept array<integer,string>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
161 2
        $modelKey = $client->key();
162 2
        $clientId = isset($values[$modelKey]) ? $values[$modelKey] : $client->getKey();
163 2
        $clientKey = $this->getClientKey($clientId);
164
165
166 2
        if (isset($values[$modelKey]) === true) {
167 1
            $newClientKey = $this->getClientKey($values[$modelKey]);
168 1
            $entityStatus = (int)$this->db->executeCommand('EXISTS', [$newClientKey]);
169 1
            if ($entityStatus === 1) {
170 1
                throw new DuplicateKeyException('Duplicate key "'.$newClientKey.'"');
171
            }
172 1
        }
173
174 2
        $this->db->executeCommand('MULTI');
175
        try {
176 2
            if (array_key_exists($modelKey, $values) === true) {
177 1
                $oldId = $client->getOldKey();
178 1
                $oldClientKey = $this->getClientKey($oldId);
179
180 1
                $this->db->executeCommand('RENAMENX', [$oldClientKey, $clientKey]);
181 1
            }
182
183 2
            $redisUpdateParameters = [$clientKey];
184 2
            $redisDeleteParameters = [$clientKey];
185 2
            $this->setAttributesDefinitions($client->attributesDefinition());
186 2
            foreach ($values as $key => $value)
187
            {
188 2
                if ($value === null) {
189 1
                    $redisDeleteParameters[] = $key;
190 1
                } else {
191 2
                    $redisUpdateParameters[] = $key;
192 2
                    $redisUpdateParameters[] = $this->convertToDatabase($key, $value);
193
                }
194 2
            }
195 2
            if (count($redisDeleteParameters) > 1) {
196 1
                $this->db->executeCommand('HDEL', $redisDeleteParameters);
197 1
            }
198 2
            if (count($redisUpdateParameters) > 1) {
199 2
                $this->db->executeCommand('HMSET', $redisUpdateParameters);
200 2
            }
201
202 2
            $this->db->executeCommand('EXEC');
203 2
        } catch (DatabaseException $e) {
204
            // @codeCoverageIgnoreStart
205
            // we have a REDIS exception, we should not discard
206
            Yii::trace('Error while updating entity', __METHOD__);
207
            throw $e;
208
            // @codeCoverageIgnoreEnd
209
        }
210
211 2
        $changedAttributes = [];
212 2
        foreach ($values as $name => $value) {
213 2
            $oldAttributes = $client->getOldAttributes();
214 2
            $changedAttributes[$name] = isset($oldAttributes[$name]) ? $oldAttributes[$name] : null;
215 2
            $client->setOldAttribute($name, $value);
216 2
        }
217 2
        $client->afterSave(false, $changedAttributes);
218 2
        return true;
219
    }
220
221
    /**
222
     * @inheritdoc
223
     */
224 23
    public function findOne($key)
225
    {
226 23
        $record = null;
227 23
        $clientKey = $this->getClientKey($key);
228 23
        $clientExists = (bool)$this->db->executeCommand('EXISTS', [$clientKey]);
229 23
        if ($clientExists === true) {
230 18
            $clientData = $this->db->executeCommand('HGETALL', [$clientKey]);
231 18
            $record = Yii::createObject('sweelix\oauth2\server\interfaces\ClientModelInterface');
232
            /** @var ClientModelInterface $record */
233 18
            $properties = $record->attributesDefinition();
234 18
            $this->setAttributesDefinitions($properties);
235 18
            $attributes = [];
236 18
            for ($i = 0; $i < count($clientData); $i += 2) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
237 18
                if (isset($properties[$clientData[$i]]) === true) {
238 18
                    $clientData[$i + 1] = $this->convertToModel($clientData[$i], $clientData[($i + 1)]);
239 18
                    $record->setAttribute($clientData[$i], $clientData[$i + 1]);
240 18
                    $attributes[$clientData[$i]] = $clientData[$i + 1];
241
                // @codeCoverageIgnoreStart
242
                } elseif ($record->canSetProperty($clientData[$i])) {
243
                    // TODO: find a way to test attribute population
244
                    $record->{$clientData[$i]} = $clientData[$i + 1];
245
                }
246
                // @codeCoverageIgnoreEnd
247 18
            }
248 18
            if (empty($attributes) === false) {
249 18
                $record->setOldAttributes($attributes);
250 18
            }
251 18
            $record->afterFind();
252 18
        }
253 23
        return $record;
254
    }
255
256
    /**
257
     * @inheritdoc
258
     */
259 1
    public function delete(ClientModelInterface $client)
260
    {
261 1
        $result = false;
262 1
        if ($client->beforeDelete()) {
263 1
            $id = $client->getOldKey();
264 1
            $clientKey = $this->getClientKey($id);
265 1
            $clientUsersListKey = $this->getClientUsersListKey($id);
266
267
268
            // before cleaning the client, drop all access tokens and refresh tokens
269 1
            $token = Yii::createObject('sweelix\oauth2\server\interfaces\RefreshTokenModelInterface');
270 1
            $tokenClass = get_class($token);
271 1
            $tokenClass::deleteAllByClientId($id);
272
273 1
            $token = Yii::createObject('sweelix\oauth2\server\interfaces\AccessTokenModelInterface');
274 1
            $tokenClass = get_class($token);
275 1
            $tokenClass::deleteAllByClientId($id);
276
277 1
            $usersList = $this->db->executeCommand('SMEMBERS', [$clientUsersListKey]);
278
279 1
            $this->db->executeCommand('MULTI');
280
            // remove client from all userClient sets
281 1
            foreach($usersList as $user) {
282
                $userClientKey = $this->getUserClientsListKey($user);
283
                $this->db->executeCommand('SREM', [$userClientKey, $id]);
284 1
            }
285 1
            $this->db->executeCommand('DEL', [$clientKey]);
286 1
            $this->db->executeCommand('DEL', [$clientUsersListKey]);
287
            //TODO: check results to return correct information
288 1
            $queryResult = $this->db->executeCommand('EXEC');
0 ignored issues
show
Unused Code introduced by
$queryResult is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
289 1
            $client->setIsNewRecord(true);
290 1
            $client->afterDelete();
291 1
            $result = true;
292 1
        }
293 1
        return $result;
294
    }
295
296
    /**
297
     * @inheritdoc
298
     */
299 5
    public function hasUser(ClientModelInterface $client, $userId)
300
    {
301 5
        $key = $client->getKey();
302 5
        $clientUsersListKey = $this->getClientUsersListKey($key);
303 5
        return (bool)$this->db->executeCommand('SISMEMBER', [$clientUsersListKey, $userId]);
304
    }
305
306
    /**
307
     * @inheritdoc
308
     */
309 3
    public function addUser(ClientModelInterface $client, $userId)
310
    {
311 3
        $key = $client->getKey();
312 3
        $clientUsersListKey = $this->getClientUsersListKey($key);
313 3
        $userClientsListKey = $this->getUserClientsListKey($userId);
314 3
        $this->db->executeCommand('MULTI');
315 3
        $this->db->executeCommand('SADD', [$clientUsersListKey, $userId]);
316 3
        $this->db->executeCommand('SADD', [$userClientsListKey, $key]);
317 3
        $queryResult = $this->db->executeCommand('EXEC');
0 ignored issues
show
Unused Code introduced by
$queryResult is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
318
        //TODO: check if we should send back false or not
319 3
        return true;
320
    }
321
322
    /**
323
     * @inheritdoc
324
     */
325 4
    public function removeUser(ClientModelInterface $client, $userId)
326
    {
327 4
        $key = $client->getKey();
328 4
        $clientUsersListKey = $this->getClientUsersListKey($key);
329 4
        $userClientsListKey = $this->getUserClientsListKey($userId);
330 4
        $this->db->executeCommand('MULTI');
331 4
        $this->db->executeCommand('SREM', [$clientUsersListKey, $userId]);
332 4
        $this->db->executeCommand('SREM', [$userClientsListKey, $key]);
333 4
        $queryResult = $this->db->executeCommand('EXEC');
0 ignored issues
show
Unused Code introduced by
$queryResult is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
334
        //TODO: check if we should send back false or not
335 4
        return true;
336
    }
337
338
    /**
339
     * @inheritdoc
340
     */
341 1
    public function findAllByUserId($userId)
342
    {
343 1
        $userClientsListKey = $this->getUserClientsListKey($userId);
344 1
        $clientsList = $this->db->executeCommand('SMEMBERS', [$userClientsListKey]);
345 1
        $clients = [];
346 1
        foreach($clientsList as $clientId) {
347 1
            $result = $this->findOne($clientId);
348 1
            if ($result instanceof ClientModelInterface) {
349 1
                $clients[] = $result;
350 1
            }
351 1
        }
352 1
        return $clients;
353
    }
354
355
}
356