1<?php
2
3namespace dokuwiki\plugin\config\core;
4
5use dokuwiki\plugin\config\core\Setting\Setting;
6use dokuwiki\Logger;
7
8/**
9 * Writes the settings to the correct local file
10 */
11class Writer
12{
13    /** @var string header info */
14    protected $header = 'Dokuwiki\'s Main Configuration File - Local Settings';
15
16    /** @var string the file where the config will be saved to */
17    protected $savefile;
18
19    /**
20     * Writer constructor.
21     */
22    public function __construct()
23    {
24        global $config_cascade;
25        $this->savefile = end($config_cascade['main']['local']);
26    }
27
28    /**
29     * Save the given settings
30     *
31     * @param Setting[] $settings
32     * @throws \Exception
33     */
34    public function save($settings)
35    {
36        global $conf;
37        if ($this->isLocked()) throw new \Exception('no save');
38
39        // backup current file (remove any existing backup)
40        if (file_exists($this->savefile)) {
41            if (file_exists($this->savefile . '.bak.php')) @unlink($this->savefile . '.bak.php');
42            if (!io_rename($this->savefile, $this->savefile . '.bak.php')) throw new \Exception('no backup');
43        }
44
45        if (!$fh = @fopen($this->savefile, 'wb')) {
46            io_rename($this->savefile . '.bak.php', $this->savefile); // problem opening, restore the backup
47            throw new \Exception('no save');
48        }
49
50        $out = '';
51        foreach ($settings as $setting) {
52            if ($setting->shouldBeSaved()) {
53                $out .= $setting->out('conf', 'php');
54            }
55        }
56
57        if ($out === '') {
58            throw new \Exception('empty config');
59        }
60        $out = $this->getHeader() . $out;
61
62        fwrite($fh, $out);
63        fclose($fh);
64        if ($conf['fperm']) chmod($this->savefile, $conf['fperm']);
65        $this->opcacheUpdate($this->savefile);
66    }
67
68    /**
69     * Update last modified time stamp of the config file
70     *
71     * Will invalidate all DokuWiki caches
72     *
73     * @throws \Exception when the config isn't writable
74     */
75    public function touch()
76    {
77        if ($this->isLocked()) throw new \Exception('no save');
78        @touch($this->savefile);
79        $this->opcacheUpdate($this->savefile);
80    }
81
82    /**
83     * Invalidate the opcache of the given file (if possible)
84     *
85     * @todo this should probably be moved to core
86     * @param string $file
87     */
88    protected function opcacheUpdate($file)
89    {
90        if (!function_exists('opcache_invalidate')) return;
91        set_error_handler(function ($errNo, $errMsg) {
92            Logger::debug('Unable to invalidate opcache: ' . $errMsg);
93        });
94        opcache_invalidate($file);
95        restore_error_handler();
96    }
97
98    /**
99     * Configuration is considered locked if there is no local settings filename
100     * or the directory its in is not writable or the file exists and is not writable
101     *
102     * @return bool true: locked, false: writable
103     */
104    public function isLocked()
105    {
106        if (!$this->savefile) return true;
107        if (!is_writable(dirname($this->savefile))) return true;
108        if (file_exists($this->savefile) && !is_writable($this->savefile)) return true;
109        return false;
110    }
111
112    /**
113     * Returns the PHP intro header for the config file
114     *
115     * @return string
116     */
117    protected function getHeader()
118    {
119        return implode(
120            "\n",
121            [
122                '<?php',
123                '/*',
124                ' * ' . $this->header,
125                ' * Auto-generated by config plugin',
126                ' * Run for user: ' . ($_SERVER['REMOTE_USER'] ?? 'Unknown'),
127                ' * Date: ' . date('r'),
128                ' */',
129                '',
130                ''
131            ]
132        );
133    }
134}
135