Passed
Pull Request — master (#24)
by Michael
27:32 queued 12:57
created

SysUtility::queryAndCheck()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
nc 2
nop 4
dl 0
loc 10
rs 10
c 1
b 0
f 0
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 (!$GLOBALS['xoopsDB']->isResultSet($result)) {
191
           \trigger_error(\sprintf(\_DB_QUERY_ERROR, $sql) . $GLOBALS['xoopsDB']->error(), \E_USER_ERROR);
192
        
193
        }
194
        // Return the new id
195
        return $GLOBALS['xoopsDB']->getInsertId();
196
    }
197
198
    /**
199
     * truncateHtml can truncate a string up to a number of characters while preserving whole words and HTML tags
200
     * www.gsdesign.ro/blog/cut-html-string-without-breaking-the-tags
201
     * www.cakephp.org
202
     *
203
     * @TODO: Refactor to consider HTML5 & void (self-closing) elements
204
     * @TODO: Consider using https://github.com/jlgrall/truncateHTML/blob/master/truncateHTML.php
205
     *
206
     * @param string      $text         String to truncate.
207
     * @param int|null    $length       Length of returned string, including ellipsis.
208
     * @param string|null $ending       Ending to be appended to the trimmed string.
209
     * @param bool|null   $exact        If false, $text will not be cut mid-word
210
     * @param bool        $considerHtml If true, HTML tags would be handled correctly
211
     *
212
     * @return string Trimmed string.
213
     */
214
    public static function truncateHtml(
215
        string  $text,
216
        ?int    $length = null,
217
        ?string $ending = null,
218
        ?bool   $exact = null,
219
        ?bool   $considerHtml = true
220
    ): string {
221
        $length   ??= 100;
222
        $ending   ??= '...';
223
        $exact    ??= false;
224
        $openTags = [];
225
        if ($considerHtml) {
226
            // if the plain text is shorter than the maximum length, return the whole text
227
            if (\mb_strlen(\preg_replace('/<.*?' . '>/', '', $text)) <= $length) {
228
                return $text;
229
            }
230
            // splits all html-tags to scanable lines
231
            \preg_match_all('/(<.+?' . '>)?([^<>]*)/s', $text, $lines, \PREG_SET_ORDER);
232
            $totalLength = \mb_strlen($ending);
233
            //$openTags    = [];
234
            $truncate = '';
235
            foreach ($lines as $lineMatchings) {
236
                // if there is any html-tag in this line, handle it and add it (uncounted) to the output
237
                if (!empty($lineMatchings[1])) {
238
                    // if it's an "empty element" with or without xhtml-conform closing slash
239
                    if (\preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $lineMatchings[1])) {
240
                        // do nothing
241
                        // if tag is a closing tag
242
                    } elseif (\preg_match('/^<\s*\/(\S+?)\s*>$/s', $lineMatchings[1], $tag_matchings)) {
243
                        // delete tag from $openTags list
244
                        $pos = \array_search($tag_matchings[1], $openTags, true);
245
                        if (false !== $pos) {
246
                            unset($openTags[$pos]);
247
                        }
248
                        // if tag is an opening tag
249
                    } elseif (\preg_match('/^<\s*([^\s>!]+).*?' . '>$/s', $lineMatchings[1], $tag_matchings)) {
250
                        // add tag to the beginning of $openTags list
251
                        \array_unshift($openTags, \mb_strtolower($tag_matchings[1]));
252
                    }
253
                    // add html-tag to $truncate'd text
254
                    $truncate .= $lineMatchings[1];
255
                }
256
                // calculate the length of the plain text part of the line; handle entities as one character
257
                $contentLength = \mb_strlen(\preg_replace('/&[0-9a-z]{2,8};|&#\d{1,7};|[0-9a-f]{1,6};/i', ' ', $lineMatchings[2]));
258
                if ($totalLength + $contentLength > $length) {
259
                    // the number of characters which are left
260
                    $left            = $length - $totalLength;
261
                    $entities_length = 0;
262
                    // search for html entities
263
                    if (\preg_match_all('/&[0-9a-z]{2,8};|&#\d{1,7};|[0-9a-f]{1,6};/i', $lineMatchings[2], $entities, \PREG_OFFSET_CAPTURE)) {
264
                        // calculate the real length of all entities in the legal range
265
                        foreach ($entities[0] as $entity) {
266
                            if ($left >= $entity[1] + 1 - $entities_length) {
267
                                $left--;
268
                                $entities_length += \mb_strlen($entity[0]);
269
                            } else {
270
                                // no more characters left
271
                                break;
272
                            }
273
                        }
274
                    }
275
                    $truncate .= \mb_substr($lineMatchings[2], 0, $left + $entities_length);
276
                    // maximum length is reached, so get off the loop
277
                    break;
278
                }
279
                $truncate    .= $lineMatchings[2];
280
                $totalLength += $contentLength;
281
282
                // if the maximum length is reached, get off the loop
283
                if ($totalLength >= $length) {
284
                    break;
285
                }
286
            }
287
        } else {
288
            if (\mb_strlen($text) <= $length) {
289
                return $text;
290
            }
291
            $truncate = \mb_substr($text, 0, $length - \mb_strlen($ending));
292
        }
