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