xref: /plugin/captcha/IpCounter.php (revision 194d338681b559bf46a61e6fd49d98dea7193d22)
1969b14c4SAndreas Gohr<?php
2969b14c4SAndreas Gohr
3969b14c4SAndreas Gohrnamespace dokuwiki\plugin\captcha;
4969b14c4SAndreas Gohr
5969b14c4SAndreas Gohr/**
6969b14c4SAndreas Gohr * A simple mechanism to count login failures for IP addresses
7*194d3386SAndreas Gohr *
8*194d3386SAndreas Gohr * Counter files are stored in date-based directories for easy cleanup.
9*194d3386SAndreas Gohr * Note: Counters reset at midnight when a new directory is used.
10969b14c4SAndreas Gohr */
11969b14c4SAndreas Gohrclass IpCounter
12969b14c4SAndreas Gohr{
13*194d3386SAndreas Gohr    /** @var string The client IP address being tracked */
14969b14c4SAndreas Gohr    protected $ip;
15*194d3386SAndreas Gohr
16*194d3386SAndreas Gohr    /** @var string File path where the failure counter is stored */
17969b14c4SAndreas Gohr    protected $store;
18969b14c4SAndreas Gohr
19*194d3386SAndreas Gohr    /** @var int Base delay in seconds for exponential timeout calculation */
20*194d3386SAndreas Gohr    protected $base;
21*194d3386SAndreas Gohr
22*194d3386SAndreas Gohr    /** @var int Maximum delay in seconds (cap for exponential timeout) */
23*194d3386SAndreas Gohr    protected $max;
24*194d3386SAndreas Gohr
25969b14c4SAndreas Gohr    /**
26969b14c4SAndreas Gohr     * Initialize the counter
27969b14c4SAndreas Gohr     */
28969b14c4SAndreas Gohr    public function __construct()
29969b14c4SAndreas Gohr    {
30*194d3386SAndreas Gohr        global $conf;
31969b14c4SAndreas Gohr        $this->ip = clientIP(true);
32*194d3386SAndreas Gohr        $this->store = $conf['tmpdir'] . '/captcha/ip/' . date('Y-m-d') . '/' . md5($this->ip) . '.ip';
33*194d3386SAndreas Gohr        io_makeFileDir($this->store);
34*194d3386SAndreas Gohr
35*194d3386SAndreas Gohr        $this->base = (int)($conf['plugin']['captcha']['logindenial'] ?? 0);
36*194d3386SAndreas Gohr        $this->max = (int)($conf['plugin']['captcha']['logindenial_max'] ?? 3600);
37969b14c4SAndreas Gohr    }
38969b14c4SAndreas Gohr
39969b14c4SAndreas Gohr    /**
40969b14c4SAndreas Gohr     * Increases the counter by adding a byte
41969b14c4SAndreas Gohr     *
42969b14c4SAndreas Gohr     * @return void
43969b14c4SAndreas Gohr     */
44969b14c4SAndreas Gohr    public function increment()
45969b14c4SAndreas Gohr    {
46969b14c4SAndreas Gohr        io_saveFile($this->store, '1', true);
47969b14c4SAndreas Gohr    }
48969b14c4SAndreas Gohr
49969b14c4SAndreas Gohr    /**
50969b14c4SAndreas Gohr     * Return the current counter
51969b14c4SAndreas Gohr     *
52969b14c4SAndreas Gohr     * @return int
53969b14c4SAndreas Gohr     */
54969b14c4SAndreas Gohr    public function get()
55969b14c4SAndreas Gohr    {
56969b14c4SAndreas Gohr        return (int)@filesize($this->store);
57969b14c4SAndreas Gohr    }
58969b14c4SAndreas Gohr
59969b14c4SAndreas Gohr    /**
60969b14c4SAndreas Gohr     * Reset the counter to zero
61969b14c4SAndreas Gohr     *
62969b14c4SAndreas Gohr     * @return void
63969b14c4SAndreas Gohr     */
64969b14c4SAndreas Gohr    public function reset()
65969b14c4SAndreas Gohr    {
66969b14c4SAndreas Gohr        @unlink($this->store);
67969b14c4SAndreas Gohr    }
68563fb566SAndreas Gohr
69563fb566SAndreas Gohr    /**
70563fb566SAndreas Gohr     * Get timestamp of last failed attempt
71563fb566SAndreas Gohr     *
72563fb566SAndreas Gohr     * @return int Unix timestamp, 0 if no attempts
73563fb566SAndreas Gohr     */
74563fb566SAndreas Gohr    public function getLastAttempt()
75563fb566SAndreas Gohr    {
76563fb566SAndreas Gohr        return (int)@filemtime($this->store);
77563fb566SAndreas Gohr    }
78563fb566SAndreas Gohr
79563fb566SAndreas Gohr    /**
80563fb566SAndreas Gohr     * Calculate required timeout in seconds based on failure count
81563fb566SAndreas Gohr     *
82*194d3386SAndreas Gohr     * Uses exponential backoff: base * 2^(count-1), capped at max.
83*194d3386SAndreas Gohr     * First failed attempt is okay, second requires base seconds wait.
84563fb566SAndreas Gohr     *
85*194d3386SAndreas Gohr     * @return int Timeout in seconds (0 if no failures or feature disabled)
86563fb566SAndreas Gohr     */
87*194d3386SAndreas Gohr    public function calculateTimeout()
88563fb566SAndreas Gohr    {
89*194d3386SAndreas Gohr        if ($this->base < 1) return 0;
90563fb566SAndreas Gohr        $count = $this->get();
91563fb566SAndreas Gohr        if ($count < 1) return 0;
92*194d3386SAndreas Gohr        $timeout = $this->base * pow(2, $count - 1); // -1 because first failure is free
93*194d3386SAndreas Gohr        return (int)min($timeout, $this->max);
94563fb566SAndreas Gohr    }
95563fb566SAndreas Gohr
96563fb566SAndreas Gohr    /**
97563fb566SAndreas Gohr     * Get remaining wait time in seconds
98563fb566SAndreas Gohr     *
99*194d3386SAndreas Gohr     * @return int Seconds remaining (0 if no wait needed or feature disabled)
100563fb566SAndreas Gohr     */
101*194d3386SAndreas Gohr    public function getRemainingTime()
102563fb566SAndreas Gohr    {
103*194d3386SAndreas Gohr        $timeout = $this->calculateTimeout();
104563fb566SAndreas Gohr        if ($timeout === 0) return 0;
105563fb566SAndreas Gohr        $elapsed = time() - $this->getLastAttempt();
106563fb566SAndreas Gohr        return max(0, $timeout - $elapsed);
107563fb566SAndreas Gohr    }
108*194d3386SAndreas Gohr
109*194d3386SAndreas Gohr    /**
110*194d3386SAndreas Gohr     * Remove all outdated IP counter directories
111*194d3386SAndreas Gohr     *
112*194d3386SAndreas Gohr     * Deletes counter directories older than today, similar to FileCookie::clean()
113*194d3386SAndreas Gohr     *
114*194d3386SAndreas Gohr     * @return void
115*194d3386SAndreas Gohr     */
116*194d3386SAndreas Gohr    public static function clean()
117*194d3386SAndreas Gohr    {
118*194d3386SAndreas Gohr        global $conf;
119*194d3386SAndreas Gohr        $path = $conf['tmpdir'] . '/captcha/ip/';
120*194d3386SAndreas Gohr        $dirs = glob("$path/*", GLOB_ONLYDIR);
121*194d3386SAndreas Gohr        if (!$dirs) return;
122*194d3386SAndreas Gohr
123*194d3386SAndreas Gohr        $today = date('Y-m-d');
124*194d3386SAndreas Gohr        foreach ($dirs as $dir) {
125*194d3386SAndreas Gohr            if (basename($dir) === $today) continue;
126*194d3386SAndreas Gohr            if (!preg_match('/\/captcha\/ip\//', $dir)) continue; // safety net
127*194d3386SAndreas Gohr            io_rmdir($dir, true);
128*194d3386SAndreas Gohr        }
129*194d3386SAndreas Gohr    }
130969b14c4SAndreas Gohr}
131