Issues (326)

src/Model/Table/UserOnlineTable.php (2 issues)

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\Core\Configure;
16
use Cake\Log\LogTrait;
17
use Cake\ORM\Query;
18
use Cake\ORM\Table;
19
use Cake\Validation\Validator;
20
use Psr\Log\LogLevel;
21
use Stopwatch\Lib\Stopwatch;
22
23
/**
24
 * Stores which users are online
25
 *
26
 * Storage can be nopersistent as it is constantly rebuild with live-data.
27
 *
28
 * Field notes:
29
 * - `time` - Timestamp as int unix-epoch instead regular DATETIME. Makes it
30
 *   cheap to clear out-timed users by comparing int values.
31
 */
32
class UserOnlineTable extends Table
33
{
34
    use LogTrait;
35
36
    /**
37
     * Time in seconds until a user is considered offline.
38
     *
39
     * Default: 20 Minutes.
40
     *
41
     * @var int
42
     */
43
    private $timeUntilOffline = 1200;
44
45
    /**
46
     * {@inheritDoc}
47
     */
48
    public function initialize(array $config)
49
    {
50
        $this->setTable('useronline');
51
52
        $this->addBehavior('Timestamp');
53
54
        $this->addBehavior(
55
            'Cron.Cron',
56
            [
57
                'gc' => [
58
                    'id' => 'UserOnline.deleteGone',
59
                    'due' => '+1 minute',
60
                ],
61
            ]
62
        );
63
64
        $this->belongsTo('Users', ['foreignKey' => 'user_id']);
65
    }
66
67
    /**
68
     * {@inheritDoc}
69
     */
70
    public function validationDefault(Validator $validator)
71
    {
72
        /// uuid
73
        $validator
0 ignored issues
show
Deprecated Code introduced by
The function Cake\Validation\Validator::notEmpty() has been deprecated: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

73
        /** @scrutinizer ignore-deprecated */ $validator

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

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

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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