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