1<?php
2/**
3 * DokuWiki Plugin cleanoldips (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_cleanoldips 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($ID);
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($ID, $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($mediaID, $changelogFN);
85        }
86    }
87
88    /**
89     * Remove IPs from changelog entries that are older than $conf['recent_days']
90     *
91     * @param string $id
92     * @param string $changelogFN
93     */
94    public function cleanChangelog($id, $changelogFN)
95    {
96        if (!file_exists($changelogFN)) {
97            return;
98        }
99        global $conf;
100
101        $cacheFile = $this->getOurCacheFilename($id, true);
102        $cacheStartPosition = (int)file_get_contents($cacheFile);
103        $startPosition = $this->validateStartPosition($cacheStartPosition, $changelogFN);
104
105        $handle = fopen($changelogFN, 'rb+');
106        fseek($handle, $startPosition);
107        $ageCutoff = (int)$conf['recent_days'] * self::SECONDS_IN_A_DAY;
108
109        while (($line = fgets($handle)) !== false) {
110            list($timestamp, $ip, $rest) = explode("\t", $line, 3);
111            $ageOfEntry = time() - (int)$timestamp;
112            if ($ageOfEntry < $ageCutoff) {
113                // this and the remaining lines are newer than $conf['recent_days']
114                $positionAtBeginningOfLine = ftell($handle) - strlen($line);
115                fseek($handle, $positionAtBeginningOfLine);
116                break;
117            }
118
119            $cleanedLine = implode("\t", [$timestamp, str_pad('', strlen($ip)), $rest]);
120            $writeOffset = ftell($handle) - strlen($line);
121            fseek($handle, $writeOffset);
122            $bytesWritten = fwrite($handle, $cleanedLine);
123            if ($bytesWritten === false) {
124                throw new RuntimeException('There was an unknown error writing the changlog for ' . $id);
125            }
126        }
127
128        file_put_contents($cacheFile, ftell($handle));
129        fclose($handle);
130    }
131
132    /**
133     * Get the start position from cache and ensure its valid by performing some sanity checks
134     *
135     * @param int $cacheStartPosition
136     * @param string $changelogFile the changelog for pageid
137     *
138     * @return int the start position
139     */
140    public function validateStartPosition($cacheStartPosition, $changelogFile)
141    {
142        if ($cacheStartPosition > filesize($changelogFile)) {
143            return 0;
144        }
145        if ($cacheStartPosition > 0) {
146            $handle = fopen($changelogFile, 'rb');
147            fseek($handle, $cacheStartPosition - 1);
148            $previousChar = fread($handle, 1);
149            fclose($handle);
150
151            if ($previousChar !== "\n") {
152                return 0;
153            }
154        }
155        return $cacheStartPosition;
156    }
157
158    /**
159     * Get the filename of this plugin's cachefile for a page
160     *
161     * @param string $pageid full id of the page
162     * @param bool   $create create the cache file if it doesn't exists
163     *
164     * @return string the filename of this plugin's cachefile
165     */
166    protected function getOurCacheFilename($pageid, $create = false)
167    {
168        $cacheFN = getCacheName('_' . $pageid . 'cleanoldips');
169        if ($create && !file_exists($cacheFN)) {
170            touch($cacheFN);
171        }
172        return $cacheFN;
173    }
174}
175