Passed
Push — develop ( 0715e6...fa87e0 )
by Nikolay
04:58
created

DockerEntrypoint::prepareDatabase()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 13
rs 9.9332
cc 2
nc 2
nop 0
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\Core\System;
21
22
use Error;
23
use JsonException;
24
use MikoPBX\Common\Models\LanInterfaces;
25
use MikoPBX\Common\Models\PbxSettingsConstants;
26
use Phalcon\Di;
27
use ReflectionClass;
28
29
require_once 'Globals.php';
30
31
/**
32
 * Defines the entry point for the MikoPBX system when deployed in a Docker environment.
33
 * This class is responsible for initializing the system, configuring environment settings,
34
 * preparing databases, and handling system startup and shutdown behaviors.
35
 */
36
class DockerEntrypoint extends Di\Injectable
37
{
38
    private const  PATH_DB = '/cf/conf/mikopbx.db';
39
    private const  pathInc = '/etc/inc/mikopbx-settings.json';
40
    public float $workerStartTime;
41
    private array $jsonSettings;
42
    private array $settings;
43
44
    /**
45
     * Constructor for the DockerEntrypoint class.
46
     * Registers the shutdown handler and enables asynchronous signal handling.
47
     */
48
    public function __construct()
49
    {
50
        pcntl_async_signals(true);
51
        register_shutdown_function([$this, 'shutdownHandler']);
52
53
    }
54
55
    /**
56
     * Handles the shutdown event for the Docker container.
57
     * Logs the time taken since the worker start and any last-minute errors.
58
     */
59
    public function shutdownHandler(): void
60
    {
61
        $e = error_get_last();
62
        $delta = round(microtime(true) - $this->workerStartTime, 2);
63
        if ($e === null) {
64
            SystemMessages::sysLogMsg(static::class, "shutdownHandler after $delta seconds", LOG_DEBUG);
65
        } else {
66
            $details = (string)print_r($e, true);
67
            SystemMessages::sysLogMsg(static::class, "shutdownHandler after $delta seconds with error: $details", LOG_DEBUG);
68
        }
69
    }
70
71
    /**
72
     * Initiates the startup sequence for the MikoPBX system.
73
     * Processes include system log initialization, database preparation, settings retrieval and application,
74
     * and triggering system startup routines.
75
     */
76
    public function start(): void
77
    {
78
        $this->workerStartTime = microtime(true);
79
        $syslogd = Util::which('syslogd');
80
        // Start the system log.
81
        Processes::mwExecBg($syslogd . ' -S -C512');
82
83
        // Update WWW user id and group id.
84
        $this->changeWwwUserID();
85
86
        // Prepare database
87
        $this->prepareDatabase();
88
89
        // Get default settings
90
        $this->getDefaultSettings();
91
92
        // Update DB values
93
        $this->applyEnvironmentSettings();
94
95
        // Start the MikoPBX system.
96
        $this->startTheMikoPBXSystem();
97
    }
98
99
    /**
100
     * Updates the system user 'www' with new user and group IDs if they are provided through environment variables
101
     * or from existing configuration files.
102
     */
103
    private function changeWwwUserID(): void
104
    {
105
        $newUserId = getenv('ID_WWW_USER');
106
        $newGroupId = getenv('ID_WWW_GROUP');
107
        SystemMessages::sysLogMsg(__METHOD__, ' - Check user id and group id for www', LOG_INFO);
108
        $pidIdPath = '/cf/conf/user.id';
109
        $pidGrPath = '/cf/conf/group.id';
110
111
        if (empty($newUserId) && file_exists($pidIdPath)) {
112
            $newUserId = file_get_contents($pidIdPath);
113
        }
114
        if (empty($newGroupId) && file_exists($pidGrPath)) {
115
            $newGroupId = file_get_contents($pidGrPath);
116
        }
117
118
        $commands = [];
119
        $userID = 'www';
120
        $grep = Util::which('grep');
121
        $find = Util::which('find');
122
        $sed = Util::which('sed');
123
        $cut = Util::which('cut');
124
        $chown = Util::which('chown');
125
        $chgrp = Util::which('chgrp');
126
        $currentUserId = trim(shell_exec("$grep '^$userID:' < /etc/shadow | $cut -d ':' -f 3"));
127
        if ($currentUserId!=='' && !empty($newUserId) && $currentUserId !== $newUserId) {
128
            SystemMessages::sysLogMsg(__METHOD__, " - Old $userID user id: $currentUserId; New $userID user id: $newUserId", LOG_DEBUG);
129
            $commands[] = "$sed -i 's/$userID:x:$currentUserId:/$userID:x:$newUserId:/g' /etc/shadow*";
130
            $id = '';
131
            if (file_exists($pidIdPath)) {
132
                $id = file_get_contents($pidIdPath);
133
            }
134
            if ($id !== $newUserId) {
135
                $commands[] = "$find / -not -path '/proc/*' -user $currentUserId -exec $chown -h $userID {} \;";
136
                file_put_contents($pidIdPath, $newUserId);
137
            }
138
        }
139
140
        $currentGroupId = trim(shell_exec("$grep '^$userID:' < /etc/group | $cut -d ':' -f 3"));
141
        if ($currentGroupId!=='' && !empty($newGroupId) && $currentGroupId !== $newGroupId) {
142
            SystemMessages::sysLogMsg(__METHOD__, " - Old $userID group id: $currentGroupId; New $userID group id: $newGroupId", LOG_DEBUG);
143
            $commands[] = "$sed -i 's/$userID:x:$currentGroupId:/$userID:x:$newGroupId:/g' /etc/group";
144
            $commands[] = "$sed -i 's/:$currentGroupId:Web/:$newGroupId:Web/g' /etc/shadow";
145
146
            $id = '';
147
            if (file_exists($pidGrPath)) {
148
                $id = file_get_contents($pidGrPath);
149
            }
150
            if ($id !== $newGroupId) {
151
                $commands[] = "$find / -not -path '/proc/*' -group $currentGroupId -exec $chgrp -h $newGroupId {} \;";
152
                file_put_contents($pidGrPath, $newGroupId);
153
            }
154
        }
155
        if (!empty($commands)) {
156
            passthru(implode('; ', $commands));
157
        }
158
    }
159
160
    /**
161
     * Prepares the SQLite database for use, checking for table existence and restoring from defaults if necessary.
162
     * @return array An array containing the results of the database check commands.
163
     */
164
    public function prepareDatabase(): array
165
    {
166
        $sqlite3 = Util::which('sqlite3');
167
        $rm = Util::which('rm');
168
        $cp = Util::which('cp');
169
        $out = [];
170
        Processes::mwExec("$sqlite3 " . self::PATH_DB . ' .tables', $out);
171
        if (trim(implode('', $out)) === '') {
172
            Util::mwMkdir(dirname(self::PATH_DB));
173
            Processes::mwExec("$rm -rf " . self::PATH_DB . "; $cp /conf.default/mikopbx.db " . self::PATH_DB);
174
            Util::addRegularWWWRights(self::PATH_DB);
175
        }
176
        return array($rm, $out);
177
    }
178
179
    /**
180
     * Retrieves default settings from JSON configuration and the database,
181
     * setting up initial configuration states required for system operations.
182
     */
183
    private function getDefaultSettings(): void
184
    {
185
        // Get settings from mikopbx-settings.json
186
        $jsonString = file_get_contents(self::pathInc);
187
        try {
188
            $this->jsonSettings = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR);
189
        } catch (JsonException $exception) {
190
            $this->jsonSettings = [];
191
            throw new Error(self::pathInc . " has broken format");
192
        }
193
194
        // Get settings from DB
195
        $out = [];
196
        $sqlite3 = Util::which('sqlite3');
197
        Processes::mwExec("$sqlite3 " . self::PATH_DB . " 'SELECT * FROM m_PbxSettings'", $out);
198
        $this->settings = [];
199
        foreach ($out as $row) {
200
            $data = explode('|', $row);
201
            $key = $data[0] ?? '';
202
            $value = $data[1] ?? '';
203
            $this->settings[$key] = $value;
204
        }
205
206
        // Add some extra information
207
        putenv("VIRTUAL_HARDWARE_TYPE=Docker");
208
    }
