@anonympins/fingerprint 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +36 -9
  2. package/README.md +1 -1
  3. package/package.json +109 -107
  4. package/public/fingerprint-anti-bot.zip +0 -0
  5. package/src/js/fingerprint.client.js +252 -45
  6. package/src/js/fingerprint.client.obfuscated.js +1 -1
  7. package/src/js/fingerprint.js +265 -185
  8. package/src/js/fingerprint.utils.js +32 -5
  9. package/src/js/fingerprint.worker.js +81 -0
  10. package/src/js/library.js +6 -2
  11. package/src/js/pow.solver.js +2 -3
  12. package/src/js/problem-manager.js +113 -113
  13. package/src/js/tests/fingerprint.client.init.test.js +30 -0
  14. package/src/js/tests/fingerprint.test.js +136 -4
  15. package/src/js/tests/threatIntel.test.js +76 -0
  16. package/src/php/AutoTuner.php +36 -18
  17. package/src/php/Challenge/ChallengeUtils.php +121 -240
  18. package/src/php/DirectFingerprint.php +16 -4
  19. package/src/php/FingerprintClient.php +17 -19
  20. package/src/php/FingerprintEngine.php +77 -39
  21. package/src/php/Ja3AnomalyDetector.php +20 -12
  22. package/src/php/Optimization/Optimization.php +5 -1
  23. package/src/php/Optimization/OptimizationOperators.php +5 -1
  24. package/src/php/Optimization/ProblemInitializers.php +16 -4
  25. package/src/php/ProblemManager.php +20 -14
  26. package/src/php/Tests/ChallengeUtilsTest.php +469 -453
  27. package/src/php/Tests/FingerprintEngineTest.php +6 -1
  28. package/src/php/Tests/RequestUtilsTest.php +1 -1
  29. package/src/php/Tests/ThreatIntelTest.php +64 -0
  30. package/src/php/Utils/Env.php +51 -49
  31. package/src/php/Utils/MaliciousPatterns.php +74 -74
  32. package/src/php/Utils/RequestUtils.php +231 -132
  33. package/src/php/WordPress/WpDbStore.php +4 -0
  34. package/src/php/WordPress/{fingerprint-wordpress.php → fingerprint-anti-bot.php} +131 -126
  35. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  36. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +1 -1
  37. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  38. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +1 -1
  39. package/src/php/WordPress/package.php +6 -3
  40. package/src/php/WordPress/readme.txt +36 -0
  41. package/public/fingerprint-wordpress.zip +0 -0
@@ -127,41 +127,27 @@
127
127
  $useAsymmetric = ($securityConfig['useAsymmetricTickets'] ?? false) === true || ($securityConfig['ed25519'] ?? '') === 'auto';
128
128
  $envPrivate = Env::get('ED25519_PRIVATE_KEY');
