• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visavi / rotor / 26723576697

31 May 2026 08:27PM UTC coverage: 14.172% (-0.4%) from 14.618%
26723576697

push

github

visavi
Перенес файлы обновлений бд в отдельную папку

785 of 5539 relevant lines covered (14.17%)

1.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

58.68
/app/helpers.php
1
<?php
2

3
use App\Classes\CloudFlare;
4
use App\Classes\HtmlRenderer;
5
use App\Classes\Metrika;
6
use App\Models\Antimat;
7
use App\Models\Ban;
8
use App\Models\Banhist;
9
use App\Models\BlackList;
10
use App\Models\Counter;
11
use App\Models\Error;
12
use App\Models\Notice;
13
use App\Models\Online;
14
use App\Models\Setting;
15
use App\Models\Spam;
16
use App\Models\Sticker;
17
use App\Models\User;
18
use cbschuld\Browser;
19
use Illuminate\Mail\Message;
20
use Illuminate\Pagination\LengthAwarePaginator;
21
use Illuminate\Pagination\Paginator;
22
use Illuminate\Support\Arr;
23
use Illuminate\Support\Collection;
24
use Illuminate\Support\Facades\Cache;
25
use Illuminate\Support\Facades\Date;
26
use Illuminate\Support\Facades\DB;
27
use Illuminate\Support\Facades\Mail;
28
use Illuminate\Support\HtmlString;
29
use Illuminate\Support\Str;
30
use Illuminate\Support\ViewErrorBag;
31
use ReCaptcha\ReCaptcha;
32

33
const ROTOR_VERSION = '14.0.0';
34
define('SITETIME', time());
×
35

36
/**
37
 * Форматирует время с учетом часовых поясов
38
 */
39
function dateFixed(
40
    DateTimeInterface|int|null $timestamp,
41
    string $format = 'd.m.Y / H:i',
42
    bool $original = false,
43
): string {
44
    $date = Date::parse($timestamp)->setTimezone(config('app.timezone'));
1✔
45
    $shift = (int) getUser('timezone');
1✔
46
    $dateStamp = $date->addHours($shift)->format($format);
1✔
47

48
    if ($original) {
1✔
49
        return $dateStamp;
1✔
50
    }
51

52
    $today = Date::now()->addHours($shift)->format('d.m.Y');
1✔
53
    $yesterday = Date::now()->addHours($shift)->subDay()->format('d.m.Y');
1✔
54

55
    $replaces = [
1✔
56
        $today      => __('main.today'),
1✔
57
        $yesterday  => __('main.yesterday'),
1✔
58
        'January'   => __('main.january'),
1✔
59
        'February'  => __('main.february'),
1✔
60
        'March'     => __('main.march'),
1✔
61
        'April'     => __('main.april'),
1✔
62
        'May'       => __('main.may'),
1✔
63
        'June'      => __('main.june'),
1✔
64
        'July'      => __('main.july'),
1✔
65
        'August'    => __('main.august'),
1✔
66
        'September' => __('main.september'),
1✔
67
        'October'   => __('main.october'),
1✔
68
        'November'  => __('main.november'),
1✔
69
        'December'  => __('main.december'),
1✔
70
    ];
1✔
71

72
    return strtr($dateStamp, $replaces);
1✔
73
}
74

75
/**
76
 * Преобразует специальные символы в HTML-сущности
77
 */
78
function check(array|string|null $string, bool $doubleEncode = true): array|string
79
{
80
    if (is_array($string)) {
1✔
81
        foreach ($string as $key => $val) {
1✔
82
            $string[$key] = check($val, $doubleEncode);
1✔
83
        }
84
    } else {
85
        $string = htmlspecialchars($string, ENT_QUOTES, 'UTF-8', $doubleEncode);
1✔
86
        $search = [chr(0), "\x00", "\x1A", chr(226) . chr(128) . chr(174)];
1✔
87
        $string = str_replace($search, [], $string);
1✔
88
    }
89

90
    return $string;
1✔
91
}
92

93
/**
94
 * Преобразует в положительное число
95
 */
96
function int(array|int|string|null $num): int
97
{
98
    return abs((int) $num);
12✔
99
}
100

101
/**
102
 * Преобразует все элементы массива в int
103
 */
104
function intar(array|int|string|null $numbers): ?array
105
{
106
    if (! $numbers) {
1✔
107
        return null;
1✔
108
    }
109

110
    if (is_array($numbers)) {
1✔
111
        $numbers = array_map('intval', $numbers);
1✔
112
    } else {
113
        $numbers = [(int) $numbers];
1✔
114
    }
115

116
    return $numbers;
1✔
117
}
118

