Passed
Push — develop ( 5c22cc...5ac99a )
by Портнов
04:58
created

Extensions::getCidByPhoneNumber()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 29
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 21
dl 0
loc 29
rs 9.584
c 0
b 0
f 0
cc 4
nc 5
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\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
    /**
117
     * Get the next available application number from the database.
118
     *
119
     * @return string The next free application number.
120
     */
121
    public static function getNextFreeApplicationNumber(): string
122
    {
123
        $parameters = [
124
            'columns' => 'number',
125
        ];
126
        // Retrieve all existing numbers from the database
127
        $result = self::find($parameters)->toArray();
128
129
        // Find the next available application number starting from 2200100
130
        $freeExtension = '2200100';
131
        for ($i = 100; ; $i++) {
132
            $freeExtension = "2200{$i}";
133
            if (!in_array(['number' => $freeExtension], $result, false)) {
134
                break;
135
            }
136
        }
137
        return $freeExtension;
138
    }
139
140
    /**
141
     * Returns Caller ID by phone number.
142
     * @param string $number
143
     * @return string
144
     */
145
    public static function getCidByPhoneNumber(string $number):string
146
    {
147
        if(empty($number)){
148
            return $number;
149
        }
150
        $number = preg_replace('/\D+/', '', $number);
151
        $extensionLength = PbxSettings::getValueByKey(PbxSettingsConstants::PBX_INTERNAL_EXTENSION_LENGTH);
152
        if(strlen($number) > $extensionLength){
153
            $query = 'number LIKE :phone:';
154
            $phone = '%'.substr($number, -9);
155
        }else{
156
            $query = 'number = :phone:';
157
            $phone = $number;
158
        }
159
        $filter = [
160
            $query,
161
            'columns' => 'callerid',
162
            'bind' => [
163
                'phone' => $phone
164
            ]
165
        ];
166
        $data = self::findFirst($filter);
167
        if($data){
168
            $cid = $data->callerid;
169
        }else{
170
            $cid = $number;
171
        }
172
173
        return $cid;
174
    }
175
176
    /**
177
     * Get the next available internal extension number.
178
     *
179
     * This function retrieves the minimum existing internal extension number from the database.
180
     * If there are no existing internal numbers, it starts from 200.
181
     * It then checks for available extension numbers within the range and returns the next available one.
182
     *
183
     * @return string The next available internal extension number, or an empty string if none are available.
184
     */
185
    public static function getNextInternalNumber(): string
186
    {
187
        $parameters = [
188
            'column' => 'number',
189
            'conditions'=>'type="'.Extensions::TYPE_SIP.'" and userid is not null'
190
        ];
191
        $started = Extensions::minimum($parameters);
192
        if ($started === null) {
193
            // If there are no existing internal numbers, start from 200
194
            $started = 200;
195
        }
196
197
        $extensionsLength = PbxSettings::getValueByKey(PbxSettingsConstants::PBX_INTERNAL_EXTENSION_LENGTH);
198
        $maxExtension = (10 ** $extensionsLength) - 1;
199
200
        $occupied = Extensions::find(['columns' => 'number'])->toArray();
201
        $occupied = array_column($occupied, 'number');
202
203
        for ($i = $started; $i <= $maxExtension ; $i++) {
204
            if (!in_array((string)$i, $occupied)){
205
                return (string)$i;
206
            }
207
        }
208
        // There is no available extensions
209
        return '';
210
    }
211
212
    /**
213
     * Initialize the model.
214
     */
215
    public function initialize(): void
