Passed
Push — main ( a1dfd6...7044f1 )
by Miaad
01:36
created

tools.php (2 issues)

1
<?php
2
3
namespace BPT;
4
5
use BPT\api\request;
6
use BPT\api\telegram;
7
use BPT\constants\chatMemberStatus;
8
use BPT\constants\cryptoAction;
9
use BPT\constants\loggerTypes;
10
use BPT\constants\parseMode;
11
use BPT\constants\pollType;
12
use BPT\exception\bptException;
13
use BPT\types\inlineKeyboardButton;
14
use BPT\types\inlineKeyboardMarkup;
15
use BPT\types\keyboardButton;
16
use BPT\types\keyboardButtonPollType;
17
use BPT\types\replyKeyboardMarkup;
18
use BPT\types\webAppInfo;
19
use DateTime;
20
use Exception;
21
use FilesystemIterator;
22
use RecursiveDirectoryIterator;
23
use RecursiveIteratorIterator;
24
use ZipArchive;
25
26
class tools {
27
    /**
28
     * Check the given username format
29
     *
30
     * e.g. => tools::isUsername('BPT_CH');
31
     *
32
     * e.g. => tools::isUsername(username: 'BPT_CH');
33
     *
34
     * @param string $username Your text to be check is it username or not , @ is not needed
35
     *
36
     * @return bool
37
     */
38
    public static function isUsername (string $username): bool {
39
        $length = strlen($username);
40
        return !str_contains($username, '__') && $length >= 5 && $length <= 33 && preg_match('/^@?([a-zA-Z])(\w{4,31})$/', $username);
41
    }
42
43
    /**
44
     * Check given IP is in the given IP range or not
45
     *
46
     * e.g. => tools::ipInRange('192.168.1.1','149.154.160.0/20');
47
     *
48
     * e.g. => tools::ipInRange(ip: '192.168.1.1',range: '149.154.160.0/20');
49
     *
50
     * @param string $ip    Your ip
51
     * @param string $range Your range ip for check , if you didn't specify the block , it will be 32
52
     *
53
     * @return bool
54
     */
55
    public static function ipInRange (string $ip, string $range): bool {
56
        if (!str_contains($range, '/')) {
57
            $range .= '/32';
58
        }
59
        $range_full = explode('/', $range, 2);
60
        $netmask_decimal = ~(pow(2, (32 - $range_full[1])) - 1);
61
        return (ip2long($ip) & $netmask_decimal) == (ip2long($range_full[0]) & $netmask_decimal);
62
    }
63
64
    /**
65
     * Check the given IP is from telegram or not
66
     *
67
     * e.g. => tools::isTelegram('192.168.1.1');
68
     *
69
     * e.g. => tools::isTelegram(ip: '192.168.1.1');
70
     *
71
     * @param string $ip Your ip to be check is telegram or not e.g. '192.168.1.1'
72
     *
73
     * @return bool
74
     */
75
    public static function isTelegram (string $ip): bool {
76
        return self::ipInRange($ip, '149.154.160.0/20') || self::ipInRange($ip, '91.108.4.0/22');
77
    }
78
79
    /**
80
     * Check the given IP is from CloudFlare or not
81
     *
82
     * e.g. => tools::isCloudFlare('192.168.1.1');
83
     *
84
     * e.g. =>tools::isCloudFlare(ip: '192.168.1.1');
85
     *
86
     * @param string $ip Your ip to be check is CloudFlare or not
87
     *
88
     * @return bool
89
     */
90
    public static function isCloudFlare (string $ip): bool {
91
        $cf_ips = ['173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/12', '172.64.0.0/13', '131.0.72.0/22'];
92
        foreach ($cf_ips as $cf_ip) {
93
            if (self::ipInRange($ip,$cf_ip)) {
94
                return true;
95
            }
96
        }
97
        return false;
98
    }
99
100
    /**
101
     * Check the given token format
102
     *
103
     * if you want to verify token with telegram , you should set `verify` parameter => true.
104
     * in that case , if token was right , you will receive getMe result , otherwise you will receive false
105
     *
106
     * e.g. => tools::isToken('123123123:abcabcabcabc');
107
     *
108
     * @param string $token  your token e.g. => '123123123:abcabcabcabc'
109
     * @param bool   $verify check token with telegram or not
110
     *
111
     * @return bool|types\user return array when verify is active and token is true array of telegram getMe result
112
     */
113
    public static function isToken (string $token, bool $verify = false): bool|types\user {
114
        if (preg_match('/^(\d{8,10}):[\w\-]{35}$/', $token)) {
115
            if ($verify){
116
                $res = telegram::me($token);
117
                if (telegram::$status) {
118
                    return $res;
119
                }
120
                return false;
121
            }
122
            return true;
123
        }
124
        return false;
125
    }
126
127
    /**
128
     * check user joined in channels or not
129
     *
130
     * this method only return true or false, if user join in all channels true, and if user not joined in one channel false
131
     *
132
     * this method does not care about not founded channel and count them as joined channel
133
     *
134
     * ids parameter can be array for multi channels or can be string for one channel
135
     *
136
     * NOTE : each channel will decrease speed a little(because of request count)
137
     *
138
     * e.g. => tools::isJoined('BPT_CH','442109602');
139
     *
140
     * e.g. => tools::isJoined(['BPT_CH','-1005465465454']);
141
     *
142
     * @param array|string|int $ids     could be username or id, you can pass multi or single id
143
     * @param int|null         $user_id if not set , will generate by request::catchFields method
144
     *
145
     * @return bool
146
     */
147
    public static function isJoined (array|string|int $ids , int|null $user_id = null): bool {
148
        if (!is_array($ids)) {
0 ignored issues
show
The condition is_array($ids) is always true.
Loading history...
149
            $ids = [$ids];
150
        }
151
        $user_id = $user_id ?? request::catchFields('user_id');
152
153
        foreach ($ids as $id) {
154
            $check = telegram::getChatMember($id,$user_id);
155
            if (telegram::$status) {
156
                $check = $check->status;
157
                if ($check === chatMemberStatus::LEFT || $check === chatMemberStatus::KICKED) {
158
                    return false;
159
                }
160
            }
161
        }
162
        return true;
163
    }
164
165
    /**
166
     * check user joined in channels or not
167
     *
168
     * ids parameter can be array for multi channels or can be string for one channel
169
     *
170
     * NOTE : each channel will decrease speed a little(because of request count)
171
     *
172
     * e.g. => tools::joinChecker('BPT_CH','442109602');
173
     *
174
     * e.g. => tools::joinChecker(['BPT_CH','-1005465465454']);
175
     *
176
     * @param array|string|int $ids     could be username or id, you can pass multi or single id
177
     * @param int|null         $user_id if not set , will generate by request::catchFields method
178
     *
179
     * @return array keys will be id and values will be bool(null for not founded ids)
180
     */
181
    public static function joinChecker (array|string|int $ids , int|null $user_id = null): array {
182
        if (!is_array($ids)) {
0 ignored issues
show
The condition is_array($ids) is always true.
Loading history...
183
            $ids = [$ids];
184
        }
185
        $user_id = $user_id ?? request::catchFields('user_id');
186
187
        $result = [];
188
        foreach ($ids as $id) {
189
            $check = telegram::getChatMember($id,$user_id);
190
            if (telegram::$status) {
191
                $check = $check->status;
192
                $result[$id] = $check !== chatMemberStatus::LEFT && $check !== chatMemberStatus::KICKED;
193
            }
194
            else $result[$id] = null;
195
        }
196
        return $result;
197
    }
198
199
    /**
200
     * Generate random string
201
     *
202
     * e.g. => tools::randomString();
203
     *
204
     * e.g. => tools::randomString(16,'abcdefg');
205
     *
206
     * e.g. => tools::randomString(length: 16,characters: 'abcdefg');
207
     *
208
     * @param int    $length     length of generated string
209
     * @param string $characters string constructor characters
210
     *
211
     * @return string
212
     */
213
    public static function randomString (int $length = 16, string $characters = 'aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ'): string {
214
        $rand_string = '';
215
        $char_len = strlen($characters) - 1;
216
        for ($i = 0; $i < $length; $i ++) {
217
            $rand_string .= $characters[rand(0, $char_len)];
218
        }
219
        return $rand_string;
220
    }
221
222
    /**
223
     * Escape text for different parse_modes
224
     *
225
     * mode parameter can be : `MarkdownV2` , `Markdown` , `HTML` , default : `parseMode::HTML`(`HTML`)
226
     *
227
     * e.g. => tools::modeEscape('hello men! *I* Have nothing anymore');
228
     *
229
     * e.g. => tools::modeEscape(text: 'hello men! *I* Have nothing anymore');
230
     *
231
     * @param string $text Your text e.g. => 'hello men! *I* Have nothing anymore'
232
     * @param string $mode Your selected mode e.g. => `parseMode::HTML` | `HTML`
233
     *
234
     * @return string|false return false when mode is incorrect
235
     */
236
    public static function modeEscape (string $text, string $mode = parseMode::HTML): string|false {
237
        return match ($mode) {
238
            parseMode::HTML => str_replace(['&', '<', '>',], ["&amp;", "&lt;", "&gt;",], $text),
239
            parseMode::MARKDOWN => str_replace(['\\', '_', '*', '`', '['], ['\\\\', '\_', '\*', '\`', '\[',], $text),
240
            parseMode::MARKDOWNV2 => str_replace(
241
                ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!', '\\'],
242
                ['\_', '\*', '\[', '\]', '\(', '\)', '\~', '\`', '\>', '\#', '\+', '\-', '\=', '\|', '\{', '\}', '\.', '\!', '\\\\'],
243
                $text),
244
            default => false
245
        };
246
    }
247
248
    /**
249
     * Convert byte to symbolic size like 2.98 MB
250
     *
251
     * You could set `precision` to configure decimals after number(2 for 2.98 and 3 for 2.987)
252
     *
253
     * e.g. => tools::byteFormat(123456789);
254
     *
255
     * e.g. => tools::byteFormat(byte: 123456789);
256
     *
257
     * @param int $byte      size in byte
258
     * @param int $precision decimal precision
259
     *
260
     * @return string
261
     */
262
    public static function byteFormat (int $byte, int $precision = 2): string {
263
        $rate_counter = 0;
264
265
        while ($byte > 1024){
266
            $byte /= 1024;
267
            $rate_counter++;
268
        }
269
270
        if ($rate_counter !== 0) {
271
            $byte = round($byte, $precision);
272
        }
273
274
        return $byte . ' ' . ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'][$rate_counter];
275
    }
276
277
    /**
278
     * receive size from path(can be url or file path)
279
     *
280
     * NOTE : some url will not return real size!
281
     *
282
     * e.g. => tools::size('xFile.zip');
283
     *
284
     * e.g. => tools::size(path: 'xFile.zip');
285
     *
286
     * @param string $path   file path, could be url
287
     * @param bool   $format if you set this true , you will receive symbolic string like 2.76MB for return
288
     *
289
     * @return string|int|false string for formatted data , int for normal data , false when size can not be found(file not found or ...)
290
     */
291
    public static function size (string $path, bool $format = true): string|int|false {
292
        if (filter_var($path, FILTER_VALIDATE_URL)) {
293
            $ch = curl_init($path);
294
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
295
            curl_setopt($ch, CURLOPT_HEADER, true);
296
            curl_setopt($ch, CURLOPT_NOBODY, true);
297
            curl_exec($ch);
298
            $size = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
299
            curl_close($ch);
300
        }
301
        else {
302
            $size = file_exists($path) ? filesize($path) : false;
303
        }
304
305
        if (isset($size) && is_numeric($size)) {
306
            return $format ? tools::byteFormat($size) : $size;
307
        }
308
        else return false;
309
    }
310
311
    /**
312
     * Delete a folder or file if exist
313
     *
314
     * e.g. => tools::delete(path: 'xfolder/yfolder');
315
     *
316
     * e.g. => tools::delete('xfolder/yfolder',false);
317
     *
318
     * @param string $path folder or file path
319
     * @param bool   $sub  set true for removing subFiles too, if folder has subFiles and this set to false , you will receive error
320
     *
321
     * @return bool
322
     * @throws bptException
323
     */
324
    public static function delete (string $path, bool $sub = true): bool {
325
        if (is_dir($path)) {
326
            if (count(scandir($path)) > 2) {
327
                if ($sub) {
328
                    $it = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
329
                    $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
330
                    foreach ($files as $file) {
331
                        $file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath());
332
                    }
333
                    return rmdir($path);
334
                }
335
                else {
336
                    logger::write("tools::delete function used\ndelete function cannot delete folder because its have subFiles and sub parameter haven't true value",loggerTypes::ERROR);
337
                    throw new bptException('DELETE_FOLDER_HAS_SUB');
338
                }
339
            }
340
            else return rmdir($path);
341
        }
342
        else return unlink($path);
343
    }