119
/**
120
 * Возвращает размер в человеко читаемом формате
121
 */
122
function formatSize(int $bytes, int $precision = 2): string
123
{
124
    $units = ['B', 'Kb', 'Mb', 'Gb', 'Tb'];
8✔
125
    $pow = floor(($bytes ? log($bytes) : 0) / log(1000));
8✔
126
    $pow = min($pow, count($units) - 1);
8✔
127

128
    $bytes /= (1 << (10 * $pow));
8✔
129

130
    return round($bytes, $precision) . $units[$pow];
8✔
131
}
132

133
/**
134
 * Возвращает размер файла человеко-читаемом формате
135
 */
136
function formatFileSize(string $file): string
137
{
138
    if (file_exists($file) && is_file($file)) {
1✔
139
        return formatSize(filesize($file));
×
140
    }
141

142
    return formatSize(0);
1✔
143
}
144

145
/**
146
 * Возвращает время в человеко-читаемом формате
147
 */
148
function formatTime(int $time, int $crumbs = 2): string
149
{
150
    if ($time < 1) {
5✔
151
        return '0';
1✔
152
    }
153

154
    $units = [
4✔
155
        __('main.plural_years')   => 31536000,
4✔
156
        __('main.plural_months')  => 2592000,
4✔
157
        __('main.plural_days')    => 86400,
4✔
158
        __('main.plural_hours')   => 3600,
4✔
159
        __('main.plural_minutes') => 60,
4✔
160
        __('main.plural_seconds') => 1,
4✔
161
    ];
4✔
162

163
    $return = [];
4✔
164

165
    foreach ($units as $unit => $seconds) {
4✔
166
        $format = (int) ($time / $seconds);
4✔
167
        $time %= $seconds;
4✔
168

169
        if ($format >= 1) {
4✔
170
            $return[] = plural($format, $unit);
4✔
171
        }
172
    }
173

174
    return implode(' ', array_slice($return, 0, $crumbs));
4✔
175
}
176

177
/**
178
 * Очищает строку от мата по базе слов
179
 */
180
function antimat(?string $str): string
181
{
182
    return Antimat::replace((string) $str);
1✔
183
}
184

185
/**
186
 * Возвращает количество пользователей онлайн по типам
187
 */
188
function statsOnline(): array
189
{
190
    return Cache::remember('online', 60, static function () {
3✔
191
        $rows = Online::query()->select('user_id')->get();
3✔
192

193
        $users = $rows->whereNotNull('user_id')->unique('user_id');
3✔
194
        $usersCount = $users->count();
3✔
195
        $guestsCount = $rows->whereNull('user_id')->count();
3✔
196
        $total = $usersCount + $guestsCount;
3✔
197

198
        $metrika = new Metrika();
3✔
199
        $metrika->getCounter($total);
3✔
200

201
        return [$usersCount, $guestsCount, $total, $users];
3✔
202
    });
3✔
203
}
204

205
/**
206
 * Возвращает количество пользователей онлайн
207
 */
208
function showOnline(): ?HtmlString
209
{
210
    if (setting('onlines')) {
3✔
211
        $online = statsOnline();
3✔
212

213
        return new HtmlString(view('app/_online', compact('online')));
3✔
214
    }
215

216
    return null;
×
217
}
218

219
/**
220
 * Возвращает статистику посещений
221
 */
222
function statsCounter(): array
223
{
224
    return Cache::remember('counter', 30, static function () {
3✔
225
        $counter = Counter::query()->first();
3✔
226

227
        return $counter ? $counter->toArray() : [];
3✔
228
    });
3✔
229
}
230

231
/**
232
 * Выводит счетчик посещений
233
 */
234
function showCounter(): ?HtmlString
235
{
236
    $counter = statsCounter();
3✔
237

238
    if (setting('incount') > 0) {
3✔
239
        return new HtmlString(view('app/_counter', compact('counter')));
3✔
240
    }
241

242
    return null;
×
243
}
244

245
/**
246
 * Возвращает количество пользователей
247
 */
248
function statsUsers(): string
249
{
250
    return Cache::remember('statUsers', 1800, static function () {
3✔
251
        $stat = User::query()->count();
3✔
252
        $new = User::query()->where('created_at', '>', strtotime('-1 day', SITETIME))->count();
3✔
253

254
        if ($new) {
3✔
255
            $stat .= '/+' . $new;
3✔
256
        }
257

258
        return $stat;
3✔
259
    });
3✔
260
}
261

