SysUtility::truncateHtml()   F
last analyzed

Complexity

Conditions 19
Paths 194

Size

Total Lines 95
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 19
eloc 47
c 1
b 0
f 0
nc 194
nop 5
dl 0
loc 95
rs 3.7333

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
3
namespace XoopsModules\Xoopspoll\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 XoopsModules\Xoopspoll\{
27
    Helper
28
};
29
30
/**
31
 * Class SysUtility
32
 */
33
class SysUtility
34
{
35
    use VersionChecks;    //checkVerXoops, checkVerPhp Traits
0 ignored issues
show
introduced by
The trait XoopsModules\Xoopspoll\Common\VersionChecks requires some properties which are not provided by XoopsModules\Xoopspoll\Common\SysUtility: $tag_name, $prerelease
Loading history...
36
    use ServerStats;    // getServerStats Trait
37
    use FilesManagement;    // Files Management Trait
38
    //    use ModuleStats;    // ModuleStats Trait
39
40
    //--------------- Common module methods -----------------------------
41
42
    /**
43
     * Access the only instance of this class
44
     *
45
     * @return SysUtility
46
     *
47
     */
48
    public static function getInstance(): self
49
    {
50
        static $instance;
51
        if (null === $instance) {
52
            $instance = new static();
53
        }
54
55
        return $instance;
56
    }
57
58
    /**
59
     * @param string $text
60
     * @param string $form_sort
61
     * @return string
62
     */
63
    public static function selectSorting(string $text, string $form_sort): string
64
    {
65
        global $start, $order, $sort;
66
67
        $selectViewForm = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $selectViewForm is dead and can be removed.
Loading history...
68
        $moduleDirName  = \basename(\dirname(__DIR__));
0 ignored issues
show
Unused Code introduced by
The assignment to $moduleDirName is dead and can be removed.
Loading history...
69
        $helper         = Helper::getInstance();
70
71
        //$pathModIcon16 = XOOPS_URL . '/modules/' . $moduleDirName . '/' . $helper->getConfig('modicons16');
72
        $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

72
        $pathModIcon16 = $helper->url(/** @scrutinizer ignore-type */ $helper->getModule()->getInfo('modicons16'));
Loading history...
73
74
        $selectViewForm = '<form name="form_switch" id="form_switch" action="' . Request::getString('REQUEST_URI', '', 'SERVER') . '" method="post"><span style="font-weight: bold;">' . $text . '</span>';
75
        //$sorts =  $sort ==  'asc' ? 'desc' : 'asc';
76
        if ($form_sort == $sort) {
77
            $sel1 = 'asc' === $order ? 'selasc.png' : 'asc.png';
78
            $sel2 = 'desc' === $order ? 'seldesc.png' : 'desc.png';
79
        } else {
80
            $sel1 = 'asc.png';
81
            $sel2 = 'desc.png';
82
        }
83
        $selectViewForm .= '  <a href="' . Request::getString('SCRIPT_NAME', '', 'SERVER') . '?start=' . $start . '&sort=' . $form_sort . '&order=asc"><img src="' . $pathModIcon16 . '/' . $sel1 . '" title="ASC" alt="ASC"></a>';
84
        $selectViewForm .= '<a href="' . Request::getString('SCRIPT_NAME', '', 'SERVER') . '?start=' . $start . '&sort=' . $form_sort . '&order=desc"><img src="' . $pathModIcon16 . '/' . $sel2 . '" title="DESC" alt="DESC"></a>';
85
        $selectViewForm .= '</form>';
86
87
        return $selectViewForm;
88
    }
89
90
    /***************Blocks***************/
91
92
    public static function blockAddCatSelect(array $cats): string
93
    {
94
        $cat_sql = '';
95
        if (\is_array($cats) && !empty($cats)) {
96
            $cat_sql = '(' . \current($cats);
97
            \array_shift($cats);
98
            foreach ($cats as $cat) {
99
                $cat_sql .= ',' . $cat;
100
            }
101
            $cat_sql .= ')';
102
        }
103
104
        return $cat_sql;
105
    }
106
107
    /**
108
     * @param string $content
109
     */
110
    public static function metaKeywords(string $content): void
111
    {
112
        global $xoopsTpl, $xoTheme;
113
        $myts    = \MyTextSanitizer::getInstance();
114
        $content = $myts->undoHtmlSpecialChars($myts->displayTarea($content));
115
        if (\is_object($xoTheme)) {
116
            $xoTheme->addMeta('meta', 'keywords', \strip_tags($content));
117
        } else {    // Compatibility for old Xoops versions
118
            $xoopsTpl->assign('xoops_metaKeywords', \strip_tags($content));
119
        }
120
    }
121
122
    /**
123
     * @param string $content
124
     */
125
    public static function metaDescription(string $content): void
126
    {
127
        global $xoopsTpl, $xoTheme;
128
        $myts    = \MyTextSanitizer::getInstance();
129
        $content = $myts->undoHtmlSpecialChars($myts->displayTarea($content));
130
        if (\is_object($xoTheme)) {
131
            $xoTheme->addMeta('meta', 'description', \strip_tags($content));
132
        } else {    // Compatibility for old Xoops versions
133
            $xoopsTpl->assign('xoops_metaDescription', \strip_tags($content));
134
        }
135
    }
136
137
    /**
138
     * @return array|false
139
     */
140
    public static function enumerate(string $tableName, string $columnName)
141
    {
142
        $table = $GLOBALS['xoopsDB']->prefix($tableName);
143
144
        //    $result = $GLOBALS['xoopsDB']->query("SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS
145
        //        WHERE TABLE_NAME = '" . $table . "' AND COLUMN_NAME = '" . $columnName . "'")
146
        //    || exit ($GLOBALS['xoopsDB']->error());
147
148
        $sql    = 'SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = "' . $table . '" AND COLUMN_NAME = "' . $columnName . '"';
149
        $result = $GLOBALS['xoopsDB']->query($sql);
150
        if (!$result) {
151
            //            \trigger_error($GLOBALS['xoopsDB']->error());
152
            $logger = \XoopsLogger::getInstance();
153
            $logger->handleError(\E_USER_WARNING, $sql, __FILE__, __LINE__);
154
155
            return false;
156
        }
157
158
        $row      = $GLOBALS['xoopsDB']->fetchBoth($result);
159
        $enumList = \explode(',', \str_replace("'", '', \mb_substr($row['COLUMN_TYPE'], 5, -6)));
160
161
        return $enumList;
162
    }
163
164
    /**
165
     * Clone a record in a dB
166
     *
167
     * @TODO need to exit more gracefully on error. Should throw/trigger error and then return false
168
     *
169
     * @param string $tableName name of dB table (without prefix)
170
     * @param string $idField   name of field (column) in dB table
171
     * @param int    $id        item id to clone
172
     *
173
     * @return mixed
174
     */
175
    public static function cloneRecord(string $tableName, string $idField, int $id): mixed
176
    {
177
        $newId = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $newId is dead and can be removed.
Loading history...
178
        $table = $GLOBALS['xoopsDB']->prefix($tableName);
179
        // copy content of the record you wish to clone
180
        $sql       = "SELECT * FROM $table WHERE $idField='" . $id . "' ";
181
        $tempTable = $GLOBALS['xoopsDB']->fetchArray($GLOBALS['xoopsDB']->query($sql), \MYSQLI_ASSOC);
182
        if (!$tempTable) {
183
            \trigger_error($GLOBALS['xoopsDB']->error());
184
        }
185
        // set the auto-incremented id's value to blank.
186
        unset($tempTable[$idField]);
187
        // insert cloned copy of the original  record
188
        $sql    = "INSERT INTO $table (" . \implode(', ', \array_keys($tempTable)) . ") VALUES ('" . \implode("', '", $tempTable) . "')";
189
        $result = $GLOBALS['xoopsDB']->queryF($sql);
190
        if (!$result) {
191
            \trigger_error($GLOBALS['xoopsDB']->error());
192
        }
193
        // Return the new id
194
        return $GLOBALS['xoopsDB']->getInsertId();
195
    }
196
197
    /**
198
     * truncateHtml can truncate a string up to a number of characters while preserving whole words and HTML tags
199
     * www.gsdesign.ro/blog/cut-html-string-without-breaking-the-tags
200
     * www.cakephp.org
201
     *
202
     * @TODO: Refactor to consider HTML5 & void (self-closing) elements
203
     * @TODO: Consider using https://github.com/jlgrall/truncateHTML/blob/master/truncateHTML.php
204
     *
205
     * @param string      $text         String to truncate.
206
     * @param int|null    $length       Length of returned string, including ellipsis.
207
     * @param string|null $ending       Ending to be appended to the trimmed string.
208
     * @param bool        $exact        If false, $text will not be cut mid-word
209
     * @param bool        $considerHtml If true, HTML tags would be handled correctly
210
     *
211
     * @return string Trimmed string.
212
     */
213
    public static function truncateHtml(
214
        string $text,
215
        ?int $length = 100,
216
        ?string $ending = '...',
217
        ?bool $exact = false,
218
        ?bool $considerHtml = true
219
    ): string {
220
        $openTags = [];
221
        if ($considerHtml) {
222
            // if the plain text is shorter than the maximum length, return the whole text
223
            if (\mb_strlen(\preg_replace('/<.*?' . '>/', '', $text)) <= $length) {
224
                return $text;
225
            }
226
            // splits all html-tags to scanable lines
227
            \preg_match_all('/(<.+?' . '>)?([^<>]*)/s', $text, $lines, \PREG_SET_ORDER);
228
            $totalLength = \mb_strlen($ending);
0 ignored issues
show
Bug introduced by
It seems like $ending can also be of type null; however, parameter $string of mb_strlen() 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

228
            $totalLength = \mb_strlen(/** @scrutinizer ignore-type */ $ending);
Loading history...
229
            //$openTags    = [];
230
            $truncate = '';
231
            foreach ($lines as $lineMatchings) {
232
                // if there is any html-tag in this line, handle it and add it (uncounted) to the output
233
                if (!empty($lineMatchings[1])) {
234
                    // if it's an "empty element" with or without xhtml-conform closing slash
235
                    if (\preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $lineMatchings[1])) {
236
                        // do nothing
237
                        // if tag is a closing tag
238
                    } elseif (\preg_match('/^<\s*\/(\S+?)\s*>$/s', $lineMatchings[1], $tag_matchings)) {
239
                        // delete tag from $openTags list
240
                        $pos = \array_search($tag_matchings[1], $openTags, true);
241
                        if (false !== $pos) {
242
                            unset($openTags[$pos]);
243
                        }
244
                        // if tag is an opening tag
245
                    } elseif (\preg_match('/^<\s*([^\s>!]+).*?' . '>$/s', $lineMatchings[1], $tag_matchings)) {
246
                        // add tag to the beginning of $openTags list
247
                        \array_unshift($openTags, \mb_strtolower($tag_matchings[1]));
248
                    }
249
                    // add html-tag to $truncate'd text
250
                    $truncate .= $lineMatchings[1];
251
                }
252
                // calculate the length of the plain text part of the line; handle entities as one character
253
                $contentLength = \mb_strlen(\preg_replace('/&[0-9a-z]{2,8};|&#\d{1,7};|[0-9a-f]{1,6};/i', ' ', $lineMatchings[2]));
254
                if ($totalLength + $contentLength > $length) {
255
                    // the number of characters which are left
256
                    $left            = $length - $totalLength;
257
                    $entities_length = 0;
258
                    // search for html entities
259
                    if (\preg_match_all('/&[0-9a-z]{2,8};|&#\d{1,7};|[0-9a-f]{1,6};/i', $lineMatchings[2], $entities, \PREG_OFFSET_CAPTURE)) {
260
                        // calculate the real length of all entities in the legal range
261
                        foreach ($entities[0] as $entity) {
262
                            if ($left >= $entity[1] + 1 - $entities_length) {
263
                                $left--;
264
                                $entities_length += \mb_strlen($entity[0]);
265
                            } else {
266
                                // no more characters left
267
                                break;
268
                            }
269
                        }
270
                    }
271
                    $truncate .= \mb_substr($lineMatchings[2], 0, $left + $entities_length);
272
                    // maximum length is reached, so get off the loop
273
                    break;
274
                }
275
                $truncate    .= $lineMatchings[2];
276
                $totalLength += $contentLength;
277
278
                // if the maximum length is reached, get off the loop
279
                if ($totalLength >= $length) {
280
                    break;
281
                }
282
            }
283
        } else {
284
            if (\mb_strlen($text) <= $length) {
285
                return $text;
286
            }
287
            $truncate = \mb_substr($text, 0, $length - \mb_strlen($ending));
288
        }
289
        // if the words shouldn't be cut in the middle...
290
        if (!$exact) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $exact of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
291
            // ...search the last occurance of a space...
292
            $spacepos = \mb_strrpos($truncate, ' ');
293
            if (isset($spacepos)) {
294
                // ...and cut the text in this position
295
                $truncate = \mb_substr($truncate, 0, $spacepos);
296
            }
297
        }
