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