ThreemaGateway_DataWriter_Messages   B
last analyzed

Complexity

Total Complexity 41

Size/Duplication

Total Lines 483
Duplicated Lines 6.63 %

Coupling/Cohesion

Components 2
Dependencies 3

Importance

Changes 0
Metric Value
wmc 41
lcom 2
cbo 3
dl 32
loc 483
rs 8.2769
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
B _getFields() 0 124 1
A roundReceiveDate() 0 4 1
A normalizeFilePath() 0 4 1
A _getUpdateCondition() 0 4 1
C _preSave() 0 67 16
B _preDelete() 0 20 5
B _getExistingData() 32 65 6
B _postSave() 0 32 5
B _postDelete() 0 31 2
A getRoundedReceiveDate() 0 16 2
A _getMessagesModel() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ThreemaGateway_DataWriter_Messages often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ThreemaGateway_DataWriter_Messages, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * DataWriter for Threema messages.
4
 *
5
 * @package ThreemaGateway
6
 * @author rugk
7
 * @copyright Copyright (c) 2016 rugk
8
 * @license MIT
9
 */
10
11
class ThreemaGateway_DataWriter_Messages extends XenForo_DataWriter
12
{
13
    /**
14
     * @var string extra data - files
15
     */
16
    const DATA_FILES = 'files';
17
18
    /**
19
     * @var string extra data - acknowledged message IDs
20
     */
21
    const DATA_ACKED_MSG_IDS = 'ack_message_id';
22
23
    /**
24
     * Gets the fields that are defined for the table. See parent for explanation.
25
     *
26
     * @see XenForo_DataWriter::_getFields()
27
     * @return array
28
     */
29
    protected function _getFields()
30
    {
31
        return [
32
            ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES => [
33
                'message_id' => [
34
                    'type' => self::TYPE_STRING,
35
                    'required'  => true,
36
                    'maxLength' => 16
37
                ],
38
                'message_type_code' => [
39
                    'type' => self::TYPE_UINT
40
                ],
41
                'sender_threema_id' => [
42
                    'type' => self::TYPE_STRING,
43
                    'maxLength' => 8
44
                ],
45
                'date_send' => [
46
                    'type' => self::TYPE_UINT
47
                ],
48
                'date_received' => [
49
                    'type' => self::TYPE_UINT,
50
                    'required' => true,
51
                    'default' => XenForo_Application::$time
52
                ]
53
            ],
54
            ThreemaGateway_Model_Messages::DB_TABLE_FILES => [
55
                'file_id' => [
56
                    'type' => self::TYPE_UINT,
57
                    'autoIncrement' => true
58
                ],
59
                'message_id' => [
60
                    'type' => self::TYPE_STRING,
61
                    'required'  => true,
62
                    'maxLength' => 16
63
                ],
64
                'file_path' => [
65
                    'type' => self::TYPE_STRING,
66
                    'required'  => true,
67
                    'maxLength' => 255
68
                ],
69
                'file_type' => [
70
                    'type' => self::TYPE_STRING,
71
                    'required'  => true,
72
                    'maxLength' => 100
73
                ],
74
                'is_saved' => [
75
                    'type' => self::TYPE_BOOLEAN,
76
                    'required'  => true,
77
                    'maxLength' => 1,
78
                    'default' => true
79
                ]
80
            ],
81
            ThreemaGateway_Model_Messages::DB_TABLE_DELIVERY_RECEIPT => [
82
                'ack_id' => [
83
                    'type' => self::TYPE_UINT,
84
                    'autoIncrement' => true
85
                ],
86
                'message_id' => [
87
                    'type' => self::TYPE_STRING,
88
                    'required'  => true,
89
                    'maxLength' => 16
90
                ],
91
                'ack_message_id' => [
92
                    'type' => self::TYPE_STRING,
93
                    'required'  => true,
94
                    'maxLength' => 16
95
                ]
96
            ],
97
            ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES . '_delivery_receipt' => [
98
                'message_id' => [
99
                    'type' => self::TYPE_STRING,
100
                    'required'  => true,
101
                    'maxLength' => 16
102
                ],
103
                'receipt_type' => [
104
                    'type' => self::TYPE_UINT,
105
                    'required'  => true
106
                ]
107
            ],
108
            ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES . '_file' => [
109
                'message_id' => [
110
                    'type' => self::TYPE_STRING,
111
                    'required'  => true,
112
                    'maxLength' => 16
113
                ],
114
                'file_size' => [
115
                    'type' => self::TYPE_UINT,
116
                    'required'  => true
117
                ],
118
                'file_name' => [
119
                    'type' => self::TYPE_STRING,
120
                    'required'  => true,
121
                    'maxLength' => 255
122
                ],
123
                'mime_type' => [
124
                    'type' => self::TYPE_STRING,
125
                    'required'  => true,
126
                    'maxLength' => 255
127
                ]
128
            ],
129
            ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES . '_image' => [
130
                'message_id' => [
131
                    'type' => self::TYPE_STRING,
132
                    'required'  => true,
133
                    'maxLength' => 16
134
                ],
135
                'file_size' => [
136
                    'type' => self::TYPE_UINT,
137
                    'required'  => true
138
                ]
139
            ],
140
            ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES . '_text' => [
141
                'message_id' => [
142
                    'type' => self::TYPE_STRING,
143
                    'required'  => true,
144
                    'maxLength' => 16
145
                ],
146
                'text' => [
147
                    'type' => self::TYPE_STRING,
148
                    'required'  => true
149
                ]
150
            ]
151
        ];
152
    }
153
154
155
    /**
156
     * Generalises the receive date to reduce the amount of stored meta data.
157
     *
158
     * Generally you may also want to call this if the data you are inserting
159
     * is only placeholder data (aka the message ID + receive date).
160
     * All existing data should already be set when calling this function.
161
     */
162
    public function roundReceiveDate()
163
    {
164
        $this->set('date_received', $this->getRoundedReceiveDate());
165
    }
166
167
    /**
168
     * Normalizes the file path returned by the PHP SDK to a common format.
169
     *
170
     * Currently this removes the directory structure, so that only the file
171
     * name is saved.
172
     *
173
     * @param  string $filepath
174
     * @return string
175
     */
176
    public function normalizeFilePath($filepath)
177
    {
178
        return basename($filepath);
179
    }
180
181
    /**
182
     * Gets the actual existing data out of data that was passed in. See parent for explanation.
183
     *
184
     * The implementation is incomplete as it only builds an array with message
185
     * ids and no real data. This is however done on purpose as this function is
186
     * currently only used for deleting data. Updates can never happen in any
187
     * message table.
188
     *
189
     * @param mixed $data
190
     * @see XenForo_DataWriter::_getExistingData()
191
     * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be false|array?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
192
     */
193
    protected function _getExistingData($data)
194
    {
195
        /** @var string $messageId */
196
        if (!$messageId = $this->_getExistingPrimaryKey($data, 'message_id')) {
197
            return false;
198
        }
199
200
        /** @var array $existing Array of existing data. (filled below) */
201
        $existing = [];
202
203
        $this->_getMessagesModel()->setMessageId($messageId);
204
        /** @var array $metaData */
205
        $metaData = $this->_getMessagesModel()->getMessageMetaData();
206
207
        // add main table to array (this is the only complete table using)
208
        $existing[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES] = reset($metaData);
209
210
        /** @var int $messageType Extracted message type from metadata. */
211
        $messageType = reset($metaData)['message_type_code'];
212
213
        // conditionally add data from other tables depending on message
214
        // type
215
        switch ($messageType) {
216 View Code Duplication
            case ThreemaGateway_Model_Messages::TYPE_DELIVERY_MESSAGE:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
217
                $existing[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES . '_delivery_receipt'] = [
218
                    'message_id' => $messageId
219
                ];
220
                $existing[ThreemaGateway_Model_Messages::DB_TABLE_DELIVERY_RECEIPT] = [
221
                    'message_id' => $messageId
222
                ];
223
                break;
224
225 View Code Duplication
            case ThreemaGateway_Model_Messages::TYPE_FILE_MESSAGE:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
226
                $existing[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES . '_file'] = [
227
                    'message_id' => $messageId
228
                ];
229
                $existing[ThreemaGateway_Model_Messages::DB_TABLE_FILES] = [
230
                    'message_id' => $messageId
231
                ];
232
                break;
233
234 View Code Duplication
            case ThreemaGateway_Model_Messages::TYPE_IMAGE_MESSAGE:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
235
                $existing[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES . '_image'] = [
236
                    'message_id' => $messageId
237
                ];
238
                $existing[ThreemaGateway_Model_Messages::DB_TABLE_FILES] = [
239
                    'message_id' => $messageId
240
                ];
241
                break;
242
243 View Code Duplication
            case ThreemaGateway_Model_Messages::TYPE_TEXT_MESSAGE:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
244
                $existing[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES . '_text'] = [
245
                    'message_id' => $messageId
246
                ];
247
                $existing[ThreemaGateway_Model_Messages::DB_TABLE_FILES] = [
248
                    'message_id' => $messageId
249
                ];
250
                break;
251
252
            default:
253
                throw new XenForo_Exception(new XenForo_Phrase('threemagw_unknown_message_type'));
254
        }
255
256
        return $existing;
257
    }
258
259
    /**
260
     * Gets SQL condition to update the existing record.
261
     *
262
     * @param string $tableName
263
     * @see XenForo_DataWriter::_getUpdateCondition()
264
     * @return string
265
     */
266
    protected function _getUpdateCondition($tableName)
267
    {
268
        return 'message_id = ' . $this->_db->quote($this->getExisting('message_id'));
269
    }
270
271
    /**
272
     * Pre-save: Removes tables, which should not be touched.
273
     *
274
     * The function searches for invalid tables and removes them from the query.
275
     * This is necessary as a message can only be an instance of one message
276
     * type and as by default all tables (& therefore types) are included in the
277
     * fields, we have to confitionally remove them.
278
     * Additionally it ses the correct character encoding.
279
     *
280
     * @see XenForo_DataWriter::_preSave()
281
     */
282
    protected function _preSave()
283
    {
284
        // filter data
285
        // also uses existing data as a data base as otherwise the main table
286
        // may also get deleted because of missing message id
287
        $newData = array_merge($this->getNewData(), $this->_existingData);
288
289
        foreach ($this->getTables() as $tableName) {
290
            // search for (invalid) tables with
291
            if (
292
                !array_key_exists($tableName, $newData) || // no data OR
293
                !array_key_exists('message_id', $newData[$tableName]) || // missing message_id OR
294
                (count($newData[$tableName]) == 1 && $tableName != ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES) // message_id as the only data set (and it's not the main message table where this is valid)
295
            ) {
296
                // and remove them
297
                unset($this->_fields[$tableName]);
298
            }
299
        }
300
301
        // check whether there is other data in the main table
302
        /** @var bool $isData whether in the main table is other data than the message ID */
303
        $isData = false;
304
        foreach ($this->_fields[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES] as $field => $fieldData) {
305
            if ($field == 'message_id') {
306
                // skip as requirement already checked
307
                continue;
308
            }
309
            if ($field == 'date_received') {
310
                continue;
311
            }
312
313
            if ($this->getNew($field, ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES)) {
314
                $isData = true;
315
                break;
316
            }
317
        }
318
319
        // validate data (either main table contains only basic data *OR* it requires all data fields)
320
        foreach ($this->_fields[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES] as $field => $fieldData) {
321
            if ($field == 'message_id') {
322
                // skip as requirement already checked
323
                continue;
324
            }
325
            if ($field == 'date_received') {
326
                continue;
327
            }
328
329
            // when table does not contain data
330
            if (!$isData) {
331
                // make sure data is really "null" and not some other type of data by removing it completly from the model
332
                unset($this->_newData[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES][$field]);
333
                unset($this->_fields[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES][$field]);
334
                continue;
335
            }
336
337
            // table contains data, but required key is missing
338
            if (
339
                !$this->getNew($field, ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES) &&
340
                !isset($fieldData['default']) // exception: a default value is set
341
            ) {
342
                $this->_triggerRequiredFieldError(ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES, $field);
343
            }
344
        }
345
346
        // set correct character encoding
347
        $this->_db->query('SET NAMES utf8mb4');
348
    }
349
350
    /**
351
     * Pre-delete: Remove main table & unused tables from selected existing data.
352
     *
353
     * The reason for the deletion is, that the message ID should stay in the
354
     * database and must not be deleted.
355
     *
356
     * @see XenForo_DataWriter::_preDelete()
357
     */
358
    protected function _preDelete()
359
    {
360
        // we may need to store the message ID to prevent replay attacks
361
        if (ThreemaGateway_Helper_Message::isAtRiskOfReplayAttack($this->_existingData[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES])) {
362
            // remove main table from deletion as it is handled in _postDelete().
363
            unset($this->_fields[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES]);
364
        }
365
366
        // similar to _preSave() filter data
367
        foreach ($this->getTables() as $tableName) {
368
            // search for (invalid) tables with
369
            if (
370
                !array_key_exists($tableName, $this->_existingData) || // no data OR
371
                !array_key_exists('message_id', $this->_existingData[$tableName]) // missing message_id
372
            ) {
373
                // and remove them
374
                unset($this->_fields[$tableName]);
375
            }
376
        }
377
    }
378
379
    /**
380
     * Post-save: Add additional data supplied as extra data.
381
     *
382
     * This function writes the missing datasets into the files and the
383
     * acknowleged messages table.
384
     *
385
     * @see XenForo_DataWriter::_postSave()
386
     */
387
    protected function _postSave()
388
    {
389
        // get data
390
        $allFiles    = $this->getExtraData(self::DATA_FILES);
391
        $ackedMsgIds = $this->getExtraData(self::DATA_ACKED_MSG_IDS);
392
393
        // add additional data
394
        if ($allFiles) {
395
            foreach ($allFiles as $fileType => $filePath) {
396
                // insert additional files into database
397
                $this->_db->insert(ThreemaGateway_Model_Messages::DB_TABLE_FILES,
398
                    [
399
                        'message_id' => $this->get('message_id'),
400
                        'file_path' => $this->normalizeFilePath($filePath),
401
                        'file_type' => $fileType
402
                    ]
403
                );
404
            }
405
        }
406
407
        if ($ackedMsgIds) {
408
            foreach ($ackedMsgIds as $ackedMessageId) {
409
                // insert additional data into database
410
                $this->_db->insert(ThreemaGateway_Model_Messages::DB_TABLE_DELIVERY_RECEIPT,
411
                    [
412
                        'message_id' => $this->get('message_id'),
413
                        'ack_message_id' => $ackedMessageId
414
                    ]
415
                );
416
            }
417
        }
418
    }
419
420
    /**
421
     * Post-delete: Remove all data from main table, except of message ID &
422
     * the receive date.
423
     *
424
     * The reason for the deletion is, that the message ID should stay in the
425
     * database and must not be deleted as this prevents replay attacks
426
     * ({@see ThreemaGateway_Handler_Action_Receiver->removeMessage()}).
427
     *
428
     * @see XenForo_DataWriter::_postDelete()
429
     */
430
    protected function _postDelete()
431
    {
432
        // skip custom deletion if main table has already been deleted and is
433
        // therefore stil in the fields array
434
        if (isset($this->_fields[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES])) {
435
            return;
436
        }
437
438
        // get table fields
439
        /** @var array $tableFields fields of main message table */
440
        $tableFields = $this->_getFields()[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES];
441
        // remove keys, which should stay in the database
442
        unset($tableFields['message_id']);
443
        unset($tableFields['date_received']);
444
        // date_received is not removed as this is needed for real deletion;
445
        // below it is also updated to a generalised value to reduce the amount
446
        // of saved meta data
447
448
        // we do only care about the keys
449
        /** @var array $tableKeys extracted keys from fields */
450
        $tableKeys = array_keys($tableFields);
451
452
        // remove values from database
453
        $this->_db->update(ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES,
454
            array_merge(
455
                array_fill_keys($tableKeys, null),
456
                ['date_received' => $this->getRoundedReceiveDate()]
457
            ),
458
            $this->getUpdateCondition(ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES)
459
        );
460
    }
461
462
    /**
463
     * Gets the receive date in a rounded way.
464
     *
465
     * @return int
466
     */
467
    protected function getRoundedReceiveDate()
468
    {
469
        /* @var int|null $receiveDate */
470
        // get specified value
471
        $receiveDate = $this->get('date_received');
472
473
        // get default if not set
474
        if (!$receiveDate) {
475
            $receiveDate = $this->_getFields()[ThreemaGateway_Model_Messages::DB_TABLE_MESSAGES]['date_received']['default'];
476
        }
477
478
        // round unix time to day (00:00)
479
        $receiveDate = ThreemaGateway_Helper_General::roundToDay($receiveDate);
480
481
        return (int) $receiveDate;
482
    }
483
484
    /**
485
     * Get the messages model.
486
     *
487
     * @return ThreemaGateway_Model_Messages
488
     */
489
    protected function _getMessagesModel()
490
    {
491
        return $this->getModelFromCache('ThreemaGateway_Model_Messages');
492
    }
493
}
494