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