129
129
  if ($useAsymmetric && ($envPrivate === null || $envPrivate === '')) {
130
- $configDir = dirname(__DIR__, 1) . '/config';
131
- $persistentKeyPath = $configDir . '/ed25519_key.json';
132
-
133
- if (file_exists($persistentKeyPath)) {
134
- try {
135
- $keys = json_decode(file_get_contents($persistentKeyPath), true);
136
- if (isset($keys['privateKey'], $keys['publicKey'])) {
137
- Env::set('ED25519_PRIVATE_KEY', $keys['privateKey']);
138
- Env::set('ED25519_PUBLIC_KEY', $keys['publicKey']);
130
+ if (function_exists('get_option') && function_exists('update_option')) {
131
+ // In WordPress, persist cryptographic keys in the database options table
132
+ $storedKeys = get_option('fingerprint_ed25519_keys');
133
+ if (is_array($storedKeys) && isset($storedKeys['privateKey'], $storedKeys['publicKey'])) {
134
+ Env::set('ED25519_PRIVATE_KEY', $storedKeys['privateKey']);
135
+ Env::set('ED25519_PUBLIC_KEY', $storedKeys['publicKey']);
136
+ } elseif (defined('OPENSSL_KEYTYPE_ED25519')) {
137
+ $pkey = openssl_pkey_new(["private_key_type" => OPENSSL_KEYTYPE_ED25519]);
138
+ if ($pkey && openssl_pkey_export($pkey, $privateKeyPem)) {
139
+ $details = openssl_pkey_get_details($pkey); // @phpstan-ignore-line
140
+ $publicKeyPem = $details['key'] ?? ''; // @phpstan-ignore-line
141
+ Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
142
+ Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
143
+ update_option('fingerprint_ed25519_keys', [
144
+ 'privateKey' => $privateKeyPem,
145
+ 'publicKey' => $publicKeyPem,
146
+ ], false);
139
147
  }
140
- } catch (\Throwable $e) {
141
- error_log('[Fingerprint] Failed to load persistent Ed25519 keys: ' . $e->getMessage());
142
148
  }
143
149
  } else {
144
- try {
145
- if (defined('OPENSSL_KEYTYPE_ED25519')) {
146
- $pkey = openssl_pkey_new(["private_key_type" => OPENSSL_KEYTYPE_ED25519]);
147
- if ($pkey && openssl_pkey_export($pkey, $privateKeyPem)) {
148
- $details = openssl_pkey_get_details($pkey); // @phpstan-ignore-line
149
- $publicKeyPem = $details['key']; // @phpstan-ignore-line
150
- Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
151
- Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
152
- // Also update $_ENV and $_SERVER for consistency
153
- if (!is_dir($configDir)) {
154
- mkdir($configDir, 0777, true);
155
- }
156
- file_put_contents($persistentKeyPath, json_encode([
157
- 'privateKey' => $privateKeyPem,
158
- 'publicKey' => $publicKeyPem
159
- ], JSON_PRETTY_PRINT));
160
- }
161
- }
162
- } catch (\Throwable $e) {
163
- error_log('[Fingerprint] Native Ed25519 key generation failed: ' . $e->getMessage());
164
- }
150
+ self::initStandaloneEd25519Keys();
165
151
  }
166
152
  }
167
153
 
@@ -175,7 +161,7 @@
175
161
  $securityConfig = SecurityProfiles::deepMerge($securityConfig, $savedConfig);
176
162
  }
177
163
  } catch (\Throwable $e) {
178
- error_log("[FingerprintEngine] Failed to auto-load optimized config from {$savePath}: " . $e->getMessage());
164
+ self::logError("[FingerprintEngine] Failed to auto-load optimized config from {$savePath}: " . $e->getMessage());
179
165
  }
180
166
  }
181
167
  }
@@ -196,6 +182,49 @@
196
182
  }
197
183
  }
198
184
 