262
/**
263
 * Возвращает количество администраторов
264
 */
265
function statsAdmins(): int
266
{
267
    return Cache::remember('statAdmins', 3600, static function () {
×
268
        return User::query()->whereIn('level', User::ADMIN_GROUPS)->count();
×
269
    });
×
270
}
271

272
/**
273
 * Возвращает количество жалоб
274
 */
275
function statsSpam(): int
276
{
277
    return Spam::query()->count();
1✔
278
}
279

280
/**
281
 * Возвращает количество забанненых пользователей
282
 */
283
function statsBanned(): int
284
{
285
    return User::query()
×
286
        ->where('level', User::BANNED)
×
287
        ->where('timeban', '>', SITETIME)
×
288
        ->count();
×
289
}
290

291
/**
292
 * Возвращает количество записей в истории банов
293
 */
294
function statsBanHist(): int
295
{
296
    return Banhist::query()->count();
×
297
}
298

299
/**
300
 * Возвращает количество ожидающих подтверждения регистрации
301
 */
302
function statsRegList(): int
303
{
304
    return User::query()->where('level', User::PENDED)->count();
×
305
}
306

307
/**
308
 * Возвращает количество забаненных по IP
309
 */
310
function statsIpBanned(): int
311
{
312
    return Ban::query()->count();
×
313
}
314

315
/**
316
 * Возвращает количество записей в черном списке
317
 */
318
function statsBlacklist(): string
319
{
320
    $blacklist = BlackList::query()
×
321
        ->selectRaw('type, count(*) as total')
×
322
        ->groupBy('type')
×
323
        ->pluck('total', 'type')
×
324
        ->all();
×
325

326
    $list = $blacklist + ['login' => 0, 'email' => 0, 'domain' => 0];
×
327

328
    return $list['login'] . '/' . $list['email'] . '/' . $list['domain'];
×
329
}
330

331
/**
332
 * Возвращает количество записей в антимате
333
 */
334
function statsAntimat(): int
335
{
336
    return Antimat::query()->count();
×
337
}
338

339
/**
340
 * Возвращает количество стикеров
341
 */
342
function statsStickers(): int
343
{
344
    return Sticker::query()->count();
×
345
}
346

347
/**
348
 * Частично скрывает email
349
 */
350
function hideMail(string $email): string
351
{
352
    return preg_replace('/(?<=.).(?=.*@)/u', '*', $email);
4✔
353
}
354

355
/**
356
 * Возвращает иконку расширения
357
 */
358
function icons(string $ext): HtmlString
359
{
360
    $icons = [
6✔
361
        'php'  => 'fa-regular fa-file-code',
6✔
362
        'ppt'  => 'fa-regular fa-file-powerpoint',
6✔
363
        'doc'  => 'fa-regular fa-file-word',
6✔
364
        'docx' => 'fa-regular fa-file-word',
6✔
365
        'xls'  => 'fa-regular fa-file-excel',
6✔
366
        'xlsx' => 'fa-regular fa-file-excel',
6✔
367
        'txt'  => 'fa-regular fa-file-alt',
6✔
368
        'css'  => 'fa-regular fa-file-alt',
6✔
369
        'dat'  => 'fa-regular fa-file-alt',
6✔
370
        'html' => 'fa-regular fa-file-alt',
6✔
371
        'htm'  => 'fa-regular fa-file-alt',
6✔
372
        'wav'  => 'fa-regular fa-file-audio',
6✔
373
        'amr'  => 'fa-regular fa-file-audio',
6✔
374
        'mp3'  => 'fa-regular fa-file-audio',
6✔
375
        'mid'  => 'fa-regular fa-file-audio',
6✔
376
        'zip'  => 'fa-regular fa-file-archive',
6✔
377
        'rar'  => 'fa-regular fa-file-archive',
6✔
378
        '7z'   => 'fa-regular fa-file-archive',
6✔
379
        'gz'   => 'fa-regular fa-file-archive',
6✔
380
        '3gp'  => 'fa-regular fa-file-video',
6✔
381
        'mp4'  => 'fa-regular fa-file-video',
6✔
382
        'jpg'  => 'fa-regular fa-file-image',
6✔
383
        'jpeg' => 'fa-regular fa-file-image',
6✔
384
        'bmp'  => 'fa-regular fa-file-image',
6✔
385
        'wbmp' => 'fa-regular fa-file-image',
6✔
386
        'gif'  => 'fa-regular fa-file-image',
6✔
387
        'png'  => 'fa-regular fa-file-image',
6✔
388
        'webp' => 'fa-regular fa-file-image',
6✔
389
        'ttf'  => 'fa-solid fa-font',
6✔
390
        'pdf'  => 'fa-regular fa-file-pdf',
6✔
391
        'csv'  => 'fa-regular fa-file-csv',
6✔
392
    ];
6✔
393

394
    $ico = $icons[$ext] ?? 'fa-regular fa-file';
6✔
395

396
    return new HtmlString('<i class="' . $ico . '"></i>');
6✔
397
}
398