298
        // add the defined ending to the text
299
        $truncate .= $ending;
300
        if ($considerHtml) {
301
            // close all unclosed html-tags
302
            foreach ($openTags as $tag) {
303
                $truncate .= '</' . $tag . '>';
304
            }
305
        }
306
307
        return $truncate;
308
    }
309
310
    /**
311
     * Get correct text editor based on user rights
312
     *
313
     *
314
     * @return \XoopsFormDhtmlTextArea|\XoopsFormEditor
315
     */
316
    public static function getEditor(?\Xmf\Module\Helper $helper = null, ?array $options = null)
317
    {
318
        /** @var Helper $helper */
319
        if (null === $options) {
320
            $options           = [];
321
            $options['name']   = 'Editor';
322
            $options['value']  = 'Editor';
323
            $options['rows']   = 10;
324
            $options['cols']   = '100%';
325
            $options['width']  = '100%';
326
            $options['height'] = '400px';
327
        }
328
329
        if (null === $helper) {
330
            $helper = Helper::getInstance();
331
        }
332
333
        $isAdmin = $helper->isUserAdmin();
334
335
        if (\class_exists('XoopsFormEditor')) {
336
            if ($isAdmin) {
337
                $descEditor = new \XoopsFormEditor(\ucfirst($options['name']), $helper->getConfig('editorAdmin'), $options, $nohtml = false, $onfailure = 'textarea');
338
            } else {
339
                $descEditor = new \XoopsFormEditor(\ucfirst($options['name']), $helper->getConfig('editorUser'), $options, $nohtml = false, $onfailure = 'textarea');
340
            }
341
        } else {
342
            $descEditor = new \XoopsFormDhtmlTextArea(\ucfirst($options['name']), $options['name'], $options['value'], '100%', '100%');
0 ignored issues
show
Bug introduced by
'100%' of type string is incompatible with the type integer expected by parameter $rows of XoopsFormDhtmlTextArea::__construct(). ( Ignorable by Annotation )

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

342
            $descEditor = new \XoopsFormDhtmlTextArea(\ucfirst($options['name']), $options['name'], $options['value'], /** @scrutinizer ignore-type */ '100%', '100%');
Loading history...
Bug introduced by
'100%' of type string is incompatible with the type integer expected by parameter $cols of XoopsFormDhtmlTextArea::__construct(). ( Ignorable by Annotation )

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

342
            $descEditor = new \XoopsFormDhtmlTextArea(\ucfirst($options['name']), $options['name'], $options['value'], '100%', /** @scrutinizer ignore-type */ '100%');
Loading history...
343
        }
344
345
        //        $form->addElement($descEditor);
346
347
        return $descEditor;
348
    }
