| Total Complexity | 57 |
| Total Lines | 316 |
| Duplicated Lines | 0 % |
| Changes | 1 | ||
| Bugs | 0 | Features | 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 |
||
| 30 | class AnonymousUserSubscriber implements EventSubscriberInterface |
||
| 31 | { |
||
| 32 | private const FIREWALL_NAME = 'main'; |
||
| 33 | private const MAX_ANONYMOUS_USERS = 5; |
||
| 34 | |||
| 35 | // Session flags for the “active public course” context |
||
| 36 | private const S_ACTIVE_CID = '_active_public_cid'; |
||
| 37 | private const S_ACTIVE_PUBLIC = '_active_public_flag'; |
||
| 38 | private const S_ACTIVE_EXPIRES_AT = '_active_public_expires_at'; |
||
| 39 | private const S_SECURITY_TOKEN = '_security_'.self::FIREWALL_NAME; |
||
| 40 | |||
| 41 | // TTL (in seconds) for the “public course anonymous session” window |
||
| 42 | private const ACTIVE_TTL_SECONDS = 600; // 10 minutes |
||
| 43 | |||
| 44 | /** |
||
| 45 | * Whitelist: only preserve the anonymous context on the contact pages. |
||
| 46 | * NOTE: We intentionally avoid whitelisting all LP paths here to keep scope tight. |
||
| 47 | */ |
||
| 48 | private const ANON_WHITELIST_PREFIXES = [ |
||
| 49 | '/contact', |
||
| 50 | '/main/lp/contact', |
||
| 51 | ]; |
||
| 52 | |||
| 53 | public function __construct( |
||
| 59 | |||
| 60 | public static function getSubscribedEvents(): array |
||
| 61 | { |
||
| 62 | return [ KernelEvents::REQUEST => 'onKernelRequest' ]; |
||
| 63 | } |
||
| 64 | |||
| 65 | public function onKernelRequest(RequestEvent $event): void |
||
| 133 | } |
||
| 134 | } |
||
| 135 | } |
||
| 136 | |||
| 137 | /** |
||
| 138 | * Extract course id from: |
||
| 139 | * - Query ?cid=... |
||
| 140 | * - Path /course/{id}/... |
||
| 141 | * - Path /api/courses/{id} |
||
| 142 | */ |
||
| 143 | private function extractCid(Request $request): int |
||
| 144 | { |
||
| 145 | $cid = $request->query->get('cid'); |
||
| 146 | if (is_numeric($cid) && (int) $cid > 0) { |
||
| 147 | return (int) $cid; |
||
| 148 | } |
||
| 149 | |||
| 150 | $path = $request->getPathInfo(); |
||
| 151 | |||
| 152 | if (preg_match('#^/course/(\d+)(?:/|$)#', $path, $m)) { |
||
| 153 | return (int) $m[1]; |
||
| 154 | } |
||
| 155 | |||
| 156 | if (preg_match('#^/api/courses/(\d+)(?:/|$)#', $path, $m)) { |
||
| 157 | return (int) $m[1]; |
||
| 158 | } |
||
| 159 | |||
| 160 | return 0; |
||
| 161 | } |
||
| 162 | |||
| 163 | private function isCoursePublic(int $cid): bool |
||
| 164 | { |
||
| 165 | /** @var Course|null $course */ |
||
| 166 | $course = $this->em->getRepository(Course::class)->find($cid); |
||
| 167 | return $course?->isPublic() ?? false; |
||
| 168 | } |
||
| 169 | |||
| 170 | /** Store/renew the active public course context in session. */ |
||
| 171 | private function rememberActivePublicCid(Request $request, int $cid): void |
||
| 172 | { |
||
| 173 | $session = $request->getSession(); |
||
| 174 | $session->set(self::S_ACTIVE_CID, $cid); |
||
| 175 | $session->set(self::S_ACTIVE_PUBLIC, true); |
||
| 176 | $session->set(self::S_ACTIVE_EXPIRES_AT, time() + self::ACTIVE_TTL_SECONDS); |
||
| 177 | } |
||
| 178 | |||
| 179 | /** Log in as an anonymous entity User (create/reuse and set a UsernamePasswordToken). */ |
||
| 180 | private function loginAnonymousEntity(Request $request): void |
||
| 181 | { |
||
| 182 | $userIp = $request->getClientIp() ?: '127.0.0.1'; |
||
| 183 | $anonId = $this->getOrCreateAnonymousUserId($userIp); |
||
| 184 | if (null === $anonId) { |
||
| 185 | return; |
||
| 186 | } |
||
| 187 | |||
| 188 | // Register login if it doesn't exist yet |
||
| 189 | $trackRepo = $this->em->getRepository(TrackELogin::class); |
||
| 190 | if (!$trackRepo->findOneBy(['userIp' => $userIp, 'user' => $anonId])) { |
||
| 191 | $trackLogin = (new TrackELogin()) |
||
| 192 | ->setUserIp($userIp) |
||
| 193 | ->setLoginDate(new DateTime()) |
||
| 194 | ->setUser($this->em->getReference(User::class, $anonId)); |
||
| 195 | $this->em->persist($trackLogin); |
||
| 196 | $this->em->flush(); |
||
| 197 | } |
||
| 198 | |||
| 199 | // Set token |
||
| 200 | $userRepo = $this->em->getRepository(User::class); |
||
| 201 | $user = $userRepo->find($anonId); |
||
| 202 | if (!$user) { |
||
| 203 | return; |
||
| 204 | } |
||
| 205 | |||
| 206 | if ($request->hasSession()) { |
||
| 207 | $request->getSession()->set('_user', [ |
||
| 208 | 'user_id' => $user->getId(), |
||
| 209 | 'username' => $user->getUsername(), |
||
| 210 | 'firstname' => $user->getFirstname(), |
||
| 211 | 'lastname' => $user->getLastname(), |
||
| 212 | 'firstName' => $user->getFirstname(), |
||
| 213 | 'lastName' => $user->getLastname(), |
||
| 214 | 'email' => $user->getEmail(), |
||
| 215 | 'official_code' => $user->getOfficialCode(), |
||
| 216 | 'picture_uri' => $user->getPictureUri(), |
||
| 217 | 'status' => $user->getStatus(), |
||
| 218 | 'active' => $user->isActive(), |
||
| 219 | 'theme' => $user->getTheme(), |
||
| 220 | 'language' => $user->getLocale(), |
||
| 221 | 'created_at' => $user->getCreatedAt()->format('Y-m-d H:i:s'), |
||
| 222 | 'expiration_date' => $user->getExpirationDate() ? $user->getExpirationDate()->format('Y-m-d H:i:s') : null, |
||
| 223 | 'last_login' => $user->getLastLogin() ? $user->getLastLogin()->format('Y-m-d H:i:s') : null, |
||
| 224 | 'is_anonymous' => true, |
||
| 225 | ]); |
||
| 226 | } |
||
| 227 | |||
| 228 | $roles = $user->getRoles(); |
||
| 229 | $this->tokenStorage->setToken(new UsernamePasswordToken($user, self::FIREWALL_NAME, $roles)); |
||
| 230 | } |
||
| 231 | |||
| 232 | /** Clear token and session flags. */ |
||
| 233 | private function clearAnon(Request $request): void |
||
| 234 | { |
||
| 235 | $this->tokenStorage->setToken(null); |
||
| 236 | |||
| 237 | if ($request->hasSession()) { |
||
| 238 | $session = $request->getSession(); |
||
| 239 | $session->remove('_user'); |
||
| 240 | $session->remove(self::S_SECURITY_TOKEN); |
||
| 241 | $session->remove(self::S_ACTIVE_CID); |
||
| 242 | $session->remove(self::S_ACTIVE_PUBLIC); |
||
| 243 | $session->remove(self::S_ACTIVE_EXPIRES_AT); |
||
| 244 | } |
||
| 245 | } |
||
| 246 | |||
| 247 | /** |
||
| 248 | * Consider it “top-level document navigation” if: |
||
| 249 | * - It is NOT an XHR (no `X-Requested-With: XMLHttpRequest`) |
||
| 250 | * - and browser sends `Sec-Fetch-Mode: navigate` and `Sec-Fetch-Dest: document` |
||
| 251 | * - or the Accept header includes `text/html` |
||
| 252 | */ |
||
| 253 | private function isTopLevelNavigation(Request $request): bool |
||
| 254 | { |
||
| 255 | if ($request->isXmlHttpRequest()) { |
||
| 256 | return false; |
||
| 257 | } |
||
| 258 | |||
| 259 | $mode = (string) $request->headers->get('Sec-Fetch-Mode', ''); |
||
| 260 | $dest = (string) $request->headers->get('Sec-Fetch-Dest', ''); |
||
| 261 | if ($mode === 'navigate' && $dest === 'document') { |
||
| 262 | return true; |
||
| 263 | } |
||
| 264 | |||
| 265 | $accept = (string) $request->headers->get('Accept', ''); |
||
| 266 | return str_contains($accept, 'text/html'); |
||
| 267 | } |
||
| 268 | |||
| 269 | /** |
||
| 270 | * Only contact-related paths preserve the anonymous context (tight scope). |
||
| 271 | * Examples matched: |
||
| 272 | * - /contact |
||
| 273 | * - /contact/ |
||
| 274 | * - /main/lp/contact |
||
| 275 | * - /main/lp/contact/... |
||
| 276 | */ |
||
| 277 | private function isWhitelistedPath(Request $request): bool |
||
| 286 | } |
||
| 287 | |||
| 288 | private function getOrCreateAnonymousUserId(string $userIp): ?int |
||
| 289 | { |
||
| 290 | $userRepo = $this->em->getRepository(User::class); |
||
| 291 | $trackRepo = $this->em->getRepository(TrackELogin::class); |
||
| 292 | $autoProv = 'true' === $this->settings->getSetting('security.anonymous_autoprovisioning'); |
||
| 293 | |||
| 294 | if (!$autoProv) { |
||
| 295 | $u = $userRepo->findOneBy(['status' => User::ANONYMOUS], ['createdAt' => 'ASC']); |
||
| 296 | return $u ? $u->getId() : $this->createAnonymousUser()->getId(); |
||
| 297 | } |
||
| 298 | |||
| 299 | $max = (int) $this->settings->getSetting('admin.max_anonymous_users') ?: self::MAX_ANONYMOUS_USERS; |
||
| 300 | $list = $userRepo->findBy(['status' => User::ANONYMOUS], ['createdAt' => 'ASC']); |
||
| 301 | |||
| 302 | // Reuse by IP if there is a previous login record |
||
| 303 | foreach ($list as $u) { |
||
| 304 | if ($trackRepo->findOneBy(['userIp' => $userIp, 'user' => $u])) { |
||
| 305 | return $u->getId(); |
||
| 306 | } |
||
| 307 | } |
||
| 308 | |||
| 309 | // Trim excess anonymous users |
||
| 310 | while (\count($list) >= $max) { |
||
| 311 | $oldest = array_shift($list); |
||
| 312 | if ($oldest) { |
||
| 313 | $this->em->remove($oldest); |
||
| 314 | $this->em->flush(); |
||
| 315 | } |
||
| 316 | } |
||
| 317 | |||
| 318 | return $this->createAnonymousUser()->getId(); |
||
| 319 | } |
||
| 320 | |||
| 321 | private function createAnonymousUser(): User |
||
| 346 | } |
||
| 347 | } |
||
| 348 |