Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.19% covered (success)
95.19%
99 / 104
77.78% covered (warning)
77.78%
7 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Writer
95.19% covered (success)
95.19%
99 / 104
77.78% covered (warning)
77.78%
7 / 9
49
0.00% covered (danger)
0.00%
0 / 1
 __construct
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 set
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
15
 clear
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 save
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
7.19
 buildContentValue
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 buildContentLine
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 buildContentArrayLines
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 buildContent
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
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
30namespace VuFind\Config;
31
32use function dirname;
33use function is_array;
34use function is_int;
35use 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 */
46class 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}