Passed
Push — develop ( a4a90f...fc5c50 )
by Nikolay
05:38
created

ModelsBase::initialize()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
c 0
b 0
f 0
dl 0
loc 25
rs 9.7333
cc 3
nc 1
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\Common\Models;
21
22
use MikoPBX\AdminCabinet\Controllers\AsteriskManagersController;
23
use MikoPBX\AdminCabinet\Controllers\BaseController;
24
use MikoPBX\AdminCabinet\Controllers\CallQueuesController;
25
use MikoPBX\AdminCabinet\Controllers\ConferenceRoomsController;
26
use MikoPBX\AdminCabinet\Controllers\CustomFilesController;
27
use MikoPBX\AdminCabinet\Controllers\DialplanApplicationsController;
28
use MikoPBX\AdminCabinet\Controllers\ExtensionsController;
29
use MikoPBX\AdminCabinet\Controllers\Fail2BanController;
30
use MikoPBX\AdminCabinet\Controllers\FirewallController;
31
use MikoPBX\AdminCabinet\Controllers\GeneralSettingsController;
32
use MikoPBX\AdminCabinet\Controllers\IncomingRoutesController;
33
use MikoPBX\AdminCabinet\Controllers\IvrMenuController;
34
use MikoPBX\AdminCabinet\Controllers\NetworkController;
35
use MikoPBX\AdminCabinet\Controllers\OutboundRoutesController;
36
use MikoPBX\AdminCabinet\Controllers\OutOffWorkTimeController;
37
use MikoPBX\AdminCabinet\Controllers\ProvidersController;
38
use MikoPBX\AdminCabinet\Controllers\SoundFilesController;
39
use MikoPBX\AdminCabinet\Providers\SecurityPluginProvider;
40
use MikoPBX\Common\Providers\BeanstalkConnectionModelsProvider;
41
use MikoPBX\Common\Providers\CDRDatabaseProvider;
42
use MikoPBX\Common\Providers\ManagedCacheProvider;
43
use MikoPBX\Common\Providers\ModelsCacheProvider;
44
use MikoPBX\Common\Providers\ModelsMetadataProvider;
45
use MikoPBX\Common\Providers\TranslationProvider;
46
use MikoPBX\Modules\PbxExtensionUtils;
47
use Phalcon\Db\Adapter\AdapterInterface;
48
use Phalcon\Di;
49
use Phalcon\Events\Event;
50
use Phalcon\Events\Manager;
51
use Phalcon\Messages\Message;
52
use Phalcon\Messages\MessageInterface;
53
use Phalcon\Mvc\Model;
54
use Phalcon\Mvc\Model\Relation;
55
use Phalcon\Mvc\Model\Resultset;
56
use Phalcon\Mvc\Model\Resultset\Simple;
57
use Phalcon\Mvc\Model\ResultsetInterface;
58
use Phalcon\Text;
59
use Phalcon\Url;
60
61
/**
62
 * Class ModelsBase
63
 *
64
 * @method static mixed findFirstById(array|string|int $parameters = null)
65
 * @method static mixed findFirstByKey(string|null $parameters)
66
 * @method static mixed findFirstByUniqid(array|string|int $parameters = null)
67
 * @method static mixed findFirst(array|string|int $parameters = null)
68
 * @method static ResultsetInterface find(array|string|int $parameters = null)
69
 * @method static mixed count(array $parameters = null)
70
 * @method  bool create()
71
 * @method  bool delete()
72
 * @method  bool save()
73
 * @method  bool update()
74
 * @method  array|MessageInterface[] getMessages(mixed $filter = null)
75
 * @method static AdapterInterface getReadConnection()
76
 * @method  Simple|false getRelated(string $alias, $arguments = null)
77
 *
78
 * @property Model\Manager _modelsManager
79
 * @property Di di
80
 *
81
 * @package MikoPBX\Common\Models
82
 */