344
345
    /**
346
     * Show time different in array format
347
     *
348
     * Its calculated different between given time and now
349
     *
350
     * e.g. => tools::time2string(datetime: 1636913656);
351
     *
352
     * e.g. => tools::time2string(time());
353
     *
354
     * @param int|string $target_time your chosen time for compare with base_time, could be timestamp or could be a string like `next sunday`
355
     * @param int|string|null $base_time base time, could be timestamp or could be a string like `next sunday`, set null for current time
356
     *
357
     * @return array{status: string,year: string,month: string,day: string,hour: string,minute: string,second: string}
358
     * @throws Exception
359
     */
360
    public static function timeDiff (int|string $target_time, int|string|null $base_time = null): array {
361
        if (empty($base_time)) {
362
            $base_time = '@'.time();
363
        }
364
        $base_time = new DateTime($base_time);
365
        $target_time = new DateTime(is_numeric($target_time) ? '@' . $target_time : $target_time . ' +00:00');
366
367
        $status = $base_time < $target_time ? 'later' : 'ago';
368
        $diff = $base_time->diff($target_time);
369
370
        $string = ['year' => 'y', 'month' => 'm', 'day' => 'd', 'hour' => 'h', 'minute' => 'i', 'second' => 's'];
371
        foreach ($string as $k => &$v) {
372
            if ($diff->$v) {
373
                $v = $diff->$v;
374
            }
375
            else unset($string[$k]);
376
        }
377
        $string['status'] = $status;
378
379
        return count($string) > 1 ? $string : ['status' => 'now'];
380
    }
