1<?php 2if (!defined('DOKU_INC')) 3 die(); 4 5/** 6 * Fastwiki plugin, used for inline section editing, and loading of do= actions without a page refresh. 7 * 8 * @see http://dokuwiki.org/plugin:fastwiki 9 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 10 * @author Eli Fenton 11 */ 12class action_plugin_fastwiki extends DokuWiki_Action_Plugin { 13 protected $m_inPartial = false; 14 protected $m_no_content = false; 15 protected $m_preload_head = '====47hsjwycv782nwncv8b920m8bv72jmdm3929bno3b3===='; 16 protected $m_orig_act; 17 18 /** 19 * Register callback functions 20 * 21 * @param {Doku_Event_Handler} $controller DokuWiki's event controller object 22 */ 23 public function register(Doku_Event_Handler $controller) { 24 // Listed in order of when they happen. 25 $controller->register_hook('DOKUWIKI_STARTED', 'BEFORE', $this, 'handle_start'); 26 $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'override_loadskin'); 27 $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_action_before'); 28 $controller->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, 'unknown_action'); 29 $controller->register_hook('ACTION_SHOW_REDIRECT', 'BEFORE', $this, 'block_redirect'); 30 $controller->register_hook('ACTION_HEADERS_SEND', 'BEFORE', $this, 'block_headers'); 31 $controller->register_hook('ACTION_HEADERS_SEND', 'AFTER', $this, 'instead_of_template'); 32 $controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'pre_render'); 33 } 34 35 36 /** 37 * Start processing the request. This happens after doku init. 38 * 39 * @param {Doku_Event} $event - The DokuWiki event object. 40 * @param {mixed} $param - The fifth argument to register_hook(). 41 */ 42 public function handle_start(Doku_Event &$event, $param) { 43 global $conf, $INPUT, $ACT; 44 45 $this->m_orig_act = $ACT; 46 47 if ($INPUT->str('partial') == '1') { 48 $this->m_inPartial = true; 49 // Because so much is declared in global scope in doku.php, it's impossible to call tpl_content() without 50 // rendering the whole template. This hack loads a blank template, so we only render the page's inner content. 51 $conf['template'] = '../plugins/fastwiki/tplblank'; 52 } 53 else { 54 global $lang, $JSINFO; 55 56 $JSINFO['fastwiki'] = array( 57 // Configuration 58 'secedit' => $this->getConf('secedit'), 59 'preview' => $this->getConf('preview'), 60 'fastpages' => $this->getConf('fastpages'), 61 'save' => $this->getConf('save'), 62 'fastshow' => $this->getConf('fastshow'), 63 'fastshow_same_ns' => $this->getConf('fastshow_same_ns'), 64 'fastshow_include' => $this->getConf('fastshow_include'), 65 'fastshow_exclude' => $this->getConf('fastshow_exclude'), 66 'preload' => function_exists('curl_init') ? $this->getConf('preload') : false, 67 'preload_head' => $this->m_preload_head, 68 'preload_batchsize'=> $this->getConf('preload_batchsize'), 69 'preload_per_page' => $this->getConf('preload_per_page'), 70 71 // Needed for the initialization of the partial edit page. 72 'locktime' => $conf['locktime'] - 60, 73 'usedraft' => $conf['usedraft'] ? $conf['usedraft'] : '0', 74 75 // Miscellaneous 76 'text_btn_show' => $lang['btn_show'], 77 'templatename' => $conf['template'] 78 ); 79 } 80 } 81 82 83 /** 84 * The Loadskin plugin changes $conf['template'] in multiple places. Make sure we cover them all. 85 * 86 * @param {Doku_Event} $event - The DokuWiki event object. 87 * @param {mixed} $param - The fifth argument to register_hook(). 88 */ 89 public function override_loadskin(Doku_Event &$event, $param) { 90 global $conf; 91 if ($this->m_inPartial) 92 $conf['template'] = '../plugins/fastwiki/tplblank'; 93 } 94 95 96 /** 97 * Define special actions. 98 * 99 * @param {Doku_Event} $event - The DokuWiki event object. 100 * @param {mixed} $param - The fifth argument to register_hook(). 101 */ 102 public function unknown_action(Doku_Event &$event, $param) { 103 if ($event->data == 'fastwiki_preload') 104 $event->preventDefault(); 105 } 106 107 108 /** 109 * Hook into the pre-processor for the action handler to catch subscribe sub-actions before the action name changes. 110 * 111 * @param {Doku_Event} $event - The DokuWiki event object. 112 * @param {mixed} $param - The fifth argument to register_hook(). 113 */ 114 public function handle_action_before(Doku_Event &$event, $param) { 115 if (!$this->m_inPartial) 116 return; 117 global $ACT, $INPUT; 118 119 // For partials, we don't want output from subscribe actions -- just success/error messages. 120 if ($this->m_orig_act == 'subscribe' && $INPUT->str('sub_action')) 121 $this->m_no_content = true; 122 else if ($this->getConf('preload') && $this->m_orig_act == 'fastwiki_preload') 123 $event->preventDefault(); 124 } 125 126 127 /** 128 * Don't output headers while proxying preload pages. 129 * 130 * @param {Doku_Event} $event - The DokuWiki event object. 131 * @param {mixed} $param - The fifth argument to register_hook(). 132 */ 133 public function block_headers(Doku_Event &$event, $param) { 134 global $INPUT; 135 if ($INPUT->str('fastwiki_preload_proxy')) 136 $event->preventDefault(); 137 } 138 139 140 /** 141 * Some actions, like save and subscribe, normally redirect. Block that for partials. 142 * 143 * @param {Doku_Event} $event - The DokuWiki event object. 144 * @param {mixed} $param - The fifth argument to register_hook(). 145 */ 146 function block_redirect(Doku_Event &$event, $param) { 147 if ($this->m_inPartial) 148 $event->preventDefault(); 149 } 150 151 152 /** 153 * Handle the "partial" action, using the blank template to deliver nothing but the inner page content. 154 * This happens right before the template code would normally execute. 155 * 156 * @param {Doku_Event} $event - The DokuWiki event object. 157 * @param {mixed} $param - The fifth argument to register_hook(). 158 */ 159 public function instead_of_template(Doku_Event &$event, $param) { 160 if (!$this->m_inPartial) 161 return; 162 global $ACT, $INPUT, $ID, $INFO; 163 $preload = $this->getConf('preload') && $this->m_orig_act == 'fastwiki_preload'; 164 165 // Output error messages. 166 html_msgarea(); 167 168 $compareid = $INPUT->str('fastwiki_compareid'); 169 if ($compareid && (auth_quickaclcheck($ID) != auth_quickaclcheck($compareid))) 170 echo 'PERMISSION_CHANGE'; 171 172 // Some partials only want an error message. 173 else if (!$this->m_no_content) { 174 // Update revision numbers for section edit, in case the file was saved. 175 if ($this->m_orig_act == 'save') 176 $INFO['lastmod'] = @filemtime($INFO['filepath']); 177 178 // Preload page content. 179 else if ($preload) 180 $this->_preload_pages(); 181 182 else { 183 //global $_COOKIE; 184 //$cookies = array(); 185 //foreach ($_COOKIE as $name=>$value) 186 // array_push($cookies, $name . '=' . addslashes($value)); 187 //$cookies = join('; ', $cookies); 188 //echo "[{$_SERVER["REMOTE_USER"]}, $cookies]"; 189 } 190 // Section save. This won't work, unless I return new "range" inputs for all sections. 191// $secedit = $ACT == 'show' && $INPUT->str('target') == 'section' && ($INPUT->str('prefix') || $INPUT->str('suffix')); 192// if ($secedit) 193// $this->render_text($INPUT->str('wikitext')); //+++ render_text isn't outputting anything. 194// else 195 196 197 if (!$preload) 198 tpl_content($ACT == 'show'); 199 } 200 } 201 202 203 /** 204 * The template is about to render the main content area. Plop in a marker div so the javascript can 205 * figure out where the main content area is. NOTE: Templates that don't wrap tpl_content() 206 * in an HTML tag won't work with this plugin. 207 * 208 * @param {Doku_Event} $event - The DokuWiki event object. 209 * @param {mixed} $param - The fifth argument to register_hook(). 210 */ 211 public function pre_render(Doku_Event &$event, $param) { 212 global $ACT, $INPUT, $ID; 213 if (!$this->m_inPartial) 214 print '<div class="plugin_fastwiki_marker" style="display:none"></div>'; 215 } 216 217 218 /** 219 * Preload pages based on URL parameters, and return them. 220 */ 221 protected function _preload_pages() { 222 global $INPUT, $_COOKIE, $ID; 223 224 $maxpages = $this->getConf('preload_batchsize'); 225 $pages = explode(',', $INPUT->str('fastwiki_preload_pages')); 226 $count = min($maxpages, count($pages)); 227 $headers = getallheaders(); 228 $requests = array(); 229 230 $filtered = array(); 231 for ($x=0; $x<$count; $x++) { 232 $newid = cleanID($pages[$x]); 233 // ACL must be exactly the same. 234 if (page_exists($newid) && (auth_quickaclcheck($ID) == auth_quickaclcheck($newid))) 235 $filtered[] = $newid; 236 } 237 $pages = $filtered; 238 $count = count($pages); 239 240 if (function_exists('curl_init')) { 241 for ($x=0; $x<$count; $x++) { 242 $newid = $pages[$x]; 243 // Because there's no way to call doku recursively, curl is the only way to get a fresh context. 244 // Without a fresh context, there's no easy way to get action plugins to run or TOC to render properly. 245 /* 246 From include plugin. Interesting. 247 extract($page); 248 $id = $page['id']; 249 $exists = $page['exists']; 250 251 Or maybe open a new doku process with popen? 252 */ 253 $ch = curl_init(DOKU_URL.'doku.php'); 254 curl_setopt($ch, CURLOPT_POST, 1); 255 curl_setopt($ch, CURLOPT_POSTFIELDS, "id={$newid}&partial=1&fastwiki_preload_proxy=1"); 256 curl_setopt($ch, CURLOPT_COOKIE, $headers['Cookie']); 257 curl_setopt($ch, CURLOPT_USERAGENT, $headers['User-Agent']); 258 curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept-Language: ' . $headers['Accept-Language'])); 259 curl_setopt($ch, CURLOPT_REFERER, $headers['Referer']); 260 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); // Ignore redirects. TODO: Really? What about redirect plugin? 261 curl_setopt($ch, CURLOPT_HEADER, 0); 262 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 263 array_push($requests, array($ch, $newid)); 264 } 265 266 // Request URLs with multiple threads. 267 // TODO: This currently hangs. Enable the array_push above, and remove curl_exec, to test. 268 if (count($requests) > 0) { 269 $multicurl = curl_multi_init(); 270 foreach ($requests as $req) 271 curl_multi_add_handle($multicurl, $req[0]); 272 273 $active = null; 274 // Strange loop becuase php 5.3.18 broke curl_multi_select 275 do { 276 do { 277 $mrc = curl_multi_exec($multicurl, $active); 278 } while ($mrc == CURLM_CALL_MULTI_PERFORM); 279 // Wait 10ms to fix a bug where multi_select returns -1 forever. 280 usleep(10000); 281 } while(curl_multi_select($multicurl) === -1); 282 283 while ($active && $mrc == CURLM_OK) { 284 if (curl_multi_select($multicurl) != -1) { 285 do { 286 $mrc = curl_multi_exec($multicurl, $active); 287 } while ($mrc == CURLM_CALL_MULTI_PERFORM); 288 } 289 } 290 291 foreach ($requests as $idx=>$req) { 292 if ($idx > 0) 293 print $this->m_preload_head; 294 print $req[1] . "\n"; 295 echo curl_multi_getcontent($req[0]); 296 curl_multi_remove_handle($multicurl, $req[0]); 297 } 298 curl_multi_close($multicurl); 299 } 300 } 301 // TODO: WORKING 302 // Fallback when curl isn't installed. Not parallelized, but it works! 303 // Note that this will not work with connections that do chunking. 304 //TODO DOCUMENT: Needs allow_url_fopen. 305 //TODO Replicate client's User-Agent, Accept-Language header. Copy COOKIE header instead of reconstructing. 306 //TODO: This is VERY slow. 307 else { 308 return; 309 310 global $_SERVER; 311 $hostname = $_SERVER['SERVER_NAME']; 312 for ($x=0; $x<$count; $x++) { 313 $newid = $pages[$x]; 314 315 $headers = array( 316 "POST " . DOKU_URL . "doku.php HTTP/1.1", 317 "Host: " . $hostname, 318 "Cookie: " . $cookies, 319 "Content-Type: application/x-www-form-urlencoded; charset=UTF-8", 320 //"Accept: text/plain, */*", 321 "", ""); 322 $body = "id={$newid}&partial=1&fastwiki_preload_proxy=1"; 323 324print implode("\r\n", $headers) . "id={$newid}&partial=1&fastwiki_preload_proxy=1\n\n\n"; 325continue; 326 $remote = fsockopen($hostname, 80, $errno, $errstr, 5); 327 fwrite($remote, implode("\r\n", $headers) . $body); 328 329 $response = ''; 330 while (!feof($remote)) 331 $response .= fread($remote, 8192); 332 fclose($remote); 333 334 if ($x > 0) 335 print $this->m_preload_head; 336 print "$newid\n"; 337 echo $response; 338 } 339 } 340 } 341} 342 343 344if (!function_exists('getallheaders')) { 345 function getallheaders() { 346 $headers = ''; 347 foreach ($_SERVER as $name => $value) { 348 if (substr($name, 0, 5) == 'HTTP_') 349 $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; 350 } 351 return $headers; 352 } 353} 354