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
|
|||||
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
![]() |
|||||
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 |
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.