83
class ModelsBase extends Model
84
{
85
    /**
86
     * All models with lover than this version in module.json won't be attached as children
87
     * We use this constant to disable old modules that may not be compatible with the current version of MikoPBX
88
     */
89
    public const MIN_MODULE_MODEL_VER = '2020.2.468';
90
91
    /**
92
     * Returns Cache key for the models cache service
93
     *
94
     * @param string $modelClass
95
     * @param string $keyName
96
     *
97
     * @return string
98
     */
99
    public static function makeCacheKey(string $modelClass, string $keyName): string
100
    {
101
        $category = explode('\\', $modelClass)[3];
102
        return "{$category}:{$keyName}";
103
    }
104
105
    /**
106
     * Initialize the model.
107
     */
108
    public function initialize(): void
109
    {
110
        self::setup(['orm.events' => true]);
111
        $this->keepSnapshots(true);
112
        $this->addExtensionModulesRelations();
113
114
        $eventsManager = new Manager();
115
116
        $eventsManager->attach(
117
            'model',
118
            function (Event $event, $record) {
119
                $type = $event->getType();
120
                switch ($type) {
121
                    case 'afterSave':
122
                    case 'afterDelete':
123
                        $record->processSettingsChanges($type);
124
                        self::clearCache(get_class($record));
125
                        break;
126
                    default:
127
128
                }
129
            }
130
        );
131
132
        $this->setEventsManager($eventsManager);
133
134
    }
135
136
    /**
137
     * Attaches model's relationships from modules models classes
138
     */
139
    private function addExtensionModulesRelations()
140
    {
141
        $modules = PbxExtensionModules::getEnabledModulesArray();
142
        foreach ($modules as $module) {
143
            $moduleDir = PbxExtensionUtils::getModuleDir($module['uniqid']);
144
145
            $moduleJson = "{$moduleDir}/module.json";
146
            if (!file_exists($moduleJson)) {
147
                continue;
148
            }
149
            $jsonString = file_get_contents($moduleJson);
150
            $jsonModuleDescription = json_decode($jsonString, true);
151
            $minPBXVersion = $jsonModuleDescription['min_pbx_version'] ?? '1.0.0';
152
            if (version_compare($minPBXVersion, self::MIN_MODULE_MODEL_VER, '<')) {
153
                continue;
154
            }
155
156
            $moduleModelsDir = "{$moduleDir}/Models";
157
            $results = glob($moduleModelsDir . '/*.php', GLOB_NOSORT);
158
            foreach ($results as $file) {
159
                $className = pathinfo($file)['filename'];
160
                $moduleModelClass = "Modules\\{$module['uniqid']}\\Models\\{$className}";
161
162
                if (class_exists($moduleModelClass)
163
                    && method_exists($moduleModelClass, 'getDynamicRelations')) {
164
                    $moduleModelClass::getDynamicRelations($this);
165
                }
166
            }
167
        }
168
    }
169
170
    /**
171
     * Sends changed fields and settings to backend worker WorkerModelsEvents
172
     *
173
     * @param $action string may be afterSave or afterDelete
174
     */
175
    private function processSettingsChanges(string $action): void
176
    {
177
        $doNotTrackThisDB = [
178
            CDRDatabaseProvider::SERVICE_NAME,
179
        ];
180
181
        if (in_array($this->getReadConnectionService(), $doNotTrackThisDB)) {
182
            return;
183
        }
184
185
        if (!$this->hasSnapshotData()) {
186
            return;
187
        } // nothing changed
188
189
        $changedFields = $this->getUpdatedFields();
190
        if (empty($changedFields) && $action === 'afterSave') {
191
            return;
192
        }
193
        $this->sendChangesToBackend($action, $changedFields);
194
    }
195
196
    /**
197
     * Sends changed fields and class to WorkerModelsEvents
198
     *
199
     * @param $action
200
     * @param $changedFields
201
     */
202
    private function sendChangesToBackend($action, $changedFields): void
203
    {
204
        // Add changed fields set to Beanstalkd queue
205
        $queue = $this->di->getShared(BeanstalkConnectionModelsProvider::SERVICE_NAME);
206
        if ($queue === null) {
207
            return;
208
        }
209
        if ($this instanceof PbxSettings) {
210
            $idProperty = 'key';
211
        } else {
212
            $idProperty = 'id';
213
        }
214
        $id = $this->$idProperty;
215
        $jobData = json_encode(
216
            [
217
                'source' => BeanstalkConnectionModelsProvider::SOURCE_MODELS_CHANGED,
218
                'model' => get_class($this),
219
                'recordId' => $id,
220
                'action' => $action,
221
                'changedFields' => $changedFields,
222
            ]
223
        );
224
        $queue->publish($jobData);
225
    }
226
227
    /**
228
     * Invalidates cached records contains model name in cache key value
229
     *
230
     * @param      $calledClass string full model class name
231
     *
232
     */
233
    public static function clearCache(string $calledClass): void
234
    {
235
        $di = Di::getDefault();
236
        if ($di === null) {
237
            return;
238
        }
239
240
        if ($di->has(ManagedCacheProvider::SERVICE_NAME)) {
241
            $managedCache = $di->get(ManagedCacheProvider::SERVICE_NAME);
242
            $category = explode('\\', $calledClass)[3];
243
            $keys = $managedCache->getKeys($category);
244
            $prefix = $managedCache->getPrefix();
245
            // Delete all items from the managed cache
246
            foreach ($keys as $key) {
247
                $cacheKey = str_ireplace($prefix, '', $key);
248
                $managedCache->delete($cacheKey);
249
            }
250
        }
251
        if ($di->has(ModelsCacheProvider::SERVICE_NAME)) {
252
            $modelsCache = $di->getShared(ModelsCacheProvider::SERVICE_NAME);
253
            $category = explode('\\', $calledClass)[3];
254
            $keys = $modelsCache->getKeys($category);
255
            $prefix = $modelsCache->getPrefix();
256
            // Delete all items from the models cache
257
            foreach ($keys as $key) {
258
                $cacheKey = str_ireplace($prefix, '', $key);
259
                $modelsCache->delete($cacheKey);
260
            }
261
        }
262
263
    }
264
265
    /**
266
     * Error handler for validation failures.
267
     * This function is called when a model fails to save or delete correctly,
268
     * or when the dependencies between models are not properly configured.
269
     * It generates a list of links to the objects that we are trying to delete.
270
     */
271
    public function onValidationFails(): void
272
    {
273
        $errorMessages = $this->getMessages();
274
        foreach ($errorMessages as $errorMessage) {
275
            if ($errorMessage->getType() === 'ConstraintViolation') {
276
                // Extract the related model name from the error message
277
                $arrMessageParts = explode('Common\\Models\\', $errorMessage->getMessage());
278
                if (count($arrMessageParts) === 2) {
279
                    $relatedModel = $arrMessageParts[1];
280
                } else {
281
                    $relatedModel = $errorMessage->getMessage();
282
                }
283
284
                // Get the related records
285
                $relatedRecords = $this->getRelated($relatedModel);
286
287
                // Create a new error message template
288
                $newErrorMessage = '<div class="ui header">'.$this->t('ConstraintViolation').'</div>';
289
                $newErrorMessage .= "<ul class='list'>";
290
                if ($relatedRecords === false) {
291
                    // Throw an exception if there is an error in the model relationship
292
                    throw new Model\Exception('Error on models relationship ' . $errorMessage);
293
                }
294
                if ($relatedRecords instanceof Resultset) {
295
                    // If there are multiple related records, iterate through them
296
                    foreach ($relatedRecords as $item) {
297
                        if ($item instanceof ModelsBase) {
298
                            // Append each related record's representation to the error message
299
                            $newErrorMessage .= '<li>' . $item->getRepresent(true) . '</li>';
300
                        }
301
                    }
302
                } elseif ($relatedRecords instanceof ModelsBase) {
303
                    // If there is a single related record, append its representation to the error message
304
                    $newErrorMessage .= '<li>' . $relatedRecords->getRepresent(true) . '</li>';
305
                } else {
306
                    // If the related records are of an unknown type, indicate it in the error message
307
                    $newErrorMessage .= '<li>Unknown object</li>';
308
                }
309
                $newErrorMessage .= '</ul>';
310
311
                // Set the new error message
312
                $errorMessage->setMessage($newErrorMessage);
313
                break;
314
            }
315
        }
316
    }
317
318
    /**
319
     * Function to access the translation array from models.
320
     * It is used for messages in a user-friendly language.
321
     *
322
     * @param $message
323
     * @param array $parameters
324
     *
325
     * @return mixed
326
     */
327
    public function t($message, $parameters = [])
328
    {
329
        return $this->getDI()->getShared(TranslationProvider::SERVICE_NAME)->t($message, $parameters);
330
    }
331
332
    /**
333
     * Returns a model's element representation
334
     *
335
     * @param bool $needLink add link to element
336
     *
337
     * @return string
338
     */
339
    public function getRepresent(bool $needLink = false): string
340
    {
341
        switch (static::class) {
342
            case AsteriskManagerUsers::class:
343
                $name = '<i class="asterisk icon"></i> ';
344
                $name .= empty($this->id)
345
                    ? $this->t('mo_NewElementAsteriskManagerUsers')
346
                    : $this->t('repAsteriskManagerUsers', ['represent' => $this->username]);
347
                break;
348
            case CallQueueMembers::class:
349
                $name = $this->Extensions->getRepresent();
350
                break;
351
            case CallQueues::class:
352
                $name = '<i class="users icon"></i> ';
353
                $name .= empty($this->id)
354
                    ? $this->t('mo_NewElementCallQueues')
355
                    : $this->t('mo_CallQueueShort4Dropdown') . ': ' . $this->name;
356
                break;
357
            case ConferenceRooms::class:
358
                $name = '<i class="phone volume icon"></i> ';
359
                $name .= empty($this->id)
360
                    ? $this->t('mo_NewElementConferenceRooms')
361
                    : $this->t('mo_ConferenceRoomsShort4Dropdown') . ': ' . $this->name;
362
                break;
363
            case CustomFiles::class:
364
                $name = "<i class='file icon'></i> {$this->filepath}";
365
                break;
366
            case DialplanApplications::class:
367
                $name = '<i class="php icon"></i> ';
368
                $name .= empty($this->id)
369
                    ? $this->t('mo_NewElementDialplanApplications')
370
                    : $this->t('mo_ApplicationShort4Dropdown') . ': ' . $this->name;
371
                break;
372
                break;
373
            case ExtensionForwardingRights::class:
374
                $name = $this->Extensions->getRepresent();
375
                break;
376
            case Extensions::class:
377
                if ($this->type === Extensions::TYPE_EXTERNAL) {
378
                    $icon = '<i class="icons"><i class="user outline icon"></i><i class="top right corner alternate mobile icon"></i></i>';
379
                } else {
380
                    $icon = '<i class="icons"><i class="user outline icon"></i></i>';
381
                }
382
                if (empty($this->id)) {
383
                    $name = "{$icon} {$this->t('mo_NewElementExtensions')}";
384
                } elseif ($this->userid > 0) {
385
                    $name = '';
386
                    if (isset($this->Users->username)) {
387
                        $name = $this->trimName($this->Users->username);
388
                    }
389
                    $name = "{$icon} {$name} <{$this->number}>";
390
                } else {
391
                    switch (strtoupper($this->type)) {
392
                        case Extensions::TYPE_CONFERENCE:
393
                            $name = $this->ConferenceRooms->getRepresent();
394
                            break;
395
                        case Extensions::TYPE_QUEUE:
396
                            $name = $this->CallQueues->getRepresent();
397
                            break;
398
                        case Extensions::TYPE_DIALPLAN_APPLICATION:
399
                            $name = $this->DialplanApplications->getRepresent();
400
                            break;
401
                        case Extensions::TYPE_IVR_MENU:
402
                            $name = $this->IvrMenu->getRepresent();
403
                            break;
404
                        case Extensions::TYPE_MODULES:
405
                            $name = '<i class="puzzle piece icon"></i> '
406
                                . $this->t('mo_ModuleShort4Dropdown')
407
                                . ': '
408
                                . $this->callerid;
409
                            break;
410
                        case Extensions::TYPE_SYSTEM:
411
                            $name = '<i class="cogs icon"></i> '
412
                                . $this->t('mo_SystemExten_' . $this->number);
413
                            break;
414
                        case Extensions::TYPE_EXTERNAL:
415
                        case Extensions::TYPE_SIP:
416
                        default:
417
                            $name = "{$this->callerid} <{$this->number}>";
418
                    }
419
                }
420
                break;
421
            case ExternalPhones::class:
422
                $name = $this->Extensions->getRepresent();
423
                break;
424
            case Fail2BanRules::class:
425
                $name = '';
426
                break;
427
            case FirewallRules::class:
428
                $name = $this->category;
429
                break;
430
            case Iax::class:
431
                $name = '<i class="server icon"></i> ';
432
                if (empty($this->id)) {
433
                    $name .= $this->t('mo_NewElementIax');
434
                } elseif ($this->disabled === '1') {
435
                    $name .= "{$this->description} ({$this->t( 'mo_Disabled' )})";
436
                } else {
437
                    $name .= $this->description;
438
                }
439
                break;
440
            case IvrMenu::class:
441
                $name = '<i class="sitemap icon"></i> ';
442
                $name .= empty($this->id)
443
                    ? $this->t('mo_NewElementIvrMenu')
444
                    : $this->t('mo_IVRMenuShort4Dropdown') . ': ' . $this->name;
445
                break;
446
            case IvrMenuActions::class:
447
                $name = $this->IvrMenu->name;
448
                break;
449
            case Codecs::class:
450
                $name = $this->name;
451
                break;
452
            case IncomingRoutingTable::class:
453
                $name = '<i class="map signs icon"></i> ';
454
                if (empty($this->id)) {
455
                    $name .= $this->t('mo_NewElementIncomingRoutingTable');
456
                } elseif (!empty($this->rulename)) {
457
                    $name .= $this->t('repIncomingRoutingTable', ['represent' => $this->rulename]);
458
                } else {
459
                    $name .= $this->t('repIncomingRoutingTableNumber', ['represent' => $this->id]);
460
                }
461
                break;
462
            case LanInterfaces::class:
463
                $name = $this->name;
464
                break;
465
            case NetworkFilters::class:
466
                $name = '<i class="globe icon"></i> ';
467
                if (empty($this->id)) {
468
                    $name .= $this->t('mo_NewElementNetworkFilters');
469
                } else {
470
                    $name .= $this->description . '('
471
                        . $this->t('fw_PermitNetwork') . ': ' . $this->permit
472
                        . ')';
473
                }
474
                break;
475
            case OutgoingRoutingTable::class:
476
                $name = '<i class="random icon"></i> ';
477
                if (empty($this->id)) {
478
                    $name .= $this->t('mo_NewElementOutgoingRoutingTable');
479
                } elseif (!empty($this->rulename)) {
480
                    $name .= $this->t('repOutgoingRoutingTable', ['represent' => $this->rulename]);
481
                } else {
482
                    $name .= $this->t('repOutgoingRoutingTableNumber', ['represent' => $this->id]);
483
                }
484
                break;
485
            case OutWorkTimes::class:
486
                $name = '<i class="time icon"></i> ';
487
                if (empty($this->id)) {
488
                    $name .= $this->t('mo_NewElementOutWorkTimes');
489
                } else {
490
                    $represent = '';
491
                    if (!empty($this->date_from)) {
492
                        $represent .= "<i class='icon outline calendar alternate' ></i>";
493
                        $date_from = date("d.m.Y", $this->date_from);
494
                        $represent .= "$date_from";
495
                        $date_to = date("d.m.Y", $this->date_to)??$date_from;
496
                        if ($date_from !== $date_to){
497
                            $represent .= " - $date_to";
498
                        }
499
                    }
500
                    if (!empty($this->weekday_from)) {
501
                        if (!empty($represent)){
502
                            $represent.=' ';
503
                        }
504
                        $weekday_from = $this->t(date('D',strtotime("Sunday +{$this->weekday_from} days")));
505
                        $represent .= "<i class='icon outline calendar minus' ></i>";
506
                        $represent .= "$weekday_from";
507
                        if (!empty($this->weekday_to) && $this->weekday_from !== $this->weekday_to){
508
                            $weekday_to = $this->t(date('D',strtotime("Sunday +{$this->weekday_to} days")));
509
                            $represent .= " - $weekday_to";
510
                        }
511
                    }
512
513
                    if (!empty($this->time_from)) {
514
                        if (!empty($represent)){
515
                            $represent.=' ';
516
                        }
517
                        $represent .= "<i class='icon clock outline' ></i>";
518
                        $represent .= "$this->time_from";
519
                        if ($this->time_from !== $this->time_to){
520
                            $represent .= " - $this->time_to";
521
                        }
522
                    }
523
                    $name = $this->t('repOutWorkTimes', ['represent' => $represent]);
524
                }
525
                break;
526
            case Providers::class:
527
                if ($this->type === "IAX") {
528
                    $name = $this->Iax->getRepresent();
529
                } else {
530
                    $name = $this->Sip->getRepresent();
531
                }
532
                break;
533
            case PbxSettings::class:
534
                $name = $this->key;
535
                break;
536
            case PbxExtensionModules::class:
537
                $name = '<i class="puzzle piece icon"></i> '
538
                    . $this->t('mo_ModuleShort4Dropdown') . ': '
539
                    . $this->name;
540
                break;
541
            case Sip::class:
542
                $name = '<i class="server icon"></i> ';
543
                if (empty($this->id)) {
544
                    $name .= $this->t('mo_NewElementSip');
545
                } elseif ($this->disabled === '1') {
546
                    $name .= "{$this->description} ({$this->t( 'mo_Disabled' )})";
547
                } else {
548
                    $name .= $this->description;
549
                }
550
551
                break;
552
            case Users::class:
553
                $name = '<i class="user outline icon"></i> ' . $this->username;
554
                break;
555
            case SoundFiles::class:
556
                $name = '<i class="file audio outline icon"></i> ';
557
                $name .= empty($this->id)
558
                    ? $this->t('mo_NewElementSoundFiles')
559
                    : $this->t('repSoundFiles', ['represent' => $this->name]);
560
                break;
561
            default:
562
                $name = 'Unknown';
563
        }
564
565
        if ($needLink) {
566
            $link = $this->getWebInterfaceLink();
567
            $category = explode('\\', static::class)[3];
568
            $result = $this->t(
569
                'rep' . $category,
570
                [
571
                    'represent' => "<a href='{$link}'>{$name}</a>",
572
                ]
573
            );
574
        } else {
575
            $result = $name;
576
        }
577
578
        return $result;
579
    }
580
581
    /**
582
     * Trims long names.
583
     *
584
     * @param $s
585
     *
586
     * @return string
587
     */
588
    private function trimName($s): string
589
    {
590
        $max_length = 64;
591
592
        if (strlen($s) > $max_length) {
593
            $offset = ($max_length - 3) - strlen($s);
594
            $s = substr($s, 0, strrpos($s, ' ', $offset)) . '...';
595
        }
596
597
        return $s;
598
    }
599
600
    /**
601
     * Return link on database record in web interface
602
     *
603
     * @return string
604
     */
605
    public function getWebInterfaceLink(): string
606
    {
607
        $link = '#';
608
609
        switch (static::class) {
610
            case AsteriskManagerUsers::class:
611
                $link = $this->buildRecordUrl(AsteriskManagersController::class, 'modify',  $this->id);
612
                break;
613
            case CallQueueMembers::class:
614
                $link = $this->buildRecordUrl(CallQueuesController::class, 'modify',  $this->CallQueues->uniqid);
615
                break;
616
            case CallQueues::class:
617
                $link = $this->buildRecordUrl(CallQueuesController::class, 'modify',  $this->uniqid);
618
                break;
619
            case ConferenceRooms::class:
620
                $link = $this->buildRecordUrl(ConferenceRoomsController::class, 'modify',  $this->uniqid);
621
                break;
622
            case CustomFiles::class:
623
                $link = $this->buildRecordUrl(CustomFilesController::class, 'modify',  $this->id);
624
                break;
625
            case DialplanApplications::class:
626
                $link = $this->buildRecordUrl(DialplanApplicationsController::class, 'modify',  $this->uniqid);
627
                break;
628
            case ExtensionForwardingRights::class:
629
630
                break;
631
            case Extensions::class:
632
                $link = $this->buildRecordUrl(ExtensionsController::class, 'modify',  $this->id);
633
                break;
634
            case ExternalPhones::class:
635
                if ( $this->di->get(SecurityPluginProvider::SERVICE_NAME, [static::class, 'modify'])
636
                    && $this->Extensions->is_general_user_number === "1") {
637
                    $parameters = [
638
                        'conditions' => 'is_general_user_number="1" AND type="' . Extensions::TYPE_EXTERNAL . '" AND userid=:userid:',
639
                        'bind' => [
640
                            'userid' => $this->Extensions->userid,
641
                        ],
642
                    ];
643
                    $needExtension = Extensions::findFirst($parameters);
644
                    $link = $this->buildRecordUrl(ExtensionsController::class, 'modify', $needExtension->id);
645
                }
646
                break;
647
            case Fail2BanRules::class:
648
                $link = $this->buildRecordUrl(Fail2BanController::class, 'index');
649
                break;
650
            case FirewallRules::class:
651
                $link = $this->buildRecordUrl(FirewallController::class, 'modify', $this->NetworkFilters->id);
652
                break;
653
            case Iax::class:
654
                $link = $this->buildRecordUrl(ProvidersController::class, 'modifyiax', $this->Providers->id);
655
                break;
656
            case IvrMenu::class:
657
                $link = $this->buildRecordUrl(IvrMenuController::class, 'modify', $this->uniqid);
658
                break;
659
            case IvrMenuActions::class:
660
                $link = $this->buildRecordUrl(IvrMenuController::class, 'modify', $this->IvrMenu->uniqid);
661
                break;
662
            case IncomingRoutingTable::class:
663
                $link = $this->buildRecordUrl(IncomingRoutesController::class, 'modify', $this->id);
664
                break;
665
            case LanInterfaces::class:
666
                $link = $this->buildRecordUrl(NetworkController::class, 'modify');
667
                break;
668
            case NetworkFilters::class:
669
                $link = $this->buildRecordUrl(FirewallController::class, 'modify', $this->id);
670
                break;
671
            case OutgoingRoutingTable::class:
672
                $link = $this->buildRecordUrl(OutboundRoutesController::class, 'modify', $this->id);
673
                break;
674
            case OutWorkTimes::class:
675
                $link = $this->buildRecordUrl(OutOffWorkTimeController::class, 'modify', $this->id);
676
                break;
677
            case Providers::class:
678
                if ($this->type === "IAX") {
679
                    $link = $this->buildRecordUrl(ProvidersController::class, 'modifyiax', $this->uniqid);
680
                } else {
681
                    $link = $this->buildRecordUrl(ProvidersController::class, 'modifysip', $this->uniqid);
682
                }
683
                break;
684
            case PbxSettings::class:
685
                $link = $this->buildRecordUrl(GeneralSettingsController::class, 'index');
686
                break;
687
            case PbxExtensionModules::class:
688
                $moduleMainController = "Modules\\{$this->uniqid}\\App\\Controllers\\{$this->uniqid}Controller";
689
                if ($this->di->get(SecurityPluginProvider::SERVICE_NAME, [$moduleMainController, 'index'])) {
690
                    $url = new Url();
691
                    $baseUri = $this->di->getShared('config')->path('adminApplication.baseUri');
692
                    $unCamelizedModuleId = Text::uncamelize($this->uniqid, '-');
693
                    $link = $url->get("$unCamelizedModuleId/$unCamelizedModuleId/index", null, null, $baseUri);
694
                }
695
                break;
696
            case Sip::class:
697
                if ($this->Extensions) {
698
                    if ($this->Extensions->is_general_user_number === "1") {
699
                        $link = $this->buildRecordUrl(ExtensionsController::class, 'modify', $this->Extensions->id);
700
                    }
701
                } elseif ($this->Providers) {
702
                    $link = $this->buildRecordUrl(ProvidersController::class, 'modifysip', $this->Providers->id);
703
                }
704
                break;
705
            case Users::class:
706
                $parameters = [
707
                    'conditions' => 'userid=:userid:',
708
                    'bind' => [
709
                        'userid' => $this->id,
710
                    ],
711
                ];
712
                $needExtension = Extensions::findFirst($parameters);
713
                if ($needExtension === null) {
714
                    $link = $this->buildRecordUrl(ExtensionsController::class, 'index');
715
                } else {
716
                    $link = $this->buildRecordUrl(ExtensionsController::class, 'modify', $needExtension->id);
717
                }
718
                break;
719
            case SoundFiles::class:
720
                $link = $this->buildRecordUrl(SoundFilesController::class, 'modify',$this->id);
721
                break;
722
            default:
723
        }
724
725
        return $link;
726
    }
727
728
    /**
729
     * Build a record URL based on the controller class, action, and record ID.
730
     *
731
     * @param string $controllerClass The controller class name.
732
     * @param string $action The action name.
733
     * @param string $recordId The record ID (optional).
734
     *
735
     * @return string The generated record URL.
736
     */
737
    private function buildRecordUrl(string $controllerClass, string  $action, string $recordId=''):string
738
    {
739
        $link = '#';
740
        if ($this->di->get(SecurityPluginProvider::SERVICE_NAME, [$controllerClass, $action])) {
741
            $url = new Url();
742
            $baseUri = $this->di->getShared('config')->path('adminApplication.baseUri');
743
            $controllerParts = explode('\\', $controllerClass);
744
            $controllerName = end($controllerParts);
745
            // Remove the "Controller" suffix if present
746
            $controllerName = str_replace("Controller", "", $controllerName);
747
            $unCamelizedControllerName = Text::uncamelize($controllerName, '-');
748
            $link = $url->get("{$unCamelizedControllerName}//{$action}//{$recordId}", null, null, $baseUri);
749
        }
750
        return $link;
751
    }
752
753
    /**
754
     * Fill default values from annotations
755
     */
756
    public function beforeValidationOnCreate(): void
757
    {
758
        $metaData = $this->di->get(ModelsMetadataProvider::SERVICE_NAME);
759
        $defaultValues = $metaData->getDefaultValues($this);
760
        foreach ($defaultValues as $field => $value) {
761
            if (!isset($this->{$field})) {
762
                $this->{$field} = $value;
763
            }
764
        }
765
    }
766
767
    /**
768
     * Checks if there are any dependencies that prevent the deletion of the current entity.
769
     * It displays a list of related entities with links that hinder the deletion.
770
     *
771
     * @return bool
772
     */
773
    public function beforeDelete(): bool
774
    {
775
        return $this->checkRelationsSatisfaction($this, $this);
776
    }
777
778
    /**
779
     * Check whether this object has unsatisfied relations or not.
780
     *
781
     * @param object $theFirstDeleteRecord The first delete record.
782
     * @param object $currentDeleteRecord  The current delete record.
783
     *
784
     * @return bool True if all relations are satisfied, false otherwise.
785
     */
786
    private function checkRelationsSatisfaction($theFirstDeleteRecord, $currentDeleteRecord): bool
787
    {
788
        $result = true;
789
        $relations = $currentDeleteRecord->_modelsManager->getRelations(get_class($currentDeleteRecord));
790
        foreach ($relations as $relation) {
791
            $foreignKey = $relation->getOption('foreignKey');
792
            if (!array_key_exists('action', $foreignKey)) {
793
                continue;
794
            }
795
            // Check if there are some record which restrict delete current record
796
            $relatedModel = $relation->getReferencedModel();
797
            $mappedFields = $relation->getFields();
798
            $mappedFields = is_array($mappedFields)
799
                ? $mappedFields : [$mappedFields];
800
            $referencedFields = $relation->getReferencedFields();
801
            $referencedFields = is_array($referencedFields)
802
                ? $referencedFields : [$referencedFields];
803
            $parameters['conditions'] = '';
804
            $parameters['bind'] = [];
805
            foreach ($referencedFields as $index => $referencedField) {
806
                $parameters['conditions'] .= $index > 0
807
                    ? ' OR ' : '';
808
                $parameters['conditions'] .= $referencedField
809
                    . '= :field'
810
                    . $index . ':';
811
                $bindField
812
                    = $mappedFields[$index];
813
                $parameters['bind']['field' . $index] = $currentDeleteRecord->$bindField;
814
            }
815
            $relatedRecords = $relatedModel::find($parameters);
816
            switch ($foreignKey['action']) {
817
                case Relation::ACTION_RESTRICT:
818
                    // Restrict deletion and add message about unsatisfied undeleted links
819
                    foreach ($relatedRecords as $relatedRecord) {
820
                        if (serialize($relatedRecord) === serialize($theFirstDeleteRecord)
821
                            || serialize($relatedRecord) === serialize($currentDeleteRecord)
822
                        ) {
823
                            continue;
824
                            // It is the checked object
825
                        }
826
                        $message = new Message(
827
                            $theFirstDeleteRecord->t(
828
                                'mo_BeforeDeleteFirst',
829
                                [
830
                                    'represent' => $relatedRecord->getRepresent(true),
831
                                ]
832
                            )
833
                        );
834
                        $theFirstDeleteRecord->appendMessage($message);
835
                        $result = false;
836
                    }
837
                    break;
838
                case Relation::ACTION_CASCADE:
839
                    // Delete all related records
840
                    foreach ($relatedRecords as $relatedRecord) {
841
                        if (serialize($relatedRecord) === serialize($theFirstDeleteRecord)
842
                            || serialize($relatedRecord) === serialize($currentDeleteRecord)
843
                        ) {
844
                            continue;
845
                            // It is the checked object
846
                        }
847
                        $result = $result && $relatedRecord->checkRelationsSatisfaction(
848
                                $theFirstDeleteRecord,
849
                                $relatedRecord
850
                            );
851
                        if ($result) {
852
                            $result = $relatedRecord->delete();
853
                        }
854
                        if ($result === false) {
855
                            $messages = $relatedRecord->getMessages();
856
                            foreach ($messages as $message) {
857
                                $theFirstDeleteRecord->appendMessage($message);
858
                            }
859
                        }
860
                    }
861
                    break;
862
                case Relation::NO_ACTION:
863
                    // Clear all refs
864
                    break;
865
                default:
866
                    break;
867
            }
868
        }
869
870
        return $result;
871
    }
872
873
    /**
874
     * Returns Identity field name for current model
875
     *
876
     * @return string
877
     */
878
    public function getIdentityFieldName(): string
879
    {
880
        $metaData = $this->di->get(ModelsMetadataProvider::SERVICE_NAME);
881
882
        return $metaData->getIdentityField($this);
883
    }
884
}