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
10 if(!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  */
43 class 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 */
473 function array_prefix ($arr, $prefix) {
474     return array_map (
475         function ($p) use ($prefix) { return $prefix.$p; },
476         $arr);
477 }
478