349
350
    /**
351
     * Check if column in dB table exists
352
     *
353
     * @param string $fieldname name of dB table field
354
     * @param string $table     name of dB table (including prefix)
355
     *
356
     * @return bool true if table exists
357
     * @deprecated
358
     */
359
    public static function fieldExists(string $fieldname, string $table): bool
360
    {
361
        $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1);
362
        \trigger_error(__METHOD__ . " is deprecated, use Xmf\Database\Tables instead - instantiated from {$trace[0]['file']} line {$trace[0]['line']},");
363
364
        $result = $GLOBALS['xoopsDB']->queryF("SHOW COLUMNS FROM   $table LIKE '$fieldname'");
365
366
        return ($GLOBALS['xoopsDB']->getRowsNum($result) > 0);
367
    }
368
369
    /**
370
     * Function responsible for checking if a directory exists, we can also write in and create an index.html file
371
     *
372
     * @param string $folder The full path of the directory to check
373
     */
374
    public static function prepareFolder(string $folder): void
375
    {
376
        try {
377
            if (!@\mkdir($folder) && !\is_dir($folder)) {
378
                throw new \RuntimeException(\sprintf('Unable to create the %s directory', $folder));
379
            }
380
            file_put_contents($folder . '/index.html', '<script>history.go(-1);</script>');
381
        } catch (\Exception $e) {
382
            echo 'Caught exception: ', $e->getMessage(), "\n", '<br>';
383
        }
384
    }
385
386
    /**
387
     * Check if dB table exists
388
     *
389
     * @param string $tablename dB tablename with prefix
390
     * @return bool true if table exists
391
     */
392
    public static function tableExists(string $tablename): bool
393
    {
394
        $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1);
395
        \trigger_error(__FUNCTION__ . " is deprecated, called from {$trace[0]['file']} line {$trace[0]['line']}");
396
        $GLOBALS['xoopsLogger']->addDeprecated(
397
            \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']}"
398
        );
399
        $result = $GLOBALS['xoopsDB']->queryF("SHOW TABLES LIKE '$tablename'");
400
401
        return $GLOBALS['xoopsDB']->getRowsNum($result) > 0;
402
    }
403
404
    /**
405
     * Add a field to a mysql table
406
     *
407
     * @param string $field
408
     * @param string $table
409
     * @return bool|\mysqli_result
410
     */
411
    public static function addField(string $field, string $table)
412
    {
413
        global $xoopsDB;
414
415
        return $xoopsDB->queryF('ALTER TABLE ' . $table . " ADD $field;");
416
    }
417
}
418