1<?php
2/**
3 * DokuWiki Plugin dokutranslate (Action Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Martin Doucha <next_ghost@quick.cz>
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) die();
11
12if (!defined('DOKU_LF')) define('DOKU_LF', "\n");
13if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t");
14if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
15
16require_once DOKU_PLUGIN.'action.php';
17# Needed for lexer state constants used in syntax plugin instructions
18require_once DOKU_INC.'inc/parser/lexer.php';
19require_once 'utils.php';
20
21function allRevisions($id) {
22	$ret = array();
23	$lines = @file(metaFN($id, '.changes'));
24
25	if (!$lines) {
26		return $ret;
27	}
28
29	foreach ($lines as $line) {
30		$tmp = parseChangelogLine($line);
31		$ret[] = $tmp['date'];
32	}
33
34	return $ret;
35}
36
37function genTranslateFile($ins) {
38	$ret = "~~DOKUTRANSLATE_START~~\n\n";
39	$par = "~~DOKUTRANSLATE_PARAGRAPH~~\n\n";
40
41	for ($i = 0; $i < count($ins) - 1; $i++) {
42		$ret .= $par;
43	}
44
45	$ret .= "~~DOKUTRANSLATE_END~~";
46
47	return $ret;
48}
49
50function genMeta($lineCount) {
51	$ret = array();
52
53	# Generate paragraph info
54	for ($i = 0; $i < $lineCount; $i++) {
55		$ret[$i]['changed'] = '';
56		$ret[$i]['ip'] = clientIP(true);
57		$ret[$i]['user'] = $_SERVER['REMOTE_USER'];
58		$ret[$i]['reviews'] = array();
59	}
60
61	return $ret;
62}
63
64function updateMeta($id, $parid, $lastrev, $revert = -1) {
65	$meta = unserialize(io_readFile(metaFN($id, '.translateHistory'), false));
66
67	for ($i = 0; $i < count($meta['current']); $i++) {
68		if (!empty($meta['current'][$i]['changed'])) {
69			# This paragraph was not changed in the last revision,
70			# copy last change entry only
71			$meta[$lastrev][$i]['changed'] = $meta['current'][$i]['changed'];
72		} else {
73			# This paragraph has been changed, copy full entry
74			# and set revision pointer
75			$meta[$lastrev][$i] = $meta['current'][$i];
76			$meta['current'][$i]['changed'] = $lastrev;
77		}
78	}
79
80	$revert = intval($revert);
81
82	if ($revert < 0) {
83		# Saving new data, reset entry for changed paragraph
84		$meta['current'][$parid]['changed'] = '';
85		$meta['current'][$parid]['ip'] = clientIP(true);
86		$meta['current'][$parid]['user'] = isset($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'] : '';
87		$meta['current'][$parid]['reviews'] = array();
88	} else {
89		# Reverting old revision, restore metadata of reverted page
90		for ($i = 0; $i < count($meta['current']); $i++) {
91			if (empty($meta[$revert][$i]['changed'])) {
92				# Paragraph last changed in the reverted
93				# revision
94				$meta['current'][$i] = $meta[$revert][$i];
95				$meta['current'][$i]['changed'] = $revert;
96			} else {
97				# Paragraph last changed in even earlier
98				# revision
99				$tmp = $meta[$revert][$i]['changed'];
100				$meta['current'][$i] = $meta[$tmp][$i];
101				$meta['current'][$i]['changed'] = $tmp;
102			}
103		}
104	}
105
106	# Save metadata
107	io_saveFile(metaFN($id, '.translateHistory'), serialize($meta));
108	io_saveFile(metaFN($id, '.translate'), serialize($meta['current']));
109}
110
111class action_plugin_dokutranslate extends DokuWiki_Action_Plugin {
112
113	public function register(Doku_Event_Handler $controller) {
114		$this->setupLocale();
115		$controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handle_html_editform_output');
116		$controller->register_hook('HTML_SECEDIT_BUTTON', 'BEFORE', $this, 'handle_disabled');
117		$controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_action_act_preprocess');
118		$controller->register_hook('ACTION_SHOW_REDIRECT', 'BEFORE', $this, 'handle_action_show_redirect');
119		$controller->register_hook('PARSER_HANDLER_DONE', 'BEFORE', $this, 'handle_parser_handler_done');
120		$controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'handle_parser_cache_use');
121		$controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'handle_tpl_act_render');
122		$controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'handle_tpl_content_display');
123	}
124
125	public function handle_html_editform_output(Doku_Event &$event, $param) {
126		global $ID;
127
128		if (!@file_exists(metaFN($ID, '.translate'))) {
129			# Check permissions to begin translation
130			if (!isModerator($ID)) {
131				return;
132			}
133
134			# No submit button => preview, don't modify the form
135			if(!$event->data->findElementByAttribute('type', 'submit')) {
136				return;
137			}
138
139			# Place the checkbox after minor edit checkbox or
140			# summary text box if minor edit checkbox is not present
141			$pos = $event->data->findElementByAttribute('name', 'minor');
142
143			if (!$pos) {
144				$pos = $event->data->findElementByAttribute('name', 'summary');
145			}
146
147			# Create the checkbox
148			$p = array('tabindex' => 4);
149
150			if (!empty($_REQUEST['translate'])) {
151				$p['checked'] = 'checked';
152			}
153
154			$elem = form_makeCheckboxField('translate', '1', $this->lang['translate_begin'], 'translate_begin', 'nowrap', $p);
155
156			# Insert checkbox into the form
157			$event->data->insertElement(++$pos, $elem);
158		} else {
159			# Translation in progress, add paragraph ID to the form
160			$event->data->addHidden('parid', strval(getParID()));
161		}
162	}
163
164	public function handle_action_act_preprocess(Doku_Event &$event, $param) {
165		global $ID;
166		global $TEXT;
167		global $ACT;
168		global $SUM;
169		global $RANGE;
170		global $REV;
171
172		$act = $event->data;
173
174		if ($act != 'dokutranslate_review') {
175			$act = act_clean($act);
176			$act = act_permcheck($act);
177		}
178
179		# Ignore drafts if the page is being translated
180		# FIXME: Find a way to save $_REQUEST['parid'] into the draft
181		if (@file_exists(metaFN($ID, '.translate')) && in_array($act, array('draft', 'recover'))) {
182			act_draftdel('draftdel');
183			$ACT = $act = 'edit';
184		}
185
186		if ($act == 'save') {
187			# Take over save action if translation is in progress
188			# or we're starting it
189			if (!@file_exists(metaFN($ID, '.translate')) && empty($_REQUEST['translate'])) {
190				return;
191			}
192
193			if (!checkSecurityToken()) {
194				return;
195			}
196
197			# We're starting a translation
198			if (!@file_exists(metaFN($ID, '.translate')) && !empty($_REQUEST['translate'])) {
199				# Check if the user has permission to start
200				# translation in this namespace
201				if (!isModerator($ID)) {
202					return;
203				}
204
205				# Take the event over
206				$event->stopPropagation();
207				$event->preventDefault();
208
209				# Save the data but exit if it fails
210				$ACT = act_save($act);
211
212				if ($ACT != 'show') {
213					return;
214				}
215
216				# Page was deleted, exit
217				if (!@file_exists(wikiFN($ID))) {
218					return;
219				}
220
221				# Prepare data path
222				$datapath = dataPath($ID);
223				io_mkdir_p($datapath, 0755, true);
224
225				# Backup the original page
226				io_rename(wikiFN($ID), $datapath . '/orig.txt');
227
228				# Backup old revisions
229				$revisions = allRevisions($ID);
230
231				foreach ($revisions as $rev) {
232					$tmp = wikiFN($ID, $rev);
233					io_rename($tmp, $datapath . '/' . basename($tmp));
234				}
235
236				# Backup meta files
237				$metas = metaFiles($ID);
238
239				foreach ($metas as $f) {
240					io_rename($f, $datapath . '/' . basename($f));
241				}
242
243				# Generate empty page to hold translated text
244				$data = getCleanInstructions($datapath . '/orig.txt');
245				saveWikiText($ID, genTranslateFile($data), $SUM, $_REQUEST['minor']);
246
247				$translateMeta = genMeta(count($data));
248				# create meta file for current translation state
249				io_saveFile(metaFN($ID, '.translate'), serialize($translateMeta));
250				# create separate meta file for translation history
251				io_saveFile(metaFN($ID, '.translateHistory'), serialize(array('current' => $translateMeta)));
252			} else {
253				# Translation in progress, take the event over
254				$event->preventDefault();
255
256				# Save the data but exit if it fails
257				$ACT = act_save($act);
258
259				# Save failed, exit
260				if ($ACT != 'show') {
261					return;
262				}
263
264				# Save successful, update translation metadata
265				$lastrev = getRevisions($ID, 0, 1, 1024);
266				updateMeta($ID, getParID(), $lastrev[0]);
267			}
268		} else if ($act == 'revert') {
269			# Take over save action if translation is in progress
270			if (!@file_exists(metaFN($ID, '.translate'))) {
271				return;
272			}
273
274			if (!checkSecurityToken()) {
275				return;
276			}
277
278			# Translation in progress, take the event over
279			$event->preventDefault();
280
281			# Save the data but exit if it fails
282			$revert = $REV;
283			$ACT = act_revert($act);
284
285			# Revert failed, exit
286			if ($ACT != 'show') {
287				return;
288			}
289
290			# Revert successful, update translation metadata
291			$lastrev = getRevisions($ID, 0, 1, 1024);
292			updateMeta($ID, getParID(), $lastrev[0], $revert);
293		} else if (in_array($act, array('edit', 'preview'))) {
294			if (!@file_exists(metaFN($ID, '.translate')) || isset($TEXT)) {
295				return;
296			}
297
298			$parid = getParID();
299			$instructions = p_cached_instructions(wikiFN($ID));
300			$separators = array();
301
302			# Build array of paragraph separators
303			foreach ($instructions as $ins) {
304				if ($ins[0] == 'plugin' && $ins[1][0] == 'dokutranslate' && in_array($ins[1][1][0], array(DOKU_LEXER_ENTER, DOKU_LEXER_SPECIAL, DOKU_LEXER_EXIT))) {
305					$separators[] = $ins[1][1];
306				}
307			}
308
309			# Validate paragraph ID
310			if ($parid >= count($separators) - 1) {
311				$parid = 0;
312			}
313
314			# Build range for paragraph
315			$RANGE = strval($separators[$parid][2] + 1) . '-' . strval($separators[$parid + 1][1] - 1);
316		} else if ($act == 'dokutranslate_review') {
317			# This action is mine
318			$event->stopPropagation();
319			$event->preventDefault();
320
321			# Show the page when done
322			$ACT = 'show';
323
324			# Load data
325			$meta = unserialize(io_readFile(metaFN($ID, '.translateHistory'), false));
326			$parid = getParID();
327			$writeRev = empty($REV) ? 'current' : intval($REV);
328			$writeRev = empty($meta[$writeRev][$parid]['changed']) ? $writeRev : $meta[$writeRev][$parid]['changed'];
329			$user = $_SERVER['REMOTE_USER'];
330
331			# Check for permission to write reviews
332			if (!canReview($ID, $meta[$writeRev], $parid)) {
333				return;
334			}
335
336			# Add review to meta array
337			$data['message'] = $_REQUEST['review'];
338			$data['quality'] = intval($_REQUEST['quality']);
339			$data['incomplete'] = !empty($_REQUEST['incomplete']);
340			$meta[$writeRev][$parid]['reviews'][$user] = $data;
341
342			# Review applies to latest revision as well
343			if (empty($REV) || $meta['current'][$parid]['changed'] == $writeRev) {
344				$meta['current'][$parid]['reviews'][$user] = $data;
345				io_saveFile(metaFN($ID, '.translate'), serialize($meta['current']));
346			}
347
348			# Save metadata
349			io_saveFile(metaFN($ID, '.translateHistory'), serialize($meta));
350		}
351	}
352
353	public function handle_action_show_redirect(Doku_Event &$event, $param) {
354		$act = $event->data['preact'];
355
356		if ($act != 'dokutranslate_review') {
357			$act = act_clean($act);
358		}
359
360		if (($act == 'save' || $act == 'draftdel') && @file_exists(metaFN($event->data['id'], '.translate'))) {
361			$event->data['fragment'] = '_par' . getParID();
362		}
363	}
364
365	public function handle_parser_handler_done(Doku_Event &$event, $param) {
366		global $ID;
367		$erase = array('section_open', 'section_close');
368
369		# Exit if the page is not being translated
370		if (!@file_exists(metaFN($ID, '.translate'))) {
371			return;
372		}
373
374		$length = count($event->data->calls);
375
376		# Erase section instructions from the instruction list
377		for ($i = 0; $i < $length; $i++) {
378			if (in_array($event->data->calls[$i][0], $erase)) {
379				unset($event->data->calls[$i]);
380			}
381		}
382	}
383
384	public function handle_parser_cache_use(Doku_Event &$event, $param) {
385		global $ACT;
386		$cache =& $event->data;
387
388		if (empty($cache->page) || empty($cache->mode) || $cache->mode != 'xhtml' || !@file_exists(metaFN($cache->page, '.translate'))) {
389			return;
390		}
391
392		# Ensure refresh on plugin update
393		$cache->depends['files'][] = dirname(__FILE__) . '/plugin.info.txt';
394
395		if (substr($ACT, 0, 7) == 'export_') {
396			# Don't write XHTML page and XHTML export data into
397			# the same cache file.
398			# Props to Michitux for suggesting this
399			$cache->cache .= '_export';
400		} else {
401			# Separate cache file for each moderator
402			if (isModerator($cache->page)) {
403				$cache->cache .= '.' . urlencode($_SERVER['REMOTE_USER']);
404			}
405
406			# Ensure refresh with every new review
407			$cache->depends['files'][] = metaFN($cache->page, '.translate');
408		}
409	}
410
411	# Hijack edit page rendering
412	public function handle_tpl_act_render(Doku_Event &$event, $param) {
413		global $ID;
414		global $INFO;
415		global $DOKUTRANSLATE_EDITFORM;
416
417		if (!@file_exists(metaFN($ID, '.translate'))) {
418			return;
419		}
420
421		# Disable TOC on translated pages
422		$INFO['prependTOC'] = false;
423
424		if (in_array($event->data, array('edit', 'preview'))) {
425			# Take the event over
426			$event->preventDefault();
427
428			# Save the edit form for later
429			html_edit();
430			$DOKUTRANSLATE_EDITFORM = ob_get_clean();
431			ob_start();
432
433			# Render the page (renderer inserts saved edit form
434			# and preview in the right cell)
435			echo p_render('xhtml', p_cached_instructions(wikiFN($ID)), $INFO);
436		}
437	}
438
439	# Erase content replaced by edit form
440	public function handle_tpl_content_display(Doku_Event &$event, $param) {
441		global $ID;
442
443		if (!@file_exists(metaFN($ID, '.translate'))) {
444			preg_match_all('/<a [^>]* class="wikilink1" title="([^"]*)"[^>]*>/', $event->data, $out, PREG_SET_ORDER);
445			$status = array();
446
447			# Gather internal links
448			foreach ($out as $link) {
449				if (isset($status[$link[1]])) {
450					continue;
451				}
452
453				# Calculate translation status for each link
454				$status[$link[1]] = $this->_translationStatus($link[1]);
455			}
456
457			# Write translation status next to each link
458			while (list($key, $value) = each($status)) {
459				if (empty($value)) {
460					continue;
461				}
462
463				$event->data = preg_replace("#<a ([^>]*) class=\"wikilink1\" title=\"$key\"([^>]*)>(.*)</a>#U", "<a \\1 class=\"wikilink1\" title=\"$key\"\\2>\\3</a> ($value)", $event->data);
464			}
465		} else {
466			# Erase everything between markers
467			$event->data = preg_replace("/<!-- DOKUTRANSLATE ERASE START -->.*<!-- DOKUTRANSLATE ERASE STOP -->/sm", '', $event->data);
468		}
469	}
470
471	# Generic event eater
472	public function handle_disabled(Doku_Event &$event, $param) {
473		global $ID;
474
475		# Translation in progress, eat the event
476		if (@file_exists(metaFN($ID, '.translate'))) {
477			$event->preventDefault();
478		}
479
480		return;
481	}
482
483	function _translationStatus($id) {
484		if (!@file_exists(metaFN($id, '.translate'))) {
485			return '';
486		}
487
488		$meta = unserialize(io_readFile(metaFN($id, '.translate'), false));
489		$total = 0;
490		$reviewme = false;
491
492		while (list($key, $value) = each($meta)) {
493			$rating = empty($value['reviews']) ? 0 : 4;
494
495			foreach ($value['reviews'] as $review) {
496				$tmp = intval($review['quality']) * 2;
497
498				if ($review['incomplete']) {
499					$tmp--;
500				}
501
502				$tmp = $tmp < 0 ? 0 : $tmp;
503				$rating = $tmp < $rating ? $tmp : $rating;
504			}
505
506			$total += $rating;
507
508			if (needsReview($id, $meta, $key)) {
509				$reviewme = true;
510			}
511		}
512
513		$ret = sprintf($this->getLang('trans_percentage'), 25 * $total / count($meta));
514
515		if ($reviewme) {
516			$ret .= ', ' . $this->getLang('reviewme');
517		}
518
519		return $ret;
520	}
521}
522
523// vim:ts=4:sw=4:et:
524