1<?php
2/**
3 * Delete unnecessary languages -> administration function
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Taggic <taggic@t-online.de>
7 */
8
9// must be run within Dokuwiki
10if(!defined('DOKU_INC')) die();
11
12/** Implicit data type:
13 *
14 * ^Lang is an array that looks like the following
15 * { "core": [ $lang... ],
16 *      "templates": [ $tpl_name: [ $lang... ], ... ],
17 *      "plugins": [ $plugin_name: [ $lang... ], ... ]
18 * }
19 *     where $lang is a DokuWiki language code
20 *           $tpl_name is the template name
21 *           $plugin_name is the plugin name
22 *  The $lang arrays are zero-indexed
23 */
24
25/** CSS Classes:
26 *
27 * ul.languages is an inline list of language codes
28 *   if li.active is set, the text will be highlighted
29 *   if li.enabled is set, the text is normal,
30 *       otherwise it's red and striked-out
31 * .module is set on text that represent module names: template names,
32 *     plugin names and "dokuwiki"
33 *
34 * #langshortlist is the list of language with checkboxes
35 * #langlonglist is the list of list of languages available for each module
36 * .langdelete__text is the class set on the section wrapper around all the text
37 */
38
39/**
40 * All DokuWiki plugins to extend the admin function
41 * need to inherit from this class
42 */
43class admin_plugin_langdelete extends DokuWiki_Admin_Plugin {
44
45    /** Fallback language */
46    const DEFAULT_LANG = 'en';
47    /** data stdObject  assigned by ->handle() and used in ->html() */
48    private $d;
49
50    /** return sort order for position in admin menu */
51    function getMenuSort() { return 20; }
52
53    /** Called when dispatching the DokuWiki action;
54     * Puts the required data for ->html() in $->d */
55    function handle() {
56        $d =& $this->d;
57        $d = new stdClass; // reset
58
59        $d->submit = isset($_REQUEST['submit']);
60        $submit =& $d->submit;
61
62		/* Check security token */
63        if ($submit) {
64            $valid =& $d->valid;
65            $valid = True;
66            if (!checkSecurityToken()) {
67                $valid = False;
68                return;
69            }
70		}
71
72		/* Set DokuWiki language info */
73        $d->langs = $this->list_languages();
74        $langs =& $d->langs;
75
76        // $u_langs is in alphabetical (?) order because directory listing
77        $d->u_langs = $this->lang_unique($langs);
78        $u_langs =& $d->u_langs;
79
80		/* Grab form data */
81		if ($submit) {
82			$d->dryrun = $_REQUEST['dryrun'];
83            $lang_str = $_REQUEST['langdelete_w'];
84		}
85
86		/* What languages do we keep ? */
87        $lang_keep[] = self::DEFAULT_LANG; // add 'en', the fallback
88        $lang_keep[] = $conf['lang'];      // add current lang
89
90        if ($submit) {
91            /* Add form data to languages to keep */
92            if (strlen ($lang_str) > 0) {
93                $lang_keep = array_merge ($lang_keep, explode(',', $lang_str));
94            }
95        } else {
96            // Keep every language on first run
97            $lang_keep = $u_langs;
98        }
99
100        $lang_keep = array_values(array_filter(array_unique($lang_keep)));
101        $d->lang_keep =& $lang_keep;
102
103        /* Does the language we want to keep actually exist ? */
104        $non_langs = array_diff ($lang_keep, $u_langs);
105        if ($non_langs) {
106            $d->nolang_s = implode (",", $non_langs);
107        }
108
109		/* Prepare data for deletion */
110		if ($submit) {
111            $d->langs_to_delete = $this->_filter_out_lang ($langs, $lang_keep);
112		}
113
114		/* What do the checkboxes say ? */
115        if ($submit) {
116            /* Grab checkboxes */
117            $d->shortlang = array_keys ($_REQUEST['shortlist']);
118            $shortlang =& $d->shortlang;
119
120            /* Prevent discrepancy between shortlist and text form */
121            if (array_diff ($lang_keep, $shortlang)
122                || array_diff ($shortlang, $lang_keep))
123            {
124                $d->discrepancy = True;
125            }
126        } else {
127            // Keep every language on first run
128            $d->shortlang = $u_langs;
129        }
130    }
131
132    /**
133     * langdelete Output function
134     *
135     * Prints a table with all found language folders.
136     * HTML and data processing are done here at the same time
137     *
138     * @author  Taggic <taggic@t-online.de>
139     */
140    function html() {
141        global $conf; // access DW configuration array
142        $d =& $this->d; // from ->handle()
143
144        // In case we want to fetch the files from gh
145        #$version = getVersionData();
146
147        // langdelete__intro
148        echo $this->locale_xhtml('intro');
149
150        // input anchor
151        echo '<a name="langdelete_inputbox"></a>'.NL;
152        echo $this->locale_xhtml('guide');
153        // input form
154        $this->_html_form($d);
155
156
157        $langs = $this->list_languages();
158        $u_langs = $this->lang_unique($langs);
159
160
161        /* Switch on form submission state */
162        if (!$d->submit) {
163            /* Show available languages */
164            echo '<section class="langdelete__text">';
165            echo $this->getLang('available_langs');
166            $this->print_shortlist ($d);
167            $this->html_print_langs($d->langs);
168            echo '</section>';
169
170        } else {
171            /* Process form */
172
173            /* Check token */
174            if (!$d->valid) {
175                echo "<p>Invalid security token</p>";
176                return;
177            }
178
179            if ($d->discrepancy) {
180                msg($this->getLang('discrepancy_warn'), 2);
181            }
182            if ($d->nolang_s) {
183                msg($this->getLang('nolang') . $d->nolang_s , 2);
184            }
185
186            echo '<h2>'.$this->getLang('h2_output').'</h2>'.NL;
187
188            if ($d->dryrun) {
189                /* Display what will be deleted */
190                msg($this->getLang('langdelete_willmsg'), 2);
191                echo '<section class="langdelete__text">';
192                echo $this->getLang('available_langs');
193                $this->print_shortlist ($d);
194                $this->html_print_langs($d->langs, $d->lang_keep);
195                echo '</section>';
196
197                msg($this->getLang('langdelete_attention'), 2);
198                echo '<a href="#langdelete_inputbox">'.$this->getLang('backto_inputbox').'</a>'.NL;
199
200            } else {
201                /* Delete and report what was deleted */
202                msg($this->getLang('langdelete_delmsg'), 0);
203
204                echo '<section class="langdelete__text">';
205                $this->html_print_langs($d->langs_to_delete);
206                echo '</section>';
207
208                echo '<pre>';
209                $this->remove_langs($d->langs_to_delete);
210                echo '</pre>';
211            }
212        }
213    }
214
215    /**
216     * Display the form with input control to let the user specify,
217     * which languages to be kept beside en
218     *
219     * @author  Taggic <taggic@t-online.de>
220     */
221    private function _html_form (&$d) {
222        global $ID, $conf;
223
224        echo '<form id="langdelete__form" action="'.wl($ID).'" method="post">';
225        echo     '<input type="hidden" name="do" value="admin" />'.NL;
226        echo     '<input type="hidden" name="page" value="'.$this->getPluginName().'" />'.NL;
227        formSecurityToken();
228
229        echo     '<fieldset class="langdelete__fieldset"><legend>'.$this->getLang('i_legend').'</legend>'.NL;
230
231        echo         '<label class="formTitle">'.$this->getLang('i_using').':</label>';
232        echo         '<div class="box">'.$conf['lang'].'</div>'.NL;
233
234        echo         '<label class="formTitle" for="langdelete_w">'.$this->getLang('i_shouldkeep').':</label>';
235        echo         '<input type="text" name="langdelete_w" class="edit" value="'.hsc(implode(',', $d->lang_keep)).'" />'.NL;
236
237        echo         '<label class="formTitle" for="option">'.$this->getLang('i_runoption').':</label>';
238        echo         '<div class="box">'.NL;
239        echo             '<input type="checkbox" name="dryrun" checked="checked" /> ';
240        echo             '<label for="dryrun">'.$this->getLang('i_dryrun').'</label>'.NL;
241        echo         '</div>'.NL;
242
243        echo         '<button name="submit">'.$this->getLang('btn_start').'</button>'.NL;
244
245        echo     '</fieldset>'.NL;
246        echo '</form>'.NL;
247    }
248
249    /** Print the language shortlist and cross-out those not in $keep */
250    function print_shortlist (&$d) {
251        $shortlang =& $d->shortlang;
252
253        echo '<ul id="langshortlist" class="languages">';
254
255        # As the disabled input won't POST
256        echo '<input type="hidden" name="shortlist['.self::DEFAULT_LANG.']"'
257            .' form="langdelete__form" />';
258
259        foreach ($d->u_langs as $l) {
260            $checked = in_array($l, $shortlang) || $l == self::DEFAULT_LANG;
261
262            echo '<li'.($checked ? ' class="enabled"' : '').'>';
263
264            echo '<input type="checkbox" id="shortlang-'.$l.'"'
265                .' name="shortlist['.$l.']"'
266                .' form="langdelete__form"'
267                .($checked ? ' checked' : '')
268                .($l == self::DEFAULT_LANG ? ' disabled' : '')
269                .' />';
270
271            echo '<label for="shortlang-'.$l.'">';
272            if ($checked) {
273                echo $l;
274            } else {
275                echo '<del>'.$l.'</del>';
276            }
277            echo '</label>';
278
279            echo '</li>';
280        }
281        echo '</ul>';
282    }
283
284
285    /** Display the languages in $langs for each module as a HTML list;
286     * Cross-out those not in $keep
287     *
288     * Signature: ^Lang, Array => () */
289    private function html_print_langs ($langs, $keep = null) {
290        /* Print language list, $langs being an array;
291         * Cross out those not in $keep */
292        $print_lang_li = function ($langs) use ($keep) {
293            echo '<ul class="languages">';
294            foreach ($langs as $val) {
295                // If $keep is null, we keep everything
296                $enabled = is_null($keep) || in_array ($val, $keep);
297
298                echo '<li val="'.$val.'"'
299                    .($enabled ? ' class="enabled"' : '')
300                    .'>';
301                if ($enabled) {
302                    echo $val;
303                } else {
304                    echo '<del>'.$val.'</del>';
305                }
306                echo '</li>';
307            }
308            echo '</ul>';
309        };
310
311
312        echo '<ul id="langlonglist">';
313
314        // Core
315        echo '<li><span class="module">'.$this->getLang('dokuwiki_core').'</span>';
316        $print_lang_li ($langs['core']);
317        echo '</li>';
318
319        // Templates
320        echo '<li>'.$this->getLang('templates');
321        echo     '<ul>';
322        foreach ($langs['templates'] as $name => $l) {
323            echo '<li><span class="module">'.$name.':</span>';
324            $print_lang_li ($l);
325            echo '</li>';
326        }
327        echo     '</ul>';
328        echo '</li>';
329
330        // Plugins
331        echo '<li>'.$this->getLang('plugins');
332        echo     '<ul>';
333        foreach ($langs['plugins'] as $name => $l) {
334            echo '<li><span class="module">'.$name.':</span>';
335            $print_lang_li ($l);
336            echo '</li>';
337        }
338        echo     '</ul>';
339        echo '</li>';
340
341        echo '</ul>';
342    }
343
344    /** Returns the available languages for each module
345     * (core, template or plugin)
346     *
347     * Signature: () => ^Lang
348     */
349    private function list_languages () {
350        // See https://www.dokuwiki.org/devel:localization
351
352        /* Returns the subfolders of $dir as an array */
353        $dir_subfolders = function ($dir) {
354            $sub = scandir($dir);
355            $sub = array_filter ($sub, function ($e) use ($dir) {
356                return is_dir ("$dir/$e")
357                       && !in_array ($e, array('.', '..'));
358            } );
359            return $sub;
360        };
361
362        /* Return an array of template names */
363        $list_templates = function () use ($dir_subfolders) {
364            return $dir_subfolders (DOKU_INC."lib/tpl");
365        };
366
367        /* Return an array of languages available for the module
368         * (core, template or plugin) given its $root directory */
369        $list_langs = function ($root) use ($dir_subfolders) {
370            $dir = "$root/lang";
371            if (!is_dir ($dir)) return;
372
373            return $dir_subfolders ($dir);
374        };
375
376        /* Get templates and plugins names */
377        global $plugin_controller;
378        $plugins = $plugin_controller->getList();
379        $templates = $list_templates();
380
381        return array(
382            "core" => $list_langs (DOKU_INC."inc"),
383            "templates" => array_combine ($templates,
384                array_map ($list_langs,
385                    array_prefix ($templates, DOKU_INC."lib/tpl/"))),
386            "plugins" => array_combine ($plugins,
387                array_map ($list_langs,
388                    array_prefix ($plugins, DOKU_PLUGIN)))
389        );
390    }
391
392    /** Remove $lang_keep from the module languages $e
393     *
394     * Signature: ^Lang, Array => ^Lang */
395    private function _filter_out_lang ($e, $lang_keep) {
396        // Recursive function with cases being an array of arrays, or an array
397        if (count ($e) > 0 && is_array (array_values($e)[0])) {
398            foreach ($e as $k => $elt) {
399                $out[$k] = $this->_filter_out_lang ($elt, $lang_keep);
400            }
401            return $out;
402
403        } else {
404            return array_filter ($e, function ($v) use ($lang_keep) {
405                return !in_array ($v, $lang_keep);
406            });
407        }
408    }
409
410    /** Return an array of the languages in $l
411     *
412     * Signature: ^Lang => Array */
413    private function lang_unique ($l) {
414        foreach ($l['core'] as $lang) {
415            $count[$lang]++;
416        }
417        foreach ($l['templates'] as $tpl => $arr) {
418            foreach ($arr as $lang) {
419                $count[$lang]++;
420            }
421        }
422        foreach ($l['plugins'] as $plug => $arr) {
423            foreach ($arr as $lang) {
424                $count[$lang]++;
425            }
426        }
427
428        return array_keys ($count);
429    }
430
431    /** Delete the languages from the modules as specified by $langs
432     *
433     * Signature: ^Lang => () */
434    private function remove_langs($langs) {
435        foreach ($langs['core'] as $l) {
436            $this->rrm(DOKU_INC."inc/lang/$l");
437        }
438
439        foreach ($langs['templates'] as $tpl => $arr) {
440            foreach ($arr as $l) {
441                $this->rrm(DOKU_INC."lib/tpl/$tpl/lang/$l");
442            }
443        }
444
445        foreach ($langs['plugins'] as $plug => $arr) {
446            foreach ($arr as $l) {
447                $this->rrm(DOKU_INC."lib/plugins/$plug/lang/$l");
448            }
449        }
450    }
451
452    /** Recursive file removal of $path with reporting */
453    private function rrm ($path) {
454        if (is_dir ($path)) {
455            $objects = scandir ($path);
456            foreach ($objects as $object) {
457                if (!in_array ($object, array('.', '..'))) {
458                    $this->rrm("$path/$object");
459                }
460            }
461            $sucess = @rmdir ($path);
462            if (!$sucess) { echo "Failed to delete $path/\n"; }
463            else echo "Delete $path\n";
464        } else {
465            $sucess = @unlink ($path);
466            if (!$sucess) { echo "Failed to delete $path\n"; }
467            else echo "Delete $path\n";
468        }
469    }
470}
471
472/** Returns an array with each element of $arr prefixed with $prefix */
473function array_prefix ($arr, $prefix) {
474    return array_map (
475        function ($p) use ($prefix) { return $prefix.$p; },
476        $arr);
477}
478