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