381
382
    /**
383
     * Clear text and make it safer to use
384
     *
385
     * e.g. => tools::clearText(text: 'asdasdasdas');
386
     *
387
     * e.g. => tools::clearText($message->text);
388
     *
389
     * @param string $text your text to be cleaned
390
     *
391
     * @return string
392
     */
393
    public static function clearText(string $text): string {
394
        return htmlentities(strip_tags(htmlspecialchars(stripslashes(trim($text)))));
395
    }
396
397
    /**
398
     * encrypt or decrypt a text with really high security
399
     *
400
     * action parameter must be encrypt or decrypt
401
     *
402
     * string parameter is your hash(received when use encrypt) or the text you want to encrypt
403
     *
404
     * for decrypt , you must have key and iv parameter. you can found them in result of encrypt
405
     *
406
     * e.g. => tools::crypto(action: 'decrypt', text: '9LqUf9DSuRRwfo03RnA5Kw==', key: '39aaadf402f9b921b1d44e33ee3b022716a518e97d6a7b55de8231de501b4f34', iv: 'a2e5904a4110169e');
407
     *
408
     * e.g. => tools::crypto(cryptoAction::ENCRYPT,'hello world');
409
     *
410
     * @param string      $action e.g. => cryptoAction::ENCRYPT | 'encrypt'
411
     * @param string      $text   e.g. => 'hello world'
412
     * @param null|string $key    e.g. => Optional, 39aaadf402f9b921b1d44e33ee3b022716a518e97d6a7b55de8231de501b4f34
413
     * @param null|string $iv     e.g. => Optional, a2e5904a4110169e
414
     *
415
     * @return array|string|bool
416
     * @throws bptException
417
     */