399
/**
400
 * Возвращает обрезанную до заданного количества слов строку
401
 */
402
function truncateHtml(?string $html, int $words = 20, string $end = '...'): HtmlString
403
{
404
    $text = strip_tags((string) $html);
×
405

406
    return new HtmlString(Str::words($text, $words, $end));
×
407
}
408

409
/**
410
 * Возвращает обрезанную строку с удалением перевода строки
411
 */
412
function truncateDescription(HtmlString|string $value, int $words = 20, string $end = ''): string
413
{
414
    $value = strip_tags(preg_replace('/[\s\n\r]+/', ' ', $value));
1✔
415

416
    return Str::words(trim($value), $words, $end);
1✔
417
}
418

419
/**
420
 * Форматирует вывод числа
421
 */
422
function formatNum(int|float $num): HtmlString
423
{
424
    if ($num > 0) {
1✔
425
        $data = '<span style="color:#00aa00">+' . $num . '</span>';
1✔
426
    } elseif ($num < 0) {
1✔
427
        $data = '<span style="color:#ff0000">' . $num . '</span>';
1✔
428
    } else {
429
        $data = '<span>0</span>';
1✔
430
    }
431

432
    return new HtmlString($data);
1✔
433
}
434

435
/**
436
 * Форматирует вывод числа
437
 */
438
function formatShortNum(int $num): int|string
439
{
440
    $thresholds = [
9✔
441
        1_000_000_000_000 => 'T',
9✔
442
        1_000_000_000     => 'B',
9✔
443
        1_000_000         => 'M',
9✔
444
        1_000             => 'K',
9✔
445
    ];
9✔
446

447
    foreach ($thresholds as $threshold => $suffix) {
9✔
448
        if ($num > $threshold) {
9✔
449
            return round($num / $threshold, 1) . $suffix;
4✔
450
        }
451
    }
452

453
    return $num;
5✔
454
}
455

456
/**
457
 * Удаляет директорию рекурсивно
458
 */
459
function deleteDir(string $dir): void
460
{
461
    if (file_exists($dir)) {
×
462
        if ($files = glob($dir . '/*')) {
×
463
            foreach ($files as $file) {
×
464
                is_dir($file) ? deleteDir($file) : unlink($file);
×
465
            }
466
        }
467
        rmdir($dir);
×
468
    }
469
}
470

471
/**
472
 * Удаляет файл
473
 */
474
function deleteFile(string $path): bool
475
{
476
    if (file_exists($path) && is_file($path)) {
×
477
        unlink($path);
×
478
    }
479

480
    return true;
×
481
}
482

483
/**
484
 * Отправляет уведомление об упоминании в приват
485
 */
486
function sendNotify(string $text, string $url, string $title, array $exclude = []): void
487
{
488
    if (! $login = getUser('login')) {
×
489
        return;
×
490
    }
491

492
    $excludeLogins = array_merge([$login], $exclude);
×
493

494
    preg_match_all('/<a[^>]+class="user"[^>]*href="\/users\/([\w\-]+)"/', $text, $matches);
×
495

496
    if (! empty($matches[1])) {
×
497
        $usersAnswer = array_unique(array_diff($matches[1], $excludeLogins));
×
498

499
        foreach ($usersAnswer as $user) {
×
500
            $user = getUserByLogin($user);
×
501

502
            if ($user?->notify_mention) {
×
503
                $notify = textNotice('notify', compact('login', 'url', 'title', 'text'));
×
504
                $user->sendMessage(null, $notify);
×
505
            }
506
        }
507
    }
508
}
509

510
/**
511
 * Возвращает приватное сообщение
512
 */
