Completed
Push — master ( bceb3a...6ded55 )
by Schlaefer
06:24 queued 03:26
created

UserOnlineTable::validationDefault()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 18
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Saito - The Threaded Web Forum
7
 *
8
 * @copyright Copyright (c) the Saito Project Developers
9
 * @link https://github.com/Schlaefer/Saito
10
 * @license http://opensource.org/licenses/MIT
11
 */
12
13
namespace App\Model\Table;
14
15
use Cake\Log\LogTrait;
16
use Cake\ORM\Query;
17
use Cake\ORM\Table;
18
use Cake\Validation\Validator;
19
use Psr\Log\LogLevel;
20
use Stopwatch\Lib\Stopwatch;
21
22
/**
23
 * Stores which users are online
24
 *
25
 * Storage can be nopersistent as it is constantly rebuild with live-data.
26
 *
27
 * Field notes:
28
 * - `time` - Timestamp as int unix-epoch instead regular DATETIME. Makes it
29
 *   cheap to clear out-timed users by comparing int values.
30
 */
31
class UserOnlineTable extends Table
32
{
33
    use LogTrait;
34
35
    /**
36
     * Time in seconds until a user is considered offline.
37
     *
38
     * Default: 20 Minutes.
39
     *
40
     * @var int
41
     */
42
    private $timeUntilOffline = 1200;
43
44
    /**
45
     * {@inheritDoc}
46
     */
47
    public function initialize(array $config)
48
    {
49
        $this->setTable('useronline');
50
51
        $this->addBehavior('Timestamp');
52
53
        $this->addBehavior(
54
            'Cron.Cron',
55
            [
56
                'gc' => [
57
                    'id' => 'UserOnline.deleteGone',
58
                    'due' => '+1 minutes',
59
                ],
60
            ]
61
        );
62
63
        $this->belongsTo('Users', ['foreignKey' => 'user_id']);
64
    }
65
66
    /**
67
     * {@inheritDoc}
68
     */
69
    public function validationDefault(Validator $validator)
70
    {
71
        /// uuid
72
        $validator
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::notEmpty() has been deprecated with message: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
73
            ->notEmpty('uuid')
74
            ->requirePresence('uuid')
75
            ->add(
76
                'uuid',
77
                [
78
                    'isUnique' => [
79
                        'rule' => 'validateUnique',
80
                        'provider' => 'table'
81
                    ],
82
                ]
83
            );
84
85
        return $validator;
86
    }
87
88
    /**
89
     * Sets time in secondes until a user is considered offline.
90
     *
91
     * Adjust to sane values taking JS-frontend status ping time intervall into
92
     * account, which also keeps the user online.
93
     *
94
     * @param int $period Time in seconds
95
     * @return void
96
     */
97
    public function setOnlinePeriod(int $period): void
98
    {
99
        $this->timeUntilOffline = $period;
100
    }
101
102
    /**
103
     * Sets user with `$id` online
104
     *
105
     * @param string $id usually user-ID (logged-in user) or session_id (not logged-in)
106
     * @param bool $loggedIn user is logged-in
107
     * @return void
108
     */
109
    public function setOnline(string $id, bool $loggedIn): void
110
    {
111
        $now = time();
112
        $id = $this->getShortendedId((string)$id);
113
114
        $user = $this->find()->where(['uuid' => $id])->first();
115
116
        if ($user) {
117
            /// [Performance] Only hit database if timestamp is about to get outdated.
118
            $updateIfOlderThan = $now - (int)$this->timeUntilOffline * 0.75;
119
            if ($user->get('time') < $updateIfOlderThan) {
120
                $user->set('time', $now);
121
                $this->save($user);
0 ignored issues
show
Bug introduced by
It seems like $user defined by $this->find()->where(arr...uuid' => $id))->first() on line 114 can also be of type array; however, Cake\ORM\Table::save() does only seem to accept object<Cake\Datasource\EntityInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
122
            }
123
124
            return;
125
        }
126
127
        $data = ['logged_in' => $loggedIn, 'time' => $now, 'uuid' => $id];
128
        if ($loggedIn) {
129
            $data['user_id'] = (int)$id;
130
        }
131
        $user = $this->newEntity($data);
132
133
        try {
134
            $this->save($user);
135
        } catch (\PDOException $e) {
136
            // We saw that some mobile browsers occasionaly send two requests at
137
            // the same time. On of the two requests was always the status-ping.
138
            // Working theory: cause is a power-coalesced status-ping now
139
            // bundled with a page reload on tab-"resume" esp. with http/2.
140
            //
141
            // When the second request arrives (ns later) the first request
142
            // hasn't persistet its save to the DB yet (which happens in the ms
143
            // range). So the second request doesn't see the user online and
144
            // tries to save the same "uuid" again. The DB will not have any of
145
            // that nonsense after it set the "uuid" from the first request on a
146
            // unique column, and raises an error which may be experienced by
147
            // the user.
148
            //
149
            // Since the first request did the necessary work of marking the
150
            // user online, we suppress this error, assuming it will only happen
151
            // in this particular situation. *knocks on wood*
152
            if ($e->getCode() == 23000 && strstr($e->getMessage(), 'uuid')) {
153
                $this->log(
154
                    sprintf('Cought duplicate uuid-key %s exception in UserOnline::setOnline.', $id),
155
                    LogLevel::INFO,
156
                    'saito.info'
157
                );
158
            }
159
        }
160
    }
161
162
    /**
163
     * Removes user with uuid `$id` from UserOnline
164
     *
165
     * @param int|string $id id
166
     * @return void
167
     */
168
    public function setOffline($id): void
169
    {
170
        $id = $this->getShortendedId((string)$id);
171
        $this->deleteAll(['UserOnline.uuid' => $id]);
172
    }
173
174
    /**
175
     * Get all logged-in users
176
     *
177
     * Don't use directly but use \Saito\App\Stats
178
     *
179
     * @td @sm make finder
180
     *
181
     * @return Query
182
     */
183
    public function getLoggedIn(): Query
184
    {
185
        Stopwatch::start('UserOnline->getLoggedIn()');
186
        $loggedInUsers = $this->find(
187
            'all',
188
            [
189
                'contain' => [
190
                    'Users' => [
191
                        'fields' => ['id', 'user_type', 'username']
192
                    ]
193
                ],
194
                'conditions' => ['UserOnline.logged_in' => true],
195
                'fields' => ['id'],
196
                'order' => ['LOWER(Users.username)' => 'ASC']
197
            ]
198
        );
199
        Stopwatch::stop('UserOnline->getLoggedIn()');
200
201
        return $loggedInUsers;
202
    }
203
204
    /**
205
     * Removes users which weren't online $timeDiff seconds.
206
     *
207
     * @return void
208
     */
209
    public function gc(): void
210
    {
211
        $this->deleteAll(['time <' => time() - $this->timeUntilOffline]);
212
    }
213
214
    /**
215
     * Shortens a string to fit in the uuid table-field.
216
     *
217
     * @param string $id The string to shorten.
218
     * @return string The shoretened string.
219
     */
220
    protected function getShortendedId(string $id): string
221
    {
222
        return substr($id, 0, 32);
223
    }
224
}
225