BaseController::deleteEntity()   B
last analyzed

Complexity

Conditions 7
Paths 12

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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