513
function textNotice(string $type, array $replace = []): string
514
{
515
    $message = Notice::query()->where('type', $type)->first();
×
516

517
    if (! $message) {
×
518
        return __('main.text_missing');
×
519
    }
520

521
    if (isset($replace['url'])) {
×
522
        $replace['page'] = '<a href="' . $replace['url'] . '">' . ($replace['title'] ?? $replace['url']) . '</a>';
×
523
        unset($replace['url'], $replace['title']);
×
524
    }
525

526
    foreach ($replace as $key => $val) {
×
527
        if ($key === 'login') {
×
528
            $val = '<a class="user" href="/users/' . $val . '">@' . $val . '</a>';
×
529
        }
530

531
        if ($key === 'text') {
×
532
            $message->text = str_replace('<p>%text%</p>', '%text%', $message->text);
×
533
        }
534

535
        $message->text = str_replace('%' . $key . '%', $val, $message->text);
×
536
    }
537

538
    return $message->text;
×
539
}
540

541
/**
542
 * Возвращает блок статистики производительности
543
 */
544
function performance(): ?HtmlString
545
{
546
    if (isAdmin() && setting('performance')) {
3✔
547
        $queries = getQueryLog();
1✔
548
        $timeQueries = array_sum(array_column($queries, 'time'));
1✔
549

550
        return new HtmlString(view('app/_performance', compact('queries', 'timeQueries')));
1✔
551
    }
552

553
    return null;
2✔
554
}
555

556
/**
557
 * Очистка кеш-файлов
558
 */
559
function clearCache(array|string|null $keys = null): bool
560
{
561
    if ($keys) {
4✔
562
        Cache::deleteMultiple((array) $keys);
4✔
563
    } else {
564
        Cache::flush();
1✔
565
    }
566

567
    return true;
4✔
568
}
569

570
/**
571
 * Возвращает текущую страницу
572
 */
573
function returnUrl(?string $url = null): ?string
574
{
575
    $request = request();
3✔
576

577
    if ($request->is('/', 'login', 'register', 'recovery', 'restore', 'ban', 'closed')) {
3✔
578
        return null;
×
579
    }
580

581
    $query = $request->has('return') ? $request->input('return') : $request->path();
3✔
582

583
    return '?return=' . urlencode($url ?? '/' . $query);
3✔
584
}
585

586
/**
587
 * Saves error logs
588
 */
589
function saveErrorLog(int $code, ?string $message = null): void
590
{
591
    $errorCodes = [400, 401, 403, 404, 405, 419, 429, 500, 503, 666];
3✔
592

593
    if (setting('errorlog') && in_array($code, $errorCodes, true)) {
3✔
594
        Error::query()->create([
3✔
595
            'code'       => $code,
3✔
596
            'request'    => Str::substr(request()->getRequestUri(), 0, 250),
3✔
597
            'referer'    => Str::substr(request()->header('referer'), 0, 250),
3✔
598
            'user_id'    => getUser('id'),
3✔
599
            'message'    => Str::substr($message, 0, 250),
3✔
600
            'ip'         => getIp(),
3✔
601
            'brow'       => getBrowser(),
3✔
602
            'created_at' => SITETIME,
3✔
603
        ]);
3✔
604
    }
605
}
606

607
/**
608
 * Возвращает ошибку
609
 */
610
function showError(string|array $errors): HtmlString
611
{
612
    $errors = (array) $errors;
×
613

614
    return new HtmlString(view('app/_error', compact('errors')));
×
615
}
616

617
/**
618
 * Get captcha
619
 */
620
function getCaptcha(): HtmlString
621
{
622
    return new HtmlString(view('app/_captcha'));
×
623
}
624

625
/**
626
 * Проверяет captcha
627
 */
628
function captchaVerify(): bool
629
{
630
    $request = request();
×
631

632
    if (setting('captcha_type') === 'recaptcha_v2') {
×
633
        $recaptcha = new ReCaptcha(setting('recaptcha_private'));
×
634

635
        $response = $recaptcha->setExpectedHostname($request->getHost())
×
636
            ->verify($request->input('g-recaptcha-response'), getIp());
×
637

638
        return $response->isSuccess();
×
639
    }
640

641
    if (setting('captcha_type') === 'recaptcha_v3') {
×
642
        $recaptcha = new ReCaptcha(setting('recaptcha_private'));
×
643

644
        $response = $recaptcha->setExpectedHostname($request->getHost())
×
645
            ->setExpectedAction('submit')
×
646
            ->setScoreThreshold(0.5)
×
647
            ->verify($request->input('protect'), getIp());
×
648

649
        return $response->isSuccess();
×
650
    }
651

652
    if (in_array(setting('captcha_type'), ['graphical', 'animated'], true)) {
×
653
        return strtolower($request->input('protect')) === strtolower($request->session()->get('protect'));
×
654
    }
655

656
    return true;
×
657
}
658

