Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
60 / 60 |
|
100.00% |
9 / 9 |
CRAP | |
100.00% |
1 / 1 |
ExtendedIniNormalizer | |
100.00% |
60 / 60 |
|
100.00% |
9 / 9 |
31 | |
100.00% |
1 / 1 |
normalizeDirectory | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
normalizeFile | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
normalizeFileToString | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
loadFileIntoArray | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
normalizeArray | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
formatAsString | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
7 | |||
extractComments | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
filenameMatchesFilter | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
checkFileFormat | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
7 |
1 | <?php |
2 | |
3 | /** |
4 | * Class to consistently format ExtendedIni language files. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Translator |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org Main Site |
28 | */ |
29 | |
30 | namespace VuFind\I18n; |
31 | |
32 | use Laminas\I18n\Translator\TextDomain; |
33 | |
34 | use function in_array; |
35 | |
36 | /** |
37 | * Class to consistently format ExtendedIni language files. |
38 | * |
39 | * @category VuFind |
40 | * @package Translator |
41 | * @author Demian Katz <demian.katz@villanova.edu> |
42 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
43 | * @link https://vufind.org Main Site |
44 | */ |
45 | class ExtendedIniNormalizer |
46 | { |
47 | use \VuFind\I18n\Translator\TranslatorAwareTrait; |
48 | |
49 | /** |
50 | * Reserved words that need to be quoted when used as keys. |
51 | * |
52 | * @var string[] |
53 | */ |
54 | protected $reservedWords = ['yes']; |
55 | |
56 | /** |
57 | * Normalize a directory on disk. |
58 | * |
59 | * @param string $dir Directory to normalize. |
60 | * @param string $filter File name filter. |
61 | * |
62 | * @return void |
63 | */ |
64 | public function normalizeDirectory($dir, $filter) |
65 | { |
66 | $dir = rtrim($dir, '/'); |
67 | $handle = opendir($dir); |
68 | while ($file = readdir($handle)) { |
69 | $full = $dir . '/' . $file; |
70 | if ($file != '.' && $file != '..' && is_dir($full)) { |
71 | $this->normalizeDirectory($full, $filter); |
72 | } elseif ($this->filenameMatchesFilter($file, $filter)) { |
73 | $this->normalizeFile($full); |
74 | } |
75 | } |
76 | closedir($handle); |
77 | } |
78 | |
79 | /** |
80 | * Normalize a file on disk. |
81 | * |
82 | * @param string $file Filename. |
83 | * |
84 | * @return void |
85 | */ |
86 | public function normalizeFile($file) |
87 | { |
88 | file_put_contents($file, $this->normalizeFileToString($file)); |
89 | } |
90 | |
91 | /** |
92 | * Normalize a file from disk and returns the result as a string. |
93 | * |
94 | * @param string $file Filename. |
95 | * |
96 | * @return string |
97 | */ |
98 | public function normalizeFileToString($file) |
99 | { |
100 | // Safeguard to avoid messing up wrong ini files: |
101 | $fileArray = $this->loadFileIntoArray($file); |
102 | $this->checkFileFormat($fileArray, $file); |
103 | return $this->normalizeArray($fileArray); |
104 | } |
105 | |
106 | /** |
107 | * Load a language file into an array of lines, stripping UTF-8 BOM if necessary. |
108 | * |
109 | * @param string $filename File to load |
110 | * |
111 | * @return array |
112 | */ |
113 | public function loadFileIntoArray(string $filename): array |
114 | { |
115 | $fileArray = file($filename); |
116 | |
117 | // Strip off UTF-8 BOM if necessary. |
118 | if ($fileArray) { |
119 | $bom = html_entity_decode('', ENT_NOQUOTES, 'UTF-8'); |
120 | $fileArray[0] = str_replace($bom, '', $fileArray[0]); |
121 | } |
122 | |
123 | return $fileArray; |
124 | } |
125 | |
126 | /** |
127 | * Normalize an array of lines from a file and return the result as a string. |
128 | * |
129 | * @param string[] $fileArray Array of lines to normalize |
130 | * |
131 | * @return string |
132 | */ |
133 | public function normalizeArray(array $fileArray): string |
134 | { |
135 | // Reading and rewriting the file by itself will eliminate all comments; |
136 | // we should extract comments separately and then recombine the parts. |
137 | $comments = $this->extractComments($fileArray); |
138 | $reader = new Translator\Loader\ExtendedIniReader(); |
139 | $strings = $this->formatAsString($reader->getTextDomain($fileArray, false)); |
140 | return $comments . $strings; |
141 | } |
142 | |
143 | /** |
144 | * Normalize a TextDomain or array to a string that can be written to file. |
145 | * |
146 | * @param array|TextDomain $rawInput Language values to format. |
147 | * |
148 | * @return string |
149 | */ |
150 | public function formatAsString($rawInput) |
151 | { |
152 | // Sanitize keys before sorting: |
153 | $input = []; |
154 | foreach ($rawInput as $key => $value) { |
155 | $input[$this->sanitizeTranslationKey($key)] = $value; |
156 | } |
157 | |
158 | // Perform a case-insensitive sort: |
159 | $sortCallback = function ($a, $b) { |
160 | // We need absolutely consistent sorting; a pure case-insensitive |
161 | // sort will randomly reorder strings that evaluate to the same |
162 | // thing (e.g. "by" vs. "By"). In our custom sort function, we'll |
163 | // do a case-sensitive sort on otherwise identical strings to |
164 | // ensure 100% consistent behavior. |
165 | $lowerA = strtolower($a); |
166 | $lowerB = strtolower($b); |
167 | if ($lowerA === $lowerB) { |
168 | return strcmp($a, $b); |
169 | } |
170 | return strcmp($lowerA, $lowerB); |
171 | }; |
172 | uksort($input, $sortCallback); |
173 | |
174 | // Format the lines: |
175 | $output = ''; |
176 | foreach ($input as $key => $value) { |
177 | // Put purely numeric keys in single quotes for Lokalise compatibility: |
178 | $normalizedKey = is_numeric($key) || in_array($key, $this->reservedWords) |
179 | ? "'$key'" : $key; |
180 | // Choose most appropriate type of outer quotes to reduce need for escaping: |
181 | $quote = str_contains($value, '"') ? "'" : '"'; |
182 | // Apply minimal escaping (to existing slashes and quotes matching the outer ones): |
183 | $escapedValue = str_replace(['\\', $quote], ['\\\\', '\\' . $quote], $value); |
184 | // Put it all together! |
185 | $output .= "$normalizedKey = $quote$escapedValue$quote\n"; |
186 | } |
187 | return trim($output) . "\n"; |
188 | } |
189 | |
190 | /** |
191 | * Extract comments from an array of lines read from a file. |
192 | * |
193 | * @param array $contents Contents to scan for comments. |
194 | * |
195 | * @return string |
196 | */ |
197 | public function extractComments($contents) |
198 | { |
199 | $comments = ''; |
200 | foreach ($contents as $line) { |
201 | if (str_starts_with(trim($line), ';')) { |
202 | $comments .= $line; |
203 | } |
204 | } |
205 | return $comments; |
206 | } |
207 | |
208 | /** |
209 | * Check if the given filename matches the filter pattern |
210 | * |
211 | * @param string $filename Filename |
212 | * @param string $filter Filter |
213 | * |
214 | * @return bool |
215 | */ |
216 | protected function filenameMatchesFilter(string $filename, string $filter): bool |
217 | { |
218 | foreach (explode('|', $filter) as $pattern) { |
219 | if (fnmatch($pattern, $filename)) { |
220 | return true; |
221 | } |
222 | } |
223 | return false; |
224 | } |
225 | |
226 | /** |
227 | * Check that the file to process is a valid language file. |
228 | * |
229 | * Throws an exception if unexpected content is detected. |
230 | * |
231 | * @param array $lines File contents |
232 | * @param string $filename Filename |
233 | * |
234 | * @return void |
235 | * @throws \Exception |
236 | */ |
237 | protected function checkFileFormat(array $lines, string $filename): void |
238 | { |
239 | $lineNum = 0; |
240 | foreach ($lines as $line) { |
241 | ++$lineNum; |
242 | $line = trim($line); |
243 | if ('' === $line || strncmp($line, ';', 1) === 0) { |
244 | continue; |
245 | } |
246 | if (str_starts_with($line, '[') && str_ends_with($line, ']')) { |
247 | throw new \Exception( |
248 | "Cannot normalize a file with sections; $filename line $lineNum" |
249 | . " contains: $line" |
250 | ); |
251 | } |
252 | if (strstr($line, '=') === false) { |
253 | throw new \Exception( |
254 | "Equals sign not found in $filename line $lineNum: $line" |
255 | ); |
256 | } |
257 | } |
258 | } |
259 | } |