1<?php 2 3namespace dokuwiki\plugin\captcha; 4 5/** 6 * A simple mechanism to count login failures for IP addresses 7 * 8 * Counter files are stored in date-based directories for easy cleanup. 9 * Note: Counters reset at midnight when a new directory is used. 10 */ 11class IpCounter 12{ 13 /** @var string The client IP address being tracked */ 14 protected $ip; 15 16 /** @var string File path where the failure counter is stored */ 17 protected $store; 18 19 /** @var int Base delay in seconds for exponential timeout calculation */ 20 protected $base; 21 22 /** @var int Maximum delay in seconds (cap for exponential timeout) */ 23 protected $max; 24 25 /** 26 * Initialize the counter 27 */ 28 public function __construct() 29 { 30 global $conf; 31 $this->ip = clientIP(true); 32 $this->store = $conf['tmpdir'] . '/captcha/ip/' . date('Y-m-d') . '/' . md5($this->ip) . '.ip'; 33 io_makeFileDir($this->store); 34 35 $this->base = (int)($conf['plugin']['captcha']['logindenial'] ?? 0); 36 $this->max = (int)($conf['plugin']['captcha']['logindenial_max'] ?? 3600); 37 } 38 39 /** 40 * Increases the counter by adding a byte 41 * 42 * @return void 43 */ 44 public function increment() 45 { 46 io_saveFile($this->store, '1', true); 47 } 48 49 /** 50 * Return the current counter 51 * 52 * @return int 53 */ 54 public function get() 55 { 56 return (int)@filesize($this->store); 57 } 58 59 /** 60 * Reset the counter to zero 61 * 62 * @return void 63 */ 64 public function reset() 65 { 66 @unlink($this->store); 67 } 68 69 /** 70 * Get timestamp of last failed attempt 71 * 72 * @return int Unix timestamp, 0 if no attempts 73 */ 74 public function getLastAttempt() 75 { 76 return (int)@filemtime($this->store); 77 } 78 79 /** 80 * Calculate required timeout in seconds based on failure count 81 * 82 * Uses exponential backoff: base * 2^(count-1), capped at max. 83 * First failed attempt is okay, second requires base seconds wait. 84 * 85 * @return int Timeout in seconds (0 if no failures or feature disabled) 86 */ 87 public function calculateTimeout() 88 { 89 if ($this->base < 1) return 0; 90 $count = $this->get(); 91 if ($count < 1) return 0; 92 $timeout = $this->base * pow(2, $count - 1); // -1 because first failure is free 93 return (int)min($timeout, $this->max); 94 } 95 96 /** 97 * Get remaining wait time in seconds 98 * 99 * @return int Seconds remaining (0 if no wait needed or feature disabled) 100 */ 101 public function getRemainingTime() 102 { 103 $timeout = $this->calculateTimeout(); 104 if ($timeout === 0) return 0; 105 $elapsed = time() - $this->getLastAttempt(); 106 return max(0, $timeout - $elapsed); 107 } 108 109 /** 110 * Remove all outdated IP counter directories 111 * 112 * Deletes counter directories older than today, similar to FileCookie::clean() 113 * 114 * @return void 115 */ 116 public static function clean() 117 { 118 global $conf; 119 $path = $conf['tmpdir'] . '/captcha/ip/'; 120 $dirs = glob("$path/*", GLOB_ONLYDIR); 121 if (!$dirs) return; 122 123 $today = date('Y-m-d'); 124 foreach ($dirs as $dir) { 125 if (basename($dir) === $today) continue; 126 if (!preg_match('/\/captcha\/ip\//', $dir)) continue; // safety net 127 io_rmdir($dir, true); 128 } 129 } 130} 131