209
210
    /**
211
     * Applies configuration settings from environment variables to the system,
212
     * updating both database and JSON stored settings as necessary.
213
     */
214
    private function applyEnvironmentSettings(): void
215
    {
216
        $reflection = new ReflectionClass(PbxSettingsConstants::class);
217
        $constants = $reflection->getConstants();
218
219
        foreach ($constants as $name => $dbKey) {
220
            $envValue = getenv($name);
221
            if ($envValue !== false) {
222
                switch ($dbKey) {
223
                    case PbxSettingsConstants::BEANSTALK_PORT:
224
                    case PbxSettingsConstants::REDIS_PORT:
225
                    case PbxSettingsConstants::GNATS_PORT:
226
                        $this->updateJsonSettings($dbKey, 'port', intval($envValue));
227
                        break;
228
                    case PbxSettingsConstants::GNATS_HTTP_PORT:
229
                        $this->updateJsonSettings('gnats', 'httpPort', intval($envValue));
230
                        break;
231
                    case PbxSettingsConstants::ENABLE_USE_NAT:
232
                        if ($envValue==='1'){
233
                            $this->enableNat();
234
                        }
235
                        break;
236
                    default:
237
                        $this->updateDBSetting($dbKey, $envValue);
238
                        break;
239
                }
240
            }
241
        }
242
    }
