1<?php
2/**
3 * Translation Plugin: Simple multilanguage plugin
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Andreas Gohr <andi@splitbrain.org>
7 */
8
9// must be run within Dokuwiki
10if(!defined('DOKU_INC')) die();
11
12class helper_plugin_autotranslation extends DokuWiki_Plugin {
13
14    public $translations = array();
15    public $translationNs = '';
16    public $defaultlang = '';
17
18    private $opts = array(); // display options
19    private $LN = array(); // hold native names
20
21    /**
22     * Initialize
23     */
24    function __construct() {
25        global $conf, $JSINFO;
26        require_once(DOKU_INC . 'inc/pageutils.php');
27        require_once(DOKU_INC . 'inc/utf8.php');
28
29        // load wanted translation into array
30        $this->translations = strtolower(str_replace(',', ' ', $this->getConf('translations')));
31        $this->translations = array_unique(array_filter(explode(' ', $this->translations)));
32        sort($this->translations);
33
34        // load language names
35        $this->LN = confToHash(dirname(__FILE__) . '/lang/langnames.txt');
36
37        // display options
38        $this->opts = $this->getConf('display');
39        $this->opts = explode(',', $this->opts);
40        $this->opts = array_map('trim', $this->opts);
41        $this->opts = array_fill_keys($this->opts, true);
42
43        // get default translation
44        if(empty($conf['lang_before_translation'])) {
45            $dfl = $conf['lang'];
46        } else {
47            $dfl = $conf['lang_before_translation'];
48        }
49        if(in_array($dfl, $this->translations)) {
50            $this->defaultlang = $dfl;
51        } else {
52            $this->defaultlang = '';
53            array_unshift($this->translations, '');
54        }
55
56        $this->translationsNs = $this->setupTNS();
57        $JSINFO['conf']['lang'] = $dfl;
58    }
59
60    /**
61     * Find the current translation namespace
62     * This may be detected automatically or defined by the config option
63     **/
64    function setupTNS($ID="", $forceAutotranslation=false) {
65        global $conf;
66
67        if ( !empty( $this->translationsNs) ) { return $this->translationsNs; }
68        if ( empty($ID) ) { $ID = getID(); }
69
70        // autodetect?
71        // this will only work for namespaces other than the root and default language
72        if ( $forceAutotranslation || $this->getConf('autodetectnamespace') )
73        {
74            $lang = explode(':', $ID);
75            foreach( array_reverse($lang) as $tns )
76            {
77                array_pop($lang);
78                if ( in_array($tns, $this->translations) )
79                {
80                    // Found
81                    $tns = implode(":", $lang) . ':';
82                    if($tns == ':' ) { $tns = ''; }
83                    return $tns;
84                }
85            }
86        }
87
88        // Array of translations can be givven
89        $tnsA = explode(' ', $this->getConf('translationns'));
90        if ( empty($tnsA) ) return ''; // there is just this one - and translation is active.
91
92        usort($tnsA,array($this, 'lensort') );
93        foreach ( $tnsA as $tns ) {
94            $tns = cleanID(trim($tns));
95            if($tns && substr($tns, -1) != ':') { $tns .= ':'; }
96            if($tns && strpos($ID,$tns) === false) continue;
97            if($tns == ':' ) { $tns = ''; }
98
99            return $tns;
100        }
101
102        return false;
103    }
104
105    // Inner function for sorting
106    private function lensort($a,$b){
107        return strlen($b)-strlen($a);
108    }
109
110    /**
111     * Check if the given ID is a translation and return the language code.
112     */
113    function getLangPart($id) {
114        list($lng) = $this->getTransParts($id);
115        return $lng;
116    }
117
118    /**
119     * Check if the given ID is a translation and return the ID up the translation root.
120     */
121    function getIDPart($id) {
122        list($lng, $idpart) = $this->getTransParts($id);
123        return $idpart;
124    }
125
126    /**
127     * Check if the given ID is a translation and return the language code and
128     * the id part.
129     */
130    function getTransParts($id) {
131        $rx = '/^' . $this->translationsNs . '(' . join('|', $this->translations) . '):(.*)/';
132        if(preg_match($rx, $id, $match)) {
133            return array($match[1], $match[2]);
134        }
135        return array('', $id);
136    }
137
138    /**
139     * Returns the browser language if it matches with one of the configured
140     * languages
141     */
142    function getBrowserLang() {
143        if(empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
144            return false;
145        }
146        $rx = '/(^|,|:|;|-)(' . join('|', $this->translations) . ')($|,|:|;|-)/i';
147        if(preg_match($rx, $_SERVER['HTTP_ACCEPT_LANGUAGE'], $match)) {
148            return strtolower($match[2]);
149        }
150        return false;
151    }
152
153    /**
154     * Returns the ID and name to the wanted translation, empty
155     * $lng is default lang
156     */
157    function buildTransID($lng, $idpart) {
158        global $conf;
159        if($lng) {
160            $link = ':' . $this->translationsNs . $lng . ':' . $idpart;
161            $name = $lng;
162        } else {
163            $link = ':' . $this->translationsNs . $idpart;
164            $name = $this->realLC('');
165        }
166        return array($link, $name);
167    }
168
169    /**
170     * Returns the real language code, even when an empty one is given
171     * (eg. resolves th default language)
172     */
173    function realLC($lc) {
174        global $conf;
175        if($lc) {
176            return $lc;
177        } elseif(empty($conf['lang_before_translation'])) {
178            return $conf['lang'];
179        } else {
180            return $conf['lang_before_translation'];
181        }
182    }
183
184    /**
185     * Check if current ID should be translated and any GUI
186     * should be shown
187     */
188    function istranslatable($id, $checkact = true) {
189        global $ACT;
190
191        if(auth_isAdmin()) return true;
192
193        if($checkact && $ACT != 'show') return false;
194        if($this->translationsNs && strpos($id, $this->translationsNs) !== 0) return false;
195        $skiptrans = trim($this->getConf('skiptrans'));
196        if($skiptrans && preg_match('/' . $skiptrans . '/ui', ':' . $id)) return false;
197        $meta = p_get_metadata($id);
198        if(!empty($meta['plugin']['autotranslation']['notrans'])) return false;
199
200        return true;
201    }
202
203    /**
204     * Return the (localized) about link
205     */
206    function showAbout() {
207        global $ID;
208        global $conf;
209        global $INFO;
210
211        $curlc = $this->getLangPart($ID);
212
213        $about = $this->getConf('about');
214        if($this->getConf('localabout')) {
215            list($lc, $idpart) = $this->getTransParts($about);
216            list($about, $name) = $this->buildTransID($curlc, $idpart);
217            $about = cleanID($about);
218        }
219
220        $out = '';
221        $out .= '<sup>';
222        $out .= html_wikilink($about, '?');
223        $out .= '</sup>';
224
225        return $out;
226    }
227
228    /**
229     * Returns a list of (lc => link) for all existing translations of a page
230     *
231     * @param $id
232     * @return array
233     */
234    function getAvailableTranslations($id) {
235        $result = array();
236
237        list($lc, $idpart) = $this->getTransParts($id);
238        $lang = $this->realLC($lc);
239
240        foreach($this->translations as $t) {
241            if($t == $lc) continue; //skip self
242            list($link, $name) = $this->buildTransID($t, $idpart);
243            if(page_exists($link)) {
244                $result[$name] = $link;
245            }
246        }
247
248        return $result;
249    }
250
251    /**
252     * Creates an UI for linking to the available and configured translations
253     *
254     * Can be called from the template or via the ~~TRANS~~ syntax component.
255     */
256    public function showTranslations() {
257        global $conf;
258        global $INFO;
259
260        if(!$this->istranslatable($INFO['id'])) return '';
261        $this->checkage();
262
263        list($lc, $idpart) = $this->getTransParts($INFO['id']);
264        $lang = $this->realLC($lc);
265
266        $out = '<div class="plugin_autotranslation">';
267
268        //show title and about
269        if(isset($this->opts['title'])) {
270            $out .= '<span>' . $this->getLang('translations');
271            if($this->getConf('about')) $out .= $this->showAbout();
272            $out .= ':</span> ';
273            if(isset($this->opts['twolines'])) $out .= '<br />';
274        }
275
276        // open wrapper
277        if($this->getConf('dropdown')) {
278            // select needs its own styling
279            if($INFO['exists']) {
280                $class = 'wikilink1';
281            } else {
282                $class = 'wikilink2';
283            }
284            if(isset($this->opts['flag'])) {
285                $flag = DOKU_BASE . 'lib/plugins/translation/flags/' . hsc($lang) . '.gif';
286            }else{
287                $flag = '';
288            }
289
290            if($conf['userewrite']) {
291                $action = wl();
292            } else {
293                $action = script();
294            }
295
296            $out .= '<form action="' . $action . '" id="translation__dropdown">';
297            if($flag) $out .= '<img src="' . $flag . '" alt="' . hsc($lang) . '" height="11" class="' . $class . '" /> ';
298            $out .= '<select name="id" class="' . $class . '">';
299        } else {
300            $out .= '<ul>';
301        }
302
303        // insert items
304        foreach($this->translations as $t) {
305            $out .= $this->getTransItem($t, $idpart);
306        }
307
308        // close wrapper
309        if($this->getConf('dropdown')) {
310            $out .= '</select>';
311            $out .= '<input name="go" type="submit" value="&rarr;" />';
312            $out .= '</form>';
313        } else {
314            $out .= '</ul>';
315        }
316
317        // show about if not already shown
318        if(!isset($this->opts['title']) && $this->getConf('about')) {
319            $out .= '&nbsp';
320            $out .= $this->showAbout();
321        }
322
323        $out .= '</div>';
324
325        return $out;
326    }
327
328    /**
329     * Return the local name
330     *
331     * @param $lang
332     * @return string
333     */
334    function getLocalName($lang) {
335        if($this->LN[$lang]) {
336            return $this->LN[$lang];
337        }
338        return $lang;
339    }
340
341    /**
342     * Create the link or option for a single translation
343     *
344     * @param $lc string      The language code
345     * @param $idpart string  The ID of the translated page
346     * @returns string        The item
347     */
348    function getTransItem($lc, $idpart) {
349        global $ID;
350        global $conf;
351
352        list($link, $lang) = $this->buildTransID($lc, $idpart);
353        $link = cleanID($link);
354
355        // class
356        if(page_exists($link, '', false)) {
357            $class = 'wikilink1';
358        } else {
359            $class = 'wikilink2';
360        }
361
362        // local language name
363        $localname = $this->getLocalName($lang);
364
365        // current?
366        if($ID == $link) {
367            $sel = ' selected="selected"';
368            $class .= ' cur';
369        } else {
370            $sel = '';
371        }
372
373        // flag
374        $flag = $style = '';
375        if(isset($this->opts['flag'])) {
376            $flag = DOKU_BASE . 'lib/plugins/translation/flags/' . hsc($lang) . '.gif';
377            $style = ' style="background-image: url(\'' . $flag . '\')"';
378            $class .= ' flag';
379        }
380
381        // what to display as name
382        if(isset($this->opts['name'])) {
383            $display = hsc($localname);
384            if(isset($this->opts['langcode'])) $display .= ' (' . hsc($lang) . ')';
385        } elseif(isset($this->opts['langcode'])) {
386            $display = hsc($lang);
387        } else {
388            $display = '&nbsp;';
389        }
390
391        // prepare output
392        $out = '';
393        if($this->getConf('dropdown')) {
394            if($conf['useslash']) $link = str_replace(':', '/', $link);
395
396            $out .= '<option class="' . $class . '" title="' . hsc($localname) . '" value="' . $link . '"' . $sel . $style . '>';
397            $out .= $display;
398            $out .= '</option>';
399        } else {
400            $out .= '<li><div class="li">';
401            $out .= '<a href="' . wl($link, 'tns') . '" class="' . $class . '" title="' . hsc($localname) . '">';
402            if($flag) $out .= '<img src="' . $flag . '" alt="' . hsc($lang) . '" height="11" />';
403            $out .= $display;
404            $out .= '</a>';
405            $out .= '</div></li>';
406        }
407
408        return $out;
409    }
410
411    /**
412     * Checks if the current page is a translation of a page
413     * in the default language. Displays a notice when it is
414     * older than the original page. Tries to link to a diff
415     * with changes on the original since the translation
416     */
417    function checkage() {
418        global $ID;
419        global $INFO;
420        if(!$this->getConf('checkage')) return;
421        if(!$INFO['exists']) return;
422        $lng = $this->getLangPart($ID);
423        if($lng == $this->defaultlang) return;
424
425        $rx = '/^' . $this->translationsNs . '((' . join('|', $this->translations) . '):)?/';
426        $idpart = preg_replace($rx, '', $ID);
427
428        // compare modification times
429        list($orig, $name) = $this->buildTransID($this->defaultlang, $idpart);
430        $origfn = wikiFN($orig);
431        if($INFO['lastmod'] >= @filemtime($origfn)) return;
432
433        // get revision from before translation
434        $orev = 0;
435        $changelog = new PageChangelog($orig);
436        $revs = $changelog->getRevisions(0, 100);
437        foreach($revs as $rev) {
438            if($rev < $INFO['lastmod']) {
439                $orev = $rev;
440                break;
441            }
442        }
443
444        // see if the found revision still exists
445        if($orev && !page_exists($orig, $orev)) $orev = 0;
446
447        // build the message and display it
448        $orig = cleanID($orig);
449        $msg = sprintf($this->getLang('outdated'), wl($orig));
450
451        $difflink = $this->getOldDiffLink($orig, $INFO['lastmod']);
452        if ($difflink) {
453            $msg .= sprintf(' ' . $this->getLang('diff'), $difflink);
454        }
455
456        echo '<div class="notify">' . $msg . '</div>';
457    }
458
459    function getOldDiffLink($id, $lastmod) {
460        // get revision from before translation
461        $orev = false;
462        $changelog = new PageChangelog($id);
463        $revs = $changelog->getRevisions(0, 100);
464        foreach($revs as $rev) {
465            if($rev < $lastmod) {
466                $orev = $rev;
467                break;
468            }
469        }
470        if($orev && !page_exists($id, $orev)) {
471            return false;
472        }
473        $id = cleanID($id);
474        return wl($id, array('do' => 'diff', 'rev' => $orev));
475
476    }
477
478    /**
479     * Checks if the current ID has a translated page
480     */
481    function hasTranslation($inputID = null) {
482        global $ID, $INFO, $conf;
483
484        if ( empty($inputID) ) {
485            $inputID = (isset($ID) && $ID !== '') ? $ID : getID();
486        }
487        if ( $inputID === '' || $inputID === null ) {
488            return false;
489        }
490
491        if ( !$this->istranslatable($inputID) ) return false;
492
493        $idpart = $this->getIDPart($inputID);
494
495        foreach($this->translations as $t)
496        {
497            list($link,$name) = $this->buildTransID($t,$idpart);
498            $link = cleanID($link);
499
500            if( $inputID != $link && page_exists($link,'',false) ){
501                return true;
502            }
503        }
504
505        return false;
506    }
507}
508