Passed
Pull Request — master (#41)
by Michael
13:46
created

SysUtility   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 492
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 194
c 2
b 0
f 0
dl 0
loc 492
rs 3.52
wmc 61

17 Methods

Rating   Name   Duplication   Size   Complexity  
A metaDescription() 0 9 2
F truncateHtml() 0 95 19
A metaKeywords() 0 9 2
A cloneRecord() 0 26 4
A renameUploadFolder() 0 23 3
A cleanCache() 0 17 2
A fieldExists() 0 7 1
A blockAddCatSelect() 0 13 3
A prepareFolder() 0 9 5
A queryFAndCheck() 0 11 2
A getEditor() 0 34 5
A enumerate() 0 20 2
A selectSorting() 0 24 4
A queryAndCheck() 0 10 2
A tableExists() 0 14 2
A getInstance() 0 8 2
A addField() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like SysUtility 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 SysUtility, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace XoopsModules\News\Common;
4
5
/*
6
 Utility Class Definition
7
8
 You may not change or alter any portion of this comment or credits of
9
 supporting developers from this source code or any supporting source code
10
 which is considered copyrighted (c) material of the original comment or credit
11
 authors.
12
13
 This program is distributed in the hope that it will be useful, but
14
 WITHOUT ANY WARRANTY; without even the implied warranty of
15
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16
 */
17
18
/**
19
 * @license      GNU GPL 2.0 or later (https://www.gnu.org/licenses/gpl-2.0.html)
20
 * @copyright    https://xoops.org 2000-2020 &copy; XOOPS Project
21
 * @author       ZySpec <[email protected]>
22
 * @author       Mamba <[email protected]>
23
 */
24
25
use Xmf\Request;
26
use Xmf\Module\Helper\Cache;
27
use XoopsModules\News\{
28
    Helper
29
};
30
31
32
33
/**
34
 * Class SysUtility
35
 */
36
class SysUtility
37
{
38
    //traits
39
    use VersionChecks; //checkVerXoops, checkVerPhp Traits
0 ignored issues
show
introduced by
The trait XoopsModules\News\Common\VersionChecks requires some properties which are not provided by XoopsModules\News\Common\SysUtility: $tag_name, $prerelease
Loading history...
40
    use ServerStats; // getServerStats Trait
41
    use FilesManagement; // Files Management Trait
42
    use ModuleStats;    // ModuleStats Trait
0 ignored issues
show
Bug introduced by
The trait XoopsModules\News\Common\ModuleStats requires the property $moduleStats which is not provided by XoopsModules\News\Common\SysUtility.
Loading history...
43
44
    //--------------- Common module methods -----------------------------
45
46
    /**
47
     * Access the only instance of this class
48
     */
49
    public static function getInstance(): self
50
    {
51
        static $instance;
52
        if (null === $instance) {
53
            $instance = new static();
54
        }
55
56
        return $instance;
57
    }
58
59
    /**
60
     * @param string $text
61
     * @param string $form_sort
62
     *
63
     * @return string
64
     */
65
    public static function selectSorting(string $text, string $form_sort): string
66
    {
67
        global $start, $order, $sort;
68
69
        $selectView   = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $selectView is dead and can be removed.
Loading history...
70
        $helper = Helper::getInstance();
71
72
        //$pathModIcon16 = XOOPS_URL . '/modules/' . $moduleDirName . '/' . $helper->getConfig('modicons16');
73
        $pathModIcon16 = $helper->url($helper->getModule()->getInfo('modicons16'));
0 ignored issues
show
Bug introduced by
It seems like $helper->getModule()->getInfo('modicons16') can also be of type array; however, parameter $url of Xmf\Module\Helper\GenericHelper::url() does only seem to accept string, 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

73
        $pathModIcon16 = $helper->url(/** @scrutinizer ignore-type */ $helper->getModule()->getInfo('modicons16'));
Loading history...
74
75
        $selectView = '<form name="form_switch" id="form_switch" action="' . Request::getString('REQUEST_URI', '', 'SERVER') . '" method="post"><span style="font-weight: bold;">' . $text . '</span>';
76
        //$sorts =  $sort ==  'asc' ? 'desc' : 'asc';
77
        if ($form_sort == $sort) {
78
            $sel1 = 'asc' === $order ? 'selasc.png' : 'asc.png';
79
            $sel2 = 'desc' === $order ? 'seldesc.png' : 'desc.png';
80
        } else {
81
            $sel1 = 'asc.png';
82
            $sel2 = 'desc.png';
83
        }
84
        $selectView .= '  <a href="' . Request::getString('SCRIPT_NAME', '', 'SERVER') . '?start=' . $start . '&sort=' . $form_sort . '&order=asc"><img src="' . $pathModIcon16 . '/' . $sel1 . '" title="ASC" alt="ASC"></a>';
85
        $selectView .= '<a href="' . Request::getString('SCRIPT_NAME', '', 'SERVER') . '?start=' . $start . '&sort=' . $form_sort . '&order=desc"><img src="' . $pathModIcon16 . '/' . $sel2 . '" title="DESC" alt="DESC"></a>';
86
        $selectView .= '</form>';
87
88
        return $selectView;
89
    }
90
91
    //---------------  BLOCKS -----------------------------
92
93
    /**
94
     * @param array $cats
95
     *
96
     * @return string
97
     */
98
    public static function blockAddCatSelect(array $cats): string
99
    {
100
        $catSql = '';
101
        if (!empty($cats)) {
102
            $catSql = '(' . \current($cats);
103
            \array_shift($cats);
104
            foreach ($cats as $cat) {
105
                $catSql .= ',' . $cat;
106
            }
107
            $catSql .= ')';
108
        }
109
110
        return $catSql;
111
    }
112
113
    /**
114
     * @param string $content
115
     * @return void
116
     */
117
    public static function metaKeywords(string $content): void
118
    {
119
        global $xoopsTpl, $xoTheme;
120
        $myts    = \MyTextSanitizer::getInstance();
121
        $content = $myts->undoHtmlSpecialChars($myts->displayTarea($content));
122
        if (\is_object($xoTheme)) {
123
            $xoTheme->addMeta('meta', 'keywords', \strip_tags($content));
124
        } else {    // Compatibility for old Xoops versions
125
            $xoopsTpl->assign('xoops_metaKeywords', \strip_tags($content));
126
        }
127
    }
128
129
    /**
130
     * @param string $content
131
     * @return void
132
     */
133
    public static function metaDescription(string $content): void
134
    {
135
        global $xoopsTpl, $xoTheme;
136
        $myts    = \MyTextSanitizer::getInstance();
137
        $content = $myts->undoHtmlSpecialChars($myts->displayTarea($content));
138
        if (\is_object($xoTheme)) {
139
            $xoTheme->addMeta('meta', 'description', \strip_tags($content));
140
        } else {    // Compatibility for old Xoops versions
141
            $xoopsTpl->assign('xoops_metaDescription', \strip_tags($content));
142
        }
143
    }
144
145
    /**
146
     * @param string $tableName
147
     * @param string $columnName
148
     *
149
     * @return array|null
150
     */
151
    public static function enumerate(string $tableName, string $columnName): ?array
152
    {
153
        $table = $GLOBALS['xoopsDB']->prefix($tableName);
154
155
        //    $result = $GLOBALS['xoopsDB']->query("SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS
156
        //        WHERE TABLE_NAME = '" . $table . "' AND COLUMN_NAME = '" . $columnName . "'")
157
        //    || exit ($GLOBALS['xoopsDB']->error());
158
159
        $sql    = 'SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = "' . $table . '" AND COLUMN_NAME = "' . $columnName . '"';
160
        $result = $GLOBALS['xoopsDB']->query($sql);
161
        if (!$GLOBALS['xoopsDB']->isResultSet($result)) {
162
            //            \trigger_error("Query Failed! SQL: $sql- Error: " . $GLOBALS['xoopsDB']->error(), E_USER_ERROR);
163
            $logger     = \XoopsLogger::getInstance();
164
            $logger->handleError(\E_USER_WARNING, $sql, __FILE__, __LINE__);
165
            return null;
166
        }
167
168
        $row      = $GLOBALS['xoopsDB']->fetchBoth($result);
169
        $enumList = \explode(',', \str_replace("'", '', \mb_substr($row['COLUMN_TYPE'], 5, - 6)));
170
        return $enumList;
171
    }
172
173
174
    /**
175
     * Clone a record in a dB
176
     *
177
     * @TODO need to exit more gracefully on error. Should throw/trigger error and then return false
178
     *
179
     * @param string $tableName name of dB table (without prefix)
180
     * @param string $idField   name of field (column) in dB table
181
     * @param int    $id        item id to clone
182
     * @return int|null
183
     */
184
    public static function cloneRecord(string $tableName, string $idField, int $id): ?int
185
    {
186
        $newId = null;
187
        $tempTable = [];
188
        $table  = $GLOBALS['xoopsDB']->prefix($tableName);
189
        // copy content of the record you wish to clone
190
        $sql       = "SELECT * FROM $table WHERE $idField='" . $id . "' ";
191
        $result = $GLOBALS['xoopsDB']->query($sql);
192
        if ($GLOBALS['xoopsDB']->isResultSet($result)) {
193
            $tempTable = $GLOBALS['xoopsDB']->fetchArray($result, \MYSQLI_ASSOC);
194
        }
195
        if (!$tempTable) {
196
            \trigger_error("Query Failed! SQL: $sql- Error: " . $GLOBALS['xoopsDB']->error(), \E_USER_ERROR);
197
        }
198
        // set the auto-incremented id's value to blank.
199
        unset($tempTable[$idField]);
200
        // insert cloned copy of the original  record
201
        $sql    = "INSERT INTO $table (" . \implode(', ', \array_keys($tempTable)) . ") VALUES ('" . \implode("', '", $tempTable) . "')";
202
        $result = $GLOBALS['xoopsDB']->queryF($sql);
203
        if (!$result) {
204
            \trigger_error("Query Failed! SQL: $sql- Error: " . $GLOBALS['xoopsDB']->error(), \E_USER_ERROR);
205
        } else {
206
            // Return the new id
207
            $newId = $GLOBALS['xoopsDB']->getInsertId();
208
        }
209
        return $newId;
210
    }
211
212
    /**
213
     * truncateHtml can truncate a string up to a number of characters while preserving whole words and HTML tags
214
     * www.gsdesign.ro/blog/cut-html-string-without-breaking-the-tags
215
     * www.cakephp.org
216
     *
217
     * @TODO: Refactor to consider HTML5 & void (self-closing) elements
218
     * @TODO: Consider using https://github.com/jlgrall/truncateHTML/blob/master/truncateHTML.php
219
     *
220
     * @param string $text         String to truncate.
221
     * @param int|null $length       Length of returned string, including ellipsis.
222
     * @param string $ending       Ending to be appended to the trimmed string.
223
     * @param bool   $exact        If false, $text will not be cut mid-word
224
     * @param bool   $considerHtml If true, HTML tags would be handled correctly
225
     *
226
     * @return string Trimmed string.
227
     */
228
    public static function truncateHtml(
229
        string $text,
230
        ?int $length = 100,
231
        string $ending = '...',
232
        bool $exact = false,
233
        bool $considerHtml = true
234
    ): string {
235
        $openTags = [];
236
        if ($considerHtml) {
237
            // if the plain text is shorter than the maximum length, return the whole text
238
            if (\mb_strlen(\preg_replace('/<.*?' . '>/', '', $text)) <= $length) {
239
                return $text;
240
            }
241
            // splits all html-tags to scanable lines
242
            \preg_match_all('/(<.+?' . '>)?([^<>]*)/s', $text, $lines, \PREG_SET_ORDER);
243
            $totalLength = \mb_strlen($ending);
244
            //$openTags    = [];
245
            $truncate = '';
246
            foreach ($lines as $lineMatchings) {
247
                // if there is any html-tag in this line, handle it and add it (uncounted) to the output
248
                if (!empty($lineMatchings[1])) {
249
                    // if it's an "empty element" with or without xhtml-conform closing slash
250
                    if (\preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $lineMatchings[1])) {
251
                        // do nothing
252
                        // if tag is a closing tag
253
                    } elseif (\preg_match('/^<\s*\/(\S+?)\s*>$/s', $lineMatchings[1], $tagMatchings)) {
254
                        // delete tag from $openTags list
255
                        $pos = \array_search($tagMatchings[1], $openTags, true);
256
                        if (false !== $pos) {
257
                            unset($openTags[$pos]);
258
                        }
259
                        // if tag is an opening tag
260
                    } elseif (\preg_match('/^<\s*([^\s>!]+).*?' . '>$/s', $lineMatchings[1], $tagMatchings)) {
261
                        // add tag to the beginning of $openTags list
262
                        \array_unshift($openTags, \mb_strtolower($tagMatchings[1]));
263
                    }
264
                    // add html-tag to $truncate'd text
265
                    $truncate .= $lineMatchings[1];
266
                }
267
                // calculate the length of the plain text part of the line; handle entities as one character
268
                $contentLength = \mb_strlen(\preg_replace('/&[0-9a-z]{2,8};|&#\d{1,7};|[0-9a-f]{1,6};/i', ' ', $lineMatchings[2]));
269
                if ($totalLength + $contentLength > $length) {
270
                    // the number of characters which are left
271
                    $left            = $length - $totalLength;
272
                    $entitiesLength = 0;
273
                    // search for html entities
274
                    if (\preg_match_all('/&[0-9a-z]{2,8};|&#\d{1,7};|[0-9a-f]{1,6};/i', $lineMatchings[2], $entities, \PREG_OFFSET_CAPTURE)) {
275
                        // calculate the real length of all entities in the legal range
276
                        foreach ($entities[0] as $entity) {
277
                            if ($left >= $entity[1] + 1 - $entitiesLength) {
278
                                $left--;
279
                                $entitiesLength += \mb_strlen($entity[0]);
280
                            } else {
281
                                // no more characters left
282
                                break;
283
                            }
284
                        }
285
                    }
286
                    $truncate .= \mb_substr($lineMatchings[2], 0, $left + $entitiesLength);
287
                    // maximum length is reached, so get off the loop
288
                    break;
289
                }
290
                $truncate     .= $lineMatchings[2];
291
                $totalLength += $contentLength;
292
293
                // if the maximum length is reached, get off the loop
294
                if ($totalLength >= $length) {
295
                    break;
296
                }
297
            }
298
        } else {
299
            if (\mb_strlen($text) <= $length) {
300
                return $text;
301
            }
302
            $truncate = \mb_substr($text, 0, $length - \mb_strlen($ending));
303
        }
304
        // if the words shouldn't be cut in the middle...
305
        if (!$exact) {
306
            // ...search the last occurance of a space...
307
            $spacepos = \mb_strrpos($truncate, ' ');
308
            if (isset($spacepos)) {
309
                // ...and cut the text in this position
310
                $truncate = \mb_substr($truncate, 0, $spacepos);
311
            }
312
        }
313
        // add the defined ending to the text
314
        $truncate .= $ending;
315
        if ($considerHtml) {
316
            // close all unclosed html-tags
317
            foreach ($openTags as $tag) {
318
                $truncate .= '</' . $tag . '>';
319
            }
320
        }
321
322
        return $truncate;
323
    }
324
325
    /**
326
     * Get correct text editor based on user rights
327
     *
328
     * @return \XoopsFormDhtmlTextArea|\XoopsFormEditor
329
     */
330
    public static function getEditor(?\Xmf\Module\Helper $helper = null, ?array $options = null): ?\XoopsFormTextArea
331
    {
332
        $descEditor = null;
333
334
        /** @var Helper $helper */
335
        if (null === $options) {
336
            $options           = [];
337
            $options['name']   = 'Editor';
338
            $options['value']  = 'Editor';
339
            $options['rows']   = 10;
340
            $options['cols']   = '100%';
341
            $options['width']  = '100%';
342
            $options['height'] = '400px';
343
        }
344
345
        if (null === $helper) {
346
            $helper = Helper::getInstance();
347
        }
348
349
        $isAdmin = $helper->isUserAdmin();
350
351
        if (\class_exists('XoopsFormEditor')) {
352
            if ($isAdmin) {
353
                $descEditor = new \XoopsFormEditor(\ucfirst((string) $options['name']), $helper->getConfig('editorAdmin'), $options, false, 'textarea');
354
            } else {
355
                $descEditor = new \XoopsFormEditor(\ucfirst((string) $options['name']), $helper->getConfig('editorUser'), $options, false, 'textarea');
356
            }
357
        } else {
358
            $descEditor = new \XoopsFormDhtmlTextArea(\ucfirst((string) $options['name']), $options['name'], $options['value']);
359
        }
360
361
        //        $form->addElement($descEditor);
362
363
        return $descEditor;
364
    }
365
366
    /**
367
     * Check if column in dB table exists
368
     *
369
     * @param string $fieldname name of dB table field
370
     * @param string $table     name of dB table (including prefix)
371
     *
372
     * @return bool true if table exists
373
     * @deprecated
374
     */
375
    public static function fieldExists(string $fieldname, string $table): bool
376
    {
377
        $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1);
378
        $GLOBALS['xoopsLogger']->addDeprecated(__METHOD__ . "() use Xmf\Database\Tables instead - instantiated from {$trace[0]['file']} line {$trace[0]['line']}");
379
380
        $result = $GLOBALS['xoopsDB']->queryF("SHOW COLUMNS FROM   $table LIKE '$fieldname'");
381
        return ($GLOBALS['xoopsDB']->getRowsNum($result) > 0);
382
    }
