1<?php
2
3namespace dokuwiki\plugin\dev;
4
5use splitbrain\phpcli\CLI;
6
7class LangProcessor
8{
9
10    /** @var CLI */
11    protected $logger;
12
13    /** @var array The language keys used in the code */
14    protected $codeKeys;
15
16    /** @var array The language keys matching the configuration settings */
17    protected $settingKeys;
18
19    public function __construct(CLI $logger)
20    {
21        $this->logger = $logger;
22        $this->codeKeys = $this->findLanguageKeysInCode();
23        $this->settingKeys = $this->findLanguageKeysInSettings();
24    }
25
26    /**
27     * Remove the obsolete strings from the given lang.php
28     *
29     * @param string $file
30     * @return void
31     */
32    public function processLangFile($file)
33    {
34        $lang = [];
35        include $file;
36
37        $drop = array_diff_key($lang, $this->codeKeys);
38        if (isset($found['js']) && isset($lang['js'])) {
39            $drop['js'] = array_diff_key($lang['js'], $found['js']);
40            if (!count($drop['js'])) unset($drop['js']);
41        }
42
43        foreach ($drop as $key => $value) {
44            if (is_array($value)) {
45                foreach ($value as $subkey => $subvalue) {
46                    $this->removeLangKey($file, $subkey, $key);
47                }
48            } else {
49                $this->removeLangKey($file, $key);
50            }
51        }
52    }
53
54    /**
55     * Remove obsolete string from the given settings.php
56     *
57     * @param string $file
58     * @return void
59     */
60    public function processSettingsFile($file)
61    {
62        $lang = [];
63        include $file;
64
65        $drop = array_diff_key($lang, $this->settingKeys);
66        foreach ($drop as $key => $value) {
67            $this->removeLangKey($file, $key);
68        }
69    }
70
71    /**
72     * Remove the given key from the given language file
73     *
74     * @param string $file
75     * @param string $key
76     * @param string $sub
77     * @return void
78     */
79    protected function removeLangKey($file, $key, $sub = '')
80    {
81        $q = '[\'"]';
82
83        if ($sub) {
84            $re = '/\$lang\[' . $q . $sub . $q . '\]\[' . $q . $key . $q . '\]/';
85        } else {
86            $re = '/\$lang\[' . $q . $key . $q . '\]/';
87        }
88
89        if (io_deleteFromFile($file, $re, true)) {
90            $this->logger->success('{key} removed from {file}', [
91                'key' => $sub ? "$sub.$key" : $key,
92                'file' => $file,
93            ]);
94        }
95    }
96
97    /**
98     * @return array
99     */
100    public function findLanguageKeysInSettings()
101    {
102        if (file_exists('./conf/metadata.php')) {
103            return $this->metaExtract('./conf/metadata.php');
104        }
105        return [];
106    }
107
108    /**
109     * Find used language keys in the actual code
110     * @return array
111     */
112    public function findLanguageKeysInCode()
113    {
114        // get all non-hidden php and js files
115        $ite = new \RecursiveIteratorIterator(
116            new \RecursiveCallbackFilterIterator(
117                new \RecursiveDirectoryIterator('.', \RecursiveDirectoryIterator::SKIP_DOTS),
118                function ($file) {
119                    /** @var \SplFileInfo $file */
120                    if ($file->isFile() && $file->getExtension() != 'php' && $file->getExtension() != 'js') return false;
121                    return $file->getFilename()[0] !== '.';
122                }
123            )
124        );
125
126        $found = [];
127        foreach ($ite as $file) {
128            /** @var \SplFileInfo $file */
129            $path = str_replace('\\', '/', $file->getPathname());
130            if (substr($path, 0, 7) == './lang/') continue; // skip language files
131            if (substr($path, 0, 9) == './vendor/') continue; // skip vendor files
132
133            if ($file->getExtension() == 'php') {
134                $found = array_merge($found, $this->phpExtract($path));
135            } elseif ($file->getExtension() == 'js') {
136                if (!isset($found['js'])) $found['js'] = [];
137                $found['js'] = array_merge($found['js'], $this->jsExtract($path));
138            }
139        }
140
141        // admin menu entry
142        if (is_dir('admin')) {
143            $found['menu'] = 'admin/';
144        }
145        if (file_exists('admin.php')) {
146            $found['menu'] = 'admin.php';
147        }
148
149        return $found;
150    }
151
152    /**
153     * Extract language keys from given settings file
154     *
155     * @param string $file
156     * @return array
157     */
158    public function metaExtract($file)
159    {
160        $meta = [];
161        include $file;
162
163        $found = [];
164        foreach ($meta as $key => $info) {
165            $found[$key] = $file;
166
167            if (isset($info['_choices'])) {
168                foreach ($info['_choices'] as $choice) {
169                    $found[$key . '_o_' . $choice] = $file;
170                }
171            }
172        }
173
174        return $found;
175    }
176
177    /**
178     * Extract language keys from given javascript file
179     *
180     * @param string $file
181     * @return array (key => file:line)
182     */
183    public function jsExtract($file)
184    {
185        $sep = '[\[\]\.\'"]+'; // closes and opens brackets and dots - we don't care yet
186        $any = '[^\[\]\.\'"]+'; // stuff we don't care for
187        $close = '[\]\'"]*'; // closes brackets
188
189        $dotvalue = '\.(\w+)';
190        $bracketvalue = '\[[\'"](.*?)[\'"]\]';
191
192        // https://regex101.com/r/uTjHwc/1
193        $regex = '/\bLANG' . $sep . 'plugins' . $sep . $any . $close . '(?:' . $dotvalue . '|' . $bracketvalue . ')/';
194        // echo "\n\n$regex\n\n";
195
196        return $this->extract($file, $regex);
197    }
198
199    /**
200     * Extract language keys from given php file
201     *
202     * @param string $file
203     * @return array (key => file:line)
204     */
205    public function phpExtract($file)
206    {
207        $regex = '/(?:tpl_getLang|->getLang) ?\((.*?)\)/';
208        return $this->extract($file, $regex);
209    }
210
211    /**
212     * Use the given regex to extract language keys from the given file
213     *
214     * @param string $file
215     * @param string $regex
216     * @return array
217     */
218    private function extract($file, $regex)
219    {
220        $found = [];
221        $lines = file($file);
222        foreach ($lines as $lno => $line) {
223            if (!preg_match_all($regex, $line, $matches, PREG_SET_ORDER)) {
224                continue;
225            }
226            foreach ($matches as $match) {
227                $key = $match[2] ?? $match[1];
228                $key = trim($key, '\'"');
229                if (!isset($found[$key])) {
230                    $found[$key] = $file . ':' . ($lno + 1);
231                }
232
233            }
234        }
235
236        return $found;
237    }
238}
239