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