ReadHandler   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 340
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 128
dl 0
loc 340
rs 8.64
c 0
b 0
f 0
wmc 47

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 3
A clearGarbage() 0 25 5
A setReadCookie() 0 6 2
A setReadDb() 0 21 5
A setRead() 0 11 3
A isReadItemsCookie() 0 11 3
A getRead() 0 10 3
A getReadDb() 0 19 4
A isReadItemsDb() 0 30 6
A getReadCookie() 0 8 2
B clearDuplicate() 0 53 8
A isReadItems() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like ReadHandler 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.

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 ReadHandler, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace XoopsModules\Newbb;
4
5
/*
6
 * You may not change or alter any portion of this comment or credits
7
 * of supporting developers from this source code or any supporting source code
8
 * which is considered copyrighted (c) material of the original comment or credit authors.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13
 */
14
//  ------------------------------------------------------------------------ //
15
//  Author: phppp (D.J., [email protected])                                  //
16
//  URL: https://xoops.org                                                    //
17
//  Project: Article Project                                                 //
18
//  ------------------------------------------------------------------------ //
19
20
\defined('NEWBB_FUNCTIONS_INI') || require $GLOBALS['xoops']->path('modules/newbb/include/functions.ini.php');
21
22
/**
23
 * A handler for read/unread handling
24
 *
25
 *
26
 * @author        D.J. (phppp, https://xoopsforge.com)
27
 * @copyright     copyright (c) 2005 XOOPS.org
28
 */
29
30
/**
31
 * Class ReadHandler
32
 */