216
    {
217
        $this->setSource('m_Extensions');
218
        parent::initialize();
219
        $this->belongsTo(
220
            'userid',
221
            Users::class,
222
            'id',
223
            [
224
                'alias' => 'Users',
225
                'foreignKey' => [
226
                    'allowNulls' => true,
227
                    'action' => Relation::NO_ACTION,
228
                ],
229
            ]
230
        );
231
        $this->hasOne(
232
            'number',
233
            Sip::class,
234
            'extension',
235
            [
236
                'alias' => 'Sip',
237
                'foreignKey' => [
238
                    'allowNulls' => false,
239
                    'action' => Relation::ACTION_CASCADE,
240
                ],
241
            ]
242
        );
243
        $this->hasOne(
244
            'number',
245
            ExternalPhones::class,
246
            'extension',
247
            [
248
                'alias' => 'ExternalPhones',
249
                'foreignKey' => [
250
                    'allowNulls' => false,
251
                    'action' => Relation::ACTION_CASCADE,
252
                ],
253
            ]
254
        );
255
        $this->hasOne(
256
            'number',
257
            DialplanApplications::class,
258
            'extension',
259
            [
260
                'alias' => 'DialplanApplications',
261
                'foreignKey' => [
262
                    'allowNulls' => false,
263
                    'action' => Relation::ACTION_CASCADE // DialplanApplications is always deleted through its Extension
264
                ],
265
            ]
266
        );
267
        $this->hasOne(
268
            'number',
269
            ConferenceRooms::class,
270
            'extension',
271
            [
272
                'alias' => 'ConferenceRooms',
273
                'foreignKey' => [
274
                    'allowNulls' => false,
275
                    'action' => Relation::ACTION_CASCADE // ConferenceRooms is always deleted through its Extension
276
                ],
277
            ]
278
        );
279
280
        $this->hasOne(
281
            'number',
282
            CallQueues::class,
283
            'extension',
284
            [
285
                'alias' => 'CallQueues',
286
                'foreignKey' => [
287
                    'allowNulls' => false,
288
                    'action' => Relation::ACTION_CASCADE // CallQueues is always deleted through its Extension
289
                ],
290
            ]
291
        );
292
        $this->hasMany(
293
            'number',
294
            CallQueues::class,
295
            'timeout_extension',
296
            [
297
                'alias' => 'CallQueueRedirectRightsTimeout',
298
                'foreignKey' => [
299
                    'allowNulls' => true,
300
                    'message' => 'CallQueueRedirectRightsTimeout',
301
                    'action' => Relation::ACTION_RESTRICT,
302
                ],
303
            ]
304
        );
305
        $this->hasMany(
306
            'number',
307
            CallQueues::class,
308
            'redirect_to_extension_if_empty',
309
            [
310
                'alias' => 'CallQueueRedirectRightsIfEmpty',
311
                'foreignKey' => [
312
                    'allowNulls' => true,
313
                    'message' => 'CallQueueRedirectRightsIfEmpty',
314
                    'action' => Relation::ACTION_RESTRICT,
315
                ],
316
            ]
317
        );
318
        $this->hasMany(
319
            'number',
320
            CallQueues::class,
321
            'redirect_to_extension_if_unanswered',
322
            [
323
                'alias' => 'CallQueueRedirectRightsIfUnanswered',
324
                'foreignKey' => [
325
                    'allowNulls' => true,
326
                    'message' => 'CallQueueRedirectRightsIfUnanswered',
327
                    'action' => Relation::ACTION_RESTRICT,
328
                ],
329
            ]
330
        );
331
        $this->hasMany(
332
            'number',
333
            CallQueues::class,
334
            'redirect_to_extension_if_repeat_exceeded',
335
            [
336
                'alias' => 'CallQueueRedirectRightsIfRepeatExceeded',
337
                'foreignKey' => [
338
                    'allowNulls' => true,
339
                    'message' => 'CallQueueRedirectRightsIfRepeatExceeded',
340
                    'action' => Relation::ACTION_RESTRICT,
341
                ],
342
            ]
343
        );
344
345
        $this->hasMany(
346
            'number',
347
            CallQueueMembers::class,
348
            'extension',
349
            [
350
                'alias' => 'CallQueueMembers',
351
                'foreignKey' => [
352
                    'allowNulls' => false,
353
                    'action' => Relation::ACTION_CASCADE, // CallQueueMembers is always deleted through its Extension
354
                ],
355
            ]
356
        );
357
        $this->hasMany(
358
            'number',
359
            IncomingRoutingTable::class,
360
            'extension',
361
            [
362
                'alias' => 'IncomingRoutingTable',
363
                'foreignKey' => [
364
                    'allowNulls' => false,
365
                    'action' => Relation::ACTION_RESTRICT,
366
                ],
367
                'params' => [
368
                    'order' => 'priority asc',
369
                ],
370
            ]
371
        );
372
        $this->hasMany(
373
            'number',
374
            OutWorkTimes::class,
375
            'extension',
376
            [
377
                'alias' => 'OutWorkTimes',
378
                'foreignKey' => [
379
                    'allowNulls' => false,
380
                    'action' => Relation::ACTION_RESTRICT,
381
                ],
382
            ]
383
        );
384
        $this->hasOne(
385
            'number',
386
            ExtensionForwardingRights::class,
387
            'extension',
388
            [
389
                'alias' => 'ExtensionForwardingRights',
390
                'foreignKey' => [
391
                    'allowNulls' => false,
392
                    'action' => Relation::ACTION_CASCADE, // ExtensionForwardingRights is always deleted through its Extension
393
                ],
394
            ]
395
        );
396
397
        $this->hasMany(
398
            'number',
399
            ExtensionForwardingRights::class,
400
            'forwarding',
401
            [
402
                'alias' => 'ExtensionForwardingRightsForwarding',
403
                'foreignKey' => [
404
                    'allowNulls' => false,
405
                    'message' => 'ExtensionForwardingRightsForwarding',
406
                    'action' => Relation::ACTION_RESTRICT,
407
                ],
408
            ]
409
        );
410
        $this->hasMany(
411
            'number',
412
            ExtensionForwardingRights::class,
413
            'forwardingonbusy',
414
            [
415
                'alias' => 'ExtensionForwardingRightsForwardingOnBusy',
416
                'foreignKey' => [
417
                    'allowNulls' => false,
418
                    'message' => 'ExtensionForwardingRightsForwardingOnBusy',
419
                    'action' => Relation::ACTION_RESTRICT,
420
                ],
421
            ]
422
        );
423
        $this->hasMany(
424
            'number',
425
            ExtensionForwardingRights::class,
426
            'forwardingonunavailable',
427
            [
428
                'alias' => 'ExtensionForwardingRightsOnUnavailable',
429
                'foreignKey' => [
430
                    'allowNulls' => false,
431
                    'message' => 'ExtensionForwardingRightsOnUnavailable',
432
                    'action' => Relation::ACTION_RESTRICT,
433
                ],
434
            ]
435
        );
436
437
        $this->hasOne(
438
            'number',
439
            IvrMenu::class,
440
            'extension',
441
            [
442
                'alias' => 'IvrMenu',
443
                'foreignKey' => [
444
                    'allowNulls' => false,
445
                    'action' => Relation::ACTION_CASCADE // IvrMenu is always deleted through its Extension
446
                ],
447
            ]
448
        );
449
450
        $this->hasMany(
451
            'number',
452
            IvrMenu::class,
453
            'timeout_extension',
454
            [
455
                'alias' => 'IvrMenuTimeout',
456
                'foreignKey' => [
457
                    'message' => 'IvrMenuTimeout',
458
                    'allowNulls' => false,
459
                    'action' => Relation::ACTION_RESTRICT
460
                    // Restrict the deletion of an internal number if it is used in an IVR menu timeout
461
                ],
462
            ]
463
        );
464
465
        $this->hasMany(
466
            'number',
467
            IvrMenuActions::class,
468
            'extension',
469
            [
470
                'alias' => 'IvrMenuActions',
471
                'foreignKey' => [
472
                    'allowNulls' => false,
473
                    'action' => Relation::ACTION_RESTRICT
474
                    // Restrict the deletion of an internal number if it is used in an IVR menu actions
475
                ],
476
            ]
477
        );
478
    }