418
    public static function crypto (string $action, string $text, string $key = null, string $iv = null): bool|array|string {
419
420
        if (extension_loaded('openssl')) {
421
            if ($action === cryptoAction::ENCRYPT) {
422
                $key = self::randomString(64);
423
                $iv = self::randomString();
424
                $output = base64_encode(openssl_encrypt($text, 'AES-256-CBC', $key, 1, $iv));
425
                return ['hash' => $output, 'key' => $key, 'iv' => $iv];
426
            }
427
            elseif ($action === cryptoAction::DECRYPT) {
428
                if (empty($key)) {
429
                    logger::write("tools::crypto function used\nkey parameter is not set",loggerTypes::ERROR);
430
                    throw new bptException('ARGUMENT_NOT_FOUND_KEY');
431
                }
432
                if (empty($iv)) {
433
                    logger::write("tools::crypto function used\niv parameter is not set",loggerTypes::ERROR);
434
                    throw new bptException('ARGUMENT_NOT_FOUND_IV');
435
                }
436
                return openssl_decrypt(base64_decode($text), 'AES-256-CBC', $key, 1, $iv);
437
            }
438
            else {
439
                logger::write("tools::crypto function used\naction is not right, its must be `encode` or `decode`");
440
                return false;
441
            }
442
        }
443
        else {
444
            logger::write("tools::crypto function used\nopenssl extension is not found , It may not be installed or enabled",loggerTypes::ERROR);
445
            throw new bptException('OPENSSL_EXTENSION_MISSING');
446
        }
447
    }
