Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.19% |
99 / 104 |
|
77.78% |
7 / 9 |
CRAP | |
0.00% |
0 / 1 |
Writer | |
95.19% |
99 / 104 |
|
77.78% |
7 / 9 |
49 | |
0.00% |
0 / 1 |
__construct | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
set | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
15 | |||
clear | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getContent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
save | |
55.56% |
5 / 9 |
|
0.00% |
0 / 1 |
7.19 | |||
buildContentValue | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
buildContentLine | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
buildContentArrayLines | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
buildContent | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
9 |
1 | <?php |
2 | |
3 | /** |
4 | * VF Configuration Writer |
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 Config |
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\Config; |
31 | |
32 | use function dirname; |
33 | use function is_array; |
34 | use function is_int; |
35 | use function strlen; |
36 | |
37 | /** |
38 | * Class to update VuFind configuration settings |
39 | * |
40 | * @category VuFind |
41 | * @package Config |
42 | * @author Demian Katz <demian.katz@villanova.edu> |
43 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
44 | * @link https://vufind.org Main Site |
45 | */ |
46 | class Writer |
47 | { |
48 | /** |
49 | * Configuration file to write |
50 | * |
51 | * @var string |
52 | */ |
53 | protected $filename; |
54 | |
55 | /** |
56 | * Content of file |
57 | * |
58 | * @var string |
59 | */ |
60 | protected $content; |
61 | |
62 | /** |
63 | * Constructor |
64 | * |
65 | * @param string $filename Configuration file to write |
66 | * @param string|array|null $content Content to load into file (set to null to |
67 | * load contents of existing file specified by $filename; set to array to build |
68 | * string in combination with $comments; set to string to use raw config string) |
69 | * @param array $comments Comments to associate with content (ignored |
70 | * if $content is not an array). |
71 | * |
72 | * @throws \Exception |
73 | */ |
74 | public function __construct($filename, $content = null, $comments = []) |
75 | { |
76 | $this->filename = $filename; |
77 | if (null === $content) { |
78 | $this->content = file_get_contents($filename); |
79 | if (false === $this->content) { |
80 | throw new \Exception('Could not read ' . $filename); |
81 | } |
82 | } elseif (is_array($content)) { |
83 | $this->content = $this->buildContent($content, $comments); |
84 | } else { |
85 | $this->content = $content; |
86 | } |
87 | } |
88 | |
89 | /** |
90 | * Change/add a setting |
91 | * |
92 | * @param string $section Section to change/add |
93 | * @param string $setting Setting within section to change/add |
94 | * @param string $value Value to set (or null to unset) |
95 | * |
96 | * @return void |
97 | */ |
98 | public function set($section, $setting, $value) |
99 | { |
100 | // Break the configuration file into lines: |
101 | $lines = explode("\n", $this->content); |
102 | |
103 | // Reset some flags and prepare to rewrite the content: |
104 | $settingSet = false; |
105 | $currentSection = ''; |
106 | $this->content = ''; |
107 | |
108 | // Process one line at a time... |
109 | foreach ($lines as $line) { |
110 | // Separate comments from content: |
111 | $parts = explode(';', trim($line), 2); |
112 | $content = trim($parts[0]); |
113 | $comment = $parts[1] ?? ''; |
114 | |
115 | // Is this a section heading? |
116 | if (preg_match('/^\[(.+)\]$/', trim($content), $matches)) { |
117 | // If we just left the target section and didn't find the |
118 | // desired setting, we should write it to the end. |
119 | if ( |
120 | $currentSection == $section && !$settingSet |
121 | && $value !== null |
122 | ) { |
123 | $line = $this->buildContentLine($setting, $value, 0) |
124 | . "\n\n" . $line; |
125 | $settingSet = true; |
126 | } |
127 | $currentSection = $matches[1]; |
128 | } elseif (strstr($content, '=')) { |
129 | $contentParts = explode('=', $content, 2); |
130 | $key = trim($contentParts[0]); |
131 | // If the key we are trying to set is already present as an array, |
132 | // we need to clear out the multiple existing values before writing |
133 | // in a new one: |
134 | if ($key == $setting . '[]') { |
135 | continue; |
136 | } |
137 | // Standard case for match on section + key: |
138 | if ($currentSection == $section && $key == $setting) { |
139 | $settingSet = true; |
140 | if ($value === null) { |
141 | continue; |
142 | } else { |
143 | $line = $this->buildContentLine($setting, $value, 0); |
144 | } |
145 | if (!empty($comment)) { |
146 | $line .= ' ;' . $comment; |
147 | } |
148 | } |
149 | } |
150 | |
151 | // Save the current line: |
152 | $this->content .= $line . "\n"; |
153 | } |
154 | |
155 | // Did we loop through everything without finding a place to put the setting? |
156 | if (!$settingSet && $value !== null) { |
157 | // We never found the target section? |
158 | if ($currentSection != $section) { |
159 | $this->content .= '[' . $section . "]\n"; |
160 | } |
161 | $this->content .= $this->buildContentLine($setting, $value, 0) . "\n"; |
162 | } |
163 | } |
164 | |
165 | /** |
166 | * Remove a setting (convenience wrapper around set to null). |
167 | * |
168 | * @param string $section Section to change/add |
169 | * @param string $setting Setting within section to change/add |
170 | * |
171 | * @return void |
172 | */ |
173 | public function clear($section, $setting) |
174 | { |
175 | $this->set($section, $setting, null); |
176 | } |
177 | |
178 | /** |
179 | * Get the modified file's contents as a string. |
180 | * |
181 | * @return string |
182 | */ |
183 | public function getContent() |
184 | { |
185 | return $this->content; |
186 | } |
187 | |
188 | /** |
189 | * Save the modified file to disk. Return true on success, false on error. |
190 | * |
191 | * @return bool |
192 | */ |
193 | public function save() |
194 | { |
195 | // Create parent directory structure if necessary: |
196 | $stack = []; |
197 | $dirname = dirname($this->filename); |
198 | while (!empty($dirname) && !is_dir($dirname)) { |
199 | $stack[] = $dirname; |
200 | $dirname = dirname($dirname); |
201 | } |
202 | foreach (array_reverse($stack) as $dir) { |
203 | if (!mkdir($dir)) { |
204 | return false; |
205 | } |
206 | } |
207 | |
208 | // Write the file: |
209 | return file_put_contents($this->filename, $this->getContent()); |
210 | } |
211 | |
212 | /** |
213 | * Support method for buildContent -- format a value |
214 | * |
215 | * @param mixed $e Value to format |
216 | * |
217 | * @return string Value formatted for output to ini file. |
218 | */ |
219 | protected function buildContentValue($e) |
220 | { |
221 | if ($e === true) { |
222 | return 'true'; |
223 | } elseif ($e === false) { |
224 | return 'false'; |
225 | } elseif ($e == '') { |
226 | return ''; |
227 | } else { |
228 | return '"' . str_replace('"', '\"', $e) . '"'; |
229 | } |
230 | } |
231 | |
232 | /** |
233 | * Support method for buildContent -- format a line |
234 | * |
235 | * @param string $key Configuration key |
236 | * @param mixed $value Configuration value |
237 | * @param int $tab Tab size to help values line up |
238 | * |
239 | * @return string Formatted line |
240 | */ |
241 | protected function buildContentLine($key, $value, $tab = 17) |
242 | { |
243 | // Build a tab string so the equals signs line up attractively: |
244 | $tabStr = ''; |
245 | for ($i = strlen($key) + 1; $i < $tab; $i++) { |
246 | $tabStr .= ' '; |
247 | } |
248 | |
249 | // Special case: if value is an array, we need to adjust the key |
250 | // accordingly: |
251 | if (is_array($value)) { |
252 | $retVal = ''; |
253 | // TODO: replace $autoIndex code with array_is_list() check |
254 | // when supported (after PHP 8.1 is minimum required version). |
255 | $autoIndex = 0; |
256 | foreach ($value as $i => $current) { |
257 | // If the array indices are a numeric sequence starting at 0, |
258 | // omit them from the key names; any other index should be |
259 | // explicitly set: |
260 | $currentIndex = ($i === $autoIndex) ? '' : $i; |
261 | $retVal .= $key . '[' . $currentIndex . ']' . $tabStr . ' = ' |
262 | . $this->buildContentValue($current) . "\n"; |
263 | $autoIndex++; |
264 | } |
265 | return rtrim($retVal); |
266 | } |
267 | |
268 | // Standard case: value is not an array: |
269 | return $key . $tabStr . ' = ' . $this->buildContentValue($value); |
270 | } |
271 | |
272 | /** |
273 | * Support method for buildContent -- format an array into lines |
274 | * |
275 | * @param string $key Configuration key |
276 | * @param array $value Configuration value |
277 | * |
278 | * @return string Formatted line |
279 | */ |
280 | protected function buildContentArrayLines($key, $value) |
281 | { |
282 | $expectedKey = 0; |
283 | $content = ''; |
284 | foreach ($value as $key2 => $subValue) { |
285 | // We just want to use "[]" if this is a standard array with consecutive |
286 | // keys; however, if we have non-numeric keys or out-of-order keys, we |
287 | // want to retain those values as-is. |
288 | $subKey = (is_int($key2) && $key2 == $expectedKey) |
289 | ? '' |
290 | : (is_int($key2) ? $key2 : "'{$key2}'"); // quote string keys |
291 | $content .= $this->buildContentLine("{$key}[{$subKey}]", $subValue); |
292 | $content .= "\n"; |
293 | $expectedKey++; |
294 | } |
295 | return $content; |
296 | } |
297 | |
298 | /** |
299 | * Write an ini file, adapted from |
300 | * http://php.net/manual/function.parse-ini-file.php |
301 | * |
302 | * @param array $assoc_arr Array to output |
303 | * @param array $comments Comments to inject |
304 | * |
305 | * @return string |
306 | */ |
307 | protected function buildContent($assoc_arr, $comments) |
308 | { |
309 | $content = ''; |
310 | foreach ($assoc_arr as $key => $elem) { |
311 | if (isset($comments['sections'][$key]['before'])) { |
312 | $content .= $comments['sections'][$key]['before']; |
313 | } |
314 | $content .= '[' . $key . ']'; |
315 | if (!empty($comments['sections'][$key]['inline'])) { |
316 | $content .= "\t" . $comments['sections'][$key]['inline']; |
317 | } |
318 | $content .= "\n"; |
319 | foreach ($elem as $key2 => $elem2) { |
320 | if (isset($comments['sections'][$key]['settings'][$key2])) { |
321 | $settingComments |
322 | = $comments['sections'][$key]['settings'][$key2]; |
323 | $content .= $settingComments['before']; |
324 | } else { |
325 | $settingComments = []; |
326 | } |
327 | if (is_array($elem2)) { |
328 | $content .= $this->buildContentArrayLines($key2, $elem2); |
329 | } else { |
330 | $content .= $this->buildContentLine($key2, $elem2); |
331 | } |
332 | if (!empty($settingComments['inline'])) { |
333 | $content .= "\t" . $settingComments['inline']; |
334 | } |
335 | $content .= "\n"; |
336 | } |
337 | } |
338 | if (isset($comments['after'])) { |
339 | $content .= $comments['after']; |
340 | } |
341 | return $content; |
342 | } |
343 | } |