383
384
385
    /**
386
     * Function responsible for checking if a directory exists, we can also write in and create an index.html file
387
     *
388
     * @param string $folder The full path of the directory to check
389
     */
390
    public static function prepareFolder(string $folder): void
391
    {
392
        try {
393
            if (!\is_dir($folder) && !\mkdir($folder) && !\is_dir($folder)) {
394
                throw new \Exception(\sprintf('Unable to create the %s directory', $folder));
395
            }
396
            file_put_contents($folder . '/index.html', '<script>history.go(-1);</script>');
397
        } catch (\Exception $e) {
398
            echo 'Caught exception: ', $e->getMessage(), "\n", '<br>';
399
        }
400
    }
401
402
    /**
403
     * Check if dB table exists
404
     *
405
     * @param string $tablename dB tablename with prefix
406
     * @return bool true if table exists
407
     */
408
    public static function tableExists(string $tablename): bool
409
    {
410
        $ret    = false;
411
        $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1);
412
        $GLOBALS['xoopsLogger']->addDeprecated(
413
            \basename(\dirname(__DIR__, 2)) . ' Module: ' . __FUNCTION__ . ' function is deprecated, please use Xmf\Database\Tables method(s) instead.' . " Called from {$trace[0]['file']}line {$trace[0]['line']}"
414
        );
