xref: /dokuwiki/inc/TaskRunner.php (revision 3b58faf6603a05d52ceb517f2e7a172e94e393eb)
1<?php
2
3namespace dokuwiki;
4
5use Doku_Event;
6use Sitemapper;
7use Subscription;
8
9class TaskRunner
10{
11    public function run()
12    {
13        global $INPUT, $conf, $ID;
14
15        // keep running after browser closes connection
16        @ignore_user_abort(true);
17
18        // check if user abort worked, if yes send output early
19        $defer = !@ignore_user_abort() || $conf['broken_iua'];
20        $output = $INPUT->has('debug') && $conf['allowdebug'];
21        if(!$defer && !$output){
22            $this->sendGIF();
23        }
24
25        $ID = cleanID($INPUT->str('id'));
26
27        // Catch any possible output (e.g. errors)
28        if(!$output) {
29            ob_start();
30        } else {
31            header('Content-Type: text/plain');
32        }
33
34        // run one of the jobs
35        $tmp = []; // No event data
36        $evt = new Doku_Event('INDEXER_TASKS_RUN', $tmp);
37        if ($evt->advise_before()) {
38            $this->runIndexer() or
39            $this->runSitemapper() or
40            $this->sendDigest() or
41            $this->runTrimRecentChanges() or
42            $this->runTrimRecentChanges(true) or
43            $evt->advise_after();
44        }
45
46        if(!$output) {
47            ob_end_clean();
48            if($defer) {
49                $this->sendGIF();
50            }
51        }
52    }
53
54    /**
55     * Just send a 1x1 pixel blank gif to the browser
56     *
57     * @author Andreas Gohr <andi@splitbrain.org>
58     * @author Harry Fuecks <fuecks@gmail.com>
59     */
60    function sendGIF(){
61        $img = base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7');
62        header('Content-Type: image/gif');
63        header('Content-Length: '.strlen($img));
64        header('Connection: Close');
65        print $img;
66        tpl_flush();
67        // Browser should drop connection after this
68        // Thinks it's got the whole image
69    }
70
71    /**
72     * Trims the recent changes cache (or imports the old changelog) as needed.
73     *
74     * @param bool $media_changes   If the media changelog shall be trimmed instead of
75     *                              the page changelog
76     *
77     * @return bool
78     *
79     * @author Ben Coburn <btcoburn@silicodon.net>
80     */
81    protected function runTrimRecentChanges($media_changes = false)
82    {
83        global $conf;
84
85        echo "runTrimRecentChanges($media_changes): started" . NL;
86
87        $fn = ($media_changes ? $conf['media_changelog'] : $conf['changelog']);
88
89        // Trim the Recent Changes
90        // Trims the recent changes cache to the last $conf['changes_days'] recent
91        // changes or $conf['recent'] items, which ever is larger.
92        // The trimming is only done once a day.
93        if (file_exists($fn) &&
94            (@filemtime($fn . '.trimmed') + 86400) < time() &&
95            !file_exists($fn . '_tmp')) {
96            @touch($fn . '.trimmed');
97            io_lock($fn);
98            $lines = file($fn);
99            if (count($lines) <= $conf['recent']) {
100                // nothing to trim
101                io_unlock($fn);
102                echo "runTrimRecentChanges($media_changes): finished" . NL;
103                return false;
104            }
105
106            io_saveFile($fn . '_tmp', '');          // presave tmp as 2nd lock
107            $trim_time = time() - $conf['recent_days'] * 86400;
108            $out_lines = [];
109            $old_lines = [];
110            for ($i = 0; $i < count($lines); $i++) {
111                $log = parseChangelogLine($lines[$i]);
112                if ($log === false) {
113                    continue;
114                }                      // discard junk
115                if ($log['date'] < $trim_time) {
116                    $old_lines[$log['date'] . ".$i"] = $lines[$i];     // keep old lines for now (append .$i to prevent key collisions)
117                } else {
118                    $out_lines[$log['date'] . ".$i"] = $lines[$i];     // definitely keep these lines
119                }
120            }
121
122            if (count($lines) == count($out_lines)) {
123                // nothing to trim
124                @unlink($fn . '_tmp');
125                io_unlock($fn);
126                echo "runTrimRecentChanges($media_changes): finished" . NL;
127                return false;
128            }
129
130            // sort the final result, it shouldn't be necessary,
131            //   however the extra robustness in making the changelog cache self-correcting is worth it
132            ksort($out_lines);
133            $extra = $conf['recent'] - count($out_lines);        // do we need extra lines do bring us up to minimum
134            if ($extra > 0) {
135                ksort($old_lines);
136                $out_lines = array_merge(array_slice($old_lines, -$extra), $out_lines);
137            }
138
139            $eventData = [
140                'trimmedChangelogLines' => $out_lines,
141                'removedChangelogLines' => $extra > 0 ? array_slice($old_lines, 0, -$extra) : $old_lines,
142            ];
143            trigger_event('TRIM_RECENT_CHANGES', $eventData);
144            $out_lines = $eventData['trimmedChangelogLines'];
145
146            // save trimmed changelog
147            io_saveFile($fn . '_tmp', implode('', $out_lines));
148            @unlink($fn);
149            if (!rename($fn . '_tmp', $fn)) {
150                // rename failed so try another way...
151                io_unlock($fn);
152                io_saveFile($fn, implode('', $out_lines));
153                @unlink($fn . '_tmp');
154            } else {
155                io_unlock($fn);
156            }
157            echo "runTrimRecentChanges($media_changes): finished" . NL;
158            return true;
159        }
160
161        // nothing done
162        echo "runTrimRecentChanges($media_changes): finished" . NL;
163        return false;
164    }
165
166
167    /**
168     * Runs the indexer for the current page
169     *
170     * @author Andreas Gohr <andi@splitbrain.org>
171     */
172    protected function runIndexer()
173    {
174        global $ID;
175        global $conf;
176        print 'runIndexer(): started' . NL;
177
178        if (!$ID) {
179            return false;
180        }
181
182        // do the work
183        return idx_addPage($ID, true);
184    }
185
186    /**
187     * Builds a Google Sitemap of all public pages known to the indexer
188     *
189     * The map is placed in the root directory named sitemap.xml.gz - This
190     * file needs to be writable!
191     *
192     * @author Andreas Gohr
193     * @link   https://www.google.com/webmasters/sitemaps/docs/en/about.html
194     */
195    protected function runSitemapper()
196    {
197        print 'runSitemapper(): started' . NL;
198        $result = Sitemapper::generate() && Sitemapper::pingSearchEngines();
199        print 'runSitemapper(): finished' . NL;
200        return $result;
201    }
202
203    /**
204     * Send digest and list mails for all subscriptions which are in effect for the
205     * current page
206     *
207     * @author Adrian Lang <lang@cosmocode.de>
208     */
209    protected function sendDigest()
210    {
211        global $conf;
212        global $ID;
213
214        echo 'sendDigest(): started' . NL;
215        if (!actionOK('subscribe')) {
216            echo 'sendDigest(): disabled' . NL;
217            return false;
218        }
219        $sub = new Subscription();
220        $sent = $sub->send_bulk($ID);
221
222        echo "sendDigest(): sent $sent mails" . NL;
223        echo 'sendDigest(): finished' . NL;
224        return (bool)$sent;
225    }
226}
227