Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
CleanupExpiredSessionsTask
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
4 / 4
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 run
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
5
 runViaApi
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 resolvePdo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Session;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Api\ApiClientInterface;
12use App\Core\Cron\CronTaskInterface;
13use App\Core\Cron\Presentation\Api\CronApiController;
14use App\Core\Database\MultiDbRouter;
15use App\Core\Settings\SqlSettingsRepository;
16use App\Shared\Infrastructure\Config\InstallerConfigLoader;
17use Nyholm\Psr7\Factory\Psr17Factory;
18use PDO;
19
20/**
21 * Cleanup Expired User Sessions Background Task.
22 *
23 * Implements CronTaskInterface to purge expired user session records via REST API or direct SQL.
24 *
25 * @package App\Core\Session
26 */
27final readonly class CleanupExpiredSessionsTask implements CronTaskInterface
28{
29    /**
30     * CleanupExpiredSessionsTask constructor.
31     *
32     * @param CronApiController|ApiClientInterface|PDO|null $source API controller, client, or database connection.
33     * @param string $tablePrefix Database table prefix.
34     * @param int|null $maxLifetimeSeconds Session max lifetime in seconds.
35     */
36    public function __construct(
37        private CronApiController|ApiClientInterface|PDO|null $source = null,
38        private string $tablePrefix = 'a_',
39        private ?int $maxLifetimeSeconds = null
40    ) {
41    }
42
43    /**
44     * {@inheritdoc}
45     */
46    public function run(): string
47    {
48        $source = $this->source ?? $this->resolvePdo();
49        $lifetime = $this->maxLifetimeSeconds;
50        if ($lifetime === null) {
51            $settingsRepo = ($source instanceof PDO)
52                ? new SqlSettingsRepository($source, $this->tablePrefix)
53                : null;
54            $lifetime = $settingsRepo?->getInt('session_lifetime_seconds', 7200) ?? 7200;
55        }
56
57        $apiResult = $this->runViaApi($source, $lifetime);
58        if ($apiResult !== null) {
59            return $apiResult;
60        }
61
62        if ($source instanceof PDO) {
63            $tableName = $this->tablePrefix . 'mod_user_sessions';
64            $cutoff = time() - $lifetime;
65
66            $sql = sprintf('DELETE FROM `%s` WHERE `last_activity` < :cutoff', $tableName);
67            $stmt = $source->prepare($sql);
68            $stmt->execute([':cutoff' => $cutoff]);
69            $deletedCount = $stmt->rowCount();
70
71            return sprintf(
72                'Cleaned up %d expired user session(s) older than %s (Lifetime: %ds).',
73                $deletedCount,
74                date('Y-m-d H:i:s', $cutoff),
75                $lifetime
76            );
77        }
78
79        return 'Cleanup skipped: No database or API connection available.';
80    }
81
82    /**
83     * Runs session cleanup via API controller or client if available.
84     *
85     * @param mixed $source Source handler.
86     * @param int $lifetime Session max lifetime seconds.
87     * @return string|null Result message or null.
88     */
89    private function runViaApi(mixed $source, int $lifetime): ?string
90    {
91        if ($source instanceof CronApiController) {
92            $factory = new Psr17Factory();
93            $req = $factory->createServerRequest('POST', '/api/v1/cron/tasks/cleanup-sessions')
94                ->withParsedBody(['lifetime_seconds' => $lifetime]);
95            $response = $source->cleanupSessions($req);
96            $data = (array)json_decode((string)$response->getBody(), true);
97            return (string)($data['message'] ?? 'Cleanup executed via API.');
98        }
99
100        if ($source instanceof ApiClientInterface) {
101            $data = $source->post('/api/v1/cron/tasks/cleanup-sessions', [
102                'lifetime_seconds' => $lifetime,
103            ]);
104            return (string)($data['message'] ?? 'Cleanup executed via API.');
105        }
106
107        return null;
108    }
109
110    /**
111     * Resolves fallback PDO connection from installer configuration.
112     */
113    private function resolvePdo(): ?PDO
114    {
115        return \App\Core\Database\FallbackPdoResolver::resolveDefaultConnection();
116    }
117}