415
        $result = $GLOBALS['xoopsDB']->queryF("SHOW TABLES LIKE '$tablename'");
416
417
        if ($GLOBALS['xoopsDB']->isResultSet($result)) {
418
            $ret = $GLOBALS['xoopsDB']->getRowsNum($result) > 0;
419
        }
420
421
        return $ret;
422
    }
423
424
    /**
425
     * Add a field to a mysql table
426
     *
427
     * @return bool|\mysqli_result
428
     */
429
    public static function addField(string $field, string $table)
430
    {
431
        global $xoopsDB;
432
        return $xoopsDB->queryF('ALTER TABLE ' . $table . " ADD $field;");
433
    }
434
435
    /**
436
     * @return void
437
     */
438
    public static function cleanCache(): void
439
    {
440
        $myDirName = \basename(\dirname(__DIR__, 2));
441
        $cacheHelper = new Cache($myDirName);
442
        if (\method_exists($cacheHelper, 'clear')) {
443
            $cacheHelper->clear();
444
445
            return;
446
        }
447
        // for 2.5 systems, clear everything
448
        require_once XOOPS_ROOT_PATH . '/modules/system/class/maintenance.php';
449
        $maintenance = new \SystemMaintenance();
450
        $cacheList   = [
451
            3, // xoops_cache
452
        ];
453
        $maintenance->CleanCache($cacheList);
454
        \xoops_setActiveModules();
455
    }