479
480
    /**
481
     * Handlers after model data is updated.
482
     */
483
    public function afterUpdate(): void
484
    {
485
        $updatedFields = $this->getUpdatedFields();
486
        if (in_array('number', $updatedFields, false)) {
487
            $this->updateRelationshipsNumbers();
488
        }
489
    }
490
491
    /**
492
     * Update numbers in all related tables when the Extensions number is changed.
493
     */
494
    private function updateRelationshipsNumbers(): void
495
    {
496
        $snapShotData = $this->getOldSnapshotData();
497
        if (empty($snapShotData)) {
498
            return;
499
        }
500
        $relations = $this->_modelsManager->getRelations(__CLASS__);
501
        foreach ($relations as $relation) {
502
            if ($relation->getFields() === 'number'
503
                ||
504
                (
505
                    is_array($relation->getFields())
506
                    && in_array('number', $relation->getFields(), true)
507
                )
508
            ) {
509
                $referencedFields = $relation->getReferencedFields();
510
                $relatedModel = $relation->getReferencedModel();
511
                $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
512
                foreach ($referencedFields as $referencedField) {
513
                    $parameters = [
514
                        'conditions' => $referencedField . '= :oldNumber:',
515
                        'bind' => ['oldNumber' => $snapShotData['number']],
516
                    ];
517
                    $relatedRecords = call_user_func([$relatedModel, 'find'], $parameters);
518
                    foreach ($relatedRecords as $relatedRecord) {
519
                        $relatedRecord->$referencedField = $this->number;
520
                        $relatedRecord->save();
521
                    }
522
                }
523
            }
524
        }
525
    }