185
+ /**
186
+ * Initialise et persiste les cles Ed25519 pour les environnements PHP hors WordPress.
187
+ */
188
+ private static function initStandaloneEd25519Keys(): void
189
+ {
190
+ $configDir = dirname(__DIR__, 1) . '/config';
191
+ $persistentKeyPath = $configDir . '/ed25519_key.json';
192
+
193
+ if (file_exists($persistentKeyPath)) {
194
+ try {
195
+ $keys = json_decode((string)file_get_contents($persistentKeyPath), true);
196
+ if (isset($keys['privateKey'], $keys['publicKey'])) {
197
+ Env::set('ED25519_PRIVATE_KEY', $keys['privateKey']);
198
+ Env::set('ED25519_PUBLIC_KEY', $keys['publicKey']);
199
+ }
200
+ } catch (\Throwable $e) {
201
+ self::logError('[Fingerprint] Failed to load persistent Ed25519 keys: ' . $e->getMessage());
202
+ }
203
+ } else {
204
+ try {
205
+ if (defined('OPENSSL_KEYTYPE_ED25519')) {
206
+ $pkey = openssl_pkey_new(["private_key_type" => OPENSSL_KEYTYPE_ED25519]);
207
+ if ($pkey && openssl_pkey_export($pkey, $privateKeyPem)) {
208
+ $details = openssl_pkey_get_details($pkey); // @phpstan-ignore-line
209
+ $publicKeyPem = $details['key'] ?? ''; // @phpstan-ignore-line
210
+ Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
211
+ Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
212
+ if (!is_dir($configDir)) {
213
+ mkdir($configDir, 0777, true);
214
+ }
215
+ // phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.PluginDirectoryWrite -- Non-WordPress standalone environment fallback
216
+ file_put_contents($persistentKeyPath, json_encode([
217
+ 'privateKey' => $privateKeyPem,
218
+ 'publicKey' => $publicKeyPem
219
+ ], JSON_PRETTY_PRINT));
220
+ }
221
+ }
222
+ } catch (\Throwable $e) {
223
+ self::logError('[Fingerprint] Native Ed25519 key generation failed: ' . $e->getMessage());
224
+ }
225
+ }
226
+ }
227
+
199
228
  /**
200
229
  * Applique à chaud une nouvelle configuration de sécurité (poids, seuils, etc.)
201
230
  * sans nécessiter de redémarrage.
@@ -266,7 +295,7 @@
266
295
  }
267
296
  }
268
297
  } catch (\Throwable $e) {
269
- error_log("[Fingerprint] Error loading whitelist file {$filename}: " . $e->getMessage());
298
+ self::logError("[Fingerprint] Error loading whitelist file {$filename}: " . $e->getMessage());
270
299
  }
271
300
  }
272
301
  return $fallbackEntries;
@@ -358,7 +387,7 @@
358
387
  } else {
359
388
  $logMessage = "[FingerprintEngine] {$message}";
360
389
  if (!empty($data)) $logMessage .= ' ' . json_encode($data);
361
- error_log($logMessage);
390
+ self::logError($logMessage);
362
391
  }
363
392
  }
364
393
  }
@@ -1554,7 +1583,7 @@
1554
1583
  $isAsymmetric = true;
1555
1584
  }
1556
1585
  } catch (\Throwable $e) {
1557
- error_log('[Fingerprint] Asymmetric broadcast signing failed: ' . $e->getMessage());
1586
+ self::logError('[Fingerprint] Asymmetric broadcast signing failed: ' . $e->getMessage());
1558
1587
  }
1559
1588
  }
1560
1589
 
@@ -1575,7 +1604,8 @@
1575
1604
 
1576
1605
  private function asyncPost(string $url, array $params): void
1577
1606
  {
1578
- $parts = parse_url($url);
1607
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- wp_parse_url used if available
1608
+ $parts = function_exists('wp_parse_url') ? wp_parse_url($url) : parse_url($url);
1579
1609
  if ($parts === false) return;
1580
1610
 
1581
1611
  $host = $parts['host'];
@@ -1605,7 +1635,9 @@
1605
1635
  $out .= "Connection: Close\r\n\r\n";
1606
1636
  $out .= $postData;
1607
1637
 
1638
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Socket stream write
1608
1639
  @fwrite($fp, $out);
1640
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Socket stream close
1609
1641
  @fclose($fp);
1610
1642
  }
1611
1643
  }
@@ -1732,7 +1764,7 @@
1732
1764
  $b3 = ord($str[$offset++]); $b4 = ord($str[$offset++]);
1733
1765
  return ($b1 << 24) | ($b2 << 16) | ($b3 << 8) | $b4;
1734
1766
  }
1735
- throw new \Exception("Unsupported integer size: " . $val);
1767
+ throw new \Exception("Unsupported integer size: " . (int)$val);
1736
1768
  };
1737
1769
 
1738
1770
  if ($major === 0) {
@@ -1854,8 +1886,14 @@
1854
1886
  return openssl_verify($verifyBuffer, base64_decode($anchor['signature']), $publicKey, OPENSSL_ALGO_SHA256) === 1;
1855
1887
  }
1856
1888
  } catch (\Throwable $e) {
1857
- error_log('[WebAuthn-Server] PHP verification failed: ' . $e->getMessage());
1889
+ self::logError('[WebAuthn-Server] PHP verification failed: ' . $e->getMessage());
1858
1890
  }
1859
1891
  return false;
1860
1892
  }
1893
+
1894
+ private static function logError(string $message): void
1895
+ {
1896
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Engine diagnostic error log
1897
+ error_log($message);
1898
+ }
1861
1899
  }
@@ -212,10 +212,12 @@ class Ja3AnomalyDetector
212
212
 
213
213
  /**
214
214
  * Correlates low-level TCP RTT with application layer latency to detect residential proxy hops.
215
+ * Evaluates continuous physical transit discrepancies rather than arbitrary step numbers.
216
+ *
215
217
  * @param RequestContext $context
216
- * @return int Suspicion score
218
+ * @return float Suspicion score between 0.0 and 95.0
217
219
  */