456
457
    /**
458
     * @return bool
459
     */
460
    public static function renameUploadFolder(): bool
461
    {
462
        $moduleDirName      = \basename(\dirname(__DIR__));
463
        $moduleDirNameUpper = \mb_strtoupper($moduleDirName);
464
        $helper             = Helper::getInstance();
465
466
        $success = true;
467
        $helper->loadLanguage('admin');
468
469
        // Rename uploads folder to BAK and add date to name
470
        $uploadDirectory = $GLOBALS['xoops']->path("uploads/$moduleDirName");
471
        $dirInfo         = new \SplFileInfo($uploadDirectory);
472
        if ($dirInfo->isDir()) {
473
            // The directory exists so rename it
474
            $date = \date('Y-m-d');
475
            if (!\rename($uploadDirectory, $uploadDirectory . "_BAK_$date")) {
476
                $helper->getModule()->setErrors(\sprintf(\constant('CO_' . $moduleDirNameUpper . '_' . 'ERROR_BAD_DEL_PATH'), $uploadDirectory));
477
                $success = false;
478
            }
479
        }
480
        unset($dirInfo);
481
482
        return $success;
483
    }
484
485
    /**
486
     * Query and check if the result is a valid result set
487
     *
488
     * @param \XoopsMySQLDatabase $xoopsDB XOOPS Database
489
     * @param string              $sql     a valid MySQL query
490
     * @param int                 $limit   number of records to return
491
     * @param int                 $start   offset of first record to return
492
     *
493
     * @return \mysqli_result query result
494
     */