526
527
    /**
528
     * Perform validation on the model.
529
     *
530
     * @return bool Whether the validation was successful or not.
531
     */
532
    public function validation(): bool
533
    {
534
        $validation = new Validation();
535
        $validation->add(
536
            'number',
537
            new UniquenessValidator(
538
                [
539
                    'message' => $this->t('mo_ThisNumberNotUniqueForExtensionsModels'),
540
                ]
541
            )
542
        );
543
544
        return $this->validate($validation);
545
    }
546
547
    /**
548
     * Get the related links to the current record.
549
     *
550
     * @return array An array of links.
551
     */
552
    public function getRelatedLinks(): array
553
    {
554
        $result = [];
555
        $relations = $this->_modelsManager->getRelations(__CLASS__);
556
557
        // Iterate through the relations of the current model
558
        foreach ($relations as $relation) {
559
            $relationFields = $relation->getFields();
560
561
            // Check if the relation is based on the 'number' field
562
            if ($relationFields === 'number'
563
                ||
564
                (
565
                    is_array($relationFields)
566
                    && in_array('number', $relationFields, true)
567
                )
568
            ) {
569
                $referencedFields = $relation->getReferencedFields();
570
                $relatedModel = $relation->getReferencedModel();
571
                $referencedFields = is_array($referencedFields) ? $referencedFields : [$referencedFields];
572
573
                // Iterate through the referenced fields
574
                foreach ($referencedFields as $referencedField) {
575
                    $parameters = [
576
                        'conditions' => $referencedField . '= :Number:',
577
                        'bind' => ['Number' => $this->number],
578
                    ];
579
580
                    // Retrieve the related records based on the matching number
581
                    $relatedRecords = call_user_func([$relatedModel, 'find'], $parameters);
582
583
                    // Build an array of links with the related record and reference field
584
                    foreach ($relatedRecords as $relatedRecord) {
585
                        $result[] = [
586
                            'object' => $relatedRecord,
587
                            'referenceField' => $referencedField,
588
                        ];
589
                    }
590
                }
591
            }
592
        }
593
594
        return $result;
595
    }
596
597
    /**
598
     * Sanitizes the caller ID by removing any characters that are not alphanumeric or spaces.
599
     * This function is automatically triggered before saving the call model.
600
     */
601
    public function beforeSave()
602
    {
603
        // Sanitizes the caller ID by removing any characters that are not alphanumeric or spaces.
604
        $this->callerid = preg_replace('/[^a-zA-Zа-яА-Я0-9 ]/ui', '', $this->callerid);
605
    }
606
607
}