Passed
Push — develop ( ab926b...704199 )
by Nikolay
13:25 queued 08:47
created

BaseController::beforeExecuteRoute()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
c 0
b 0
f 0
dl 0
loc 22
rs 9.9
cc 4
nc 6
nop 1
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\AdminCabinet\Controllers;
21
22
use MikoPBX\Common\Providers\PBXConfModulesProvider;
23
use MikoPBX\Common\Providers\SentryErrorHandlerProvider;
24
use MikoPBX\Modules\Config\WebUIConfigInterface;
25
use MikoPBX\Common\Models\{PbxExtensionModules, PbxSettings, PbxSettingsConstants};
26
use Phalcon\Filter;
27
use Phalcon\Http\ResponseInterface;
28
use Phalcon\Mvc\{Controller, Dispatcher, View};
29
use Phalcon\Tag;
30
use Phalcon\Text;
31
32
/**
33
 * @property \Phalcon\Session\Manager session
34
 * @property \MikoPBX\Common\Providers\TranslationProvider translation
35
 * @property string language
36
 * @property bool showModuleStatusToggle if false it hides status toggle on current UI page
37
 * @property \MikoPBX\AdminCabinet\Library\Elements elements
38
 * @property \Phalcon\Flash\Session flash
39
 * @property \Phalcon\Tag tag
40
 * @property \Phalcon\Config\Adapter\Json config
41
 * @property \Phalcon\Logger loggerAuth
42
 */
