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