218
- public static function getRttProxyScore(RequestContext $context): int
220
+ public static function getRttProxyScore(RequestContext $context): float
219
221
  {
220
222
  $tcpRttHeader = $context->headers['x-tcp-rtt'] ?? $context->headers['x-real-rtt'] ?? null;
221
223
  $tcpRtt = $tcpRttHeader !== null ? (int)$tcpRttHeader : null;
@@ -223,22 +225,28 @@ class Ja3AnomalyDetector
223
225
  $behaviorHeader = $context->headers['x-behavior-metrics'] ?? null;
224
226
  if ($behaviorHeader) {
225
227
  $metrics = json_decode($behaviorHeader, true);
226
- if (json_last_error() === JSON_ERROR_NONE && isset($metrics['clientTimestamp'])) {
228
+ if (json_last_error() === JSON_ERROR_NONE && isset($metrics['clientTimestamp']) && is_numeric($metrics['clientTimestamp'])) {
227
229
  $clientTimestamp = (int)$metrics['clientTimestamp'];
228
230
  $appLatency = $context->requestTimestamp - $clientTimestamp;
229
- if ($tcpRtt !== null && $tcpRtt > 0) {
230
- $clientToProxyDelta = $appLatency - $tcpRtt;
231
- if ($tcpRtt < 35 && $clientToProxyDelta > 150) {
232
- return 85;
233
- }
234
- } else {
235
- if ($appLatency > 350) {
236
- return 40;
231
+
232
+ if ($tcpRtt !== null && $tcpRtt > 0 && $appLatency > 0) {
233
+ // Marge de tolérance défensive contre le jitter réseau et le scheduling JS / Garbage Collection
234
+ $jitterAllowance = 60.0;
235
+ $effectiveRtt = max((float)$tcpRtt, 5.0);
236
+ $tunnelDelta = $appLatency - ($tcpRtt + $jitterAllowance);
237
+ $divergenceRatio = $appLatency / $effectiveRtt;
238
+
239
+ // Condition de disjonction : liaison edge ultra-proche (< 40ms) + transit applicatif au moins 3x supérieur
240
+ if ($tcpRtt <= 40 && $divergenceRatio >= 3.0 && $tunnelDelta > 0) {
241
+ $scaling = 120.0;
242
+ $ratioWeight = min(1.0, ($divergenceRatio - 3.0) / 5.0);
243
+ $proxyScore = min(95.0, 40.0 + 55.0 * tanh($tunnelDelta / $scaling) * $ratioWeight);
244
+ return round($proxyScore, 1);
237
245
  }
238
246
  }
239
247
  }
240
248
  }
241
- return 0;
249
+ return 0.0;
242
250
  }
243
251
  }
244
252
 
@@ -17,7 +17,11 @@ class Optimization
17
17
  try {
18
18
  return random_int(0, PHP_INT_MAX - 1) / PHP_INT_MAX;
19
19
  } catch (\Exception $e) {
20
- return (float)mt_rand() / (float)mt_getrandmax(); // Fallback
20
+ if (function_exists('wp_rand')) {
21
+ return (float)wp_rand(0, 1000000) / 1000000.0;
22
+ }
23
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- Standalone fallback
24
+ return (float)mt_rand() / (float)mt_getrandmax();
21
25
  }
22
26
  }
23
27
 
@@ -44,7 +44,11 @@ class OptimizationOperators
44
44
  try {
45
45
  return random_int(0, PHP_INT_MAX - 1) / PHP_INT_MAX;
46
46
  } catch (\Exception $e) {
47
- return (float)mt_rand() / (float)mt_getrandmax(); // Fallback
47
+ if (function_exists('wp_rand')) {
48
+ return (float)wp_rand(0, 1000000) / 1000000.0;
49
+ }
50
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- Standalone fallback
51
+ return (float)mt_rand() / (float)mt_getrandmax();
48
52
  }
49
53
  }
50
54
 
@@ -12,6 +12,18 @@ class ProblemInitializers
12
12
  /** @var array<string, callable> */
13
13
  private static array $initializers = [];
14
14
 
15
+ private static function randomFloat(): float
16
+ {
17
+ try {
18
+ return (float)random_int(0, PHP_INT_MAX - 1) / (float)PHP_INT_MAX;
19
+ } catch (\Throwable $e) {
20
+ if (function_exists('wp_rand')) {
21
+ return (float)wp_rand(0, 1000000) / 1000000.0;
22
+ }
23
+ return 0.5;
24
+ }
25
+ }
26
+
15
27
  private static function initialize(): void