659
/**
660
 * Сохраняет flash уведомления
661
 *
662
 * @deprecated since 10.1 - Use redirect()->with('success', 'Message') or redirect()->withErrors($validator->getErrors())
663
 * $request->session()->flash('flash.{status}', $message);
664
 */
665
function setFlash(string $status, mixed $message): void
666
{
667
    session(['flash.' . $status => $message]);
×
668
}
669

670
/**
671
 * Сохраняет POST данные введенных пользователем
672
 *
673
 * @deprecated since 10.1 - Use $request->flash() or redirect()->withInput();
674
 */
675
function setInput(array $data): void
676
{
677
    app('session')->flash('_old_input', $data);
×
678
}
679

680
/**
681
 * Возвращает значение из POST данных
682
 *
683
 * @deprecated since 10.1 - Use old('field', 'default');
684
 */
685
function getInput(string $key, mixed $default = null): mixed
686
{
687
    if (app('session')->missing('_old_input')) {
×
688
        return $default;
×
689
    }
690

691
    $input = session('_old_input', []);
×
692

693
    return Arr::get($input, $key, $default);
×
694
}
695

696
/**
697
 * Подсвечивает блок с полем для ввода сообщения
698
 */
699
function hasError(string $field): string
700
{
701
    // Новая валидация
702
    if (session('errors')) {
×
703
        /** @var ViewErrorBag $errors */
704
        $errors = session('errors');
×
705

706
        return $errors->has($field) ? ' is-invalid' : ' is-valid';
×
707
    }
708

709
    $isValid = session('flash.danger') ? ' is-valid' : '';
×
710

711
    return session('flash.danger.' . $field) ? ' is-invalid' : $isValid;
×
712
}
713

714
/**
715
 * Возвращает блок с текстом ошибки
716
 */
717
function textError(string $field): ?string
718
{
719
    // Новая валидация
720
    if (session('errors')) {
×
721
        /** @var ViewErrorBag $errors */
722
        $errors = session('errors');
×
723

724
        return $errors->first($field);
×
725
    }
726

727
    return session('flash.danger.' . $field);
×
728
}
729

730
/**
731
 * Отправляет уведомления на email
732
 */
733
function sendMail(string $view, array $data): bool
734
{
735
    try {
736
        Mail::send($view, $data, static function (Message $message) use ($data) {
×
737
            $message->subject($data['subject'])
×
738
                ->to($data['to'])
×
739
                ->from(config('mail.from.address'), config('mail.from.name'));
×
740

741
            if (isset($data['from'])) {
×
742
                [$fromEmail, $fromName] = $data['from'];
×
743
                $message->replyTo($fromEmail, $fromName);
×
744
            }
745

746
            if (isset($data['unsubscribe'])) {
×
747
                $headers = $message->getHeaders();
×
748
                $headers->addTextHeader(
×
749
                    'List-Unsubscribe',
×
750
                    '<' . config('app.url') . '/unsubscribe?key=' . $data['unsubscribe'] . '>'
×
751
                );
×
752
            }
753
        });
×
754
    } catch (Exception) {
×
755
        return false;
×
756
    }
757

758
    return true;
×
759
}
760

761
/**
762
 * Возвращает расширение файла
763
 */
764
function getExtension(string $filename): string
765
{
766
    return pathinfo($filename, PATHINFO_EXTENSION);
4✔
767
}
768

769
/**
770
 * Возвращает имя файла без расширения
771
 */
772
function getBodyName(string $filename): string
773
{
774
    $name = pathinfo($filename, PATHINFO_FILENAME);
3✔
775

776
    return preg_replace('/\xc2\xa0/', ' ', $name);
3✔
777
}
778

779
/**
780
 * Склоняет числа
781
 */
782
function plural(int $num, mixed $forms): string
783
{
784
    if (! is_array($forms)) {
11✔
785
        $forms = explode(',', $forms);
5✔
786
    }
787

788
    if (count($forms) === 1) {
11✔
789
        return $num . ' ' . $forms[0];
1✔
790
    }
791

792
    if ($num % 100 > 10 && $num % 100 < 15) {
11✔
793
        return $num . ' ' . $forms[2];
1✔
794
    }
795

796
    if ($num % 10 === 1) {
10✔
797
        return $num . ' ' . $forms[0];
6✔
798
    }
799

800
    if ($num % 10 > 1 && $num % 10 < 5) {
4✔
801
        return $num . ' ' . $forms[1];
1✔
802
    }
803

804
    return $num . ' ' . $forms[2];
3✔
805
}
806

