SaveRecordAction::saveUser()   A
last analyzed

Complexity

Conditions 5
Paths 8

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
c 0
b 0
f 0
dl 0
loc 24
rs 9.4555
cc 5
nc 8
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\PBXCoreREST\Lib\Extensions;
21
22
use MikoPBX\Common\Handlers\CriticalErrorsHandler;
23
use MikoPBX\Common\Models\ExtensionForwardingRights;
24
use MikoPBX\Common\Models\Extensions;
25
use MikoPBX\Common\Models\ExternalPhones;
26
use MikoPBX\Common\Models\PbxSettings;
27
use MikoPBX\Common\Models\Sip;
28
use MikoPBX\Common\Models\Users;
29
use MikoPBX\Common\Providers\MainDatabaseProvider;
30
use MikoPBX\Common\Providers\ModelsMetadataProvider;
31
use MikoPBX\Core\System\SystemMessages;
32
use MikoPBX\PBXCoreREST\Lib\PBXApiResult;
33
use Phalcon\Di\Di;
34
use Phalcon\Di\Injectable;
35
36
/**
37
 * Class SaveRecord
38
 * Provides methods to save extension records with associated entities.
39
 *
40
 * @package MikoPBX\PBXCoreREST\Lib\Extensions
41
 */
42
class SaveRecordAction extends Injectable
43
{
44
    /**
45
     * Saves a record with associated entities.
46
     *
47
     * @param array $data Data to be saved.
48
     * @return PBXApiResult Result of the save operation.
49
     */
50
    public static function main(array $data): PBXApiResult
51
    {
52
        // Initialize the result object
53
        $res = new PBXApiResult();
54
        $res->processor = __METHOD__;
55
        $res->success = true;
56
        
57
        try {
58
            $di = Di::getDefault();
59
            $db = $di->get(MainDatabaseProvider::SERVICE_NAME);
60
            $db->begin();
61
            
62
            // Debug log
63
            SystemMessages::sysLogMsg(
64
                static::class,
65
                sprintf(
66
                    'Starting save with data: %s',
67
                    json_encode($data)
68
                ),
69
                LOG_DEBUG
70
            );
71
            
72
            $dataStructure = new DataStructure($data);
73
74
            // Save user entity
75
            list($userEntity, $res->success) = self::saveUser($dataStructure);
76
            if (!$res->success) {
77
                // Handle errors and rollback
78
                $res->messages['error'][] = $userEntity->getMessages();
79
                $db->rollback();
80
                return $res;
81
            } else {
82
                $dataStructure->user_id = $userEntity->id;
83
            }
84
85
            // Save extension entity
86
            list($extension, $res->success) = self::saveExtension($dataStructure, false);
87
            if (!$res->success) {
88
                // Handle errors and rollback
89
                $res->messages['error'][] = implode($extension->getMessages());
90
                $db->rollback();
91
                return $res;
92
            }
93
94
            // Save SIP entity
95
            list($sipEntity, $res->success) = self::saveSip($dataStructure);
96
            if (!$res->success) {
97
                // Handle errors and rollback
98
                $res->messages['error'][] = implode($sipEntity->getMessages());
99
                $db->rollback();
100
                return $res;
101
            }
102
103
            // Save forwarding rights entity
104
            list($fwdEntity, $res->success) = self::saveForwardingRights($dataStructure);
105
            if (!$res->success) {
106
                // Handle errors and rollback
107
                $res->messages['error'][] = implode($fwdEntity->getMessages());
108
                $db->rollback();
109
                return $res;
110
            }
111
112
            // Check mobile number presence and save related entities
113
            if (!empty($dataStructure->mobile_number)) {
114
115
                // Save mobile extension
116
                list($mobileExtension, $res->success) = self::saveExtension($dataStructure, true);
117
                if (!$res->success) {
118
                    // Handle errors and rollback
119
                    $res->messages['error'][] = implode($mobileExtension->getMessages());
120
                    $db->rollback();
121
                    return $res;
122
                }
123
124
                // Save ExternalPhones for mobile number
125
                list($externalPhone, $res->success) = self::saveExternalPhones($dataStructure);
126
                if (!$res->success) {
127
                    // Handle errors and rollback
128
                    $res->messages['error'][] = implode($externalPhone->getMessages());
129
                    $db->rollback();
130
                    return $res;
131
                }
132
            } else {
133
                // Delete mobile number if it was associated with the user
134
                list($deletedMobileNumber, $res->success) = self::deleteMobileNumber($userEntity);
135
                if (!$res->success) {
136
                    $res->messages['error'][] = implode($deletedMobileNumber->getMessages());
137
                    $db->rollback();
138
                    return $res;
139
                }
140
            }
141
            $db->commit();
142
143
            $res = GetRecordAction::main($extension->id);
144
            $res->processor = __METHOD__;
145
            return $res;
146
        } catch (\Throwable $e) {
147
            SystemMessages::sysLogMsg(
148
                static::class,
149
                sprintf(
150
                    'Error saving extension: %s',
151
                    $e->getMessage()
152
                ),
153
                LOG_ERR
154
            );
155
            
156
            // Ensure we also log through system's error handler
157
            CriticalErrorsHandler::handleExceptionWithSyslog($e);
158
            
159
            // Return error in result
160
            $res->success = false;
161
            $res->messages['error'][] = $e->getMessage();
162
            
163
            // Try to roll back if database was started
164
            if(isset($db) && $db) {
165
                try { $db->rollback(); } catch (\Throwable $rollbackEx) { /* ignore */ }
0 ignored issues
show
introduced by
Consider moving this CATCH statement to a new line.
Loading history...
166
            }
167
        }
168
        
169
        return $res;
170
    }
171
172
    /**
173
     * Save parameters to the Users table
174
     *
175
     * @param DataStructure $dataStructure The data structure containing the input data.
176
     * @return array An array containing the saved Users entity and the save result.
177
     */
178
    private static function saveUser(DataStructure $dataStructure): array
179
    {
180
        $userEntity = Users::findFirstById($dataStructure->user_id);
181
        if ($userEntity === null) {
182
            $userEntity = new Users();
183
        }
184
185
        // Fill in user parameters
186
        $metaData = Di::getDefault()->get(ModelsMetadataProvider::SERVICE_NAME);
187
        foreach ($metaData->getAttributes($userEntity) as $name) {
188
            switch ($name) {
189
                case 'language':
190
                    $userEntity->$name = PbxSettings::getValueByKey(PbxSettings::PBX_LANGUAGE);
191
                    break;
192
                default:
193
                    $propertyKey = 'user_' . $name;
194
                    if (property_exists($dataStructure, $propertyKey)) {
195
                        $userEntity->$name = $dataStructure->$propertyKey;
196
                    }
197
            }
198
        }
199
200
        $result = $userEntity->save();
201
        return [$userEntity, $result];
202
    }
203
204
    /**
205
     * Save the extension for a user.
206
     * @param DataStructure $dataStructure The data structure containing the input data.
207
     * @param bool $isMobile Flag indicating if it's a mobile extension.
208
     *
209
     * @return array An array containing the saved Extensions entity and the save result.
210
     */
211
    private static function saveExtension(DataStructure $dataStructure, bool $isMobile = false): array
212
    {
213
        $parameters = [];
214
        $parameters['conditions'] = 'type=:type: AND is_general_user_number = "1" AND userid=:userid:';
215
        $parameters['bind']['type'] = $isMobile ? Extensions::TYPE_EXTERNAL : Extensions::TYPE_SIP;
216
        $parameters['bind']['userid'] = $dataStructure->user_id ;
217
218
        $extension = Extensions::findFirst($parameters);
219
        if ($extension === null) {
220
            $extension = new Extensions();
221
        }
222
        $metaData = Di::getDefault()->get(ModelsMetadataProvider::SERVICE_NAME);
223
        foreach ($metaData->getAttributes($extension) as $name) {
224
            switch ($name) {
225
                case 'id':
226
                    // Skip saving the 'id' field
227
                    break;
228
                case 'type':
229
                    // Set the 'type' based on the value of $isMobile
230
                    $extension->$name = $isMobile ? Extensions::TYPE_EXTERNAL : Extensions::TYPE_SIP;
231
                    break;
232
                case 'callerid':
233
                    // Sanitize the caller ID based on 'user_username'
234
                    $extension->$name = self::sanitizeCallerId( $dataStructure->user_username);
235
                    break;
236
                case 'userid':
237
                    // Set 'userid' to the ID of the user entity
238
                    $extension->$name = $dataStructure->user_id;
239
                    break;
240
                case 'number':
241
                    // Set 'number' based on the value of mobile_number or number
242
                    $extension->$name = $isMobile ? $dataStructure->mobile_number : $dataStructure->number;
243
                    break;
244
                case 'search_index':
245
                    // Generate search index for the extension
246
                    $extension->$name = self::generateSearchIndex($dataStructure, $isMobile);
247
                    break;
248
                default:
249
                    if (property_exists($dataStructure, $name)) {
250
                        // Set other fields based on the values in $data
251
                        $extension->$name = $dataStructure->$name;
252
                    }
253
            }
254
        }
255
        $result = $extension->save();
256
        return [$extension, $result];
257
    }
258
259
    /**
260
     * Save the SIP entity with the provided data.
261
     *
262
     * @param DataStructure $dataStructure The data structure containing the input data.
263
     * @return array An array containing the saved SIP entity and the save result.
264
     */
265
    private static function saveSip(DataStructure $dataStructure): array
266
    {
267
        $sipEntity = SIP::findFirstByUniqid($dataStructure->sip_uniqid);
268
        if ($sipEntity === null) {
269
            $sipEntity = new SIP();
270
        }
271
272
        $metaData = Di::getDefault()->get(ModelsMetadataProvider::SERVICE_NAME);
273
        foreach ($metaData->getAttributes($sipEntity) as $name) {
274
            switch ($name) {
275
                case 'weakSecret':
276
                    $sipEntity->$name = '0';
277
                    break;
278
                case 'networkfilterid':
279
                    if ($dataStructure->sip_networkfilterid === 'none') {
280
                        $sipEntity->$name = null;
281
                    } else {
282
                        $sipEntity->$name = $dataStructure->sip_networkfilterid;
283
                    }
284
                    break;
285
                case 'extension':
286
                    // Set 'extension' based on the value of number
287
                    $sipEntity->$name = $dataStructure->number;
288
                    break;
289
                case 'description':
290
                    // Set 'description' based on the value of user_username
291
                    $sipEntity->$name = $dataStructure->user_username;
292
                    break;
293
                case 'manualattributes':
294
                    // Set 'manualattributes' using the value of sip_manualattributes
295
                    $sipEntity->setManualAttributes($dataStructure->sip_manualattributes);
296
                    break;
297
                default:
298
                    $propertyKey = 'sip_' . $name;
299
                    if (property_exists($dataStructure, $propertyKey)) {
300
                        // Set other fields based on the other fields in $dataStructure
301
                        $sipEntity->$name = $dataStructure->$propertyKey;
302
                    }
303
            }
304
        }
305
306
        $result = $sipEntity->save();
307
        return [$sipEntity, $result];
308
    }
309
310
    /**
311
     * Save the ExtensionForwardingRights entity with the provided data.
312
     *
313
     * @param DataStructure $dataStructure The data structure containing the input data.
314
     * @return array An array containing the saved ExtensionForwardingRights entity and the save result.
315
     */
316
    private static function saveForwardingRights(DataStructure $dataStructure): array
317
    {
318
        $forwardingRight = ExtensionForwardingRights::findFirstByExtension($dataStructure->number);
319
        if ($forwardingRight === null) {
320
            $forwardingRight = new ExtensionForwardingRights();
321
        }
322
        $metaData = Di::getDefault()->get(ModelsMetadataProvider::SERVICE_NAME);
323
        foreach ($metaData->getAttributes($forwardingRight) as $name) {
324
            switch ($name) {
325
                case 'extension':
326
                    // Set 'extension' based on the value of number
327
                    $forwardingRight->$name = $dataStructure->number;
328
                    break;
329
                case 'ringlength':
330
                    $forwardingRight->ringlength = 0;
331
                    if (!empty($dataStructure->fwd_ringlength)) {
332
                        $forwardingRight->ringlength = $dataStructure->fwd_ringlength;
333
                    } elseif (!empty($dataStructure->fwd_forwarding)) {
334
                        $forwardingRight->ringlength = 45;
335
                    }
336
                    break;
337
                default:
338
                    $propertyKey = 'fwd_' . $name;
339
                    if (property_exists($dataStructure, $propertyKey)) {
340
                        // Set other fields based on the other fields in $dataStructure
341
                        $forwardingRight->$name = $dataStructure->$propertyKey === -1 ? '' : $dataStructure->$propertyKey;
342
                    }
343
            }
344
        }
345
        $result = $forwardingRight->save();
346
        return [$forwardingRight, $result];
347
    }
348
349
    /**
350
     * Save parameters to the ExternalPhones table for a mobile number.
351
     *
352
     * @param DataStructure $dataStructure The data structure containing the input data.
353
     * @return array An array containing the saved ExternalPhones entity and the save result.
354
     */
355
    private static function saveExternalPhones(DataStructure $dataStructure): array
356
    {
357
        $externalPhone = ExternalPhones::findFirstByUniqid($dataStructure->mobile_uniqid);
358
        if ($externalPhone === null) {
359
            $externalPhone = new ExternalPhones();
360
        }
361
        $metaData = Di::getDefault()->get(ModelsMetadataProvider::SERVICE_NAME);
362
        foreach ($metaData->getAttributes($externalPhone) as $name) {
363
            switch ($name) {
364
                case 'extension':
365
                    $externalPhone->$name = $dataStructure->mobile_number;
366
                    break;
367
                case 'description':
368
                    $externalPhone->$name = $dataStructure->user_username;
369
                    break;
370
                default:
371
                    $propertyKey = 'mobile_' . $name;
372
                    if (property_exists($dataStructure, $propertyKey)) {
373
                        // Set other fields based on the other fields in $dataStructure
374
                        $externalPhone->$name = $dataStructure->$propertyKey;
375
                    }
376
            }
377
        }
378
379
        $result = $externalPhone->save();
380
        return [$externalPhone, $result];
381
    }
382
383
    /**
384
     * Delete a mobile number associated with a user.
385
     *
386
     * @param Users $userEntity The user entity.
387
     * @return array An array containing the deleted mobile number entity and the deletion result.
388
     */
389
    private static function deleteMobileNumber(Users $userEntity): array
390
    {
391
        $parameters = [
392
            'conditions' => 'type="' . Extensions::TYPE_EXTERNAL . '" AND is_general_user_number = "1" AND userid=:userid:',
393
            'bind' => [
394
                'userid' => $userEntity->id,
395
            ],
396
        ];
397
        $deletedMobileNumber = Extensions::findFirst($parameters);
398
        $result = true;
399
        if ($deletedMobileNumber !== null) {
400
            // Delete the mobile number entity if found
401
            $result = $deletedMobileNumber->delete();
402
        }
403
        return [$deletedMobileNumber, $result];
404
    }
405
406
    /**
407
     * Generate a search index for the extension.
408
     *
409
     * @param DataStructure $dataStructure The data structure containing the input data.
410
     * @param bool $isMobile Flag indicating if it's a mobile extension.
411
     * @return string The generated search index.
412
     */
413
    private static function generateSearchIndex(DataStructure $dataStructure, bool $isMobile = false): string
414
    {
415
        // Collect data for the search index
416
        $username = mb_strtolower($dataStructure->user_username);
417
        $callerId = mb_strtolower(self::sanitizeCallerId($dataStructure->user_username));
418
        $email = mb_strtolower($dataStructure->user_email);
419
        $internalNumber = mb_strtolower($dataStructure->number);
420
        $mobileNumber = $isMobile ? mb_strtolower($dataStructure->mobile_number) : '';
421
422
        // Combine all fields into a single string
423
        return $username . ' ' . $callerId . ' ' . $email . ' ' . $internalNumber . ' ' . $mobileNumber;
424
    }
425
426
    /**
427
     * Sanitize the caller ID by removing invalid characters.
428
     * Allows letters (from all languages), numbers, spaces, and common phone symbols.
429
     *
430
     * @param string $callerId
431
     * @return string
432
     */
433
    private static function sanitizeCallerId(string $callerId): string
434
    {
435
        // The letter Ё is not displayed correctly on some devices
436
        $callerId = str_replace(['Ё','ё'], ['E', 'e'], $callerId);
437
        // Allow letters from any language, numbers, spaces, and common phone symbols.
438
        return preg_replace('/[^\p{L}\p{N}\s\+\-\.\@]/u', '', $callerId);
439
    }
440
}