16
28
  {
17
29
  if (empty(self::$initializers)) {
@@ -22,8 +34,8 @@ class ProblemInitializers
22
34
  $points = [];
23
35
  for ($i = 0; $i < $count; $i++) {
24
36
  $points[] = [
25
- 'x' => mt_rand() / mt_getrandmax() * $bounds['x'],
26
- 'y' => mt_rand() / mt_getrandmax() * $bounds['y']
37
+ 'x' => self::randomFloat() * $bounds['x'],
38
+ 'y' => self::randomFloat() * $bounds['y']
27
39
  ];
28
40
  }
29
41
  return $points;
@@ -36,8 +48,8 @@ class ProblemInitializers
36
48
  for ($i = 0; $i < $count; $i++) {
37
49
  $assets[] = [
38
50
  'name' => 'Asset ' . ($i + 1),
39
- 'expectedReturn' => mt_rand() / mt_getrandmax() * 0.2,
40
- 'volatility' => 0.1 + mt_rand() / mt_getrandmax() * 0.3
51
+ 'expectedReturn' => self::randomFloat() * 0.2,
52
+ 'volatility' => 0.1 + self::randomFloat() * 0.3
41
53
  ];
42
54
  }
43
55
  return $assets;
@@ -59,17 +59,17 @@ class ProblemManager
59
59
  private function loadProblems(): void
60
60
  {
61
61
  if (!file_exists($this->configPath)) {
62
- error_log("[ProblemManager] Problem config file not found: {$this->configPath}");
62
+ self::logError("[ProblemManager] Problem config file not found: {$this->configPath}");
63
63
  return;
64
64
  }
65
65
  $data = file_get_contents($this->configPath);
66
66
  if ($data === false) {
67
- error_log("[ProblemManager] Failed to read problem config file: {$this->configPath}");
67
+ self::logError("[ProblemManager] Failed to read problem config file: {$this->configPath}");
68
68
  return;
69
69
  }
70
70
  $problemsFromFile = json_decode($data, true);
71
71
  if (json_last_error() !== JSON_ERROR_NONE) {
72
- error_log("[ProblemManager] Failed to parse problem config JSON: " . json_last_error_msg());
72
+ self::logError("[ProblemManager] Failed to parse problem config JSON: " . json_last_error_msg());
73
73
  return;
74
74
  }
75
75
 
@@ -88,7 +88,7 @@ class ProblemManager
88
88
  $problem['workUnit']['scoreFunction'] = FunctionRegistry::get($problem['workUnit']['scoreFunction']);
89
89
  if ($problem['workUnit']['scoreFunction'] === null) {
90
90
  // @codeCoverageIgnoreStart
91
- error_log("[ProblemManager] Warning: scoreFunction '{$problem['workUnit']['scoreFunction']}' not found in registry for problem '{$problem['id']}'.");
91
+ self::logError("[ProblemManager] Warning: scoreFunction '{$problem['workUnit']['scoreFunction']}' not found in registry for problem '{$problem['id']}'.");
92
92
  // @codeCoverageIgnoreEnd
93
93
  }
94
94
  }
@@ -167,7 +167,7 @@ class ProblemManager
167
167
  $task['solverName'] = $problem['workUnit']['solverName'];
168
168
  break;
169
169
  default:
170
- error_log("[ProblemManager] Unknown useful work type: {$problem['workUnit']['type']}");
170
+ self::logError("[ProblemManager] Unknown useful work type: {$problem['workUnit']['type']}");
171
171
  return null;
172
172
  }
173
173
 
@@ -198,17 +198,17 @@ class ProblemManager
198
198
  if (isset($solutionData['solution']) && isset($solutionData['energy'])) {
199
199
  $scoreFunction = $problem['workUnit']['scoreFunction'] ?? null;
200
200
  if (!$scoreFunction) {
201
- error_log("[ProblemManager] No score function defined for {$problemId}.");
201
+ self::logError("[ProblemManager] No score function defined for {$problemId}.");
202
202
  return;
203
203
  }
204
204
  // DoS mitigation: validate solution size and structure
205
205
  if (is_array($solutionData['solution']) && count($solutionData['solution']) > 500) {
206
- error_log("[ProblemManager] Solution array too large for {$problemId} verification.");
206
+ self::logError("[ProblemManager] Solution array too large for {$problemId} verification.");
207
207
  return;
208
208
  }
209
209
  $serializedSolution = json_encode($solutionData['solution']);
210
210
  if ($serializedSolution !== false && strlen($serializedSolution) > 65536) {
211
- error_log("[ProblemManager] Solution payload size exceeds safe limit for {$problemId}.");
211
+ self::logError("[ProblemManager] Solution payload size exceeds safe limit for {$problemId}.");
212
212
  return;
213
213
  }
214
214
  // 1. Never trust client-reported score. Recalculate server-side.
@@ -222,19 +222,19 @@ class ProblemManager
222
222
  $problem['state']['bestEnergy'] = $recalculatedEnergy; // 3. Store verified score
223
223
  $problem['state']['lastUpdate'] = (new \DateTime())->format(\DateTime::ATOM);
224
224
  $stateChanged = true;
225
- error_log("[ProblemManager] New best solution for {$problemId}: {$recalculatedEnergy}"); // @phpstan-ignore-line
225
+ self::logError("[ProblemManager] New best solution for {$problemId}: {$recalculatedEnergy}"); // @phpstan-ignore-line
226
226
  }
227
227
  }