807
/**
808
 * RenderHtml
809
 */
810
function renderHtml(?string $text, string $group = 'gallery'): HtmlString
811
{
812
    return HtmlRenderer::html($text, $group);
×
813
}
814

815
/**
816
 * Render text
817
 */
818
function renderText(?string $text): HtmlString
819
{
820
    return HtmlRenderer::text($text);
×
821
}
822

823
/**
824
 * Определяет IP пользователя
825
 */
826
function getIp(): string
827
{
828
    static $ip = null;
3✔
829

830
    return $ip ??= (new CloudFlare(request()))->ip();
3✔
831
}
832

833
/**
834
 * Определяет браузер
835
 */
836
function getBrowser(): string
837
{
838
    static $userAgent = null;
3✔
839

840
    if ($userAgent !== null) {
3✔
841
        return $userAgent;
3✔
842
    }
843

844
    $browser = new Browser();
1✔
845
    $name = $browser->getBrowser();
1✔
846
    $parts = explode('.', $browser->getVersion(), 3);
1✔
847
    $version = implode('.', array_slice($parts, 0, 2));
1✔
848

849
    $result = $version === Browser::VERSION_UNKNOWN ? $name : $name . ' ' . $version;
1✔
850

851
    return $userAgent = mb_substr($result, 0, 25, 'utf-8');
1✔
852
}
853

854
/**
855
 * Является ли пользователь администратором
856
 */
857
function isAdmin(?string $level = null): bool
858
{
859
    $user = auth()->user();
3✔
860
    if (! $user) {
3✔
861
        return false;
1✔
862
    }
863

864
    $levels = array_flip(User::ADMIN_GROUPS);
2✔
865
    $level = $level ?? User::EDITOR;
2✔
866

867
    return isset($levels[$user->level], $levels[$level])
2✔
868
        && $levels[$user->level] >= $levels[$level];
2✔
869
}
870

871
/**
872
 * Возвращает объект пользователя по логину
873
 */
874
function getUserByLogin(?string $login): ?User
875
{
876
    return User::query()->where('login', $login)->first();
×
877
}
878

879
/**
880
 * Возвращает объект пользователя по id
881
 */
882
function getUserById(?int $id): ?User
883
{
884
    return User::query()->find($id);
×
885
}
886

887
/**
888
 * Возвращает объект пользователя по токену
889
 */
890
function getUserByToken(string $token): ?User
891
{
892
    return User::query()->where('apikey', $token)->first();
×
893
}
894

895
/**
896
 * Возвращает объект пользователя по логину или email
897
 */
898
function getUserByLoginOrEmail(?string $login): ?User
899
{
900
    $field = strpos($login, '@') ? 'email' : 'login';
×
901

902
    return User::query()->where($field, $login)->first();
×
903
}
904

905
/**
906
 * Возвращает данные пользователя по ключу
907
 */
908
function getUser(?string $key = null): mixed
909
{
910
    static $user;
4✔
911

912
    if (! $user) {
4✔
913
        $user = auth()->user();
3✔
914
    }
915

916
    return $key ? ($user->$key ?? null) : $user;
4✔
917
}
918

919
/**
920
 * Разбивает данные по страницам
921
 */
922
function paginate(array|Collection $items, int $perPage, array $appends = []): LengthAwarePaginator
923
{
924
    $data = $items instanceof Collection ? $items : Collection::make($items);
×
925

926
    $currentPage = LengthAwarePaginator::resolveCurrentPage();
×
927

928
    $collection = new LengthAwarePaginator(
×
929
        $data->forPage($currentPage, $perPage),
×
930
        $data->count(),
×
931
        $perPage,
×
932
        $currentPage
×
933
    );
×
934

935
    $collection->setPath(request()->url());
×
936
    $collection->appends($appends);
×
937

938
    return $collection;
×
939
}
940

941
/**
942
 * Разбивает данные по страницам
943
 */
944
function simplePaginate(array|Collection $items, int $perPage, array $appends = []): Paginator
945
{
946
    $data = $items instanceof Collection ? $items : Collection::make($items);
×
947

948
    $currentPage = Paginator::resolveCurrentPage();
×
949

950
    $collection = new Paginator(
×
951
        $data->slice(max(0, ($currentPage - 1) * $perPage)),
×
952
        $perPage
×
953
    );
×
954

955
    $collection->setPath(request()->url());
×
956
    $collection->appends($appends);
×
957

958
    return $collection;
×
959
}
960