293
        // if the words shouldn't be cut in the middle...
294
        if (!$exact) {
295
            // ...search the last occurance of a space...
296
            $spacepos = \mb_strrpos($truncate, ' ');
297
            if (isset($spacepos)) {
298
                // ...and cut the text in this position
299
                $truncate = \mb_substr($truncate, 0, $spacepos);
300
            }
301
        }
302
        // add the defined ending to the text
303
        $truncate .= $ending;
304
        if ($considerHtml) {
305
            // close all unclosed html-tags
306
            foreach ($openTags as $tag) {
307
                $truncate .= '</' . $tag . '>';
308
            }
309
        }
310
311
        return $truncate;
312
    }
313
314
    /**
315
     * Get correct text editor based on user rights
316
     *
317
     *
318
     * @return \XoopsFormDhtmlTextArea|\XoopsFormEditor
319
     */
320
    public static function getEditor(?\Xmf\Module\Helper $helper = null, ?array $options = null)
321
    {
322
        /** @var Helper $helper */
323
        if (null === $options) {
324
            $options           = [];
325
            $options['name']   = 'Editor';
326
            $options['value']  = 'Editor';
327
            $options['rows']   = 10;
328
            $options['cols']   = '100%';
329
            $options['width']  = '100%';
330
            $options['height'] = '400px';
331
        }
332
333
        if (null === $helper) {
334
            $helper = Helper::getInstance();
335
        }
336
337
        $isAdmin = $helper->isUserAdmin();
338
339
        if (\class_exists('XoopsFormEditor')) {
340
            if ($isAdmin) {
341
                $descEditor = new \XoopsFormEditor(\ucfirst((string) $options['name']), $helper->getConfig('editorAdmin'), $options, $nohtml = false, $onfailure = 'textarea');
342
            } else {
343
                $descEditor = new \XoopsFormEditor(\ucfirst((string) $options['name']), $helper->getConfig('editorUser'), $options, $nohtml = false, $onfailure = 'textarea');
344
            }
345
        } else {
346
            $descEditor = new \XoopsFormDhtmlTextArea(\ucfirst((string) $options['name']), $options['name'], $options['value']);
347
        }
348
349
        //        $form->addElement($descEditor);
350
351
        return $descEditor;
352
    }
353
354
    /**
355
     * Check if column in dB table exists
356
     *
357
     * @param string $fieldname name of dB table field
358
     * @param string $table     name of dB table (including prefix)
359
     *
360
     * @return bool true if table exists
361
     * @deprecated
362
     */
363
    public static function fieldExists(string $fieldname, string $table): bool
364
    {
365
        $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1);