43
class BaseController extends Controller
44
{
45
    protected string $actionName;
46
    protected string $controllerName;
47
    protected string $controllerClass;
48
    protected string $controllerNameUnCamelized;
49
    protected bool $isExternalModuleController;
50
51
    /**
52
     * Initializes base class
53
     */
54
    public function initialize(): void
55
    {
56
        $this->actionName = $this->dispatcher->getActionName();
57
        /** @scrutinizer ignore-call */
58
        $this->controllerClass = $this->dispatcher->getHandlerClass();
59
        $this->controllerName = Text::camelize($this->dispatcher->getControllerName(), '_');
60
        $this->controllerNameUnCamelized = Text::uncamelize($this->controllerName, '-');
61
        $this->isExternalModuleController = str_starts_with($this->dispatcher->getNamespaceName(), 'Modules');
62
63
        if ($this->request->isAjax() === false) {
64
            $this->prepareView();
65
        }
66
    }
67
68
    /**
69
     * Prepares the view by setting necessary variables and configurations.
70
     *
71
     * @return void
72
     */
73
    protected function prepareView(): void
74
    {
75
        // Set the default timezone based on PBX settings
76
        date_default_timezone_set(PbxSettings::getValueByKey(PbxSettingsConstants::PBX_TIMEZONE));
77
78
        // Set PBXLicense view variable if session exists
79
        if ($this->session->has(SessionController::SESSION_ID)) {
80
            $this->view->PBXLicense = PbxSettings::getValueByKey(PbxSettingsConstants::PBX_LICENSE);
81
        } else {
82
            $this->view->PBXLicense = '';
83
        }
84
85
        // Set URLs for Wiki and Support based on language
86
        $this->view->urlToWiki = "https://wiki.mikopbx.com/{$this->controllerNameUnCamelized}";
87
        if ($this->language === 'ru') {
88
            $this->view->urlToSupport = 'https://www.mikopbx.ru/support/?fromPBX=true';
89
        } else {
90
            $this->view->urlToSupport = 'https://www.mikopbx.com/support/?fromPBX=true';
91
        }
92
93
        // Set the title based on the current action
94
        $title = 'MikoPBX';
95
        switch ($this->actionName) {
96
            case'index':
97
            case'delete':
98
            case'save':
99
            case'modify':
100
            case'*** WITHOUT ACTION ***':
101
                $title .= '|' . $this->translation->_("Breadcrumb{$this->controllerName}");
102
                break;
103
            default:
104
                $title .= '|' . $this->translation->_("Breadcrumb{$this->controllerName}{$this->actionName}");
105
        }
106
        Tag::setTitle($title);
107
108
        // Set other view variables
109
        $this->view->t = $this->translation;
110
        $this->view->debugMode = $this->config->path('adminApplication.debugMode');
111
        $this->view->urlToLogo = $this->url->get('assets/img/logo-mikopbx.svg');
112
        $this->view->urlToController = $this->url->get($this->controllerNameUnCamelized);
113
        $this->view->represent = '';
114
        $this->view->WebAdminLanguage = $this->session->get(PbxSettingsConstants::WEB_ADMIN_LANGUAGE)??PbxSettings::getValueByKey(PbxSettingsConstants::WEB_ADMIN_LANGUAGE);
115
        $this->view->AvailableLanguages = json_encode(LanguageController::getAvailableWebAdminLanguages());
116
        $this->view->submitMode = $this->session->get('SubmitMode') ?? 'SaveSettings';
117
        $this->view->lastSentryEventId = $this->setLastSentryEventId();
118
        $this->view->PBXVersion = PbxSettings::getValueByKey(PbxSettingsConstants::PBX_VERSION);
119
        $this->view->MetaTegHeadDescription = $this->translation->_('MetaTegHeadDescription');
120
        $this->view->isExternalModuleController = $this->isExternalModuleController;
121
122
        if ($this->controllerClass!==SessionController::class){
123
            $this->view->setTemplateAfter('main');
124
        }
125
126
        $this->view->globalModuleUniqueId = '';
127
        $this->view->actionName = $this->actionName;
128
        $this->view->controllerName = $this->controllerName;
129
        $this->view->controllerClass = $this->controllerClass;
130
131
        // Add module variables into view if it is an external module controller
132
        if ($this->isExternalModuleController) {
133
            /** @var PbxExtensionModules $module */
134
            $module = PbxExtensionModules::findFirstByUniqid($this->getModuleUniqueId());
135
            if ($module === null) {
136
                $module = new PbxExtensionModules();
137
                $module->disabled = '1';
138
                $module->name = 'Unknown module';
139
            }
140
            $this->view->module = $module->toArray();
141
            $this->view->globalModuleUniqueId = $module->uniqid;
142
        }
143
    }
144
145
    /**
146
     * Performs actions before executing the route.
147
     *
148
     * @return void
149
     */
150
    public function beforeExecuteRoute(Dispatcher $dispatcher): void
151
    {
152
        // Check if the request method is POST
153
        if ($this->request->isPost()) {
154
            // Retrieve the 'submitMode' data from the request
155
            $data = $this->request->getPost('submitMode');
156
            if (!empty($data)) {
157
                // Set the 'SubmitMode' session variable to the retrieved data
158
                $this->session->set('SubmitMode', $data);
159
            }
160
        }
161
162
        $this->actionName = $this->dispatcher->getActionName();
163
        $this->controllerName = Text::camelize($this->dispatcher->getControllerName(), '_');
164
        // Add module variables into view if it is an external module controller
165
        if (str_starts_with($this->dispatcher->getNamespaceName(), 'Modules')) {
166
            $this->view->pick("Modules/{$this->getModuleUniqueId()}/{$this->controllerName}/{$this->actionName}");
167
        } else  {
168
            $this->view->pick("{$this->controllerName}/{$this->actionName}");
169
        }
170
171
        PBXConfModulesProvider::hookModulesMethod(WebUIConfigInterface::ON_BEFORE_EXECUTE_ROUTE,[$dispatcher]);
172
    }
173
174
    /**
175
     * Performs actions after executing the route and returns the response.
176
     *
177
     * @return \Phalcon\Http\ResponseInterface
178
     */
179
    public function afterExecuteRoute(Dispatcher $dispatcher): ResponseInterface
180
    {
181
182
        if ($this->request->isAjax() === true) {
183
            $this->view->setRenderLevel(View::LEVEL_NO_RENDER);
184
            $this->response->setContentType('application/json', 'UTF-8');
185
            $data = $this->view->getParamsToView();
186
187
            /* Set global params if is not set in controller/action */
188
            if (isset($data['raw_response'])) {
189
                $result = $data['raw_response'];
190
            } else {
191
                $data['success'] = $data['success'] ?? true;
192
                $data['reload'] = $data['reload'] ?? false;
193
                $data['message'] = $data['message'] ?? $this->flash->getMessages();
194
195
                // Let's add information about the last error to display a dialog window for the user.
196
                $sentry =  $this->di->get(SentryErrorHandlerProvider::SERVICE_NAME,['admin-cabinet']);
197
                if ($sentry){
198
                    $data['lastSentryEventId'] = $sentry->getLastEventId();
199
                }
200
201
                $result = json_encode($data);
202
            }
203
            $this->response->setContent($result);
204
        }
205
206
        PBXConfModulesProvider::hookModulesMethod(WebUIConfigInterface::ON_AFTER_EXECUTE_ROUTE,[$dispatcher]);
207
208
        return $this->response->send();
209
    }
210
211
    /**
212
     * Forwards the request to a different controller and action based on the provided URI.
213
     *
214
     * @param string $uri The URI to forward to.
215
     * @return void
216
     */
217
    protected function forward(string $uri): void
218
    {
219
        $uriParts = explode('/', $uri);
220
        if ($this->isExternalModuleController and count($uriParts)>2){
221
            $params = array_slice($uriParts, 3);
222
            $moduleUniqueID = $this->getModuleUniqueId();
223
            $this->dispatcher->forward(
224
                [
225
                    'namespace'=>"Modules\\{$moduleUniqueID}\\App\\Controllers",
226
                    'controller' => $uriParts[1],
227
                    'action' => $uriParts[2],
228
                    'params' => $params,
229
                ]
230
            );
231
        } else {
232
            $params = array_slice($uriParts, 2);
233
234
            $this->dispatcher->forward(
235
                [
236
                    'namespace'=>"MikoPBX\AdminCabinet\Controllers",
237
                    'controller' => $uriParts[0],
238
                    'action' => $uriParts[1],
239
                    'params' => $params,
240
                ]
241
            );
242
        }
243
    }
244
245
    /**
246
     * Sanitizes the caller ID by removing any characters that are not alphanumeric or spaces.
247
     *
248
     * @param string $callerId The caller ID to sanitize.
249
     * @return string The sanitized caller ID.
250
     */
251
    protected function sanitizeCallerId(string $callerId): string
252
    {
253
        return preg_replace('/[^a-zA-Zа-яА-Я0-9 ]/ui', '', $callerId);
254
    }
255
256
    /**
257
     * Sorts array by priority field
258
     *
259
     * @param $a
260
     * @param $b
261
     *
262
     * @return int|null
263
     */
264
    protected function sortArrayByPriority($a, $b): ?int
265
    {
266
        if (is_array($a)) {
267
            $a = (int)$a['priority'];
268
        } else {
269
            $a = (int)$a->priority;
270
        }
271
272
        if (is_array($b)) {
273
            $b = (int)$b['priority'];
274
        } else {
275
            $b = (int)$b->priority;
276
        }
277
278
        if ($a === $b) {
279
            return 0;
280
        } else {
281
            return ($a < $b) ? -1 : 1;
282
        }
283
    }
284
285
    /**
286
     * Sets the last Sentry event ID.
287
     *
288
     * @return \Sentry\EventId|null The last Sentry event ID, or null if metrics sending is disabled.
289
     */
290
    private function setLastSentryEventId(): ?\Sentry\EventId
291
    {
292
        $result = null;
293
        // Allow anonymous statistics collection for JS code
294
        $sentry =  $this->di->get(SentryErrorHandlerProvider::SERVICE_NAME);
295
        if ($sentry){
296
            $result = $sentry->getLastEventId();
297
        }
298
        return $result;
299
    }
300
301
    /**
302
     *  Returns the unique ID of the module parsing controller namespace;
303
     * @return string
304
     */
305
    private function getModuleUniqueId():string
306
    {
307
        // Split the namespace into an array using the backslash as a separator
308
        $parts = explode('\\', get_class($this));
309
310
        // Get the second part of the namespace
311
        return $parts[1];
312
    }
313
314
    /**
315
     * Save an entity and handle success or error messages.
316
     *
317
     * @param mixed $entity The entity to be saved.
318
     * @return bool True if the entity was successfully saved, false otherwise.
319
     */
320
    protected function saveEntity($entity, string $reloadPath=''): bool
321
    {
322
        $success = $entity->save();
323
324
        if (!$success) {
325
            $errors = $entity->getMessages();
326
            $this->flash->error(implode('<br>', $errors));
327
        } elseif (!$this->request->isAjax()) {
328
            $this->flash->success($this->translation->_('ms_SuccessfulSaved'));
329
            if ($reloadPath!==''){
330
                $this->forward($reloadPath);
331
            }
332
        }
333
334
        if ($this->request->isAjax()) {
335
            $this->view->success = $success;
336
            if ($reloadPath!=='' && $success){
337
                $this->view->reload = str_replace('{id}', $entity->id, $reloadPath);
338
            }
339
        }
340
341
        return $success;
342
    }
343
344
345
    /**
346
     * Delete an entity and handle success or error messages.
347
     *
348
     * @param mixed $entity The entity to be deleted.
349
     * @return bool True if the entity was successfully deleted, false otherwise.
350
     */
351
    protected function deleteEntity($entity, string $reloadPath=''): bool
352
    {
353
        $success = $entity->delete();
354
355
        if (!$success) {
356
            $errors = $entity->getMessages();
357
            $this->flash->error(implode('<br>', $errors));
358
        } elseif (!$this->request->isAjax()) {
359
            // $this->flash->success($this->translation->_('ms_SuccessfulSaved'));
360
            if ($reloadPath!==''){
361
                $this->forward($reloadPath);
362
            }
363
        }
364
365
        if ($this->request->isAjax()) {
366
            $this->view->success = $success;
367
            if ($reloadPath!=='' && $success){
368
                $this->view->reload = $reloadPath;
369
            }
370
        }
371
372
        return $success;
373
    }
374
375
    /**
376
     * Creates a JPEG file from the provided image.
377
     *
378
     * @param string $base64_string The base64 encoded image string.
379
     * @param string $output_file The output file path to save the JPEG file.
380
     *
381
     * @return void
382
     */
383
    protected function base64ToJpegFile(string $base64_string, string $output_file): void
384
    {
385
        // Open the output file for writing
386
        $ifp = fopen($output_file, 'wb');
387
388
        if ($ifp === false) {
389
            return;
390
        }
391
        // Split the string on commas
392
        // $data[0] == "data:image/png;base64"
393
        // $data[1] == <actual base64 string>
394
        $data = explode(',', $base64_string);
395
396
        if (count($data) > 1) {
397
            // Write the base64 decoded data to the file
398
            fwrite($ifp, base64_decode($data[1]));
399
400
            // Close the file resource
401
            fclose($ifp);
402
        }
403
    }
404
405
    /**
406
     * Recursively sanitizes input data based on the provided filter.
407
     *
408
     * @param array $data The data to be sanitized.
409
     * @param \Phalcon\Filter\FilterInterface $filter The filter object used for sanitization.
410
     *
411
     * @return array The sanitized data.
412
     */
413
    public static function sanitizeData(array $data, \Phalcon\Filter\FilterInterface $filter): array
414
    {
415
        foreach ($data as $key => $value) {
416
            if (is_array($value)) {
417
                // Recursively sanitize array values
418
                $data[$key] = self::sanitizeData($value, $filter);
419
            } elseif (is_string($value)) {
420
                // Check if the string starts with 'http'
421
                if (stripos($value, 'http') === 0) {
422
                    // If the string starts with 'http', sanitize it as a URL
423
                    $data[$key] = $filter->sanitize($value, FILTER::FILTER_URL);
424
                } else {
425
                    // Sanitize regular strings (trim and remove illegal characters)
426
                    $data[$key] = $filter->sanitize($value, [FILTER::FILTER_STRING, FILTER::FILTER_TRIM]);
427
                }
428
            } elseif (is_numeric($value)) {
429
                // Sanitize numeric values as integers
430
                $data[$key] = $filter->sanitize($value, FILTER::FILTER_INT);
431
            }
432
        }
433
434
        return $data;
435
    }
436
437
}
438