1<?php
2
3/**
4 * This plugin is used to cleanup the history
5 *
6 * @see    http://dokuwiki.org/plugin:clearhistory
7 * @author Dominik Eckelmann <deckelmann@gmail.com>
8 */
9class admin_plugin_clearhistory extends DokuWiki_Admin_Plugin
10{
11
12    /**
13     * pages in a hiracical view
14     *
15     * "<namespace>" => array( ... )
16     * "pages" => array ( pagenames )
17     */
18    protected $pages = array();
19
20    /**
21     * counts deleted pages for a run
22     */
23    public $delcounter = 0;
24
25    /**
26     * handle the request befor html output
27     *
28     * @see html()
29     */
30    public function handle()
31    {
32        $onlySmall = false;
33        $onlyNoComment = false;
34        if (isset($_REQUEST['onlysmall']) && $_REQUEST['onlysmall'] == 'on') $onlySmall = true;
35        if (isset($_REQUEST['onlynocomment']) && $_REQUEST['onlynocomment'] == 'on') $onlyNoComment = true;
36
37        if (isset($_GET['clear'])) {
38            if ($_GET['clear'] == 1) {
39                $this->_scanRecents(30, $onlySmall, $onlyNoComment);
40            } else {
41                if ($_GET['clear'] == 2) {
42                    $_GET['ns'] = cleanID($_GET['ns']);
43                    $this->_scanNamespace($_GET['ns'], $onlySmall, $onlyNoComment);
44                }
45            }
46            msg(sprintf($this->getLang('deleted'), $this->delcounter), 1);
47        }
48    }
49
50    /**
51     * output html for the admin page
52     */
53    public function html()
54    {
55        echo '<h1>' . $this->getLang('menu') . '</h1>';
56        echo '<form action="' . wl() . '" method="GET"><fieldset class="clearhistory">';
57        echo '<input type="hidden" name="do" value="admin" />';
58        echo '<input type="hidden" name="page" value="clearhistory" />';
59
60        echo '<fieldset><input type="radio" name="clear" value="1" id="c1" checked="checked" /> <label for="c1">';
61        echo $this->getLang('clean on recent changes') . '</label></fieldset><br/>';
62
63        echo '<fieldset><input type="radio" name="clear" value="2" id="c2" /> <label for="c2">';
64        echo $this->getLang('clean on namespace');
65        echo '</label><br /><input type="text" name="ns" class="edit" />';
66        echo '</fieldset><br/>';
67        echo '<fieldset>';
68        echo '<input type="checkbox" name="onlysmall" id="c3" ' . ($this->getConf('autoclearonlysmall') ? 'checked="checked"' : '') . '  /> <label for="c3">' . $this->getLang('onlysmall') . '</label><br />';
69        echo '<input type="checkbox" name="onlynocomment" id="c4" ' . ($this->getConf('autoclearonlynocomment') ? 'checked="checked"' : '') . ' /> <label for="c4">' . $this->getLang('onlynocomment') . '</label><br />';
70        echo '</fieldset>';
71
72        echo '<input type="submit" value="' . $this->getLang('do') . '" class="button" /></fieldset>';
73        echo '</form>';
74
75        echo '<div class="clearhistory">' . $this->locale_xhtml('intro') . '</div>';
76    }
77
78    /**
79     * Scans throu a namespace and count the deleted pages in $this->delcounter
80     *
81     * @param string $ns the namespace to search in
82     * @param boolean $onlySmall only delete small changes on true
83     * @param boolean $onlyNoComment don't delete changes with a comment on true
84     */
85    protected function _scanNamespace($ns, $onlySmall = false, $onlyNoComment = false)
86    {
87        $this->delcounter = 0;
88        $this->_scan($ns, $onlySmall, $onlyNoComment);
89    }
90
91    /**
92     * Scans namespaces for deletable revisions
93     *
94     * @param string $ns the namespace to search in
95     * @param boolean $onlySmall only delete small changes on true
96     * @param boolean $onlyNoComment don't delete changes with a comment on true
97     */
98    protected function _scan($ns = '', $onlySmall = false, $onlyNoComment = false)
99    {
100        $dir = preg_replace('/\.txt(\.gz)?/i', '', wikiFN($ns));
101        $dir = rtrim($dir, '/');
102        if (!is_dir($dir)) return;
103        $dh = opendir($dir);
104        if (!$dh) {
105            echo 'error';
106            return;
107        }
108        while (($file = readdir($dh)) !== false) {
109            if ($file == '.' || $file == '..') continue;
110            if (is_dir($dir . '/' . $file)) {
111                $this->_scan($ns . ':' . $file);
112                continue;
113            }
114            if ($file[0] == '_') continue;
115            if (substr($file, -4) == '.txt') {
116                $name = substr($file, 0, -4);
117                $this->_parseChangesFile(metaFN($ns . ':' . $name, '.changes'), $ns . ':' . $name, $onlySmall,
118                    $onlyNoComment);
119            }
120        }
121        closedir($dh);
122    }
123
124    /**
125     * Scans the recent changed files for changes
126     *
127     * @param int $num number of last changed files to scan
128     * @param boolean $onlySmall only delete small changes on true
129     * @param boolean $onlyNoComment don't delete changes with a comment on true
130     */
131    public function _scanRecents($num = 30, $onlySmall = false, $onlyNoComment = false)
132    {
133        $recents = getRecents(0, $num);
134
135        $this->delcounter = 0;
136        foreach ($recents as $recent) {
137            $this->_parseChangesFile(metaFN($recent['id'], '.changes'), $recent['id'], $onlySmall, $onlyNoComment);
138        }
139    }
140
141    /**
142     * Parses a .changes file for deletable pages and deletes them.
143     *
144     * @param string $file the path to the change file
145     * @param string $page wiki pagename
146     * @param boolean $onlySmall deletes only small changes
147     * @param boolean $onlyNoComment deletes only entrys without a comment
148     */
149    protected function _parseChangesFile($file, $page, $onlySmall = false, $onlyNoComment = false)
150    {
151        if (!is_file($file)) return;
152        if (checklock($page)) return;
153        lock($page);
154        $content = file_get_contents($file);
155        // get page informations
156        $max = preg_match_all('/^([0-9]+)\s+?(\S+)\s+?([ECDR])\s+?(\S+)\s+?(\S*)\s+?(.*)$/im',
157            $content, $match);
158        if ($max <= 1) return;
159        // set mark to creation entry
160        $cmptime = $match[1][0] + 9999999999;
161        $cmpuser = (empty($match[5][$max - 1])) ? $match[2][$max - 1] : $match[5][$max - 1]; // user or if not logged in ip
162        $newcontent = '';
163        for ($i = $max - 1; $i >= 0; $i--) {
164            $user = (empty($match[5][$i])) ? $match[2][$i] : $match[5][$i]; // user or if not logged in ip
165            $time = $match[1][$i];
166            // Creations arnt touched
167            if ($match[3][$i] != "E" && $match[3][$i] != "e") {
168                $cmpuser = $user;
169                $cmptime = $time;
170                $newcontent = $this->_addLine($match, $i) . $newcontent;
171                continue;
172            }
173            // if not the same user -> new mark and continue
174            if ($user != $cmpuser) {
175                $cmpuser = $user;
176                $cmptime = $time;
177                $newcontent = $this->_addLine($match, $i) . $newcontent;
178                continue;
179            }
180
181            // if onlySmall is set we just want to handle the small matches
182            if ($onlySmall && $match[3][$i] == 'E') {
183                $cmpuser = $user;
184                $cmptime = $time;
185                $newcontent = $this->_addLine($match, $i) . $newcontent;
186                continue;
187            }
188
189            // if onlyNoComment is set we pass all lines with a comment
190            if ($onlyNoComment && trim($match[6][$i]) != '') {
191                $cmpuser = $user;
192                $cmptime = $time;
193                $newcontent = $this->_addLine($match, $i) . $newcontent;
194                continue;
195            }
196
197            // check the time difference between the entrys
198            if ($cmptime - (60 * 60) < $time) {
199                @unlink(wikiFN($match[4][$i], $time)); //try the original name
200                @unlink(wikiFN($match[4][$max - 1], $time));  //try the latest name (move plugin) issue #1
201                $this->delcounter++;
202                continue;
203            }
204            $cmptime = $time;
205            $newcontent = $this->_addLine($match, $i) . $newcontent;
206        }
207        unlock($page);
208        io_saveFile($file, $newcontent);
209    }
210
211    /**
212     * Return a line from the set of regex matches, terminated with a newline char
213     *
214     * @param array $match
215     * @param int $i
216     * @return string
217     */
218    protected function _addLine($match, $i)
219    {
220        return $match[0][$i] . "\n";
221    }
222
223}
224