366
        $GLOBALS['xoopsLogger']->addDeprecated(__METHOD__ . "() use Xmf\Database\Tables instead - instantiated from {$trace[0]['file']} line {$trace[0]['line']}");
367
368
        $result = $GLOBALS['xoopsDB']->queryF("SHOW COLUMNS FROM   $table LIKE '$fieldname'");
369
370
        return ($GLOBALS['xoopsDB']->getRowsNum($result) > 0);
371
    }
372
373
    /**
374
     * Function responsible for checking if a directory exists, we can also write in and create an index.html file
375
     *
376
     * @param string $folder The full path of the directory to check
377
     */
378
    public static function prepareFolder(string $folder): void
379
    {
380
try {
381
    if (!\is_dir($folder) && !\mkdir($folder) && !\is_dir($folder)) {
382
        throw new \RuntimeException(\sprintf('Unable to create the %s directory', $folder));
383
    }
384
    file_put_contents($folder . '/index.html', '<script>history.go(-1);</script>');
385
} catch (\Exception $e) {
386
    echo 'Caught exception: ', $e->getMessage(), "<br>\n";
387
}
388
    }
389
390
    /**
391
     * Check if dB table exists
392
     *
393
     * @param string $tablename dB tablename with prefix
394
     * @return bool true if table exists
395
     */
396
    public static function tableExists(string $tablename): bool
397
    {
398
        $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1);
399
        $GLOBALS['xoopsLogger']->addDeprecated(
400
            \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']}"
401
        );
402
        $sql = "SHOW TABLES LIKE '$tablename'";
403
        $result = self::queryFAndCheck($GLOBALS['xoopsDB'], $sql);
404
405
        return $GLOBALS['xoopsDB']->getRowsNum($result) > 0;
406
    }
407
408
    /**
409
     * Add a field to a mysql table
410
     *
411
     * @param string $field
412
     * @param string $table
413
     * @return bool|\mysqli_result
414
     */
415
    public static function addField(string $field, string $table)
416
    {
417
        global $xoopsDB;
418
419
        return $xoopsDB->queryF('ALTER TABLE ' . $table . " ADD $field;");
420
    }
421
422
    /**
423
     * Query and check if the result is a valid result set
424
     *
425
     * @param \XoopsMySQLDatabase $xoopsDB XOOPS Database
426
     * @param string              $sql     a valid MySQL query
427
     * @param int                 $limit   number of records to return
428
     * @param int                 $start   offset of first record to return
429
     *
430
     * @return \mysqli_result query result
431
     */
432
    public static function queryAndCheck(\XoopsMySQLDatabase $xoopsDB, string $sql, int $limit = 0, int $start = 0): \mysqli_result
433
    {
434
        $result = $xoopsDB->query($sql, $limit, $start);
435
436
        if (!$xoopsDB->isResultSet($result)) {
437
            throw new \RuntimeException(
438
                \sprintf(\_DB_QUERY_ERROR, $sql) . $xoopsDB->error(), \E_USER_ERROR);
439
        }
440
441
        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...
442
    }
443
444
    /**
445
     * QueryF and check if the result is a valid result set
446
     *
447
     * @param \XoopsMySQLDatabase $xoopsDB XOOPS Database
448
     * @param string              $sql     a valid MySQL query
449
     * @param int                 $limit   number of records to return
450
     * @param int                 $start   offset of first record to return
451
     *
452
     * @return \mysqli_result query result
453
     */
454
    public static function queryFAndCheck(\XoopsMySQLDatabase $xoopsDB, string $sql, int $limit = 0, int $start = 0): \mysqli_result
455
    {
456
        $result = $xoopsDB->queryF($sql, $limit, $start);
457
458
        if (!$xoopsDB->isResultSet($result)) {
459
            throw new \RuntimeException(
460
                \sprintf(\_DB_QUERY_ERROR, $sql) . $xoopsDB->error(), \E_USER_ERROR);
461
        }
462
463
        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...
464
    }
465
}
466
467