Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.84% covered (success)
98.84%
85 / 86
93.75% covered (success)
93.75%
15 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Application
98.84% covered (success)
98.84%
85 / 86
93.75% covered (success)
93.75%
15 / 16
26
0.00% covered (danger)
0.00%
0 / 1
 initialize
100.00% covered (success)
100.00%
7 / 7
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
 initializeSource
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 initializePlaceholderEmail
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getPlaceholderEmail
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 initializeLogger
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 initializeCaptcha
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 initializeMiddleware
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getenvInt
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getenvString
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 reinitializeMiddlewareConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLoggerConfig
100.00% covered (success)
100.00%
12 / 12
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
 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 BO\Slim\Traits\CacheInitializationTrait;
8use Psr\SimpleCache\CacheInterface;
9
10/**
11 * @SuppressWarnings(PHPMD.TooManyFields)
12 * @SuppressWarnings(PHPMD.NPathComplexity)
13 * @TODO: Refactor this class into smaller focused classes (LoggerInitializer, MiddlewareInitializer) to reduce complexity
14 */
15class Application extends \BO\Slim\Application
16{
17    use CacheInitializationTrait;
18
19    public const string IDENTIFIER = 'zms';
20    public const string MODULE_NAME = 'zmscitizenapi';
21    public static string $source_name = "dldb,zms";
22    public static $http = null;
23    public static array $http_curl_config = [];
24    public static ?CacheInterface $cache = null;
25    // Cache config
26    public static string $CACHE_DIR;
27    public static int $SOURCE_CACHE_TTL;
28    public static bool $MAINTENANCE_MODE_ENABLED;
29    // Logger config
30
31    public static int $LOGGER_MAX_REQUESTS;
32    public static int $LOGGER_MAX_ERROR_REQUESTS;
33    public static int $LOGGER_RESPONSE_LENGTH;
34    public static int $LOGGER_STACK_LINES;
35    public static int $LOGGER_MESSAGE_SIZE;
36    public static int $LOGGER_CACHE_TTL;
37    public static int $LOGGER_MAX_RETRIES;
38    public static int $LOGGER_BACKOFF_MIN;
39    public static int $LOGGER_BACKOFF_MAX;
40    public static int $LOGGER_LOCK_TIMEOUT;
41    // Captcha config
42    public static bool $CAPTCHA_ENABLED;
43    public static string $CAPTCHA_TOKEN_SECRET;
44    public static int $CAPTCHA_TOKEN_TTL;
45    public static string $ALTCHA_CAPTCHA_SITE_KEY;
46    public static string $ALTCHA_CAPTCHA_SITE_SECRET;
47    public static string $ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE;
48    public static string $ALTCHA_CAPTCHA_ENDPOINT_VERIFY;
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    // IP Filter config
61    public static string $IP_BLACKLIST;
62
63    public static string $ACCESS_UNPUBLISHED_ON_DOMAIN;
64
65    public const string DEFAULT_PLACEHOLDER_EMAIL = 'noreply-terminvereinbarung@muenchen.de';
66    public static string $PLACEHOLDER_EMAIL = self::DEFAULT_PLACEHOLDER_EMAIL;
67
68    public static function initialize(): void
69    {
70        self::initializeMaintenanceMode();
71        self::initializeSource();
72        self::initializePlaceholderEmail();
73        self::initializeLogger();
74        self::initializeCaptcha();
75        self::initializeCache(__DIR__ . '/cache');
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    private static function initializeSource(): void
85    {
86        $fromEnv = getenv('SOURCE_NAME');
87        if (is_string($fromEnv) && trim($fromEnv) !== '') {
88            self::$source_name = trim($fromEnv);
89        }
90    }
91
92    private static function initializePlaceholderEmail(): void
93    {
94        self::$PLACEHOLDER_EMAIL = self::getenvString(
95            'ZMS_CITIZENAPI_PLACEHOLDER_EMAIL',
96            self::DEFAULT_PLACEHOLDER_EMAIL
97        );
98    }
99
100    public static function getPlaceholderEmail(): string
101    {
102        return self::$PLACEHOLDER_EMAIL !== ''
103            ? self::$PLACEHOLDER_EMAIL
104            : self::DEFAULT_PLACEHOLDER_EMAIL;
105    }
106
107    /**
108     * @SuppressWarnings(PHPMD.NPathComplexity)
109     * @TODO: Extract logger initialization logic into a dedicated LoggerInitializer class
110     */
111    private static function initializeLogger(): void
112    {
113        self::$LOGGER_MAX_REQUESTS = self::getenvInt('ZMS_CITIZENAPI_LOGGER_MAX_REQUESTS', 1000);
114        self::$LOGGER_MAX_ERROR_REQUESTS = self::getenvInt('ZMS_CITIZENAPI_LOGGER_MAX_ERROR_REQUESTS', 0);
115        self::$LOGGER_RESPONSE_LENGTH = self::getenvInt('ZMS_CITIZENAPI_LOGGER_RESPONSE_LENGTH', 1048576);
116        // 1MB
117        self::$LOGGER_STACK_LINES = self::getenvInt('ZMS_CITIZENAPI_LOGGER_STACK_LINES', 20);
118        self::$LOGGER_MESSAGE_SIZE = self::getenvInt('ZMS_CITIZENAPI_LOGGER_MESSAGE_SIZE', 8192);
119        // 8KB
120        self::$LOGGER_CACHE_TTL = self::getenvInt('ZMS_CITIZENAPI_LOGGER_CACHE_TTL', 60);
121        self::$LOGGER_MAX_RETRIES = self::getenvInt('ZMS_CITIZENAPI_LOGGER_MAX_RETRIES', 3);
122        self::$LOGGER_BACKOFF_MIN = self::getenvInt('ZMS_CITIZENAPI_LOGGER_BACKOFF_MIN', 100);
123        self::$LOGGER_BACKOFF_MAX = self::getenvInt('ZMS_CITIZENAPI_LOGGER_BACKOFF_MAX', 1000);
124        self::$LOGGER_LOCK_TIMEOUT = self::getenvInt('ZMS_CITIZENAPI_LOGGER_LOCK_TIMEOUT', 5);
125        \BO\Slim\LoggerService::configure(self::getLoggerConfig());
126    }
127
128    private static function initializeCaptcha(): void
129    {
130        self::$CAPTCHA_ENABLED = filter_var(getenv('CAPTCHA_ENABLED'), FILTER_VALIDATE_BOOLEAN);
131        self::$CAPTCHA_TOKEN_SECRET = self::getenvString('CAPTCHA_TOKEN_SECRET', '');
132        $captchaTtl = self::getenvInt('CAPTCHA_TOKEN_TTL', 0);
133        self::$CAPTCHA_TOKEN_TTL = $captchaTtl === 0 ? 300 : $captchaTtl;
134        self::$ALTCHA_CAPTCHA_SITE_KEY = self::getenvString('ALTCHA_CAPTCHA_SITE_KEY', '');
135        self::$ALTCHA_CAPTCHA_SITE_SECRET = self::getenvString('ALTCHA_CAPTCHA_SITE_SECRET', '');
136        self::$ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE = self::getenvString(
137            'ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE',
138            'https://captcha.muenchen.de/api/v1/captcha/challenge'
139        );
140        self::$ALTCHA_CAPTCHA_ENDPOINT_VERIFY = self::getenvString(
141            'ALTCHA_CAPTCHA_ENDPOINT_VERIFY',
142            'https://captcha.muenchen.de/api/v1/captcha/verify'
143        );
144    }
145
146    /**
147     * @TODO: Extract middleware initialization logic into a dedicated MiddlewareInitializer class
148     */
149    private static function initializeMiddleware(): void
150    {
151        // Rate limiting
152        self::$RATE_LIMIT_MAX_REQUESTS = self::getenvInt('RATE_LIMIT_MAX_REQUESTS', 600);
153        self::$RATE_LIMIT_CACHE_TTL = self::getenvInt('RATE_LIMIT_CACHE_TTL', 60);
154        self::$RATE_LIMIT_MAX_RETRIES = self::getenvInt('RATE_LIMIT_MAX_RETRIES', 3);
155        self::$RATE_LIMIT_BACKOFF_MIN = self::getenvInt('RATE_LIMIT_BACKOFF_MIN', 10);
156        self::$RATE_LIMIT_BACKOFF_MAX = self::getenvInt('RATE_LIMIT_BACKOFF_MAX', 50);
157        self::$RATE_LIMIT_LOCK_TIMEOUT = self::getenvInt('RATE_LIMIT_LOCK_TIMEOUT', 1);
158        // Request limits
159        self::$MAX_REQUEST_SIZE = self::getenvInt('MAX_REQUEST_SIZE', 10485760);
160        // 10MB
161        self::$MAX_STRING_LENGTH = self::getenvInt('MAX_STRING_LENGTH', 32768);
162        // 32KB
163        self::$MAX_RECURSION_DEPTH = self::getenvInt('MAX_RECURSION_DEPTH', 10);
164        // IP Filter
165        self::$IP_BLACKLIST = self::getenvString('IP_BLACKLIST', '');
166
167        self::$ACCESS_UNPUBLISHED_ON_DOMAIN = self::getenvString('ACCESS_UNPUBLISHED_ON_DOMAIN', '');
168    }
169
170    private static function getenvInt(string $name, int $default): int
171    {
172        $value = getenv($name);
173        return ($value === false || $value === '') ? $default : (int) $value;
174    }
175
176    private static function getenvString(string $name, string $default): string
177    {
178        $value = getenv($name);
179        return ($value === false || $value === '') ? $default : $value;
180    }
181
182    /** @psalm-api */
183    public static function reinitializeMiddlewareConfig(): void
184    {
185        self::initializeMiddleware();
186    }
187
188    public static function getLoggerConfig(): array
189    {
190        return [
191            'maxRequests' => self::$LOGGER_MAX_REQUESTS,
192            'maxErrorRequests' => self::$LOGGER_MAX_ERROR_REQUESTS,
193            'responseLength' => self::$LOGGER_RESPONSE_LENGTH,
194            'stackLines' => self::$LOGGER_STACK_LINES,
195            'messageSize' => self::$LOGGER_MESSAGE_SIZE,
196            'cacheTtl' => self::$LOGGER_CACHE_TTL,
197            'maxRetries' => self::$LOGGER_MAX_RETRIES,
198            'backoffMin' => self::$LOGGER_BACKOFF_MIN,
199            'backoffMax' => self::$LOGGER_BACKOFF_MAX,
200            'lockTimeout' => self::$LOGGER_LOCK_TIMEOUT
201        ];
202    }
203
204    public static function getRateLimit(): array
205    {
206        return [
207            'maxRequests' => self::$RATE_LIMIT_MAX_REQUESTS,
208            'cacheExpiry' => self::$RATE_LIMIT_CACHE_TTL,
209            'maxRetries' => self::$RATE_LIMIT_MAX_RETRIES,
210            'backoffMin' => self::$RATE_LIMIT_BACKOFF_MIN,
211            'backoffMax' => self::$RATE_LIMIT_BACKOFF_MAX,
212            'lockTimeout' => self::$RATE_LIMIT_LOCK_TIMEOUT
213        ];
214    }
215
216    public static function getRequestLimits(): array
217    {
218        return [
219            'maxSize' => self::$MAX_REQUEST_SIZE,
220            'maxStringLength' => self::$MAX_STRING_LENGTH,
221            'maxRecursionDepth' => self::$MAX_RECURSION_DEPTH
222        ];
223    }
224
225    public static function getIpBlacklist(): string
226    {
227        return self::$IP_BLACKLIST ?: '';
228    }
229
230    /** @psalm-api */
231    public static function getAccessUnpublishedOnDomain(): ?string
232    {
233        return self::$ACCESS_UNPUBLISHED_ON_DOMAIN ?: null;
234    }
235}
236
237Application::initialize();