961
/**
962
 * Возвращает сформированный код base64 картинки
963
 */
964
function imageBase64(string $path, array $params = []): HtmlString
965
{
966
    $type = getExtension($path);
×
967
    $data = file_get_contents($path);
×
968

969
    if (! isset($params['class'])) {
×
970
        $params['class'] = 'img-fluid';
×
971
    }
972

973
    if (empty($params['alt'])) {
×
974
        $params['alt'] = basename($path);
×
975
    }
976

977
    $strParams = [];
×
978
    foreach ($params as $key => $param) {
×
979
        $strParams[] = $key . '="' . $param . '"';
×
980
    }
981

982
    $strParams = implode(' ', $strParams);
×
983

984
    return new HtmlString('<img src="data:image/' . $type . ';base64,' . base64_encode($data) . '"' . $strParams . '>');
×
985
}
986

987
/**
988
 * Выводит прогресс-бар
989
 */
990
function progressBar(int $percent, float|int|string|null $title = null): HtmlString
991
{
992
    if (! $title) {
×
993
        $title = $percent . '%';
×
994
    }
995

996
    return new HtmlString(view('app/_progressbar', compact('percent', 'title')));
×
997
}
998

999
/**
1000
 * Возвращает форматированный список запросов
1001
 */
1002
function getQueryLog(): array
1003
{
1004
    $queries = DB::getQueryLog();
1✔
1005

1006
    $formattedQueries = [];
1✔
1007

1008
    foreach ($queries as $query) {
1✔
1009
        foreach ($query['bindings'] as $key => $binding) {
1✔
1010
            if (is_string($binding)) {
1✔
1011
                $query['bindings'][$key] = ctype_print($binding) ? "'$binding'" : '[binary]';
1✔
1012
            } else {
1013
                $query['bindings'][$key] = $binding ?? 'null';
1✔
1014
            }
1015
        }
1016

1017
        $sql = str_replace(['%', '?'], ['%%', '%s'], $query['query']);
1✔
1018
        $sql = vsprintf($sql, $query['bindings']);
1✔
1019

1020
        $formattedQueries[] = ['query' => $sql, 'time' => $query['time']];
1✔
1021
    }
1022

1023
    return $formattedQueries;
1✔
1024
}
1025

1026
/**
1027
 * Возвращает настройки сайта по ключу
1028
 */
1029
function setting(?string $key = null, mixed $default = null): mixed
1030
{
1031
    static $settings;
60✔
1032

1033
    if (! $settings) {
60✔
1034
        $settings = Setting::getSettings();
×
1035
    }
1036

1037
    return $key ? ($settings[$key] ?? $default) : $settings;
60✔
1038
}
1039

1040
/**
1041
 * Получает версию
1042
 */
1043
function parseVersion(string $version): string
1044
{
1045
    $ver = explode('.', strtok($version, '-'));
3✔
1046

1047
    return $ver[0] . '.' . $ver[1] . '.' . ($ver[2] ?? '0');
3✔
1048
}
1049

1050
/**
1051
 * Возвращает уникальное имя
1052
 */
1053
function uniqueName(?string $extension = null): string
1054
{
1055
    if ($extension) {
×
1056
        $extension = '.' . $extension;
×
1057
    }
1058

1059
    return str_replace('.', '', uniqid('', true)) . $extension;
×
1060
}
1061

1062
/**
1063
 * Возвращает список доступных тем оформления
1064
 */
1065
function getAvailableThemes(): array
1066
{
1067
    return array_map('basename', glob(resource_path('views/themes/*'), GLOB_ONLYDIR) ?: []);
×
1068
}
1069

1070
/**
1071
 * Возвращает список доступных языков
1072
 */
1073
function getAvailableLanguages(): array
1074
{
1075
    return array_map('basename', glob(resource_path('lang/*'), GLOB_ONLYDIR) ?: []);
3✔
1076
}
1077

1078
/**
1079
 * Заменяет относительные URL на абсолютные
1080
 */
1081
function absolutizeUrls(string $text): string
1082
{
1083
    $base = config('app.url');
×
1084

1085
    return preg_replace('/(href|src)="(\/[^"]*)"/', '$1="' . $base . '$2"', $text);
×
1086
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc