Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.68% covered (success)
92.68%
76 / 82
87.50% covered (warning)
87.50%
14 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Application
92.68% covered (success)
92.68%
76 / 82
87.50% covered (warning)
87.50%
14 / 16
53.06
0.00% covered (danger)
0.00%
0 / 1
 initialize
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 initializeMaintenanceMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 initializeLogger
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
10
 initializeCaptcha
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
9
 initializeCache
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 initializeMiddleware
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
13
 reinitializeMiddlewareConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validateCacheDirectory
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
6.00
 setupCache
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLoggerConfig
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getRateLimit
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getRequestLimits
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getCsrfConfig
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getCorsAllowedOrigins
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIpBlacklist
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getAccessUnpublishedOnDomain
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace BO\Zmscitizenapi;
6
7use Psr\SimpleCache\CacheInterface;
8use Symfony\Component\Cache\Adapter\FilesystemAdapter;
9use Symfony\Component\Cache\Psr16Cache;
10
11/**
12 * @SuppressWarnings(PHPMD.TooManyFields)
13 * @SuppressWarnings(PHPMD.NPathComplexity)
14 * @TODO: Refactor this class into smaller focused classes (LoggerInitializer, MiddlewareInitializer) to reduce complexity
15 */
16class Application extends \BO\Slim\Application
17{
18    public const IDENTIFIER = 'zms';
19    public const MODULE_NAME = 'zmscitizenapi';
20    public static string $source_name = 'dldb';
21    public static $http = null;
22    public static array $http_curl_config = [];
23    public static ?CacheInterface $cache = null;
24    // Cache config
25    public static string $CACHE_DIR;
26    public static int $SOURCE_CACHE_TTL;
27    public static bool $MAINTENANCE_MODE_ENABLED;
28    // Logger config
29
30    public static int $LOGGER_MAX_REQUESTS;
31    public static int $LOGGER_RESPONSE_LENGTH;
32    public static int $LOGGER_STACK_LINES;
33    public static int $LOGGER_MESSAGE_SIZE;
34    public static int $LOGGER_CACHE_TTL;
35    public static int $LOGGER_MAX_RETRIES;
36    public static int $LOGGER_BACKOFF_MIN;
37    public static int $LOGGER_BACKOFF_MAX;
38    public static int $LOGGER_LOCK_TIMEOUT;
39    // Captcha config
40    public static bool $CAPTCHA_ENABLED;
41    public static string $FRIENDLY_CAPTCHA_SECRET_KEY;
42    public static string $FRIENDLY_CAPTCHA_SITE_KEY;
43    public static string $FRIENDLY_CAPTCHA_ENDPOINT;
44    public static string $FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE;
45    public static string $ALTCHA_CAPTCHA_SECRET_KEY;
46    public static string $ALTCHA_CAPTCHA_SITE_KEY;
47    public static string $ALTCHA_CAPTCHA_ENDPOINT;
48    public static string $ALTCHA_CAPTCHA_ENDPOINT_PUZZLE;
49    // Rate limiting config
50    public static int $RATE_LIMIT_MAX_REQUESTS;
51    public static int $RATE_LIMIT_CACHE_TTL;
52    public static int $RATE_LIMIT_MAX_RETRIES;
53    public static int $RATE_LIMIT_BACKOFF_MIN;
54    public static int $RATE_LIMIT_BACKOFF_MAX;
55    public static int $RATE_LIMIT_LOCK_TIMEOUT;
56    // Request limits config
57    public static int $MAX_REQUEST_SIZE;
58    public static int $MAX_STRING_LENGTH;
59    public static int $MAX_RECURSION_DEPTH;
60    // CSRF config
61    public static int $CSRF_TOKEN_LENGTH;
62    public static string $CSRF_SESSION_KEY;
63    // CORS config
64    public static string $CORS_ALLOWED_ORIGINS;
65    // IP Filter config
66    public static string $IP_BLACKLIST;
67
68    public static string $ACCESS_UNPUBLISHED_ON_DOMAIN;
69
70    public static function initialize(): void
71    {
72        self::initializeMaintenanceMode();
73        self::initializeLogger();
74        self::initializeCaptcha();
75        self::initializeCache();
76        self::initializeMiddleware();
77    }
78
79    private static function initializeMaintenanceMode(): void
80    {
81        self::$MAINTENANCE_MODE_ENABLED = filter_var(getenv('MAINTENANCE_ENABLED'), FILTER_VALIDATE_BOOLEAN);
82    }
83
84    /**
85     * @SuppressWarnings(PHPMD.NPathComplexity)
86     * @TODO: Extract logger initialization logic into a dedicated LoggerInitializer class
87     */
88    private static function initializeLogger(): void
89    {
90        self::$LOGGER_MAX_REQUESTS = (int) (getenv('LOGGER_MAX_REQUESTS') ?: 1000);
91        self::$LOGGER_RESPONSE_LENGTH = (int) (getenv('LOGGER_RESPONSE_LENGTH') ?: 1048576);
92        // 1MB
93        self::$LOGGER_STACK_LINES = (int) (getenv('LOGGER_STACK_LINES') ?: 20);
94        self::$LOGGER_MESSAGE_SIZE = (int) (getenv('LOGGER_MESSAGE_SIZE') ?: 8192);
95        // 8KB
96        self::$LOGGER_CACHE_TTL = (int) (getenv('LOGGER_CACHE_TTL') ?: 60);
97        self::$LOGGER_MAX_RETRIES = (int) (getenv('LOGGER_MAX_RETRIES') ?: 3);
98        self::$LOGGER_BACKOFF_MIN = (int) (getenv('LOGGER_BACKOFF_MIN') ?: 100);
99        self::$LOGGER_BACKOFF_MAX = (int) (getenv('LOGGER_BACKOFF_MAX') ?: 1000);
100        self::$LOGGER_LOCK_TIMEOUT = (int) (getenv('LOGGER_LOCK_TIMEOUT') ?: 5);
101    }
102
103    private static function initializeCaptcha(): void
104    {
105        self::$CAPTCHA_ENABLED = filter_var(getenv('CAPTCHA_ENABLED'), FILTER_VALIDATE_BOOLEAN);
106        self::$FRIENDLY_CAPTCHA_SECRET_KEY = getenv('FRIENDLY_CAPTCHA_SECRET_KEY') ?: '';
107        self::$FRIENDLY_CAPTCHA_SITE_KEY = getenv('FRIENDLY_CAPTCHA_SITE_KEY') ?: '';
108        self::$FRIENDLY_CAPTCHA_ENDPOINT = getenv('FRIENDLY_CAPTCHA_ENDPOINT')
109            ?: 'https://eu-api.friendlycaptcha.eu/api/v1/siteverify';
110        self::$FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE = getenv('FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE')
111            ?: 'https://eu-api.friendlycaptcha.eu/api/v1/puzzle';
112        self::$ALTCHA_CAPTCHA_SECRET_KEY = getenv('ALTCHA_CAPTCHA_SECRET_KEY') ?: '';
113        self::$ALTCHA_CAPTCHA_SITE_KEY = getenv('ALTCHA_CAPTCHA_SITE_KEY') ?: '';
114        self::$ALTCHA_CAPTCHA_ENDPOINT = getenv('ALTCHA_CAPTCHA_ENDPOINT')
115            ?: 'https://eu.altcha.org/form/';
116        self::$ALTCHA_CAPTCHA_ENDPOINT_PUZZLE = getenv('ALTCHA_CAPTCHA_ENDPOINT_PUZZLE')
117            ?: 'https://eu.altcha.org/';
118    }
119
120    private static function initializeCache(): void
121    {
122        self::$CACHE_DIR = getenv('CACHE_DIR') ?: __DIR__ . '/cache';
123        self::$SOURCE_CACHE_TTL = (int) (getenv('SOURCE_CACHE_TTL') ?: 3600);
124        self::validateCacheDirectory();
125        self::setupCache();
126    }
127
128    /**
129     * @SuppressWarnings(PHPMD.NPathComplexity)
130     * @TODO: Extract middleware initialization logic into a dedicated MiddlewareInitializer class
131     */
132    private static function initializeMiddleware(): void
133    {
134        // Rate limiting
135        self::$RATE_LIMIT_MAX_REQUESTS = (int) (getenv('RATE_LIMIT_MAX_REQUESTS') ?: 60);
136        self::$RATE_LIMIT_CACHE_TTL = (int) (getenv('RATE_LIMIT_CACHE_TTL') ?: 60);
137        self::$RATE_LIMIT_MAX_RETRIES = (int) (getenv('RATE_LIMIT_MAX_RETRIES') ?: 3);
138        self::$RATE_LIMIT_BACKOFF_MIN = (int) (getenv('RATE_LIMIT_BACKOFF_MIN') ?: 10);
139        self::$RATE_LIMIT_BACKOFF_MAX = (int) (getenv('RATE_LIMIT_BACKOFF_MAX') ?: 50);
140        self::$RATE_LIMIT_LOCK_TIMEOUT = (int) (getenv('RATE_LIMIT_LOCK_TIMEOUT') ?: 1);
141        // Request limits
142        self::$MAX_REQUEST_SIZE = (int) (getenv('MAX_REQUEST_SIZE') ?: 10485760);
143        // 10MB
144        self::$MAX_STRING_LENGTH = (int) (getenv('MAX_STRING_LENGTH') ?: 32768);
145        // 32KB
146        self::$MAX_RECURSION_DEPTH = (int) (getenv('MAX_RECURSION_DEPTH') ?: 10);
147        // CSRF
148        //self::$CSRF_TOKEN_LENGTH = (int) (getenv('CSRF_TOKEN_LENGTH') ?: 32);
149        //self::$CSRF_SESSION_KEY = getenv('CSRF_SESSION_KEY') ?: 'csrf_token';
150        // CORS
151        self::$CORS_ALLOWED_ORIGINS = getenv('CORS') ?: '';
152        // IP Filter
153        self::$IP_BLACKLIST = getenv('IP_BLACKLIST') ?: '';
154
155        self::$ACCESS_UNPUBLISHED_ON_DOMAIN = getenv('ACCESS_UNPUBLISHED_ON_DOMAIN') ?: '';
156    }
157
158    public static function reinitializeMiddlewareConfig(): void
159    {
160        self::initializeMiddleware();
161    }
162
163    private static function validateCacheDirectory(): void
164    {
165        if (!is_dir(self::$CACHE_DIR) && !mkdir(self::$CACHE_DIR, 0750, true)) {
166            throw new \RuntimeException(sprintf('Cache directory "%s" could not be created', self::$CACHE_DIR));
167        }
168
169        if (!is_writable(self::$CACHE_DIR)) {
170            throw new \RuntimeException(sprintf('Cache directory "%s" is not writable', self::$CACHE_DIR));
171        }
172    }
173
174    private static function setupCache(): void
175    {
176        $psr6 = new FilesystemAdapter(namespace: '', defaultLifetime: self::$SOURCE_CACHE_TTL, directory: self::$CACHE_DIR);
177        self::$cache = new Psr16Cache($psr6);
178    }
179
180    public static function getLoggerConfig(): array
181    {
182        return [
183            'maxRequests' => self::$LOGGER_MAX_REQUESTS,
184            'responseLength' => self::$LOGGER_RESPONSE_LENGTH,
185            'stackLines' => self::$LOGGER_STACK_LINES,
186            'messageSize' => self::$LOGGER_MESSAGE_SIZE,
187            'cacheTtl' => self::$LOGGER_CACHE_TTL,
188            'maxRetries' => self::$LOGGER_MAX_RETRIES,
189            'backoffMin' => self::$LOGGER_BACKOFF_MIN,
190            'backoffMax' => self::$LOGGER_BACKOFF_MAX,
191            'lockTimeout' => self::$LOGGER_LOCK_TIMEOUT
192        ];
193    }
194
195    public static function getRateLimit(): array
196    {
197        return [
198            'maxRequests' => self::$RATE_LIMIT_MAX_REQUESTS,
199            'cacheExpiry' => self::$RATE_LIMIT_CACHE_TTL,
200            'maxRetries' => self::$RATE_LIMIT_MAX_RETRIES,
201            'backoffMin' => self::$RATE_LIMIT_BACKOFF_MIN,
202            'backoffMax' => self::$RATE_LIMIT_BACKOFF_MAX,
203            'lockTimeout' => self::$RATE_LIMIT_LOCK_TIMEOUT
204        ];
205    }
206
207    public static function getRequestLimits(): array
208    {
209        return [
210            'maxSize' => self::$MAX_REQUEST_SIZE,
211            'maxStringLength' => self::$MAX_STRING_LENGTH,
212            'maxRecursionDepth' => self::$MAX_RECURSION_DEPTH
213        ];
214    }
215
216    public static function getCsrfConfig(): array
217    {
218        return [
219            'tokenLength' => self::$CSRF_TOKEN_LENGTH,
220            'sessionKey' => self::$CSRF_SESSION_KEY
221        ];
222    }
223
224    public static function getCorsAllowedOrigins(): array
225    {
226        return array_filter(explode(',', self::$CORS_ALLOWED_ORIGINS));
227    }
228
229    public static function getIpBlacklist(): string
230    {
231        return self::$IP_BLACKLIST ?: '';
232    }
233
234    public static function getAccessUnpublishedOnDomain(): ?string
235    {
236        return self::$ACCESS_UNPUBLISHED_ON_DOMAIN ?: null;
237    }
238}
239
240Application::initialize();