228
228
  break;
229
229
  case 'genetic_algorithm_generations':
230
230
  if (isset($solutionData['population']) && is_array($solutionData['population'])) {
231
231
  if (count($solutionData['population']) > 150) {
232
- error_log("[ProblemManager] Population size exceeds safe limit for {$problemId}.");
232
+ self::logError("[ProblemManager] Population size exceeds safe limit for {$problemId}.");
233
233
  return;
234
234
  }
235
235
  foreach ($solutionData['population'] as $ind) {
236
236
  if (isset($ind['chromosome']) && is_array($ind['chromosome']) && count($ind['chromosome']) > 100) {
237
- error_log("[ProblemManager] Chromosome size too large for {$problemId}.");
237
+ self::logError("[ProblemManager] Chromosome size too large for {$problemId}.");
238
238
  return;
239
239
  }
240
240
  }
@@ -251,7 +251,7 @@ class ProblemManager
251
251
  $recalculated = $fitnessFunction($individual['chromosome'], $problem['payload'] ?? []);
252
252
  if (isset($individual['fitness']) && $individual['fitness'] !== -1) {
253
253
  if (abs($individual['fitness'] - $recalculated) > 1e-4) {
254
- error_log("[ProblemManager] Cheating detected for {$problemId}! Reported: {$individual['fitness']}, recalculated: {$recalculated}");
254
+ self::logError("[ProblemManager] Cheating detected for {$problemId}! Reported: {$individual['fitness']}, recalculated: {$recalculated}");
255
255
  return; // Reject inconsistent population
256
256
  }
257
257
  }
@@ -272,7 +272,7 @@ class ProblemManager
272
272
  }
273
273
  break;
274
274
  default:
275
- error_log("[ProblemManager] Integration not implemented for useful work type: {$problem['workUnit']['type']}");
275
+ self::logError("[ProblemManager] Integration not implemented for useful work type: {$problem['workUnit']['type']}");
276
276
  break;
277
277
  }
278
278
  // Persist updated state to datastore
@@ -294,7 +294,7 @@ class ProblemManager
294
294
  if (!empty($newFront) && json_encode($newFront) !== json_encode($currentFront)) {
295
295
  $problem['state']['paretoFront'] = $newFront;
296
296
  $problem['state']['lastUpdate'] = (new \DateTime())->format(\DateTime::ATOM);
297
- error_log("[ProblemManager] New Pareto front for {$problem['id']} with " . count($newFront) . " solutions."); // @phpstan-ignore-line
297
+ self::logError("[ProblemManager] New Pareto front for {$problem['id']} with " . count($newFront) . " solutions."); // @phpstan-ignore-line
298
298
  return true;
299
299
  }
300
300
  return false;
@@ -407,4 +407,10 @@ class ProblemManager
407
407
  {
408
408
  return $this->problems;
409
409
  }
410
+
411
+ private static function logError(string $message): void
412
+ {
413
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- ProblemManager logging
414
+ error_log($message);
415
+ }
410
416
  }