1<?php
2/**
3 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
4 * @author     Viktor Söderqvist <viktor@zuiderkwast.se>
5 *
6 * Translate - A translation plugin using Dublin Core metadata. No restriction to page IDs of the translations.
7 *
8 * Metadata keys used by this plugin are
9 *
10 * relation/istranslationof
11 *      a list of source pages, normally only one (array: ID => language)
12 * relation/translations
13 *      a list of translation pages (array: ID => language)
14 * language
15 *      the language code of the page, 2-letter ISO code
16 */
17
18// must be run within Dokuwiki
19if(!defined('DOKU_INC')) die();
20if(!defined('DOKU_LF')) define ('DOKU_LF',"\n");
21
22if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
23require_once(DOKU_PLUGIN.'action.php');
24
25class action_plugin_translate extends DokuWiki_Action_Plugin {
26
27    /**
28     * register the eventhandlers
29     */
30    public function register(Doku_Event_Handler $contr) {
31        $contr->register_hook('DOKUWIKI_STARTED', 'BEFORE', $this, 'handleDokuwikiStarted');
32        $contr->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, 'handleTplActUnknown');
33        $contr->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handleActPreprocess');
34        // Events for DW > 2020-07-29 "hogfather"
35        $contr->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'handleHtmlEditformOutput', []);
36        // Events for DW ≤ 2020-07-29 "hogfather"
37        $contr->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handleHtmlEditformOutput');
38        // TODO: When a translation is deleted, delete it from the original's list of translations.
39        //$contr->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'handlePageWrite');
40        $contr->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'handleActRender');
41    }
42
43    /** Ensure translators' permission to edit the page */
44    public function handleDokuwikiStarted($event, $param) {
45        global $INFO;
46        $info = & $INFO;
47
48        // Is the user blocked from editing the page?
49        if (empty($_SERVER['REMOTE_USER']) ||
50            empty($INFO['meta']['relation']['istranslationof']) ||
51            $info['perm'] < AUTH_READ ||
52            $info['perm'] >= AUTH_EDIT) return;
53
54        // Any translator group set?
55        $grp = $this->getConf('translator_group');
56        if (empty($grp)) return;
57
58        // Is the current user member of that group?
59        if (!in_array($grp, $info['userinfo']['grps'])) return;
60
61        // Set permission to edit
62        $info['perm'] = AUTH_EDIT;
63
64        // Recreate writable and editable values (code from pageinfo in common.php)
65        if($info['exists']){
66            $info['writable'] = (is_writable($info['filepath']) &&
67                                 ($info['perm'] >= AUTH_EDIT));
68        }else{
69            $info['writable'] = ($info['perm'] >= AUTH_CREATE);
70        }
71        $info['editable']  = ($info['writable'] && empty($info['lock']));
72    }
73
74    /** Insert translation links on top of page */
75    public function handleActRender($event, $param) {
76        if ($event->data != 'show') return;
77        // show links to translations at top of page if that option is on.
78        if ($this->getConf('insert_translation_links')) {
79            $my = $this->loadHelper('translate',true);
80            echo $my->translationLinks();
81        }
82    }
83
84    /**
85     * Hook for event ACTION_ACT_PREPROCESS, action 'translate'.
86     *
87     * Redirect to the translated page if there is one already.
88     */
89    public function handleActPreprocess($event, $param) {
90        $act = $event->data;
91        if (is_array($act)) {
92            list($act) = array_keys($act);
93        }
94        switch ($act) {
95            case 'createpage':
96                $this->handleActPreprocessCreatepage($event, $param);
97                break;
98            case 'translate':
99                $this->handleActPreprocessTranslate($event, $param);
100                break;
101            default:
102                return; // not handled here
103        }
104        $event->preventDefault();
105        $event->stopPropagation();
106    }
107
108    /**
109     * Hook for event TPL_ACT_UNKNOWN, action 'translate'
110     * Show page with translation form (before the translated page is created)
111     */
112    public function handleTplActUnknown($event, $param) {
113        global $ID, $INFO; // $PRE, $TEXT, $SUF,
114        if ($event->data == 'translate') {
115            $my = $this->loadHelper('translate',true);
116            $my->printActionTranslatePage();
117        }
118        elseif ($event->data == 'createpage') {
119            $my = $this->loadHelper('translate',true);
120            $my->printActionCreatepagePage();
121        }
122        else {
123            return; // not handled here
124        }
125        $event->preventDefault();
126        $event->stopPropagation();
127    }
128
129
130    /**
131     * Hook for event HTML_EDITFORM_OUTPUT (DW ≤ 2020-07-29 "hogfather") and
132     * event FORM_EDIT_OUTPUT (DW > 2020-07-29 "hogfather" )
133     * Check and gather info on the untranslated page and the current form.
134     * If needed, call the method that adds our elements to the form.
135     */
136    public function handleHtmlEditformOutput($event, $param) {
137        global $INFO, $ID;
138
139        // Original from meta
140        if (empty($INFO['meta']['relation']['istranslationof'])) return;
141        list ($id) = array_keys($INFO['meta']['relation']['istranslationof']);
142
143        // Get original content
144        $file = wikiFN($id);
145        if (!@file_exists($file)) {
146            msg('The original file for this translation does not exist. Perhaps it has been deleted.');
147            return;
148        }
149        $origtext = io_readWikiPage($file,$id);
150
151        // Insert original page on the side
152        $form = $event->data;
153        if (is_a($form, \dokuwiki\Form\Form::class)) {
154            // DW > 2020-07-29 "hogfather"
155            $pos = $form->findPositionByType('textarea');
156            if ($pos===false) return;
157            $this->handleHtmlEditformOutputNG($form, $pos, $origtext);
158        } else {
159            // DW ≤ 2020-07-29 "hogfather"
160            $pos = $form->findElementByType('wikitext');
161            if ($pos===false) return;
162            $this->handleHtmlEditformOutputLegacy($form, $pos, $origtext);
163        }
164    }
165
166    /**
167     * Add our elements to the form. DW > 2020-07-29 "hogfather"
168     */
169    protected function handleHtmlEditformOutputNG($form, $pos, $origtext) {
170        // Before the wikitext...
171        $form->addTagOpen('div', $pos++)->id('wrapper__wikitext')->addClass('hor');
172        // After the wikitext...
173        $pos++;
174        $form->addTagClose('div', $pos++);
175        $form->addTagOpen('div', $pos++)->id('wrapper__sourcetext')->addClass('hor');
176
177        $attrs=Array();
178        $attrs['readonly']='readonly';
179        $attrs['cols']=80;
180        $attrs['rows']=10;
181        $attrs['style']='width:100%;';
182        $attrs['readonly']='readonly';
183        $form->addTextarea('origWikitext', '', $pos++)->attrs($attrs)->val($origtext)
184            ->id('translate__sourcetext')->addClass('edit');
185
186        $form->addTagClose('div', $pos++);
187        $form->addTagOpen('div', $pos++)->addClass('clearer');
188        $form->addTagClose('div', $pos++);
189    }
190
191    /**
192     * Add our elements to the form. DW ≤ 2020-07-29 "hogfather"
193     */
194    protected function handleHtmlEditformOutputLegacy($form, $pos, $origtext) {
195        // Before the wikitext...
196        $form->insertElement($pos++, form_makeOpenTag('div', array('id'=>'wrapper__wikitext','class'=>'hor')));
197        // After the wikitext...
198        $pos++;
199        $form->insertElement($pos++, form_makeCloseTag('div'));
200        $form->insertElement($pos++, form_makeOpenTag('div', array('id'=>'wrapper__sourcetext','class'=>'hor')));
201        $origelem = '<textarea id="translate__sourcetext" '.
202                    //buildAttributes($attrs,true).
203                    'class="edit" readonly="readonly" cols="80" rows="10"'.
204                    'style="width:100%;"'.//  height:600px; overflow:auto
205                    '>'.NL.
206                    hsc($origtext).
207                    '</textarea>';
208        $form->insertElement($pos++, $origelem);
209        $form->insertElement($pos++, form_makeCloseTag('div'));
210        $form->insertElement($pos++, form_makeOpenTag('div', array('class'=>'clearer')));
211        $form->insertElement($pos++, form_makeCloseTag('div'));
212    }
213
214    public function handleActPreprocessCreatepage($event, $param) {
215        global $INFO;
216        $my = $this->loadHelper('translate',true);
217        $title = $_REQUEST['title'];
218        $lang = $_REQUEST['lang'];
219
220        // Check input
221        if (empty($title) || empty($lang)) {
222            // Not filled. Show form.
223            return;
224        }
225
226        // Illegal language
227        if (!$my->languageExists($lang)) {
228            msg(sprintf("Illegal language %s",$lang), -1);
229            return;
230        }
231
232        $id = $my->suggestPageId($title, $lang);
233        if (page_exists($id)) {
234            // Error message
235            //$this->_formErrors['title'] = 1;
236            msg(sprintf($this->getLang['e_pageexists'], $title),-1);
237            return;
238        }
239
240        // Check permission to create the page.
241        $auth = auth_quickaclcheck($id);
242        $auth_ok = ($auth >= AUTH_CREATE);
243        if (!$auth_ok && $auth >= AUTH_READ) {
244            // Check special translation permission
245            // Is the current user member of the translator group?
246            $grp = $this->getConf('author_group');
247            $auth_ok = !empty($grp) &&
248                       in_array($grp, $INFO['userinfo']['grps']);
249        }
250        if (!$auth_ok) {
251            msg($this->getLang('e_denied'), -1);
252            return;
253        }
254
255        // Create and save page
256        $wikitext = "====== ".$title." ======".DOKU_LF.DOKU_LF;
257        saveWikiText($id, $wikitext, $GLOBALS['lang']['created']); //$this->getLang('translation_created'));
258
259        // Add metadata to the new page
260        $file = wikiFN($id);
261        $created = @filectime($file);
262        $meta = array();
263        $meta['date']['created'] = $created;
264        $user = $_SERVER['REMOTE_USER'];
265        if ($user) $meta['creator'] = $INFO['userinfo']['name'];
266        $meta['language'] = $lang;
267        p_set_metadata($target_id, $meta);
268
269        // Redirect to edit the new page
270        // Should we trigger some event before redirecting to edit?
271        $url = wl($id, 'do=edit');
272        send_redirect($url);
273    }
274
275    /** Handle translate action. Validates input and creates the translation page */
276    public function handleActPreprocessTranslate($event, $param) {
277        global $ID, $INFO;
278        $my = $this->loadHelper('translate',true);
279        $target_title = $_REQUEST['title'];
280        $target_lang = $_REQUEST['to'];
281        $source_lang = $my->getPageLanguage();
282
283        // Check if this is the original
284        if ($my->isTranslation()) {
285            $orig_id = $my->getOriginal();
286            $param = array('do'=>'translate','to'=>$target_lang);
287            if ($target_title) $param['title'] = $target_title;
288            $url = wl($orig_id, $param, false, '&');
289            send_redirect($url);
290            return;
291        }
292
293        // Check original language
294        if (!isset($source_lang)) {
295            // show error message and no form
296            msg($this->getLang('e_languageunknown'),-1);
297            return;
298        }
299
300        // Require target language
301        if (empty($target_lang)) {
302            // Not filled. Show form.
303            return;
304        }
305
306        // Translate to same language? Just show page.
307        if ($target_lang == $source_lang) {
308            $event->data = 'show';
309            return;
310        }
311
312        // Illegal language
313        if (!$my->languageExists($target_lang)) {
314            msg(sprintf("Illegal language %s",$target_lang), -1);
315            return;
316        }
317
318        // Check existence of source page
319        if (!page_exists($ID)) {
320            // Just ignore and show "page does not exist".
321            $event->data = 'show';
322            return;
323        }
324
325        // Check if already translated
326        $translated_id = $my->translationLookup($target_lang, $ID);
327        if (!empty($translated_id)) {
328            //$langname = $my->getLanguageName($target_lang);
329            //msg(sprintf($this->getLang('e_translationexists'), $langname));
330
331            // Redirect to already translated page
332            $opts = array('id' => $translated_id, 'preact' => 'translate');
333            trigger_event('ACTION_SHOW_REDIRECT',$opts,'act_redirect_execute');
334            // This will redirect and exit.
335        }
336
337        // Require title
338        if (empty($target_title)) {
339            // Not filled. Show form.
340            return;
341        }
342
343        // Check if target page exists
344        $target_id = $my->suggestTranslationId($target_title, $target_lang, $source_lang);
345        if (page_exists($target_id)) {
346            // Error message
347            //$this->_formErrors['title'] = 1;
348            msg(sprintf($this->getLang('e_pageexists'), $target_id),-1);
349            return;
350        }
351
352        // Check permission to create the page.
353        $auth = auth_quickaclcheck($target_id);
354        $auth_ok = ($auth >= AUTH_CREATE);
355        if (!$auth_ok && $auth >= AUTH_READ) {
356            // Check special translation permission
357            // Is the current user member of the translator group?
358            $grp = $this->getConf('translator_group');
359            $auth_ok = !empty($grp) &&
360                       in_array($grp, $INFO['userinfo']['grps']);
361        }
362        if (!$auth_ok) {
363            msg($this->getLang('e_denied'), -1);
364            return;
365        }
366
367        // Create and save page
368        $wikitext = "====== ".$target_title." ======".DOKU_LF.DOKU_LF;
369        saveWikiText($target_id, $wikitext, $this->getLang('translation_created'));
370
371        // Add metadata to the new page
372        $file = wikiFN($target_id);
373        $created = @filectime($file);
374        $meta = array();
375        $meta['date']['created'] = $created;
376        $user = $_SERVER['REMOTE_USER'];
377        if ($user) $meta['creator'] = $INFO['userinfo']['name'];
378        $meta['relation']['istranslationof'][$ID] = $source_lang;
379        $meta['language'] = $target_lang;
380        p_set_metadata($target_id, $meta);
381
382        // Add metadata to the original
383        $meta = array('relation' => array('translations' => array($target_id => $target_lang)));
384        p_set_metadata($ID, $meta);
385
386        // Redirect to edit the new page
387        // Should we trigger some event before redirecting to edit?
388        $url = wl($target_id, 'do=edit');
389        send_redirect($url);
390    }
391}
392// vim:ts=4:sw=4:et:
393