33
class ReadHandler extends \XoopsPersistableObjectHandler
34
{
35
    /**
36
     * @var \XoopsMySQLDatabase $db
37
     */
38
    public $db;
39
    /**
40
     * Object type.
41
     * <ul>
42
     *  <li>forum</li>
43
     *  <li>topic</li>
44
     * </ul>
45
     *
46
     * @var string
47
     */
48
    public string $type;
49
    /**
50
     * seconds records will persist.
51
     * assigned from $GLOBALS['xoopsModuleConfig']["read_expire"]
52
     * <ul>
53
     *  <li>positive days = delete all read records exist in the tables before expire time // irmtfan add comment</li>
54
     *  <li>0 = never expires // irmtfan change comment</li>
55
     *  <li>-1 or any negative days = never records // irmtfan change comment</li>
56
     * </ul>
57
     *
58
     * @var int
59
     */
60
    public $lifetime;
61
    /**
62
     * storage mode for records.
63
     * assigned from $GLOBALS['xoopsModuleConfig']["read_mode"]
64
     * <ul>
65
     *  <li>0 = never records</li>
66
     *  <li>1 = uses cookie</li>
67
     *  <li>2 = stores in database</li>
68
     * </ul>
69
     *
70
     * @var int
71
     */
72
    public $mode;
73
74
    /**
75
     * @param null|\XoopsMySQLDatabase $db
76
     * @param string                   $type
77
     */
78
    public function __construct(\XoopsMySQLDatabase $db, $type)
79
    {
80
        $type = ('forum' === $type) ? 'forum' : 'topic';
81
        parent::__construct($db, 'newbb_reads_' . $type, Read::class . $type, 'read_id', 'post_id');
82
        $this->type  = $type;
83
        $newbbConfig = \newbbLoadConfig();
84
        // irmtfan if read_expire = 0 dont clean
85
        $this->lifetime = isset($newbbConfig['read_expire']) ? (int)$newbbConfig['read_expire'] * 24 * 3600 : 30 * 24 * 3600;
86
        $this->mode     = $newbbConfig['read_mode'] ?? 2;
87
    }
88
89
    /**
90
     * Clear garbage
91
     *
92
     * Delete all expired and duplicated records
93
     */
94
    // START irmtfan rephrase function to 1- add clearDuplicate and 2- dont clean when read_expire = 0
95
    public function clearGarbage(): bool
96
    {
97
        // irmtfan clear duplicaed rows
98
        if (!$result = $this->clearDuplicate()) {
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
99
            return false;
100
        }
101
102
        $sql = 'DELETE bb FROM ' . $this->table . ' AS bb' . ' LEFT JOIN ' . $this->table . ' AS aa ON bb.read_item = aa.read_item ' . ' WHERE aa.post_id > bb.post_id';
103
        if (!$result = $this->db->queryF($sql)) {
104
            //xoops_error($this->db->error());
105
            return false;
106
        }
107
        // irmtfan if read_expire = 0 dont clean
108
        if (empty($this->lifetime)) {
109
            return true;
110
        }
111
        // irmtfan move here and rephrase
112
        $expire = \time() - (int)$this->lifetime;
113
        $sql    = 'DELETE FROM ' . $this->table . ' WHERE read_time < ' . $expire;
114
        if (!$result = $this->db->queryF($sql)) {
115
            //xoops_error($this->db->error());
116
            return false;
117
        }
118
119
        return true;
120
    }
121
122
    // END irmtfan rephrase function to 1- add clearDuplicate and 2- don't clean when read_expire = 0
123
    /**
124
     * @param int      $read_item
125
     * @param int|null $uid
126
     * @return bool|int
127
     */
128
    public function getRead(int $read_item, int $uid = null)
129
    {
130
        if (empty($this->mode)) {
131
            return false;
132
        }
133
        if (1 == $this->mode) {
134
            return $this->getReadCookie($read_item);
135
        }
136
137
        return $this->getReadDb($read_item, $uid);
138
    }
139
140
    /**
141
     * @param int $item_id
142
     * @return mixed
143
     */
144
    public function getReadCookie(int $item_id)
145
    {
146
        $cookie_name = ('forum' === $this->type) ? 'LF' : 'LT';
147
        $cookie_var  = $item_id;
148
        // irmtfan set true to return array
149
        $lastview = \newbbGetCookie($cookie_name, true);
150
151
        return @$lastview[$cookie_var];
152
    }
153
154
    /**
155
     * @param int $read_item
156
     * @param int|null $uid
157
     * @return bool|int|null
158
     */
159
    public function getReadDb(int $read_item, ?int $uid =  null)
160
    {
161
        if (empty($uid)) {
162
            if (\is_object($GLOBALS['xoopsUser'])) {
163
                $uid = $GLOBALS['xoopsUser']->getVar('uid');
164
            } else {
165
                return false;
166
            }
167
        }
168
        $sql = 'SELECT post_id ' . ' FROM ' . $this->table . ' WHERE read_item = ' . (int)$read_item . '     AND uid = ' . (int)$uid;
169
        $result = $this->db->queryF($sql, 1);
170
        if (!$this->db->isResultSet($result)) {
171
            //                \trigger_error("Query Failed! SQL: $sql- Error: " . $this->db->error(), E_USER_ERROR);
172
            return false;
173
        }
174
175
        [$post_id] = $this->db->fetchRow($result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type boolean; however, parameter $result of XoopsMySQLDatabase::fetchRow() does only seem to accept mysqli_result, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

175
        [$post_id] = $this->db->fetchRow(/** @scrutinizer ignore-type */ $result);
Loading history...
176
177
        return (int)$post_id;
178
    }
179
180
    /**
181
     * @param int      $read_item
182
     * @param int      $post_id
183
     * @param int|null $uid
184
     * @return bool|mixed|void
185
     */
186
    public function setRead(int $read_item, int $post_id, int $uid = null)
187
    {
188
        if (empty($this->mode)) {
189
            return true;
190
        }
191
192
        if (1 == $this->mode) {
193
            return $this->setReadCookie($read_item, $post_id);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->setReadCookie($read_item, $post_id) targeting XoopsModules\Newbb\ReadHandler::setReadCookie() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
194
        }
195
196
        return $this->setReadDb($read_item, $post_id, $uid);
197
    }
198
199
    /**
200
     * @param int $read_item
201
     * @param int $post_id
202
     */
203
    public function setReadCookie(int $read_item, int $post_id): void
0 ignored issues
show
Unused Code introduced by
The parameter $post_id is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

203
    public function setReadCookie(int $read_item, /** @scrutinizer ignore-unused */ int $post_id): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
204
    {
205
        $cookie_name          = ('forum' === $this->type) ? 'LF' : 'LT';
206
        $lastview             = \newbbGetCookie($cookie_name, true);
207
        $lastview[$read_item] = \time();
208
        \newbbSetCookie($cookie_name, $lastview);
209
    }
210
211
    /**
212
     * @param int      $read_item
213
     * @param int|null $post_id
214
     * @param int|null $uid
215
     * @return bool|mixed
216
     */
217
    public function setReadDb(int $read_item, int $post_id, ?int $uid = null)
218
    {
219
        if (empty($uid)) {
220
            if (\is_object($GLOBALS['xoopsUser'])) {
221
                $uid = $GLOBALS['xoopsUser']->getVar('uid');
222
            } else {
223
                return false;
224
            }
225
        }
226
227
        $sql = 'UPDATE ' . $this->table . ' SET post_id = ' . (int)$post_id . ',' . '     read_time =' . \time() . ' WHERE read_item = ' . (int)$read_item . '     AND uid = ' . (int)$uid;
228
        if ($this->db->queryF($sql) && $this->db->getAffectedRows()) {
229
            return true;
230
        }
231
        $object = $this->create();
232
        $object->setVar('read_item', $read_item);
233
        $object->setVar('post_id', $post_id);
234
        $object->setVar('uid', $uid);
235
        $object->setVar('read_time', \time());
236
237
        return $this->insert($object);
238
    }
239
240
    /**
241
     * @param array       $items
242
     * @param string|null $uid
243
     * @return array|null
244
     */
245
    public function isReadItems(array $items, string $uid = null): ?array
246
    {
247
        $ret = null;
248
        if (empty($this->mode)) {
249
            return null;
250
        }
251
252
        if (1 == $this->mode) {
253
            $ret = $this->isReadItemsCookie($items);
254
        } else {
255
            $ret = $this->isReadItemsDb($items, $uid);
256
        }
257
258
        return $ret;
259
    }
260
261
    /**
262
     * @param array $items
263
     * @return array
264
     */
265
    public function isReadItemsCookie(array $items): array
266
    {
267
        $cookie_name = ('forum' === $this->type) ? 'LF' : 'LT';
268
        $cookie_vars = \newbbGetCookie($cookie_name, true);
269
270
        $ret = [];
271
        foreach ($items as $key => $last_update) {
272
            $ret[$key] = (\max(@$GLOBALS['last_visit'], @$cookie_vars[$key]) >= $last_update);
273
        }
274
275
        return $ret;
276
    }
277
278
    /**
279
     * @param array       $items
280
     * @param string|null $uid
281
     * @return array
282
     */
283
    public function isReadItemsDb(array $items, ?string $uid = null): array
284
    {
285
        $ret = [];
286
        if (empty($items)) {
287
            return $ret;
288
        }
289
290
        if (empty($uid)) {
291
            if (\is_object($GLOBALS['xoopsUser'])) {
292
                $uid = $GLOBALS['xoopsUser']->getVar('uid');
293
            } else {
294
                return $ret;
295
            }
296
        }
297
298
        $criteria = new \CriteriaCompo(new \Criteria('uid', $uid));
299
        $criteria->add(new \Criteria('read_item', '(' . \implode(', ', \array_map('\intval', \array_keys($items))) . ')', 'IN'));
300
        $itemsObject = $this->getAll($criteria, ['read_item', 'post_id']);
301
302
        $items_list = [];
303
        foreach (\array_keys($itemsObject) as $key) {
304
            $items_list[$itemsObject[$key]->getVar('read_item')] = $itemsObject[$key]->getVar('post_id');
305
        }
306
        unset($itemsObject);
307
308
        foreach ($items as $key => $last_update) {
309
            $ret[$key] = (@$items_list[$key] >= $last_update);
310
        }
311
312
        return $ret;
313
    }
314
315
    // START irmtfan add clear duplicated rows function
316
317
    /**
318
     * @return bool
319
     */
320
    public function clearDuplicate(): bool
321
    {
322
        /**
323
         * This is needed for the following query GROUP BY clauses to work in MySQL 5.7.
324
         * This is a TEMPORARY fix. Needing this function is bad in the first place, but
325
         * needing sloppy SQL to make it work is worse.
326
         * @todo The schema itself should preclude the duplicates
327
         */
328
        $sql = "SET sql_mode=(SELECT REPLACE(@@sql_mode, 'ONLY_FULL_GROUP_BY', ''))";
329
        $this->db->queryF($sql);
330
331
        $sql = 'CREATE TABLE ' . $this->table . '_duplicate LIKE ' . $this->table . '; ';
332
        if (!$result = $this->db->queryF($sql)) {
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
333
            \xoops_error($this->db->error() . '<br>' . $sql);
334
335
            return false;
336
        }
337
        $sql = 'INSERT ' . $this->table . '_duplicate SELECT * FROM ' . $this->table . ' GROUP BY read_item, uid; ';
338
        if (!$result = $this->db->queryF($sql)) {
339
            \xoops_error($this->db->error() . '<br>' . $sql);
340
341
            return false;
342
        }
343
        $sql = 'RENAME TABLE ' . $this->table . ' TO ' . $this->table . '_with_duplicate; ';
344
        if (!$result = $this->db->queryF($sql)) {
345
            \xoops_error($this->db->error() . '<br>' . $sql);
346
347
            return false;
348
        }
349
        $sql = 'RENAME TABLE ' . $this->table . '_duplicate TO ' . $this->table . '; ';
350
        if (!$result = $this->db->queryF($sql)) {
351
            \xoops_error($this->db->error() . '<br>' . $sql);
352
353
            return false;
354
        }
355
        $sql    = 'SHOW INDEX FROM ' . $this->table . " WHERE KEY_NAME = 'read_item_uid'";
356
        $result = $this->db->queryF($sql);
357
        if (empty($result)) {
358
            $sql .= 'ALTER TABLE ' . $this->table . ' ADD INDEX read_item_uid ( read_item, uid ); ';
359
            if (!$result = $this->db->queryF($sql)) {
360
                \xoops_error($this->db->error() . '<br>' . $sql);
361
362
                return false;
363
            }
364
        }
365
        $sql = 'DROP TABLE ' . $this->table . '_with_duplicate; ';
366
        if (!$result = $this->db->queryF($sql)) {
367
            \xoops_error($this->db->error() . '<br>' . $sql);
368
369
            return false;
370
        }
371
372
        return true;
373
    }
374
    // END irmtfan add clear duplicated rows function
375
}
376