Total Complexity | 50 |
Total Lines | 279 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like AnonymousUserSubscriber often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use AnonymousUserSubscriber, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
29 | class AnonymousUserSubscriber implements EventSubscriberInterface |
||
30 | { |
||
31 | private const FIREWALL_NAME = 'main'; |
||
32 | private const MAX_ANONYMOUS_USERS = 5; |
||
33 | |||
34 | // Session flags for the “active public course” context |
||
35 | private const S_ACTIVE_CID = '_active_public_cid'; |
||
36 | private const S_ACTIVE_PUBLIC = '_active_public_flag'; |
||
37 | private const S_ACTIVE_EXPIRES_AT = '_active_public_expires_at'; |
||
38 | private const S_SECURITY_TOKEN = '_security_'.self::FIREWALL_NAME; |
||
39 | |||
40 | // TTL (in seconds) for the “public course anonymous session” window |
||
41 | private const ACTIVE_TTL_SECONDS = 600; // 10 minutes |
||
42 | |||
43 | public function __construct( |
||
44 | private readonly Security $security, |
||
45 | private readonly EntityManagerInterface $em, |
||
46 | private readonly SettingsManager $settings, |
||
47 | private readonly TokenStorageInterface $tokenStorage, |
||
48 | ) {} |
||
49 | |||
50 | public static function getSubscribedEvents(): array |
||
51 | { |
||
52 | return [ KernelEvents::REQUEST => 'onKernelRequest' ]; |
||
53 | } |
||
54 | |||
55 | public function onKernelRequest(RequestEvent $event): void |
||
114 | } |
||
115 | } |
||
116 | } |
||
117 | |||
118 | /** |
||
119 | * Extract the course id from: |
||
120 | * - Query ?cid=... |
||
121 | * - Path /course/{id}/... |
||
122 | * - Path /api/courses/{id} |
||
123 | */ |
||
124 | private function extractCid(Request $request): int |
||
125 | { |
||
126 | $cid = $request->query->get('cid'); |
||
127 | if (is_numeric($cid) && (int) $cid > 0) { |
||
128 | return (int) $cid; |
||
129 | } |
||
130 | |||
131 | $path = $request->getPathInfo(); |
||
132 | |||
133 | if (preg_match('#^/course/(\d+)(?:/|$)#', $path, $m)) { |
||
134 | return (int) $m[1]; |
||
135 | } |
||
136 | |||
137 | if (preg_match('#^/api/courses/(\d+)(?:/|$)#', $path, $m)) { |
||
138 | return (int) $m[1]; |
||
139 | } |
||
140 | |||
141 | return 0; |
||
142 | } |
||
143 | |||
144 | private function isCoursePublic(int $cid): bool |
||
145 | { |
||
146 | /** @var Course|null $course */ |
||
147 | $course = $this->em->getRepository(Course::class)->find($cid); |
||
148 | return $course?->isPublic() ?? false; |
||
149 | } |
||
150 | |||
151 | /** Store the active public course context in session and renew TTL. */ |
||
152 | private function rememberActivePublicCid(Request $request, int $cid): void |
||
153 | { |
||
154 | $session = $request->getSession(); |
||
155 | $session->set(self::S_ACTIVE_CID, $cid); |
||
156 | $session->set(self::S_ACTIVE_PUBLIC, true); |
||
157 | $session->set(self::S_ACTIVE_EXPIRES_AT, time() + self::ACTIVE_TTL_SECONDS); |
||
158 | } |
||
159 | |||
160 | /** Log in as an anonymous entity User (create/reuse and set a UsernamePasswordToken). */ |
||
161 | private function loginAnonymousEntity(Request $request): void |
||
162 | { |
||
163 | $userIp = $request->getClientIp() ?: '127.0.0.1'; |
||
164 | $anonId = $this->getOrCreateAnonymousUserId($userIp); |
||
165 | if (null === $anonId) { |
||
166 | return; |
||
167 | } |
||
168 | |||
169 | // Register login if it doesn't exist yet |
||
170 | $trackRepo = $this->em->getRepository(TrackELogin::class); |
||
171 | if (!$trackRepo->findOneBy(['userIp' => $userIp, 'user' => $anonId])) { |
||
172 | $trackLogin = (new TrackELogin()) |
||
173 | ->setUserIp($userIp) |
||
174 | ->setLoginDate(new DateTime()) |
||
175 | ->setUser($this->em->getReference(User::class, $anonId)); |
||
176 | $this->em->persist($trackLogin); |
||
177 | $this->em->flush(); |
||
178 | } |
||
179 | |||
180 | // Set token |
||
181 | $userRepo = $this->em->getRepository(User::class); |
||
182 | $user = $userRepo->find($anonId); |
||
183 | if (!$user) { |
||
184 | return; |
||
185 | } |
||
186 | |||
187 | if ($request->hasSession()) { |
||
188 | $request->getSession()->set('_user', [ |
||
189 | 'user_id' => $user->getId(), |
||
190 | 'username' => $user->getUsername(), |
||
191 | 'firstname' => $user->getFirstname(), |
||
192 | 'lastname' => $user->getLastname(), |
||
193 | 'firstName' => $user->getFirstname(), |
||
194 | 'lastName' => $user->getLastname(), |
||
195 | 'email' => $user->getEmail(), |
||
196 | 'official_code' => $user->getOfficialCode(), |
||
197 | 'picture_uri' => $user->getPictureUri(), |
||
198 | 'status' => $user->getStatus(), |
||
199 | 'active' => $user->isActive(), |
||
200 | 'theme' => $user->getTheme(), |
||
201 | 'language' => $user->getLocale(), |
||
202 | 'created_at' => $user->getCreatedAt()->format('Y-m-d H:i:s'), |
||
203 | 'expiration_date' => $user->getExpirationDate() ? $user->getExpirationDate()->format('Y-m-d H:i:s') : null, |
||
204 | 'last_login' => $user->getLastLogin() ? $user->getLastLogin()->format('Y-m-d H:i:s') : null, |
||
205 | 'is_anonymous' => true, |
||
206 | ]); |
||
207 | } |
||
208 | |||
209 | $roles = $user->getRoles(); |
||
210 | $this->tokenStorage->setToken(new UsernamePasswordToken($user, self::FIREWALL_NAME, $roles)); |
||
211 | } |
||
212 | |||
213 | /** Clear token and session flags when navigating away from the course (top-level document navigation). */ |
||
214 | private function clearAnon(Request $request): void |
||
215 | { |
||
216 | $this->tokenStorage->setToken(null); |
||
217 | |||
218 | if ($request->hasSession()) { |
||
219 | $session = $request->getSession(); |
||
220 | $session->remove('_user'); |
||
221 | $session->remove(self::S_SECURITY_TOKEN); |
||
222 | $session->remove(self::S_ACTIVE_CID); |
||
223 | $session->remove(self::S_ACTIVE_PUBLIC); |
||
224 | $session->remove(self::S_ACTIVE_EXPIRES_AT); |
||
225 | } |
||
226 | } |
||
227 | |||
228 | /** |
||
229 | * Consider it “top-level document navigation” if: |
||
230 | * - It is NOT an XHR (no `X-Requested-With: XMLHttpRequest`) |
||
231 | * - and browser sends `Sec-Fetch-Mode: navigate` and `Sec-Fetch-Dest: document` |
||
232 | * - or the Accept header includes `text/html` |
||
233 | */ |
||
234 | private function isTopLevelNavigation(Request $request): bool |
||
235 | { |
||
236 | if ($request->isXmlHttpRequest()) { |
||
237 | return false; |
||
238 | } |
||
239 | |||
240 | $mode = (string) $request->headers->get('Sec-Fetch-Mode', ''); |
||
241 | $dest = (string) $request->headers->get('Sec-Fetch-Dest', ''); |
||
242 | if ($mode === 'navigate' && $dest === 'document') { |
||
243 | return true; |
||
244 | } |
||
245 | |||
246 | $accept = (string) $request->headers->get('Accept', ''); |
||
247 | return str_contains($accept, 'text/html'); |
||
248 | } |
||
249 | |||
250 | private function getOrCreateAnonymousUserId(string $userIp): ?int |
||
251 | { |
||
252 | $userRepo = $this->em->getRepository(User::class); |
||
253 | $trackRepo = $this->em->getRepository(TrackELogin::class); |
||
254 | $autoProv = 'true' === $this->settings->getSetting('security.anonymous_autoprovisioning'); |
||
255 | |||
256 | if (!$autoProv) { |
||
257 | $u = $userRepo->findOneBy(['status' => User::ANONYMOUS], ['createdAt' => 'ASC']); |
||
258 | return $u ? $u->getId() : $this->createAnonymousUser()->getId(); |
||
259 | } |
||
260 | |||
261 | $max = (int) $this->settings->getSetting('admin.max_anonymous_users') ?: self::MAX_ANONYMOUS_USERS; |
||
262 | $list = $userRepo->findBy(['status' => User::ANONYMOUS], ['createdAt' => 'ASC']); |
||
263 | |||
264 | // Reuse by IP if there is a previous login record |
||
265 | foreach ($list as $u) { |
||
266 | if ($trackRepo->findOneBy(['userIp' => $userIp, 'user' => $u])) { |
||
267 | return $u->getId(); |
||
268 | } |
||
269 | } |
||
270 | |||
271 | // Trim excess anonymous users |
||
272 | while (\count($list) >= $max) { |
||
273 | $oldest = array_shift($list); |
||
274 | if ($oldest) { |
||
275 | $this->em->remove($oldest); |
||
276 | $this->em->flush(); |
||
277 | } |
||
278 | } |
||
279 | |||
280 | return $this->createAnonymousUser()->getId(); |
||
281 | } |
||
282 | |||
283 | private function createAnonymousUser(): User |
||
308 | } |
||
309 | } |
||
310 |