495
    public static function queryAndCheck(\XoopsMySQLDatabase $xoopsDB, string $sql, $limit = 0, $start = 0): \mysqli_result
496
    {
497
        $result = $xoopsDB->query($sql, $limit, $start);
498
499
        if (!$xoopsDB->isResultSet($result)) {
500
            throw new \RuntimeException(
501
                \sprintf(\_DB_QUERY_ERROR, $sql) . $xoopsDB->error(), \E_USER_ERROR);
502
        }
503
504
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could return the type boolean which is incompatible with the type-hinted return mysqli_result. Consider adding an additional type-check to rule them out.
Loading history...
505
    }
506
507
    /**
508
     * QueryF and check if the result is a valid result set
509
     *
510
     * @param \XoopsMySQLDatabase $xoopsDB XOOPS Database
511
     * @param string              $sql     a valid MySQL query
512
     * @param int                 $limit   number of records to return
513
     * @param int                 $start   offset of first record to return
514
     *
515
     * @return \mysqli_result query result
516
     */
517
    public static function queryFAndCheck(\XoopsMySQLDatabase $xoopsDB, string $sql, $limit = 0, $start = 0): \mysqli_result
518
    {
519
        $result = $xoopsDB->queryF($sql, $limit, $start);
520
521
        if (!$xoopsDB->isResultSet($result)) {
522
            throw new \RuntimeException(
523
                \sprintf(\_DB_QUERY_ERROR, $sql) . $xoopsDB->error(), \E_USER_ERROR
524
            );
525
        }
526
527
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could return the type boolean which is incompatible with the type-hinted return mysqli_result. Consider adding an additional type-check to rule them out.
Loading history...
528
    }
529
}
530