1<?php 2/** 3 * Deepl Autotranslate Plugin 4 * 5 * @author Jennifer Graul <me@netali.de> 6 */ 7 8if(!defined('DOKU_INC')) die(); 9 10use \dokuwiki\HTTP\DokuHTTPClient; 11use \dokuwiki\plugin\deeplautotranslate\MenuItem; 12 13class action_plugin_deeplautotranslate extends DokuWiki_Action_Plugin { 14 15 // manual mapping of ISO-languages to DeepL-languages to fix inconsistent naming 16 private $langs = array( 17 'bg' => 'BG', 18 'cs' => 'CS', 19 'da' => 'DA', 20 'de' => 'DE', 21 'de-informal' => 'DE', 22 'el' => 'EL', 23 'en' => 'EN-GB', 24 'es' => 'ES', 25 'et' => 'ET', 26 'fi' => 'FI', 27 'fr' => 'FR', 28 'hu' => 'HU', 29 'hu-formal' => 'HU', 30 'it' => 'IT', 31 'ja' => 'JA', 32 'lt' => 'LT', 33 'lv' => 'LV', 34 'nl' => 'NL', 35 'pl' => 'PL', 36 'pt' => 'PT-PT', 37 'ro' => 'RO', 38 'ru' => 'RU', 39 'sk' => 'SK', 40 'sl' => 'SL', 41 'sv' => 'SV', 42 'zh' => 'ZH' 43 ); 44 45 /** 46 * Register its handlers with the DokuWiki's event controller 47 */ 48 public function register(Doku_Event_Handler $controller) { 49 $controller->register_hook('ACTION_ACT_PREPROCESS','BEFORE', $this, 'preprocess'); 50 $controller->register_hook('COMMON_PAGETPL_LOAD','AFTER', $this, 'pagetpl_load'); 51 $controller->register_hook('COMMON_WIKIPAGE_SAVE','AFTER', $this, 'update_glossary'); 52 $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'add_menu_button'); 53 } 54 55 public function add_menu_button(Doku_Event $event): void { 56 global $ID; 57 global $ACT; 58 59 if ($ACT != 'show') return; 60 61 if ($event->data['view'] != 'page') return; 62 63 if (!$this->getConf('show_button')) return; 64 65 // no translations for the glossary namespace 66 if ($this->check_in_glossary_ns()) return; 67 68 $split_id = explode(':', $ID); 69 $lang_ns = array_shift($split_id); 70 // check if we are in a language namespace 71 if (array_key_exists($lang_ns, $this->langs)) { 72 if($this->getConf('default_lang_in_ns') and $lang_ns === $this->get_default_lang()) { 73 // if the default lang is in a namespace and we are in that namespace --> check for push translation 74 if (!$this->check_do_push_translate()) return; 75 } else { 76 // in language namespace --> check if we should translate 77 if (!$this->check_do_translation(true)) return; 78 } 79 } else { 80 // do not show the button if we are not in a language namespace and the default language is in a namespace 81 if($this->getConf('default_lang_in_ns')) return; 82 // not in language namespace and default language is not in a namespace --> check if we should show the push translate button 83 if (!$this->check_do_push_translate()) return; 84 } 85 86 array_splice($event->data['items'], -1, 0, [new MenuItem()]); 87 } 88 89 public function preprocess(Doku_Event $event, $param): void { 90 global $ID; 91 92 // check if action is show or translate 93 if ($event->data != 'show' and $event->data != 'translate') return; 94 95 // redirect to glossary ns start if glossary ns is called 96 if ($this->check_in_glossary_ns() and $event->data == 'show' and $ID == $this->get_glossary_ns()) { 97 send_redirect(wl($this->get_glossary_ns() . ':start')); 98 } 99 100 $split_id = explode(':', $ID); 101 $lang_ns = array_shift($split_id); 102 // check if we are in a language namespace 103 if (array_key_exists($lang_ns, $this->langs)) { 104 if($this->getConf('default_lang_in_ns') and $lang_ns === $this->get_default_lang()) { 105 // if the default lang is in a namespace and we are in that namespace --> push translate 106 $this->push_translate($event); 107 } else { 108 // in language namespace --> autotrans direct 109 $this->autotrans_direct($event); 110 } 111 } else { 112 // not in language namespace --> push translate 113 $this->push_translate($event); 114 } 115 } 116 117 public function pagetpl_load(Doku_Event $event, $param): void { 118 // handle glossary namespace init when we are in it 119 if ($this->check_in_glossary_ns()) { 120 $this->handle_glossary_init($event); 121 return; 122 } 123 124 $this->autotrans_editor($event); 125 } 126 127 public function update_glossary(Doku_Event $event, $param): void { 128 global $ID; 129 // this also checks if the glossary feature is enabled 130 if (!$this->check_in_glossary_ns()) return; 131 132 $glossary_ns = $this->get_glossary_ns(); 133 134 // check if we are in a glossary definition 135 if(preg_match('/^' . $glossary_ns . ':(\w{2})_(\w{2})$/', $ID, $id_match)) { 136 $old_glossary_id = $this->get_glossary_id($id_match[1], $id_match[2]); 137 if ($event->data['changeType'] == DOKU_CHANGE_TYPE_DELETE) { 138 // page deleted --> delete glossary 139 if ($old_glossary_id) { 140 $result = $this->delete_glossary($old_glossary_id); 141 if ($result) { 142 msg($this->getLang('msg_glossary_delete_success'), 1); 143 $this->unset_glossary_id($id_match[1], $id_match[2]); 144 } 145 } 146 return; 147 } 148 149 $entries = ''; 150 151 // grep entries from definition table 152 preg_match_all('/[ \t]*\|(.*?)\|(.*?)\|/', $event->data['newContent'], $matches, PREG_SET_ORDER); 153 foreach ($matches as $match) { 154 $src = trim($match[1]); 155 $target = trim($match[2]); 156 if ($src == '' or $target == '') { 157 msg($this->getLang('msg_glossary_empty_key'), -1); 158 return; 159 } 160 $entries .= $src . "\t" . $target . "\n"; 161 } 162 163 if (empty($matches)) { 164 // no matches --> delete glossary 165 if ($old_glossary_id) { 166 $result = $this->delete_glossary($old_glossary_id); 167 if ($result) { 168 msg($this->getLang('msg_glossary_delete_success'), 1); 169 $this->unset_glossary_id($id_match[1], $id_match[2]); 170 } 171 } 172 return; 173 } 174 175 $new_glossary_id = $this->create_glossary($id_match[1], $id_match[2], $entries); 176 177 if ($new_glossary_id) { 178 msg($this->getLang('msg_glossary_create_success'), 1); 179 $this->set_glossary_id($id_match[1], $id_match[2], $new_glossary_id); 180 } else { 181 return; 182 } 183 184 if ($old_glossary_id) $this->delete_glossary($old_glossary_id); 185 } 186 } 187 188 private function autotrans_direct(Doku_Event $event): void { 189 global $ID; 190 191 // abort if action is translate and the translate button is disabled 192 if ($event->data == 'translate' and !$this->getConf('show_button')) return; 193 194 // do nothing on show action when mode is not direct 195 if ($event->data == 'show' and $this->get_mode() != 'direct') return; 196 197 // allow translation of existing pages is we are in the translate action 198 $allow_existing = ($event->data == 'translate'); 199 200 // reset action to show 201 $event->data = 'show'; 202 203 if (!$this->check_do_translation($allow_existing)) { 204 return; 205 } 206 207 $org_page_info = $this->get_org_page_info(); 208 $translated_text = $this->deepl_translate($org_page_info["text"], $this->get_target_lang(), $org_page_info["ns"]); 209 210 if ($translated_text === '') { 211 return; 212 } 213 214 saveWikiText($ID, $translated_text, 'Automatic translation'); 215 216 msg($this->getLang('msg_translation_success'), 1); 217 218 // reload the page after translation 219 send_redirect(wl($ID)); 220 } 221 222 private function autotrans_editor(Doku_Event $event): void { 223 if ($this->get_mode() != 'editor') return; 224 225 if (!$this->check_do_translation()) return; 226 227 $org_page_info = $this->get_org_page_info(); 228 229 $event->data['tpl'] = $this->deepl_translate($org_page_info["text"], $this->get_target_lang(), $org_page_info["ns"]); 230 } 231 232 private function push_translate(Doku_Event $event): void { 233 global $ID; 234 235 // check if action is translate 236 if ($event->data != 'translate') return; 237 238 // check if button is enabled 239 if (!$this->getConf('show_button')) { 240 send_redirect(wl($ID)); 241 return; 242 } 243 244 if (!$this->check_do_push_translate()) { 245 send_redirect(wl($ID)); 246 return; 247 } 248 249 // push translate 250 $push_langs = $this->get_push_langs(); 251 $org_page_text = rawWiki($ID); 252 foreach ($push_langs as $lang) { 253 // skip invalid languages 254 if (!array_key_exists($lang, $this->langs)) { 255 msg($this->getLang('msg_translation_fail_invalid_lang') . $lang, -1); 256 continue; 257 } 258 259 if ($this->getConf('default_lang_in_ns')) { 260 // if default lang is in ns: replace language namespace in ID 261 $split_id = explode(':', $ID); 262 array_shift($split_id); 263 $lang_id = implode(':', $split_id); 264 $lang_id = $lang . ':' . $lang_id; 265 } else { 266 // if default lang is not in ns: add language namespace to ID 267 $lang_id = $lang . ':' . $ID; 268 } 269 270 // check permissions 271 $perm = auth_quickaclcheck($lang_id); 272 $exists = page_exists($lang_id); 273 if (($exists and $perm < AUTH_EDIT) or (!$exists and $perm < AUTH_CREATE)) { 274 msg($this->getLang('msg_translation_fail_no_permissions') . $lang_id, -1); 275 continue; 276 } 277 278 $translated_text = $this->deepl_translate($org_page_text, $lang, getNS($ID)); 279 saveWikiText($lang_id, $translated_text, 'Automatic push translation'); 280 } 281 282 msg($this->getLang('msg_translation_success'), 1); 283 284 // reload the page after translation to clear the action 285 send_redirect(wl($ID)); 286 } 287 288 private function handle_glossary_init(Doku_Event $event): void { 289 global $ID; 290 291 $glossary_ns = $this->get_glossary_ns(); 292 293 // create glossary landing page 294 if ($ID == $glossary_ns . ':start') { 295 $landing_page_text = '====== ' . $this->getLang('glossary_landing_heading') . ' ======' . "\n"; 296 $landing_page_text .= $this->getLang('glossary_landing_info_msg') . "\n"; 297 298 $src_lang = substr($this->get_default_lang(), 0, 2); 299 300 $available_glossaries = $this->get_available_glossaries(); 301 foreach ($available_glossaries as $glossary) { 302 if ($glossary['source_lang'] != $src_lang) continue; 303 // generate links to the available glossary pages 304 $landing_page_text .= ' * [[.:' . $glossary['source_lang'] . '_' . $glossary['target_lang'] . '|' . strtoupper($glossary['source_lang']) . ' -> ' . strtoupper($glossary['target_lang']) . ']]' . "\n"; 305 } 306 $event->data['tpl'] = $landing_page_text; 307 return; 308 } 309 310 if (preg_match('/^' . $glossary_ns . ':(\w{2})_(\w{2})$/', $ID, $match)) { 311 // check if glossaries are supported for this language pair 312 if (!$this->check_glossary_supported($match[1], $match[2])) { 313 msg($this->getLang('msg_glossary_unsupported'), -1); 314 return; 315 } 316 317 $page_text = '====== ' . $this->getLang('glossary_definition_heading') . ': ' . strtoupper($match[1]) . ' -> ' . strtoupper($match[2]) . ' ======' . "\n"; 318 $page_text .= $this->getLang('glossary_definition_help') . "\n\n"; 319 $page_text .= '^ ' . strtoupper($match[1]) . ' ^ ' . strtoupper($match[2]) . ' ^' . "\n"; 320 321 $event->data['tpl'] = $page_text; 322 return; 323 } 324 } 325 326 private function get_glossary_ns(): string { 327 return trim(strtolower($this->getConf('glossary_ns'))); 328 } 329 330 private function get_mode(): string { 331 global $ID; 332 if ($this->getConf('editor_regex')) { 333 if (preg_match('/' . $this->getConf('editor_regex') . '/', $ID) === 1) return 'editor'; 334 } 335 if ($this->getConf('direct_regex')) { 336 if (preg_match('/' . $this->getConf('direct_regex') . '/', $ID) === 1) return 'direct'; 337 } 338 return $this->getConf('mode'); 339 } 340 341 private function get_target_lang(): string { 342 global $ID; 343 $split_id = explode(':', $ID); 344 return array_shift($split_id); 345 } 346 347 private function get_default_lang(): string { 348 global $conf; 349 350 if (empty($conf['lang_before_translation'])) { 351 $default_lang = $conf['lang']; 352 } else { 353 $default_lang = $conf['lang_before_translation']; 354 } 355 356 return $default_lang; 357 } 358 359 private function get_org_page_info(): array { 360 global $ID; 361 362 $split_id = explode(':', $ID); 363 array_shift($split_id); 364 $org_id = implode(':', $split_id); 365 366 // if default lang is in ns: add default ns in front of org id 367 if ($this->getConf('default_lang_in_ns')) { 368 $org_id = $this->get_default_lang() . ':' . $org_id; 369 } 370 371 return array("ns" => getNS($org_id), "text" => rawWiki($org_id)); 372 } 373 374 private function get_available_glossaries(): array { 375 if (!trim($this->getConf('api_key'))) { 376 msg($this->getLang('msg_bad_key'), -1); 377 return array(); 378 } 379 380 if ($this->getConf('api') == 'free') { 381 $url = 'https://api-free.deepl.com/v2/glossary-language-pairs'; 382 } else { 383 $url = 'https://api.deepl.com/v2/glossary-language-pairs'; 384 } 385 386 $http = new DokuHTTPClient(); 387 388 $http->headers = array('Authorization' => 'DeepL-Auth-Key ' . $this->getConf('api_key')); 389 390 $raw_response = $http->get($url); 391 392 if ($http->status >= 400) { 393 // add error messages 394 switch ($http->status) { 395 case 403: 396 msg($this->getLang('msg_bad_key'), -1); 397 break; 398 default: 399 msg($this->getLang('msg_glossary_fetch_fail'), -1); 400 break; 401 } 402 403 // if any error occurred return an empty array 404 return array(); 405 } 406 407 $json_response = json_decode($raw_response, true); 408 409 return $json_response['supported_languages']; 410 } 411 412 private function get_glossary_id($src, $target): string { 413 if (!file_exists(DOKU_CONF . 'deepl-glossaries.json')) return ''; 414 415 $key = $src . "_" . $target; 416 417 $raw_json = file_get_contents(DOKU_CONF . 'deepl-glossaries.json'); 418 $content = json_decode($raw_json, true); 419 420 if (array_key_exists($key, $content)) { 421 return $content[$key]; 422 } else { 423 return ''; 424 } 425 } 426 427 private function set_glossary_id($src, $target, $glossary_id): void { 428 if (file_exists(DOKU_CONF . 'deepl-glossaries.json')) { 429 $raw_json = file_get_contents(DOKU_CONF . 'deepl-glossaries.json'); 430 $content = json_decode($raw_json, true); 431 } else { 432 $content = array(); 433 } 434 435 $key = $src . "_" . $target; 436 437 $content[$key] = $glossary_id; 438 439 $raw_json = json_encode($content); 440 file_put_contents(DOKU_CONF . 'deepl-glossaries.json', $raw_json); 441 } 442 443 private function unset_glossary_id($src, $target): void { 444 if (file_exists(DOKU_CONF . 'deepl-glossaries.json')) { 445 $raw_json = file_get_contents(DOKU_CONF . 'deepl-glossaries.json'); 446 $content = json_decode($raw_json, true); 447 } else { 448 return; 449 } 450 451 $key = $src . "_" . $target; 452 453 unset($content[$key]); 454 455 $raw_json = json_encode($content); 456 file_put_contents(DOKU_CONF . 'deepl-glossaries.json', $raw_json); 457 } 458 459 private function check_in_glossary_ns(): bool { 460 global $ID; 461 462 $glossary_ns = $this->get_glossary_ns(); 463 464 // check if the glossary namespace is defined 465 if (!$glossary_ns) return false; 466 467 // check if we are in the glossary namespace 468 if (substr($ID, 0, strlen($glossary_ns)) == $glossary_ns) { 469 return true; 470 } else { 471 return false; 472 } 473 } 474 475 private function check_glossary_supported($src, $target): bool { 476 if(strlen($src) != 2 or strlen($target) != 2) return false; 477 $available_glossaries = $this->get_available_glossaries(); 478 foreach ($available_glossaries as $glossary) { 479 if ($src == $glossary['source_lang'] and $target == $glossary['target_lang']) return true; 480 } 481 return false; 482 } 483 484 private function check_do_translation($allow_existing = false): bool { 485 global $INFO; 486 global $ID; 487 488 // only translate if the current page does not exist 489 if ($INFO['exists'] and !$allow_existing) return false; 490 491 // permission check 492 $perm = auth_quickaclcheck($ID); 493 if (($INFO['exists'] and $perm < AUTH_EDIT) or (!$INFO['exists'] and $perm < AUTH_CREATE)) return false; 494 495 // skip blacklisted namespaces and pages 496 if ($this->getConf('blacklist_regex')) { 497 if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false; 498 } 499 500 $split_id = explode(':', $ID); 501 $lang_ns = array_shift($split_id); 502 // only translate if the current page is in a language namespace 503 if (!array_key_exists($lang_ns, $this->langs)) return false; 504 505 $org_id = implode(':', $split_id); 506 507 // if default lang is in ns: add default ns in front of org id 508 if ($this->getConf('default_lang_in_ns')) { 509 $org_id = $this->get_default_lang() . ':' . $org_id; 510 } 511 512 // no translations for the glossary namespace 513 $glossary_ns = $this->get_glossary_ns(); 514 if ($glossary_ns and substr($org_id, 0, strlen($glossary_ns)) == $glossary_ns) return false; 515 516 // check if the original page exists 517 if (!page_exists($org_id)) return false; 518 519 return true; 520 } 521 522 private function check_do_push_translate(): bool { 523 global $ID; 524 global $INFO; 525 526 if (!$INFO['exists']) return false; 527 528 // only allow push translation if the user can edit this page 529 $perm = auth_quickaclcheck($ID); 530 if ($perm < AUTH_EDIT) return false; 531 532 // if default language is in namespace: only allow push translation from that namespace 533 if($this->getConf('default_lang_in_ns')) { 534 $split_id = explode(':', $ID); 535 $lang_ns = array_shift($split_id); 536 537 if ($lang_ns !== $this->get_default_lang()) return false; 538 } 539 540 // no translations for the glossary namespace 541 if ($this->check_in_glossary_ns()) return false; 542 543 $push_langs = $this->get_push_langs(); 544 // push_langs empty --> push_translate disabled --> abort 545 if (empty($push_langs)) return false; 546 547 // skip blacklisted namespaces and pages 548 if ($this->getConf('blacklist_regex')) { 549 // blacklist regex match --> abort 550 if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false; 551 } 552 553 return true; 554 } 555 556 private function create_glossary($src, $target, $entries): string { 557 if (!trim($this->getConf('api_key'))) { 558 msg($this->getLang('msg_bad_key'), -1); 559 return ''; 560 } 561 562 if ($this->getConf('api') == 'free') { 563 $url = 'https://api-free.deepl.com/v2/glossaries'; 564 } else { 565 $url = 'https://api.deepl.com/v2/glossaries'; 566 } 567 568 $data = array( 569 'name' => 'DokuWiki-Autotranslate-' . $src . '_' . $target, 570 'source_lang' => $src, 571 'target_lang' => $target, 572 'entries' => $entries, 573 'entries_format' => 'tsv' 574 ); 575 576 $http = new DokuHTTPClient(); 577 578 $http->headers = array('Authorization' => 'DeepL-Auth-Key ' . $this->getConf('api_key')); 579 580 $raw_response = $http->post($url, $data); 581 582 if ($http->status >= 400) { 583 // add error messages 584 switch ($http->status) { 585 case 403: 586 msg($this->getLang('msg_bad_key'), -1); 587 break; 588 case 400: 589 msg($this->getLang('msg_glossary_content_invalid'), -1); 590 break; 591 default: 592 msg($this->getLang('msg_glossary_create_fail'), -1); 593 break; 594 } 595 596 // if any error occurred return an empty string 597 return ''; 598 } 599 600 $json_response = json_decode($raw_response, true); 601 602 return $json_response['glossary_id']; 603 } 604 605 private function delete_glossary($glossary_id): bool { 606 if (!trim($this->getConf('api_key'))) { 607 msg($this->getLang('msg_bad_key'), -1); 608 return false; 609 } 610 611 if ($this->getConf('api') == 'free') { 612 $url = 'https://api-free.deepl.com/v2/glossaries'; 613 } else { 614 $url = 'https://api.deepl.com/v2/glossaries'; 615 } 616 617 $url .= '/' . $glossary_id; 618 619 $http = new DokuHTTPClient(); 620 621 $http->headers = array('Authorization' => 'DeepL-Auth-Key ' . $this->getConf('api_key')); 622 623 $http->sendRequest($url, '', 'DELETE'); 624 625 if ($http->status >= 400) { 626 // add error messages 627 switch ($http->status) { 628 case 403: 629 msg($this->getLang('msg_bad_key'), -1); 630 break; 631 default: 632 msg($this->getLang('msg_glossary_delete_fail'), -1); 633 break; 634 } 635 636 // if any error occurred return false 637 return false; 638 } 639 640 return true; 641 } 642 643 private function deepl_translate($text, $target_lang, $org_ns): string { 644 if (!trim($this->getConf('api_key'))) { 645 msg($this->getLang('msg_translation_fail_bad_key'), -1); 646 return ''; 647 } 648 649 $text = $this->patch_links($text, $target_lang, $org_ns); 650 651 $text = $this->insert_ignore_tags($text); 652 653 $data = array( 654 'source_lang' => strtoupper(substr($this->get_default_lang(), 0, 2)), // cut of things like "-informal" 655 'target_lang' => $this->langs[$target_lang], 656 'tag_handling' => 'xml', 657 'ignore_tags' => 'ignore', 658 'text' => $text 659 ); 660 661 // check if glossaries are enabled 662 if ($this->get_glossary_ns()) { 663 $src = substr($this->get_default_lang(), 0, 2); 664 $target = substr($target_lang, 0, 2); 665 $glossary_id = $this->get_glossary_id($src, $target); 666 if ($glossary_id) { 667 // use glossary if it is defined 668 $data['glossary_id'] = $glossary_id; 669 } 670 } 671 672 if ($this->getConf('api') == 'free') { 673 $url = 'https://api-free.deepl.com/v2/translate'; 674 } else { 675 $url = 'https://api.deepl.com/v2/translate'; 676 } 677 678 $http = new DokuHTTPClient(); 679 680 $http->headers = array('Authorization' => 'DeepL-Auth-Key ' . $this->getConf('api_key')); 681 682 $raw_response = $http->post($url, $data); 683 684 if ($http->status >= 400) { 685 // add error messages 686 switch ($http->status) { 687 case 403: 688 msg($this->getLang('msg_translation_fail_bad_key'), -1); 689 break; 690 case 404: 691 msg($this->getLang('msg_translation_fail_invalid_glossary'), -1); 692 break; 693 case 456: 694 msg($this->getLang('msg_translation_fail_quota_exceeded'), -1); 695 break; 696 default: 697 msg($this->getLang('msg_translation_fail'), -1); 698 break; 699 } 700 701 // if any error occurred return an empty string 702 return ''; 703 } 704 705 $json_response = json_decode($raw_response, true); 706 $translated_text = $json_response['translations'][0]['text']; 707 708 $translated_text = $this->remove_ignore_tags($translated_text); 709 710 return $translated_text; 711 } 712 713 private function get_push_langs(): array { 714 $push_langs = trim($this->getConf('push_langs')); 715 716 if ($push_langs === '') return array(); 717 718 return explode(' ', $push_langs); 719 } 720 721 private function patch_links($text, $target_lang, $ns): string { 722 /* 723 * 1. Find links in [[ aa:bb ]] or [[ aa:bb | cc ]] 724 * 2. Extract aa:bb 725 * 3. Check if lang:aa:bb exists 726 * 3.1. --> Yes --> replace 727 * 3.2. --> No --> leave it as it is 728 */ 729 730 731 /* 732 * LINKS 733 */ 734 735 preg_match_all('/\[\[([\s\S]*?)(#[\s\S]*?)?((\|)([\s\S]*?))?]]/', $text, $matches, PREG_SET_ORDER); 736 737 foreach ($matches as $match) { 738 739 // external link --> skip 740 if (strpos($match[1], '://') !== false) continue; 741 742 // skip interwiki links 743 if (strpos($match[1], '>') !== false) continue; 744 745 // skip mail addresses 746 if (strpos($match[1], '@') !== false) continue; 747 748 // skip windows share links 749 if (strpos($match[1], '\\\\') !== false) continue; 750 751 $resolved_id = trim($match[1]); 752 753 resolve_pageid($ns, $resolved_id, $exists); 754 755 $resolved_id_full = $resolved_id; 756 757 // if the link already points to a target in a language namespace drop it and add the new language namespace 758 $split_id = explode(':', $resolved_id); 759 $lang_ns = array_shift($split_id); 760 if (array_key_exists($lang_ns, $this->langs)) { 761 $resolved_id = implode(':', $split_id); 762 } 763 764 $lang_id = $target_lang . ':' . $resolved_id; 765 766 if (!page_exists($lang_id)) { 767 // Page in target lang does not exist --> replace with absolute ID in case it was a relative ID 768 $new_link = '[[' . $resolved_id_full . $match[2] . $match[3] . ']]'; 769 } else { 770 // Page in target lang exists --> replace link 771 $new_link = '[[' . $lang_id . $match[2] . $match[3] . ']]'; 772 } 773 774 $text = str_replace($match[0], $new_link, $text); 775 776 } 777 778 /* 779 * MEDIA 780 */ 781 782 preg_match_all('/\{\{(([\s\S]*?)(\?[\s\S]*?)?)(\|([\s\S]*?))?}}/', $text, $matches, PREG_SET_ORDER); 783 784 foreach ($matches as $match) { 785 786 // external image --> skip 787 if (strpos($match[1], '://') !== false) continue; 788 789 // skip things like {{tag>...}} 790 if (strpos($match[1], '>') !== false) continue; 791 792 // keep alignment 793 $align_left = ""; 794 $align_right = ""; 795 796 // align left --> space in front of ID 797 if (substr($match[1], 0, 1) == " ") $align_left = " "; 798 // align right --> space behind id 799 if (substr($match[1], -1) == " ") $align_right = " "; 800 801 $resolved_id = trim($match[2]); 802 $params = trim($match[3]); 803 804 resolve_mediaid($ns, $resolved_id, $exists); 805 806 $resolved_id_full = $resolved_id; 807 808 // if the link already points to a target in a language namespace drop it and add the new language namespace 809 $split_id = explode(':', $resolved_id); 810 $lang_ns = array_shift($split_id); 811 if (array_key_exists($lang_ns, $this->langs)) { 812 $resolved_id = implode(':', $split_id); 813 } 814 815 $lang_id = $target_lang . ':' . $resolved_id; 816 817 $lang_id_fn = mediaFN($lang_id); 818 819 if (!file_exists($lang_id_fn)) { 820 // media in target lang does not exist --> replace with absolute ID in case it was a relative ID 821 $new_link = '{{' . $align_left . $resolved_id_full . $params . $align_right . $match[4] . '}}'; 822 } else { 823 // media in target lang exists --> replace it 824 $new_link = '{{' . $align_left . $lang_id . $params . $align_right . $match[4] . '}}'; 825 } 826 827 $text = str_replace($match[0], $new_link, $text); 828 829 } 830 831 return $text; 832 } 833 834 private function insert_ignore_tags($text): string { 835 // ignore every other xml-like tags (the tags themselves, not their content), otherwise deepl would break the formatting 836 $text = preg_replace('/<[\s\S]+?>/', '<ignore>${0}</ignore>', $text); 837 838 // prevent deepl from breaking headings 839 $text = preg_replace('/={1,6}/', '<ignore>${0}</ignore>', $text); 840 841 // prevent deepl from messing with nocache-instructions 842 $text = str_replace("~~NOCACHE~~", "<ignore>~~NOCACHE~~</ignore>", $text); 843 844 // fix for plugins like tag or template 845 $text = preg_replace('/\{\{[\s\w]+?>[\s\S]*?}}/', '<ignore>${0}</ignore>', $text); 846 847 // ignore links in wikitext (outside of dokuwiki-links) 848 $text = preg_replace('/\S+:\/\/\S+/', '<ignore>${0}</ignore>', $text); 849 850 // ignore link/media ids but translate the text (if existing) 851 $text = preg_replace('/\[\[([\s\S]*?)(#[\s\S]*?)?((\|)([\s\S]*?))?]]/', '<ignore>[[${1}${2}${4}</ignore>${5}<ignore>]]</ignore>', $text); 852 $text = preg_replace('/\{\{([\s\S]*?)(\?[\s\S]*?)?((\|)([\s\S]*?))?}}/', '<ignore>{{${1}${2}${4}</ignore>${5}<ignore>}}</ignore>', $text); 853 854 // prevent deepl from messing with tables 855 $text = str_replace(" ^ ", "<ignore> ^ </ignore>", $text); 856 $text = str_replace(" ^ ", "<ignore> ^ </ignore>", $text); 857 $text = str_replace(" ^ ", "<ignore> ^ </ignore>", $text); 858 $text = str_replace("^ ", "<ignore>^ </ignore>", $text); 859 $text = str_replace(" ^", "<ignore> ^</ignore>", $text); 860 $text = str_replace("^", "<ignore>^</ignore>", $text); 861 $text = str_replace(" | ", "<ignore> | </ignore>", $text); 862 $text = str_replace(" | ", "<ignore> | </ignore>", $text); 863 $text = str_replace(" | ", "<ignore> | </ignore>", $text); 864 $text = str_replace("| ", "<ignore>| </ignore>", $text); 865 $text = str_replace(" |", "<ignore> |</ignore>", $text); 866 $text = str_replace("|", "<ignore>|</ignore>", $text); 867 868 // prevent deepl from doing strange things with dokuwiki syntax 869 // if a full line is formatted, we have to double-ignore for some reason 870 $text = str_replace("''", "<ignore><ignore>''</ignore></ignore>", $text); 871 $text = str_replace("//", "<ignore><ignore>//</ignore></ignore>", $text); 872 $text = str_replace("**", "<ignore><ignore>**</ignore></ignore>", $text); 873 $text = str_replace("__", "<ignore><ignore>__</ignore></ignore>", $text); 874 $text = str_replace("\\\\", "<ignore><ignore>\\\\</ignore></ignore>", $text); 875 876 // prevent deepl from messing with smileys 877 $smileys = array_keys(getSmileys()); 878 foreach ($smileys as $smiley) { 879 $text = str_replace($smiley, "<ignore>" . $smiley . "</ignore>", $text); 880 } 881 882 // ignore code tags 883 $text = preg_replace('/(<php[\s\S]*?>[\s\S]*?<\/php>)/', '<ignore>${1}</ignore>', $text); 884 $text = preg_replace('/(<file[\s\S]*?>[\s\S]*?<\/file>)/', '<ignore>${1}</ignore>', $text); 885 $text = preg_replace('/(<code[\s\S]*?>[\s\S]*?<\/code>)/', '<ignore>${1}</ignore>', $text); 886 887 // ignore the expressions from the ignore list 888 $ignored_expressions = explode(':', $this->getConf('ignored_expressions')); 889 890 foreach ($ignored_expressions as $expression) { 891 $text = str_replace($expression, '<ignore>' . $expression . '</ignore>', $text); 892 } 893 894 return $text; 895 } 896 897 private function remove_ignore_tags($text): string { 898 $ignored_expressions = explode(':', $this->getConf('ignored_expressions')); 899 900 foreach ($ignored_expressions as $expression) { 901 $text = str_replace('<ignore>' . $expression . '</ignore>', $expression, $text); 902 } 903 904 // prevent deepl from messing with nocache-instructions 905 $text = str_replace("<ignore>~~NOCACHE~~</ignore>", "~~NOCACHE~~", $text); 906 907 // prevent deepl from messing with tables 908 $text = str_replace("<ignore>^</ignore>", "^", $text); 909 $text = str_replace("<ignore>^ </ignore>", "^ ", $text); 910 $text = str_replace("<ignore> ^</ignore>", " ^", $text); 911 $text = str_replace("<ignore> ^ </ignore>", " ^ ", $text); 912 $text = str_replace("<ignore> ^ </ignore>", " ^ ", $text); 913 $text = str_replace("<ignore> ^ </ignore>", " ^ ", $text); 914 $text = str_replace("<ignore>|</ignore>", "|", $text); 915 $text = str_replace("<ignore>| </ignore>", "| ", $text); 916 $text = str_replace("<ignore> |</ignore>", " |", $text); 917 $text = str_replace("<ignore> | </ignore>", " | ", $text); 918 $text = str_replace("<ignore> | </ignore>", " | ", $text); 919 $text = str_replace("<ignore> | </ignore>", " | ", $text); 920 921 $text = str_replace("<ignore><ignore>''</ignore></ignore>", "''", $text); 922 $text = str_replace("<ignore><ignore>//</ignore></ignore>", "//", $text); 923 $text = str_replace("<ignore><ignore>**</ignore></ignore>", "**", $text); 924 $text = str_replace("<ignore><ignore>__</ignore></ignore>", "__", $text); 925 $text = str_replace("<ignore><ignore>\\\\</ignore></ignore>", "\\\\", $text); 926 927 // ignore links in wikitext (outside of dokuwiki-links) 928 $text = preg_replace('/<ignore>(\S+:\/\/\S+)<\/ignore>/', '${1}', $text); 929 930 $text = preg_replace('/<ignore>\[\[([\s\S]*?)(\|)?(<\/ignore>)([\s\S]*?)?<ignore>]]<\/ignore>/', '[[${1}${2}${4}]]', $text); 931 $text = preg_replace('/<ignore>\{\{([\s\S]*?)(\|)?(<\/ignore>)([\s\S]*?)?<ignore>}}<\/ignore>/', '{{${1}${2}${4}}}', $text); 932 933 // prevent deepl from messing with smileys 934 $smileys = array_keys(getSmileys()); 935 foreach ($smileys as $smiley) { 936 $text = str_replace("<ignore>" . $smiley . "</ignore>", $smiley, $text); 937 } 938 939 $text = preg_replace('/<ignore>(<php[\s\S]*?>[\s\S]*?<\/php>)<\/ignore>/', '${1}', $text); 940 $text = preg_replace('/<ignore>(<file[\s\S]*?>[\s\S]*?<\/file>)<\/ignore>/', '${1}', $text); 941 $text = preg_replace('/<ignore>(<code[\s\S]*?>[\s\S]*?<\/code>)<\/ignore>/', '${1}', $text); 942 943 // fix for plugins like tag or template 944 $text = preg_replace('/<ignore>(\{\{[\s\w]+?>[\s\S]*?}})<\/ignore>/', '${1}', $text); 945 946 // prevent deepl from breaking headings 947 $text = preg_replace('/<ignore>(={1,6})<\/ignore>/','${1}', $text); 948 949 // ignore every other xml-like tags (the tags themselves, not their content), otherwise deepl would break the formatting 950 $text = preg_replace('/<ignore>(<[\s\S]+?>)<\/ignore>/', '${1}', $text); 951 952 // restore < and > for example from arrows (-->) in wikitext 953 $text = str_replace('>', '>', $text); 954 $text = str_replace('<', '<', $text); 955 956 // restore & in wikitext 957 $text = str_replace('&', '&', $text); 958 959 return $text; 960 } 961} 962 963