1<?php
2/**
3 * DokuWiki Plugin gdpr (Action Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Michael Große <dokuwiki@cosmocode.de>
7 */
8
9class action_plugin_gdpr_oldips extends DokuWiki_Action_Plugin
10{
11
12    const SECONDS_IN_A_DAY = 86400;
13
14    /**
15     * Registers a callback function for a given event
16     *
17     * @param Doku_Event_Handler $controller DokuWiki's event controller object
18     *
19     * @return void
20     */
21    public function register(Doku_Event_Handler $controller)
22    {
23        $controller->register_hook('INDEXER_TASKS_RUN', 'BEFORE', $this, 'handleIndexerTasksRun');
24        $controller->register_hook('TASK_RECENTCHANGES_TRIM', 'BEFORE', $this, 'initiateMediaChangelogClean');
25    }
26
27    /**
28     * [Custom event handler which performs action]
29     *
30     * Called for event: INDEXER_TASKS_RUN
31     *
32     * @param Doku_Event $event  event object by reference
33     * @param mixed      $param  [the parameters passed as fifth argument to register_hook() when this
34     *                           handler was registered]
35     *
36     * @return void
37     */
38    public function handleIndexerTasksRun(Doku_Event $event, $param)
39    {
40        global $ID;
41        $changelogFN = metaFN($ID, '.changes');
42        if (!file_exists($changelogFN)) {
43            return;
44        }
45        $cacheFile = $this->getOurCacheFilename($changelogFN);
46        if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < self::SECONDS_IN_A_DAY)) {
47            // we already cleaned this page in the last 24h
48            return;
49        }
50
51        $event->preventDefault();
52        $event->stopPropagation();
53
54        touch($cacheFile);
55
56        $this->cleanChangelog($changelogFN);
57    }
58
59    /**
60     * [Custom event handler which performs action]
61     *
62     * Called for event: TASK_RECENTCHANGES_TRIM
63     *
64     * @param Doku_Event $event  event object by reference
65     * @param mixed      $param  [the parameters passed as fifth argument to register_hook() when this
66     *                           handler was registered]
67     *
68     * @return void
69     */
70    public function initiateMediaChangelogClean(Doku_Event $event, $param)
71    {
72        if (!$event->data['isMedia']) {
73            return;
74        }
75
76        foreach ($event->data['removedChangelogLines'] as $mediaChangelogLine) {
77            list(, , , $mediaID,) = explode("\t", $mediaChangelogLine, 5);
78
79            $changelogFN = mediaMetaFN($mediaID, '.changes');
80            if (!file_exists($changelogFN)) {
81                continue;
82            }
83
84            $this->cleanChangelog($changelogFN);
85        }
86    }
87
88    /**
89     * Remove IPs from changelog entries that are older than $conf['recent_days']
90     *
91     * @param string $changelogFN
92     */
93    public function cleanChangelog($changelogFN)
94    {
95        if (!file_exists($changelogFN)) {
96            return;
97        }
98        global $conf;
99
100        $cacheFile = $this->getOurCacheFilename($changelogFN, true);
101        $cacheStartPosition = (int)file_get_contents($cacheFile);
102        $startPosition = $this->validateStartPosition($cacheStartPosition, $changelogFN);
103
104        $handle = fopen($changelogFN, 'rb+');
105        fseek($handle, $startPosition);
106        $ageCutoff = (int)$conf['recent_days'] * self::SECONDS_IN_A_DAY;
107
108        while (($line = fgets($handle)) !== false) {
109            list($timestamp, $ip, $rest) = explode("\t", $line, 3);
110            $ageOfEntry = time() - (int)$timestamp;
111            if ($ageOfEntry < $ageCutoff) {
112                // this and the remaining lines are newer than $conf['recent_days']
113                $positionAtBeginningOfLine = ftell($handle) - strlen($line);
114                fseek($handle, $positionAtBeginningOfLine);
115                break;
116            }
117
118            $cleanedLine = implode("\t", [$timestamp, str_pad('', strlen($ip)), $rest]);
119            $writeOffset = ftell($handle) - strlen($line);
120            fseek($handle, $writeOffset);
121            $bytesWritten = fwrite($handle, $cleanedLine);
122            if ($bytesWritten === false) {
123                throw new RuntimeException('There was an unknown error writing the changlog ' . $changelogFN);
124            }
125        }
126
127        file_put_contents($cacheFile, ftell($handle));
128        fclose($handle);
129    }
130
131    /**
132     * Get the start position from cache and ensure its valid by performing some sanity checks
133     *
134     * @param int $cacheStartPosition
135     * @param string $changelogFile the changelog for pageid
136     *
137     * @return int the start position
138     */
139    public function validateStartPosition($cacheStartPosition, $changelogFile)
140    {
141        if ($cacheStartPosition > filesize($changelogFile)) {
142            return 0;
143        }
144        if ($cacheStartPosition > 0) {
145            $handle = fopen($changelogFile, 'rb');
146            fseek($handle, $cacheStartPosition - 1);
147            $previousChar = fread($handle, 1);
148            fclose($handle);
149
150            if ($previousChar !== "\n") {
151                return 0;
152            }
153        }
154        return $cacheStartPosition;
155    }
156
157    /**
158     * Get the filename of this plugin's cachefile for a page
159     *
160     * @param string $changelogFN filename of the changelog
161     * @param bool   $create      create the cache file if it doesn't exists
162     *
163     * @return string the filename of this plugin's cachefile
164     */
165    protected function getOurCacheFilename($changelogFN, $create = false)
166    {
167        $cacheFN = getCacheName('_' . $changelogFN, '.plugin_gdpr');
168        if ($create && !file_exists($cacheFN)) {
169            touch($cacheFN);
170        }
171        return $cacheFN;
172    }
173}
174