243
244
    /**
245
     * Updates the topology of LAN interfaces designated as public (internet-facing) to private
246
     * by executing an SQLite update command directly via the shell.
247
     *
248
     * This method finds the path of the SQLite3 executable and constructs a command to update
249
     * the `topology` field of all entries in the `m_LanInterfaces` table where `internet` is '1'.
250
     * The new topology value is set from the `LanInterfaces::TOPOLOGY_PRIVATE` constant.
251
     *
252
     */
253
    private function enableNat(): void
254
    {
255
        $sqlite3 = Util::which('sqlite3');
256
        $dbPath =  self::PATH_DB;
257
        $out = [];
258
        $private = LanInterfaces::TOPOLOGY_PRIVATE;
259
        $command = "$sqlite3 $dbPath \"UPDATE m_LanInterfaces SET topology='$private' WHERE internet='1'\"";
260
        $res = Processes::mwExec($command, $out);
261
        if ($res === 0) {
262
            SystemMessages::sysLogMsg(__METHOD__, " - Update topology to '$private' in m_LanInterfaces", LOG_INFO);
263
        } else {
264
            SystemMessages::sysLogMsg(__METHOD__, " - Update topology failed: " . implode($out) . PHP_EOL . 'Command:' . PHP_EOL . $command, LOG_ERR);
265
        }
266
    }
267
    /**
268
     * Updates the specified setting in the JSON configuration file.
269
     * @param string $path The JSON path where the setting is stored.
270
     * @param string $key The setting key to update.
271
     * @param mixed $newValue The new value to set.
272
     */
273
    private function updateJsonSettings(string $path, string $key, $newValue): void
274
    {
275
        if ($this->jsonSettings[$path][$key] ?? null !== $newValue)
276
            $this->jsonSettings[$path][$key] = $newValue;
277
        $newData = json_encode($this->jsonSettings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
278
        file_put_contents(self::pathInc, $newData);
279
        SystemMessages::sysLogMsg(__METHOD__, " - Update $path:$key to '$newValue' in /etc/inc/mikopbx-settings.json", LOG_INFO);
280
    }
281
282
    /**
283
     * Updates a specified setting directly in the database.
284
     * @param string $key The key of the setting to update.
285
     * @param string $newValue The new value for the setting.
286
     */
287
    private function updateDBSetting(string $key, string $newValue): void
288
    {
289
        if (array_key_exists($key, $this->settings) && $this->settings[$key] !== $newValue) {
290
            $sqlite3 = Util::which('sqlite3');
291
            $dbPath =  self::PATH_DB;
292
            $out = [];
293
            $command = "$sqlite3 $dbPath \"UPDATE m_PbxSettings SET value='$newValue' WHERE key='$key'\"";
294
            $res = Processes::mwExec($command, $out);
295
            if ($res === 0) {
296
                SystemMessages::sysLogMsg(__METHOD__, " - Update $key to '$newValue' in m_PbxSettings", LOG_INFO);
297
            } else {
298
                SystemMessages::sysLogMsg(__METHOD__, " - Update $key failed: " . implode($out) . PHP_EOL . 'Command:' . PHP_EOL . $command, LOG_ERR);
299
            }
300
        }
301
    }
302
303
    /**
304
     * Executes the final commands to start the MikoPBX system, clearing temporary files and running system scripts.
305
     */
306
    public function startTheMikoPBXSystem(): void
307
    {
308
        $rm = Util::which('rm');
309
        shell_exec("$rm -rf /tmp/*");
310
        $commands = 'exec </dev/console >/dev/console 2>/dev/console;' .
311
            '/etc/rc/bootup 2>/dev/null && ' .
312
            '/etc/rc/bootup_pbx 2>/dev/null';
313
        passthru($commands);
314
    }
315
}
316
317
SystemMessages::sysLogMsg(DockerEntrypoint::class, ' - Start Docker entrypoint (php)', LOG_DEBUG);
318
$main = new DockerEntrypoint();
319
$main->start();