448
449
    /**
450
     * convert all files in selected path to zip and then save it in dest path
451
     *
452
     * e.g. => tools::zip('xFolder','yFolder/xFile.zip',false,true);
453
     *
454
     * @param string $path        your file or folder to be zipped
455
     * @param string $destination destination path for create file
456
     * @param bool   $self        set true for adding main folder to zip file
457
     * @param bool   $sub_folder  set false for not adding sub_folders and save all files in main folder
458
     *
459
     * @return bool
460
     * @throws bptException when zip extension not found
461
     */
462
    public static function zip (string $path, string $destination, bool $self = true, bool $sub_folder = true): bool {
463
        if (extension_loaded('zip')) {
464
            if (file_exists($destination)) unlink($destination);
465
466
            $path = realpath($path);
467
            $zip = new ZipArchive();
468
            $zip->open($destination, ZipArchive::CREATE);
469
470
            if (is_dir($path)){
471
                if ($self){
472
                    $dirs = explode('\\',$path);
473
                    $dir_count = count($dirs);
474
                    $main_dir = $dirs[$dir_count-1];
475
476
                    $path = '';
477
                    for ($i=0; $i < $dir_count - 1; $i++) {
478
                        $path .= '\\' . $dirs[$i];
479
                    }
480
                    $path = substr($path, 1);
481
                    $zip->addEmptyDir($main_dir);
482
                }
483
484
                $it = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
485
                $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::SELF_FIRST);
486
                foreach ($files as $file) {
487
                    if ($file->isFile()){
488
                        if ($sub_folder){
489
                            $zip->addFile($file, str_replace($path . '\\', '', $file));
490
                        }
491
                        else{
492
                            $zip->addFile($file, basename($file));
493
                        }
494
                    }
495
                    elseif ($file->isDir() && $sub_folder) {
496
                        $zip->addEmptyDir(str_replace($path . '\\', '', $file . '\\'));
497
                    }
498
                }
499
            }
500
            else{
501
                $zip->addFile($path, basename($path));
502
            }
503
504
            return $zip->close();
505
        }
506
        else {
507
            logger::write("tools::zip function used\nzip extension is not found , It may not be installed or enabled",loggerTypes::ERROR);
508
            throw new bptException('ZIP_EXTENSION_MISSING');
509
        }
510
    }
