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

Extensions::beforeSave()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
nc 1
nop 0
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2024 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 Phalcon\Mvc\Model\Relation;
23
use Phalcon\Validation;
24
use Phalcon\Validation\Validator\Uniqueness as UniquenessValidator;
25
26
/**
27
 * Class Extensions
28
 *
29
 * @property Sip Sip
30
 * @property Users Users
31
 * @property ExternalPhones ExternalPhones
32
 * @property DialplanApplications DialplanApplications
33
 * @property ConferenceRooms ConferenceRooms
34
 * @property CallQueues CallQueues
35
 * @property OutWorkTimes OutWorkTimes
36
 * @property IvrMenu IvrMenu
37
 * @property ExtensionForwardingRights ExtensionForwardingRights
38
 *
39
 *
40
 * @method static mixed findFirstByNumber(string|null $number)
41
 * @method static mixed findByType(string|null $type)
42
 * @method static mixed findByUserid(int $userid)
43
 *
44
 * @package MikoPBX\Common\Models
45
 */
46
class Extensions extends ModelsBase
47
{
48
49
    public const  TYPE_DIALPLAN_APPLICATION = 'DIALPLAN APPLICATION';
50
    public const  TYPE_SIP = 'SIP';
51
    public const  TYPE_QUEUE = 'QUEUE';
52
    public const  TYPE_EXTERNAL = 'EXTERNAL';
53
    public const  TYPE_IVR_MENU = 'IVR MENU';
54
    public const  TYPE_CONFERENCE = 'CONFERENCE';
55
    public const  TYPE_MODULES = 'MODULES';
56
    public const  TYPE_SYSTEM = 'SYSTEM';
57
    public const  TYPE_PARKING = 'PARKING';
58
59
    /**
60
     * @Primary
61
     * @Identity
62
     * @Column(type="integer", nullable=false)
63
     */
64
    public $id;
65
66
    /**
67
     * Internal number or internal number pattern
68
     *
69
     * @Column(type="string", nullable=true)
70
     */
71
    public ?string $number = '';
72
73
    /**
74
     * Type of the internal number
75
     *
76
     * @Column(type="string", nullable=true)
77
     */
78
    public ?string $type = '';
79
80
    /**
81
     * Caller ID for the number
82
     *
83
     * @Column(type="string", nullable=true)
84
     */
85
    public ?string $callerid = '';
86
87
    /**
88
     * Reference to the users table, can be NULL if it's not a user
89
     *
90
     * @Column(type="integer", nullable=true)
91
     */
92
    public ?int $userid = null;
93
94
    /**
95
     * Flag indicating whether to show the number in the phonebook and selection lists
96
     *
97
     * @Column(type="string", length=1, nullable=true, default="1")
98
     */
99
    public ?string $show_in_phonebook = '1';
100
101
    /**
102
     * Flag indicating whether the number can be dialed by external callers
103
     *
104
     * @Column(type="string", length=1, nullable=true, default="1")
105
     */
106
    public ?string $public_access = '1';
107
108
    /**
109
     * Flag indicating whether it is the general user number that is edited in the user's profile
110
     *
111
     * @Column(type="string", length=1, nullable=true, default="0")
112
     */
113
    public ?string $is_general_user_number = "0";
114
115
    /**
116
     * Field with search words for full text search, consist of username, callerid, email, number, mobile in lower case
117
     *
118
     * @Column(type="string", nullable=true, default="")
119
     */
120
    public ?string $search_index = "";
121
122
123
    /**
124
     * Get the next available application number from the database.
125
     *
126
     * @return string The next free application number.
127
     */
128
    public static function getNextFreeApplicationNumber(): string
129
    {
130
        $parameters = [
131
            'columns' => 'number',
132
        ];
133
        // Retrieve all existing numbers from the database
134
        $result = self::find($parameters)->toArray();
135
136
        // Find the next available application number starting from 2200100
137
        $freeExtension = '2200100';
138
        for ($i = 100; ; $i++) {
139
            $freeExtension = "2200{$i}";
140
            if (!in_array(['number' => $freeExtension], $result, false)) {
141
                break;
142
            }
143
        }
144
        return $freeExtension;
145
    }
146
147
    /**
148
     * Returns Caller ID by phone number.
149
     * @param string $number
150
     * @return string
151
     */
152
    public static function getCidByPhoneNumber(string $number):string
153
    {
154
        if(empty($number)){
155
            return $number;
156
        }
157
        $number = preg_replace('/\D+/', '', $number);
158
        $extensionLength = PbxSettings::getValueByKey(PbxSettingsConstants::PBX_INTERNAL_EXTENSION_LENGTH);
159
        if(strlen($number) > $extensionLength){
160
            $query = 'number LIKE :phone:';
161
            $phone = '%'.substr($number, -9);
162
        }else{
163
            $query = 'number = :phone:';
164
            $phone = $number;
165
        }
166
        $filter = [
167
            $query,
168
            'columns' => 'callerid',
169
            'bind' => [
170
                'phone' => $phone
171
            ]
172
        ];
173
        $data = self::findFirst($filter);
174
        if($data){
175
            $cid = $data->callerid;
176
        }else{
177
            $cid = $number;
178
        }
179
180
        return $cid;
181
    }
182
183
    /**
184
     * Get the next available internal extension number.
185
     *
186
     * This function retrieves the minimum existing internal extension number from the database.
187
     * If there are no existing internal numbers, it starts from 200.
188
     * It then checks for available extension numbers within the range and returns the next available one.
189
     *
190
     * @return string The next available internal extension number, or an empty string if none are available.
191
     */
192
    public static function getNextInternalNumber(): string
193
    {
194
        $parameters = [
195
            'column' => 'number',
196
            'conditions'=>'type="'.Extensions::TYPE_SIP.'" and userid is not null'
197
        ];
198
        $started = Extensions::minimum($parameters);
199
        if ($started === null) {
200
            // If there are no existing internal numbers, start from 200
201
            $started = 200;
202
        }
203
204
        $extensionsLength = PbxSettings::getValueByKey(PbxSettingsConstants::PBX_INTERNAL_EXTENSION_LENGTH);
205
        $maxExtension = (10 ** $extensionsLength) - 1;
206
207
        $occupied = Extensions::find(['columns' => 'number'])->toArray();
208
        $occupied = array_column($occupied, 'number');
209
210
        for ($i = $started; $i <= $maxExtension ; $i++) {
211
            if (!in_array((string)$i, $occupied)){
212
                return (string)$i;
213
            }
214
        }
215
        // There is no available extensions
216
        return '';
217
    }
218
219
    /**
220
     * Initialize the model.
221
     */
222
    public function initialize(): void
223
    {
224
        $this->setSource('m_Extensions');
225
        parent::initialize();
226
        $this->belongsTo(
227
            'userid',
228
            Users::class,
229
            'id',
230
            [
231
                'alias' => 'Users',
232
                'foreignKey' => [
233
                    'allowNulls' => true,
234
                    'action' => Relation::NO_ACTION,
235
                ],
236
            ]
237
        );
238
        $this->hasOne(
239
            'number',
240
            Sip::class,
241
            'extension',
242
            [
243
                'alias' => 'Sip',
244
                'foreignKey' => [
245
                    'allowNulls' => false,
246
                    'action' => Relation::ACTION_CASCADE,
247
                ],
248
            ]
249
        );
250
        $this->hasOne(
251
            'number',
252
            ExternalPhones::class,
253
            'extension',
254
            [
255
                'alias' => 'ExternalPhones',
256
                'foreignKey' => [
257
                    'allowNulls' => false,
258
                    'action' => Relation::ACTION_CASCADE,
259
                ],
260
            ]
261
        );
262
        $this->hasOne(
263
            'number',
264
            DialplanApplications::class,
265
            'extension',
266
            [
267
                'alias' => 'DialplanApplications',
268
                'foreignKey' => [
269
                    'allowNulls' => false,
270
                    'action' => Relation::ACTION_CASCADE // DialplanApplications is always deleted through its Extension
271
                ],
272
            ]
273
        );
274
        $this->hasOne(
275
            'number',
276
            ConferenceRooms::class,
277
            'extension',
278
            [
279
                'alias' => 'ConferenceRooms',
280
                'foreignKey' => [
281
                    'allowNulls' => false,
282
                    'action' => Relation::ACTION_CASCADE // ConferenceRooms is always deleted through its Extension
283
                ],
284
            ]
285
        );
286
287
        $this->hasOne(
288
            'number',
289
            CallQueues::class,
290
            'extension',
291
            [
292
                'alias' => 'CallQueues',
293
                'foreignKey' => [
294
                    'allowNulls' => false,
295
                    'action' => Relation::ACTION_CASCADE // CallQueues is always deleted through its Extension
296
                ],
297
            ]
298
        );
299
        $this->hasMany(
300
            'number',
301
            CallQueues::class,
302
            'timeout_extension',
303
            [
304
                'alias' => 'CallQueueRedirectRightsTimeout',
305
                'foreignKey' => [
306
                    'allowNulls' => true,
307
                    'message' => 'CallQueueRedirectRightsTimeout',
308
                    'action' => Relation::ACTION_RESTRICT,
309
                ],
310
            ]
311
        );
312
        $this->hasMany(
313
            'number',
314
            CallQueues::class,
315
            'redirect_to_extension_if_empty',
316
            [
317
                'alias' => 'CallQueueRedirectRightsIfEmpty',
318
                'foreignKey' => [
319
                    'allowNulls' => true,
320
                    'message' => 'CallQueueRedirectRightsIfEmpty',
321
                    'action' => Relation::ACTION_RESTRICT,
322
                ],
323
            ]
324
        );
325
        $this->hasMany(
326
            'number',
327
            CallQueues::class,
328
            'redirect_to_extension_if_unanswered',
329
            [
330
                'alias' => 'CallQueueRedirectRightsIfUnanswered',
331
                'foreignKey' => [
332
                    'allowNulls' => true,
333
                    'message' => 'CallQueueRedirectRightsIfUnanswered',
334
                    'action' => Relation::ACTION_RESTRICT,
335
                ],
336
            ]
337
        );
338
        $this->hasMany(
339
            'number',
340
            CallQueues::class,
341
            'redirect_to_extension_if_repeat_exceeded',
342
            [
343
                'alias' => 'CallQueueRedirectRightsIfRepeatExceeded',
344
                'foreignKey' => [
345
                    'allowNulls' => true,
346
                    'message' => 'CallQueueRedirectRightsIfRepeatExceeded',
347
                    'action' => Relation::ACTION_RESTRICT,
348
                ],
349
            ]
350
        );
351
352
        $this->hasMany(
353
            'number',
354
            CallQueueMembers::class,
355
            'extension',
356
            [
357
                'alias' => 'CallQueueMembers',
358
                'foreignKey' => [
359
                    'allowNulls' => false,
360
                    'action' => Relation::ACTION_CASCADE, // CallQueueMembers is always deleted through its Extension
361
                ],
362
            ]
363
        );
364
        $this->hasMany(
365
            'number',
366
            IncomingRoutingTable::class,
367
            'extension',
368
            [
369
                'alias' => 'IncomingRoutingTable',
370
                'foreignKey' => [
371
                    'allowNulls' => false,
372
                    'action' => Relation::ACTION_RESTRICT,
373
                ],
374
                'params' => [
375
                    'order' => 'priority asc',
376
                ],
377
            ]
378
        );
379
        $this->hasMany(
380
            'number',
381
            OutWorkTimes::class,
382
            'extension',
383
            [
384
                'alias' => 'OutWorkTimes',
385
                'foreignKey' => [
386
                    'allowNulls' => false,
387
                    'action' => Relation::ACTION_RESTRICT,
388
                ],
389
            ]
390
        );
391
        $this->hasOne(
392
            'number',
393
            ExtensionForwardingRights::class,
394
            'extension',
395
            [
396
                'alias' => 'ExtensionForwardingRights',
397
                'foreignKey' => [
398
                    'allowNulls' => false,
399
                    'action' => Relation::ACTION_CASCADE, // ExtensionForwardingRights is always deleted through its Extension
400
                ],
401
            ]
402
        );
403
404
        $this->hasMany(
405
            'number',
406
            ExtensionForwardingRights::class,
407
            'forwarding',
408
            [
409
                'alias' => 'ExtensionForwardingRightsForwarding',
410
                'foreignKey' => [
411
                    'allowNulls' => false,
412
                    'message' => 'ExtensionForwardingRightsForwarding',
413
                    'action' => Relation::ACTION_RESTRICT,
414
                ],
415
            ]
416
        );
417
        $this->hasMany(
418
            'number',
419
            ExtensionForwardingRights::class,
420
            'forwardingonbusy',
421
            [
422
                'alias' => 'ExtensionForwardingRightsForwardingOnBusy',
423
                'foreignKey' => [
424
                    'allowNulls' => false,
425
                    'message' => 'ExtensionForwardingRightsForwardingOnBusy',
426
                    'action' => Relation::ACTION_RESTRICT,
427
                ],
428
            ]
429
        );
430
        $this->hasMany(
431
            'number',
432
            ExtensionForwardingRights::class,
433
            'forwardingonunavailable',
434
            [
435
                'alias' => 'ExtensionForwardingRightsOnUnavailable',
436
                'foreignKey' => [
437
                    'allowNulls' => false,
438
                    'message' => 'ExtensionForwardingRightsOnUnavailable',
439
                    'action' => Relation::ACTION_RESTRICT,
440
                ],
441
            ]
442
        );
443
444
        $this->hasOne(
445
            'number',
446
            IvrMenu::class,
447
            'extension',
448
            [
449
                'alias' => 'IvrMenu',
450
                'foreignKey' => [
451
                    'allowNulls' => false,
452
                    'action' => Relation::ACTION_CASCADE // IvrMenu is always deleted through its Extension
453
                ],
454
            ]
455
        );
456
457
        $this->hasMany(
458
            'number',
459
            IvrMenu::class,
460
            'timeout_extension',
461
            [
462
                'alias' => 'IvrMenuTimeout',
463
                'foreignKey' => [
464
                    'message' => 'IvrMenuTimeout',
465
                    'allowNulls' => false,
466
                    'action' => Relation::ACTION_RESTRICT
467
                    // Restrict the deletion of an internal number if it is used in an IVR menu timeout
468
                ],
469
            ]
470
        );
471
472
        $this->hasMany(
473
            'number',
474
            IvrMenuActions::class,
475
            'extension',
476
            [
477
                'alias' => 'IvrMenuActions',
478
                'foreignKey' => [
479
                    'allowNulls' => false,
480
                    'action' => Relation::ACTION_RESTRICT
481
                    // Restrict the deletion of an internal number if it is used in an IVR menu actions
482
                ],
483
            ]
484
        );
485
    }
486
487
    /**
488
     * Handlers after model data is updated.
489
     */
490
    public function afterUpdate(): void
491
    {
492
        $updatedFields = $this->getUpdatedFields();
493
        if (in_array('number', $updatedFields, false)) {
494
            $this->updateRelationshipsNumbers();
495
        }
496
    }
497
498
    /**
499
     * Update numbers in all related tables when the Extensions number is changed.
500
     */
501
    private function updateRelationshipsNumbers(): void
502
    {
503
        $snapShotData = $this->getOldSnapshotData();
504
        if (empty($snapShotData)) {
505
            return;
506
        }
507
        $relations = $this->_modelsManager->getRelations(__CLASS__);
508
        foreach ($relations as $relation) {
509
            if ($relation->getFields() === 'number'
510
                ||
511
                (
512
                    is_array($relation->getFields())
513
                    && in_array('number', $relation->getFields(), true)
514
                )
515
            ) {
516
                $referencedFields = $relation->getReferencedFields();
517
                $relatedModel = $relation->getReferencedModel();
518
                $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
519
                foreach ($referencedFields as $referencedField) {
520
                    $parameters = [
521
                        'conditions' => $referencedField . '= :oldNumber:',
522
                        'bind' => ['oldNumber' => $snapShotData['number']],
523
                    ];
524
                    $relatedRecords = call_user_func([$relatedModel, 'find'], $parameters);
525
                    foreach ($relatedRecords as $relatedRecord) {
526
                        $relatedRecord->$referencedField = $this->number;
527
                        $relatedRecord->save();
528
                    }
529
                }
530
            }
531
        }
532
    }
533
534
    /**
535
     * Perform validation on the model.
536
     *
537
     * @return bool Whether the validation was successful or not.
538
     */
539
    public function validation(): bool
540
    {
541
        $validation = new Validation();
542
        $validation->add(
543
            'number',
544
            new UniquenessValidator(
545
                [
546
                    'message' => $this->t('mo_ThisNumberNotUniqueForExtensionsModels'),
547
                ]
548
            )
549
        );
550
551
        return $this->validate($validation);
552
    }
553
554
    /**
555
     * Get the related links to the current record.
556
     *
557
     * @return array An array of links.
558
     */
559
    public function getRelatedLinks(): array
560
    {
561
        $result = [];
562
        $relations = $this->_modelsManager->getRelations(__CLASS__);
563
564
        // Iterate through the relations of the current model
565
        foreach ($relations as $relation) {
566
            $relationFields = $relation->getFields();
567
568
            // Check if the relation is based on the 'number' field
569
            if ($relationFields === 'number'
570
                ||
571
                (
572
                    is_array($relationFields)
573
                    && in_array('number', $relationFields, true)
574
                )
575
            ) {
576
                $referencedFields = $relation->getReferencedFields();
577
                $relatedModel = $relation->getReferencedModel();
578
                $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
579
580
                // Iterate through the referenced fields
581
                foreach ($referencedFields as $referencedField) {
582
                    $parameters = [
583
                        'conditions' => $referencedField . '= :Number:',
584
                        'bind' => ['Number' => $this->number],
585
                    ];
586
587
                    // Retrieve the related records based on the matching number
588
                    $relatedRecords = call_user_func([$relatedModel, 'find'], $parameters);
589
590
                    // Build an array of links with the related record and reference field
591
                    foreach ($relatedRecords as $relatedRecord) {
592
                        $result[] = [
593
                            'object' => $relatedRecord,
594
                            'referenceField' => $referencedField,
595
                        ];
596
                    }
597
                }
598
            }
599
        }
600
601
        return $result;
602
    }
603
}