511
512
    /**
513
     * create normal keyboard and inline keyboard easily
514
     *
515
     * you must set keyboard parameter(for normal keyboard) or inline parameter(for inline keyboard)
516
     *
517
     * if you set both , keyboard will be processed and inline will be ignored
518
     *
519
     *  
520
     *
521
     * con for request contact , loc for request location, web||URL for webapp, pull||POLLTYPE for poll
522
     *
523
     * e.g. => tools::easyKey([['button 1 in row 1','button 2 in row 1'],['button 1 in row 2'],['contact button in row 3||con'],['location button in row 4||loc']]);
524
     *
525
     *  
526
     *
527
     * e.g. => tools::easyKey(inline: [[['button 1 in row 1','this is callback button'],['button 2 in row 1','https://this-is-url-button.com']],[['demo button in row 2']]]);
528
     *
529
     * @param string[][] $keyboard array of array(as rows) of array(buttons) of string
530
     * @param array[][]  $inline   array of array(as rows) of array(buttons)
531
     *
532
     * @return inlineKeyboardMarkup|replyKeyboardMarkup replyKeyboardMarkup for keyboard and inlineKeyboardMarkup for inline
533
     * @throws exception
534
     */
535
    public static function easyKey(array $keyboard = [], array $inline = []): inlineKeyboardMarkup|replyKeyboardMarkup {
536
        if (!empty($keyboard)) {
537
            $keyboard_object = new replyKeyboardMarkup();
538
            $keyboard_object->setResize_keyboard($keyboard['resize'] ?? true);
539
            if (isset($keyboard['one_time'])) {
540
                $keyboard_object->setOne_time_keyboard($keyboard['one_time']) ;
541
            }
542
            foreach ($keyboard as $row) {
543
                $buttons = [];
544
                foreach ($row as $base_button) {
545
                    $button_info = explode('||', $base_button);
546
                    $button = new keyboardButton();
547
                    $button->setText($button_info[0] ?? $base_button);
548
                    if (count($button_info) > 1) {
549
                        if ($button_info[1] === 'con') {
550
                            $button->setRequest_contact(true);
551
                        }
552
                        elseif ($button_info[1] === 'loc') {
553
                            $button->setRequest_location(true);
554
                        }
555
                        elseif ($button_info[1] === 'poll') {
556
                            $type = $button_info[2] === pollType::QUIZ ? pollType::QUIZ : pollType::REGULAR;
557
                            $button->setRequest_poll((new keyboardButtonPollType())->setType($type));
558
                        }
559
                        elseif ($button_info[1] === 'web' && isset($button_info[2])) {
560
                            $url = $button_info[2];
561
                            $button->setWeb_app((new webAppInfo())->setUrl($url));
562
                        }
563
                    }
564
                    $buttons[] = $button;
565
                }
566
                $keyboard_object->setKeyboard([$buttons]);
567
            }
568
            return $keyboard_object;
569
        }
570
        elseif (!empty($inline)) {
571
            $keyboard_object = new inlineKeyboardMarkup();
572
            foreach ($inline as $row) {
573
                $buttons = [];
574
                foreach ($row as $button_info) {
575
                    $button = new inlineKeyboardButton();
576
                    if (isset($button_info[1])) {
577
                        if (filter_var($button_info[1], FILTER_VALIDATE_URL) && str_starts_with($button_info[1], 'http')) {
578
                            $button->setText($button_info[0])->setUrl($button_info[1]);
579
                        }
580
                        else {
581
                            $button->setText($button_info[0])->setCallback_data($button_info[1]);
582
                        }
583
                    }
584
                    else {
585
                        $button->setText($button_info[0])->setUrl('https://t.me/BPT_CH');
586
                    }
587
                }
588
                $keyboard_object->setInline_keyboard([$buttons]);
589
            }
590
            return $keyboard_object;
591
        }
592
        else {
593
            logger::write("tools::eKey function used\nkeyboard or inline parameter must be set",loggerTypes::ERROR);
594
            throw new bptException('ARGUMENT_NOT_FOUND_KEYBOARD_INLINE');
595
        }
596
    }
597
}