Просмотр файла app/helpers.php

Размер файла: 56.03Kb
  1. <?php
  2.  
  3. use App\Models\AdminAdvert;
  4. use App\Models\PaidAdvert;
  5. use App\Classes\{BBCode, Calendar, Metrika, CloudFlare};
  6. use App\Models\Antimat;
  7. use App\Models\Ban;
  8. use App\Models\Banhist;
  9. use App\Models\BlackList;
  10. use App\Models\Article;
  11. use App\Models\Item;
  12. use App\Models\Load;
  13. use App\Models\Chat;
  14. use App\Models\Counter;
  15. use App\Models\Down;
  16. use App\Models\Guestbook;
  17. use App\Models\Invite;
  18. use App\Models\Error;
  19. use App\Models\News;
  20. use App\Models\Notice;
  21. use App\Models\Offer;
  22. use App\Models\Online;
  23. use App\Models\Photo;
  24. use App\Models\Post;
  25. use App\Models\Advert;
  26. use App\Models\Setting;
  27. use App\Models\Sticker;
  28. use App\Models\Spam;
  29. use App\Models\Topic;
  30. use App\Models\User;
  31. use App\Models\Vote;
  32. use GuzzleHttp\Client;
  33. use Illuminate\Mail\Message;
  34. use Illuminate\Support\Facades\DB;
  35. use Illuminate\Database\Eloquent\Builder;
  36. use Illuminate\Database\Eloquent\Model;
  37. use Illuminate\Pagination\LengthAwarePaginator;
  38. use Illuminate\Support\Arr;
  39. use Illuminate\Support\Facades\Cache;
  40. use Illuminate\Support\Facades\Mail;
  41. use Illuminate\Support\HtmlString;
  42. use Illuminate\Support\Str;
  43. use Illuminate\Support\ViewErrorBag;
  44. use Intervention\Image\Constraint;
  45. use Intervention\Image\ImageManagerStatic as Image;
  46. use ReCaptcha\ReCaptcha;
  47.  
  48. const ROTOR_VERSION = '10.1';
  49. define('SITETIME', time());
  50.  
  51. /**
  52. * Форматирует вывод времени из секунд
  53. *
  54. * @param int $time секунды
  55. *
  56. * @return string форматированный вывод
  57. */
  58. function makeTime(int $time): string
  59. {
  60. $format = $time < 3600 ? 'i:s' : 'H:i:s';
  61.  
  62. return gmdate($format, $time);
  63. }
  64.  
  65. /**
  66. * Форматирует время с учетом часовых поясов
  67. *
  68. * @param int|null $timestamp Секунды
  69. * @param string $format Формат времени
  70. * @param bool $original Формат без изменения
  71. *
  72. * @return string Форматированный вывод
  73. */
  74. function dateFixed(?int $timestamp, string $format = 'd.m.Y / H:i', bool $original = false): string
  75. {
  76. if (! is_numeric($timestamp)) {
  77. $timestamp = SITETIME;
  78. }
  79.  
  80. $shift = getUser('timezone') * 3600;
  81. $dateStamp = date($format, $timestamp + $shift);
  82.  
  83. if ($original) {
  84. return $dateStamp;
  85. }
  86.  
  87. $today = date('d.m.Y', SITETIME + $shift);
  88. $yesterday = date('d.m.Y', strtotime('-1 day', SITETIME + $shift));
  89.  
  90. $replaces = [
  91. $today => __('main.today'),
  92. $yesterday => __('main.yesterday'),
  93. 'January' => __('main.january'),
  94. 'February' => __('main.february'),
  95. 'March' => __('main.march'),
  96. 'April' => __('main.april'),
  97. 'May' => __('main.may'),
  98. 'June' => __('main.june'),
  99. 'July' => __('main.july'),
  100. 'August' => __('main.august'),
  101. 'September' => __('main.september'),
  102. 'October' => __('main.october'),
  103. 'November' => __('main.november'),
  104. 'December' => __('main.december'),
  105. ];
  106.  
  107. return strtr($dateStamp, $replaces);
  108. }
  109.  
  110. /**
  111. * Конвертирует строку в кодировку utf-8
  112. *
  113. * @param string $str строка
  114. *
  115. * @return string Конвертированная строка
  116. */
  117. function winToUtf(string $str): string
  118. {
  119. return mb_convert_encoding($str, 'utf-8', 'windows-1251');
  120. }
  121.  
  122. /**
  123. * Преобразует строку в нижний регистр
  124. *
  125. * @param string $str строка
  126. *
  127. * @return string Преобразованная строка
  128. */
  129. function utfLower(string $str): string
  130. {
  131. return mb_strtolower($str, 'utf-8');
  132. }
  133.  
  134. /**
  135. * Обрезает строку
  136. *
  137. * @param mixed $str Строка
  138. * @param int $start Начало позиции
  139. * @param int|null $length Конец позиции
  140. *
  141. * @return string Обрезанная строка
  142. */
  143. function utfSubstr($str, int $start, ?int $length = null): string
  144. {
  145. if (! $length) {
  146. $length = utfStrlen($str);
  147. }
  148.  
  149. return mb_substr($str, $start, $length, 'utf-8');
  150. }
  151.  
  152. /**
  153. * Возвращает длину строки
  154. *
  155. * @param mixed $str строка
  156. *
  157. * @return int Длина строка
  158. */
  159. function utfStrlen($str): int
  160. {
  161. return mb_strlen($str, 'utf-8');
  162. }
  163.  
  164. /**
  165. * Является ли кодировка utf-8
  166. *
  167. * @param string $str строка
  168. *
  169. * @return bool
  170. */
  171. function isUtf(string $str): bool
  172. {
  173. return mb_check_encoding($str, 'utf-8');
  174. }
  175.  
  176. /**
  177. * Преобразует специальные символы в HTML-сущности
  178. *
  179. * @param mixed $string Строка или массив строк
  180. * @param bool $doubleEncode Преобразовывать существующие html-сущности
  181. *
  182. * @return array|string Обработанные данные
  183. */
  184. function check($string, bool $doubleEncode = true)
  185. {
  186. if (is_array($string)) {
  187. foreach ($string as $key => $val) {
  188. $string[$key] = check($val, $doubleEncode);
  189. }
  190. } else {
  191. $string = htmlspecialchars($string, ENT_QUOTES, 'UTF-8', $doubleEncode);
  192. $search = [chr(0), "\x00", "\x1A", chr(226) . chr(128) . chr(174)];
  193. $string = str_replace($search, [], $string);
  194. }
  195.  
  196. return $string;
  197. }
  198.  
  199. /**
  200. * Преобразует в положительное число
  201. *
  202. * @param int|string $num число
  203. *
  204. * @return int Обработанные данные
  205. */
  206. function int($num): int
  207. {
  208. return abs((int) $num);
  209. }
  210.  
  211. /**
  212. * Преобразует все элементы массива в int
  213. *
  214. * @param array|int|string $numbers массив или число
  215. *
  216. * @return array|null Обработанные данные
  217. */
  218. function intar($numbers): ?array
  219. {
  220. if ($numbers) {
  221. if (is_array($numbers)) {
  222. $numbers = array_map('intval', $numbers);
  223. } else {
  224. $numbers = [(int) $numbers];
  225. }
  226. }
  227.  
  228. return $numbers;
  229. }
  230.  
  231. /**
  232. * Возвращает размер в человеко читаемом формате
  233. *
  234. * @param int $bytes размер в байтах
  235. * @param int $precision кол. символов после запятой
  236. *
  237. * @return string Форматированный вывод размера
  238. */
  239. function formatSize(int $bytes, int $precision = 2): string
  240. {
  241. $units = ['B', 'Kb', 'Mb', 'Gb', 'Tb'];
  242. $pow = floor(($bytes ? log($bytes) : 0) / log(1000));
  243. $pow = min($pow, count($units) - 1);
  244.  
  245. $bytes /= (1 << (10 * $pow));
  246.  
  247. return round($bytes, $precision) . $units[$pow];
  248. }
  249.  
  250. /**
  251. * Возвращает размер файла человеко-читаемом формате
  252. *
  253. * @param string $file Путь к файлу
  254. *
  255. * @return string Размер в читаемом формате
  256. */
  257. function formatFileSize(string $file): string
  258. {
  259. if (file_exists($file) && is_file($file)) {
  260. return formatSize(filesize($file));
  261. }
  262.  
  263. return formatSize(0);
  264. }
  265.  
  266. /**
  267. * Возвращает время в человеко-читаемом формате
  268. *
  269. * @param int $time Кол. секунд timestamp
  270. * @param int $crumbs Кол. элементов
  271. *
  272. * @return string Время в читаемом формате
  273. */
  274. function formatTime(int $time, int $crumbs = 2): string
  275. {
  276. if ($time < 1) {
  277. return '0';
  278. }
  279.  
  280. $units = [
  281. __('main.plural_years') => 31536000,
  282. __('main.plural_months') => 2592000,
  283. __('main.plural_days') => 86400,
  284. __('main.plural_hours') => 3600,
  285. __('main.plural_minutes') => 60,
  286. __('main.plural_seconds') => 1,
  287. ];
  288.  
  289. $return = [];
  290.  
  291. foreach ($units as $unit => $seconds) {
  292. $format = floor($time / $seconds);
  293. $time %= $seconds;
  294.  
  295. if ($format >= 1) {
  296. $return[] = plural($format, $unit);
  297. }
  298. }
  299.  
  300. return implode(' ', array_slice($return, 0, $crumbs));
  301. }
  302.  
  303. /**
  304. * Очищает строку от мата по базе слов
  305. *
  306. * @param string|null $str строка
  307. *
  308. * @return string Обработанная строка
  309. */
  310. function antimat(?string $str): string
  311. {
  312. return Antimat::replace((string) $str);
  313. }
  314.  
  315. /**
  316. * Возвращает рейтинг в виде звезд
  317. *
  318. * @param int|float $rating рейтинг
  319. *
  320. * @return HtmlString Преобразованный рейтинг
  321. */
  322. function ratingVote($rating): HtmlString
  323. {
  324. $rating = round($rating / 0.5) * 0.5;
  325.  
  326. $full_stars = floor($rating);
  327. $half_stars = ceil($rating - $full_stars);
  328. $empty_stars = 5 - $full_stars - $half_stars;
  329.  
  330. $output = '<div class="star-rating fa-lg text-danger">';
  331. $output .= str_repeat('<i class="fas fa-star"></i>', $full_stars);
  332. $output .= str_repeat('<i class="fas fa-star-half-alt"></i>', $half_stars);
  333. $output .= str_repeat('<i class="far fa-star"></i>', $empty_stars);
  334. $output .= '( ' . $rating . ' )</div>';
  335.  
  336. return new HtmlString($output);
  337. }
  338.  
  339. /**
  340. * Возвращает календарь
  341. *
  342. * @param int $time
  343. *
  344. * @return HtmlString календарь
  345. */
  346. function getCalendar(int $time = SITETIME): HtmlString
  347. {
  348. $calendar = new Calendar();
  349.  
  350. return new HtmlString($calendar->getCalendar($time));
  351. }
  352.  
  353. /**
  354. * Возвращает количество пользователей онлайн по типам
  355. *
  356. * @return array Массив данных
  357. */
  358. function statsOnline(): array
  359. {
  360. return Cache::remember('online', 60, static function () {
  361. $users = Online::query()
  362. ->select('user_id')
  363. ->distinct('user_id')
  364. ->whereNotNull('user_id')
  365. ->get();
  366.  
  367. $usersCount = $users->count();
  368. $guestsCount = Online::query()->whereNull('user_id')->count();
  369. $total = $usersCount + $guestsCount;
  370.  
  371. $metrika = new Metrika();
  372. $metrika->getCounter($usersCount + $guestsCount);
  373.  
  374. return [$usersCount, $guestsCount, $total, $users];
  375. });
  376. }
  377.  
  378. /**
  379. * Возвращает количество пользователей онлайн
  380. *
  381. * @return HtmlString|null
  382. */
  383. function showOnline(): ?HtmlString
  384. {
  385. if (setting('onlines')) {
  386. $online = statsOnline();
  387.  
  388. return new HtmlString(view('app/_online', compact('online')));
  389. }
  390.  
  391. return null;
  392. }
  393.  
  394. /**
  395. * Get online widget
  396. *
  397. * @return mixed
  398. */
  399. function onlineWidget(): HtmlString
  400. {
  401. $online = statsOnline();
  402.  
  403. return new HtmlString(view('widgets/_online', compact('online')));
  404. }
  405.  
  406. /**
  407. * Возвращает статистику посещений
  408. *
  409. * @return array статистика посещений
  410. */
  411. function statsCounter(): array
  412. {
  413. return Cache::remember('counter', 30, static function () {
  414. $counter = Counter::query()->first();
  415.  
  416. return $counter ? $counter->toArray() : [];
  417. });
  418. }
  419.  
  420. /**
  421. * Выводит счетчик посещений
  422. *
  423. * @return HtmlString|null
  424. */
  425. function showCounter(): ?HtmlString
  426. {
  427. $metrika = new Metrika();
  428. $metrika->saveStatistic();
  429.  
  430. $counter = statsCounter();
  431.  
  432. if (setting('incount') > 0) {
  433. return new HtmlString(view('app/_counter', compact('counter')));
  434. }
  435.  
  436. return null;
  437. }
  438.  
  439. /**
  440. * Возвращает количество пользователей
  441. *
  442. * @return string Количество пользователей
  443. */
  444. function statsUsers(): string
  445. {
  446. return Cache::remember('statUsers', 1800, static function () {
  447. $startDay = mktime(0, 0, 0, dateFixed(SITETIME, 'n', true));
  448.  
  449. $stat = User::query()->count();
  450. $new = User::query()->where('created_at', '>', $startDay)->count();
  451.  
  452. if ($new) {
  453. $stat .= '/+' . $new;
  454. }
  455.  
  456. return $stat;
  457. });
  458. }
  459.  
  460. /**
  461. * Возвращает количество администраторов
  462. *
  463. * @return int Количество администраторов
  464. */
  465. function statsAdmins(): int
  466. {
  467. return Cache::remember('statAdmins', 3600, static function () {
  468. return User::query()->whereIn('level', User::ADMIN_GROUPS)->count();
  469. });
  470. }
  471.  
  472. /**
  473. * Возвращает количество жалоб
  474. *
  475. * @return int Количество жалоб
  476. */
  477. function statsSpam(): int
  478. {
  479. return Spam::query()->count();
  480. }
  481.  
  482. /**
  483. * Возвращает количество забанненых пользователей
  484. *
  485. * @return int Количество забаненных
  486. */
  487. function statsBanned(): int
  488. {
  489. return User::query()
  490. ->where('level', User::BANNED)
  491. ->where('timeban', '>', SITETIME)
  492. ->count();
  493. }
  494.  
  495. /**
  496. * Возвращает количество записей в истории банов
  497. *
  498. * @return int Количество записей
  499. */
  500. function statsBanHist(): int
  501. {
  502. return Banhist::query()->count();
  503. }
  504.  
  505. /**
  506. * Возвращает количество ожидающих подтверждения регистрации
  507. *
  508. * @return int Количество ожидающих
  509. */
  510. function statsRegList(): int
  511. {
  512. return User::query()->where('level', User::PENDED)->count();
  513. }
  514.  
  515. /**
  516. * Возвращает количество забаненных по IP
  517. *
  518. * @return int Количество забаненных
  519. */
  520. function statsIpBanned(): int
  521. {
  522. return Ban::query()->count();
  523. }
  524.  
  525. /**
  526. * Возвращает количество фотографий в галерее
  527. *
  528. * @return string Количество фотографий
  529. */
  530. function statsPhotos(): string
  531. {
  532. return Cache::remember('statPhotos', 900, static function () {
  533. $stat = Photo::query()->count();
  534. $totalNew = Photo::query()->where('created_at', '>', strtotime('-1 day', SITETIME))->count();
  535.  
  536. return formatShortNum($stat) . ($totalNew ? '/+' . $totalNew : '');
  537. });
  538. }
  539.  
  540. /**
  541. * Возвращает количество новостей
  542. *
  543. * @return string Количество новостей
  544. */
  545. function statsNews(): string
  546. {
  547. return Cache::remember('statNews', 300, static function () {
  548. $total = News::query()->count();
  549.  
  550. $totalNew = News::query()
  551. ->where('created_at', '>', strtotime('-1 day', SITETIME))
  552. ->count();
  553.  
  554. return formatShortNum($total) . ($totalNew ? '/+' . $totalNew : '');
  555. });
  556. }
  557.  
  558. /**
  559. * Возвращает количество записей в черном списке
  560. *
  561. * @return string Количество записей
  562. */
  563. function statsBlacklist(): string
  564. {
  565. $blacklist = BlackList::query()
  566. ->selectRaw('type, count(*) as total')
  567. ->groupBy('type')
  568. ->pluck('total', 'type')
  569. ->all();
  570.  
  571. $list = $blacklist + ['login' => 0, 'email' => 0, 'domain' => 0];
  572.  
  573. return $list['login'] . '/' . $list['email'] . '/' . $list['domain'];
  574. }
  575.  
  576. /**
  577. * Возвращает количество записей в антимате
  578. *
  579. * @return int Количество записей
  580. */
  581. function statsAntimat(): int
  582. {
  583. return Antimat::query()->count();
  584. }
  585.  
  586. /**
  587. * Возвращает количество стикеров
  588. *
  589. * @return int Количество стикеров
  590. */
  591. function statsStickers(): int
  592. {
  593. return Sticker::query()->count();
  594. }
  595.  
  596. /**
  597. * Возвращает дату последнего сканирования сайта
  598. *
  599. * @return int|string Дата последнего сканирования
  600. */
  601. function statsChecker()
  602. {
  603. if (file_exists(storage_path('framework/cache/checker.php'))) {
  604. return dateFixed(filemtime(storage_path('framework/cache/checker.php')), 'd.m.Y');
  605. }
  606.  
  607. return 0;
  608. }
  609.  
  610. /**
  611. * Возвращает количество приглашений на регистрацию
  612. *
  613. * @return string Количество приглашений
  614. */
  615. function statsInvite(): string
  616. {
  617. $invited = Invite::query()->where('used', 0)->count();
  618. $usedInvited = Invite::query()->where('used', 1)->count();
  619.  
  620. return $invited . '/' . $usedInvited;
  621. }
  622.  
  623. /**
  624. * Возвращает следующею и предыдущую фотографию в галерее
  625. *
  626. * @param int $id Id фотографий
  627. *
  628. * @return array|null Массив данных
  629. */
  630. function photoNavigation(int $id): ?array
  631. {
  632. if (! $id) {
  633. return null;
  634. }
  635.  
  636. $next = Photo::query()
  637. ->where('id', '>', $id)
  638. ->orderBy('id')
  639. ->pluck('id')
  640. ->first();
  641.  
  642. $prev = Photo::query()
  643. ->where('id', '<', $id)
  644. ->orderByDesc('id')
  645. ->pluck('id')
  646. ->first();
  647.  
  648. return compact('next', 'prev');
  649. }
  650.  
  651. /**
  652. * Возвращает количество статей в блогах
  653. *
  654. * @return string Количество статей
  655. */
  656. function statsBlog(): string
  657. {
  658. return Cache::remember('statArticles', 900, static function () {
  659. $stat = Article::query()->count();
  660. $totalNew = Article::query()->where('created_at', '>', strtotime('-1 day', SITETIME))->count();
  661.  
  662. return formatShortNum($stat) . ($totalNew ? '/+' . $totalNew : '');
  663. });
  664. }
  665.  
  666. /**
  667. * Возвращает количество тем и сообщений в форуме
  668. *
  669. * @return string Количество тем и сообщений
  670. */
  671. function statsForum(): string
  672. {
  673. return Cache::remember('statForums', 600, static function () {
  674. $topics = Topic::query()->count();
  675. $posts = Post::query()->count();
  676.  
  677. $totalNew = Post::query()
  678. ->where('created_at', '>', strtotime('-1 day', SITETIME))
  679. ->count();
  680.  
  681. return formatShortNum($topics) . '/' . formatShortNum($posts) . ($totalNew ? '/+' . $totalNew : '');
  682. });
  683. }
  684.  
  685. /**
  686. * Возвращает количество сообщений в гостевой книге
  687. *
  688. * @return string Количество сообщений
  689. */
  690. function statsGuestbook(): string
  691. {
  692. return Cache::remember('statGuestbook', 600, static function () {
  693. $total = Guestbook::query()->count();
  694.  
  695. $totalNew = Guestbook::query()
  696. ->where('created_at', '>', strtotime('-1 day', SITETIME))
  697. ->count();
  698.  
  699. return formatShortNum($total) . ($totalNew ? '/+' . $totalNew : '');
  700. });
  701. }
  702.  
  703. /**
  704. * Возвращает количество сообщений в админ-чате
  705. *
  706. * @return string Количество сообщений
  707. */
  708. function statsChat(): string
  709. {
  710. return Cache::remember('statChat', 3600, static function () {
  711. $total = Chat::query()->count();
  712.  
  713. $totalNew = Chat::query()
  714. ->where('created_at', '>', strtotime('-1 day', SITETIME))
  715. ->count();
  716.  
  717. return formatShortNum($total) . ($totalNew ? '/+' . $totalNew : '');
  718. });
  719. }
  720.  
  721. /**
  722. * Возвращает время последнего сообщения в админ-чате
  723. *
  724. * @return int Время сообщения
  725. */
  726. function statsNewChat(): int
  727. {
  728. return Chat::query()->max('created_at') ?? 0;
  729. }
  730.  
  731. /**
  732. * Возвращает количество файлов в загруз-центре
  733. *
  734. * @return string Количество файлов
  735. */
  736. function statsLoad(): string
  737. {
  738. return Cache::remember('statLoads', 900, static function () {
  739. $totalLoads = Load::query()->sum('count_downs');
  740.  
  741. $totalNew = Down::query()->where('active', 1)
  742. ->where('created_at', '>', strtotime('-1 day', SITETIME))
  743. ->count();
  744.  
  745. return formatShortNum($totalLoads) . ($totalNew ? '/+' . $totalNew : '');
  746. });
  747. }
  748.  
  749. /**
  750. * Возвращает количество новых файлов
  751. *
  752. * @return int Количество файлов
  753. */
  754. function statsNewLoad(): int
  755. {
  756. return Down::query()->where('active', 0)->count();
  757. }
  758.  
  759. /**
  760. * Возвращает количество объявлений
  761. *
  762. * @return string Количество статей
  763. */
  764. function statsBoard(): string
  765. {
  766. return Cache::remember('statBoards', 900, static function () {
  767. $stat = formatShortNum(Item::query()->where('expires_at', '>', SITETIME)->count());
  768. $totalNew = Item::query()->where('updated_at', '>', strtotime('-1 day', SITETIME))->count();
  769.  
  770. return formatShortNum($stat) . ($totalNew ? '/+' . $totalNew : '');
  771. });
  772. }
  773.  
  774. /**
  775. * Обфусцирует email
  776. *
  777. * @param string $email email
  778. *
  779. * @return string Обфусцированный email
  780. */
  781. function cryptMail(string $email): string
  782. {
  783. $output = '';
  784. $symbols = str_split($email);
  785.  
  786. foreach ($symbols as $symbol) {
  787. $output .= '&#' . ord($symbol) . ';';
  788. }
  789.  
  790. return $output;
  791. }
  792.  
  793. /**
  794. * Частично скрывает email
  795. *
  796. * @param string $email
  797. *
  798. * @return string
  799. */
  800. function hideMail(string $email): string
  801. {
  802. return preg_replace('/(?<=.).(?=.*@)/u', '*', $email);
  803. }
  804.  
  805. /**
  806. * Возвращает статистику текущих голосований из кэш-файла
  807. *
  808. * @return string Статистика текущий голосований
  809. */
  810. function statVotes(): string
  811. {
  812. return Cache::remember('statVotes', 900, static function () {
  813. $votes = Vote::query()
  814. ->selectRaw('count(*) AS cnt, coalesce(sum(count), 0) AS sum')
  815. ->where('closed', 0)
  816. ->first();
  817.  
  818. if (! $votes) {
  819. $votes->cnt = $votes->sum = 0;
  820. }
  821.  
  822. return $votes->cnt . '/' . $votes->sum;
  823. });
  824. }
  825.  
  826. /**
  827. * Возвращает дату последней новости из кэш-файла
  828. *
  829. * @return string Дата последней новости
  830. */
  831. function statsNewsDate()
  832. {
  833. $newsDate = Cache::remember('statNewsDate', 900, static function () {
  834. /** @var News $news */
  835. $news = News::query()->orderByDesc('created_at')->first();
  836.  
  837. return $news->created_at ?? 0;
  838. });
  839.  
  840. return $newsDate ? dateFixed($newsDate, 'd.m.Y') : 0;
  841. }
  842.  
  843. /**
  844. * Возвращает последние новости
  845. *
  846. * @return HtmlString Новость
  847. */
  848. function lastNews(): HtmlString
  849. {
  850. $news = null;
  851.  
  852. if (setting('lastnews') > 0) {
  853. $news = Cache::remember('lastNews', 1800, static function () {
  854. return News::query()
  855. ->where('top', 1)
  856. ->orderByDesc('created_at')
  857. ->limit(setting('lastnews'))
  858. ->get();
  859. });
  860. }
  861.  
  862. return new HtmlString(view('widgets/_news', compact('news')));
  863. }
  864.  
  865. /**
  866. * Возвращает иконку расширения
  867. *
  868. * @param string $ext Расширение файла
  869. *
  870. * @return HtmlString Иконка
  871. */
  872. function icons(string $ext): HtmlString
  873. {
  874. switch ($ext) {
  875. case 'php':
  876. $ico = 'file-code';
  877. break;
  878. case 'ppt':
  879. $ico = 'file-powerpoint';
  880. break;
  881. case 'doc':
  882. case 'docx':
  883. $ico = 'file-word';
  884. break;
  885. case 'xls':
  886. case 'xlsx':
  887. $ico = 'file-excel';
  888. break;
  889. case 'txt':
  890. case 'css':
  891. case 'dat':
  892. case 'html':
  893. case 'htm':
  894. $ico = 'file-alt';
  895. break;
  896. case 'wav':
  897. case 'amr':
  898. case 'mp3':
  899. case 'mid':
  900. $ico = 'file-audio';
  901. break;
  902. case 'zip':
  903. case 'rar':
  904. case '7z':
  905. case 'gz':
  906. $ico = 'file-archive';
  907. break;
  908. case '3gp':
  909. case 'mp4':
  910. $ico = 'file-video';
  911. break;
  912. case 'jpg':
  913. case 'jpeg':
  914. case 'bmp':
  915. case 'wbmp':
  916. case 'gif':
  917. case 'png':
  918. $ico = 'file-image';
  919. break;
  920. case 'ttf':
  921. $ico = 'font';
  922. break;
  923. case 'pdf':
  924. $ico = 'file-pdf';
  925. break;
  926. default:
  927. $ico = 'file';
  928. }
  929. return new HtmlString('<i class="far fa-' . $ico . '"></i>');
  930. }
  931.  
  932. /**
  933. * Перемешивает элементы ассоциативного массива, сохраняя ключи
  934. *
  935. * @param array &$array Исходный массив, переданный по ссылке
  936. *
  937. * @return bool Флаг успешного выполнения операции
  938. */
  939. function shuffleAssoc(array &$array): bool
  940. {
  941. $keys = array_keys($array);
  942.  
  943. shuffle($keys);
  944. $new = [];
  945.  
  946. foreach ($keys as $key) {
  947. $new[$key] = $array[$key];
  948. }
  949.  
  950. $array = $new;
  951.  
  952. return true;
  953. }
  954.  
  955. /**
  956. * Закрывает bb-теги
  957. *
  958. * @param string $html
  959. *
  960. * @return string
  961. */
  962. function closeTags(string $html): string
  963. {
  964. preg_match_all('#\[([a-z]+)(?:=.*)?(?<![/])\]#iU', $html, $result);
  965. $openTags = $result[1];
  966.  
  967. preg_match_all('#\[/([a-z]+)\]#iU', $html, $result);
  968. $closedTags = $result[1];
  969.  
  970. if ($openTags === $closedTags) {
  971. return $html;
  972. }
  973.  
  974. $diff = array_diff_assoc($openTags, $closedTags);
  975. $tags = array_reverse($diff);
  976.  
  977. foreach ($tags as $value) {
  978. $html .= '[/' . $value . ']';
  979. }
  980.  
  981. return $html;
  982. }
  983.  
  984. /**
  985. * Возвращает обрезанный текст с закрытием тегов
  986. *
  987. * @param string $value
  988. * @param int $words
  989. * @param string $end
  990. *
  991. * @return HtmlString
  992. */
  993. function bbCodeTruncate(string $value, int $words = 20, string $end = '...'): HtmlString
  994. {
  995. $value = Str::words($value, $words, $end);
  996. $bbText = bbCode(closeTags($value));
  997.  
  998. return new HtmlString(preg_replace('/\[(.*?)\]/', '', $bbText));
  999. }
  1000.  
  1001. /**
  1002. * Возвращает обрезанную до заданного количества букв строке
  1003. *
  1004. * @param HtmlString|string $value Исходная строка
  1005. * @param int $limit Максимальное количество символов в результате
  1006. * @param string $end
  1007. *
  1008. * @return string Обрезанная строка
  1009. */
  1010. function truncateString($value, int $limit = 100, string $end = '...'): string
  1011. {
  1012. $value = strip_tags($value);
  1013.  
  1014. if (mb_strlen($value, 'utf-8') <= $limit) {
  1015. return $value;
  1016. }
  1017.  
  1018. $string = mb_substr($value, 0, $limit + 1);
  1019. if ($lastSpace = mb_strrpos($string, ' ', 0, 'utf-8')) {
  1020. $string = mb_substr($string, 0, $lastSpace, 'utf-8');
  1021. } else {
  1022. $string = mb_substr($string, 0, $limit, 'utf-8');
  1023. }
  1024.  
  1025. return trim($string) . $end;
  1026. }
  1027.  
  1028. /**
  1029. * Возвращает обрезанную до заданного количества слов строке
  1030. *
  1031. * @param HtmlString|string $value Исходная строка
  1032. * @param int $words Максимальное количество слов в результате
  1033. * @param string $end
  1034. *
  1035. * @return string Обрезанная строка
  1036. */
  1037. function truncateWord($value, int $words = 20, string $end = '...'): string
  1038. {
  1039. $value = strip_tags($value);
  1040.  
  1041. return Str::words(trim($value), $words, $end);
  1042. }
  1043.  
  1044. /**
  1045. * Возвращает обрезанную строку с удалением перевода строки
  1046. *
  1047. * @param HtmlString|string $value
  1048. * @param int $words
  1049. * @param string $end
  1050. *
  1051. * @return string
  1052. */
  1053. function truncateDescription($value, int $words = 20, string $end = ''): string
  1054. {
  1055. $value = strip_tags(preg_replace('/\s+/', ' ', $value));
  1056.  
  1057. return Str::words(trim($value), $words, $end);
  1058. }
  1059.  
  1060. /**
  1061. * Возвращает код платной рекламы
  1062. *
  1063. * @param string $place
  1064. *
  1065. * @return HtmlString|null
  1066. */
  1067. function getAdvertPaid(string $place): ?HtmlString
  1068. {
  1069. $adverts = PaidAdvert::statAdverts();
  1070.  
  1071. if (isset($adverts[$place])) {
  1072. $links = [];
  1073. foreach ($adverts[$place] as $advert) {
  1074. $links[] = Arr::random($advert);
  1075. }
  1076.  
  1077. return new HtmlString(implode('<br>', $links));
  1078. }
  1079.  
  1080. return null;
  1081. }
  1082.  
  1083. /**
  1084. * Возвращает код админской рекламы
  1085. *
  1086. * @return HtmlString|null
  1087. */
  1088. function getAdvertAdmin(): ?HtmlString
  1089. {
  1090. $adverts = AdminAdvert::statAdverts();
  1091.  
  1092. if ($adverts) {
  1093. $result = Arr::random($adverts);
  1094.  
  1095. return new HtmlString(view('adverts/_admin_links', compact('result')));
  1096. }
  1097.  
  1098. return null;
  1099. }
  1100.  
  1101. /**
  1102. * Возвращает код пользовательской рекламы
  1103. *
  1104. * @return HtmlString|null
  1105. */
  1106. function getAdvertUser(): ?HtmlString
  1107. {
  1108. $adverts = Advert::statAdverts();
  1109.  
  1110. if ($adverts) {
  1111. $total = count($adverts);
  1112. $show = setting('rekusershow') > $total ? $total : setting('rekusershow');
  1113.  
  1114. $links = Arr::random($adverts, $show);
  1115. $result = implode('<br>', $links);
  1116.  
  1117. return new HtmlString(view('adverts/_links', compact('result')));
  1118. }
  1119.  
  1120. return null;
  1121. }
  1122.  
  1123. /**
  1124. * Выводит последние фотографии
  1125. *
  1126. * @param int $show Количество последних фотографий
  1127. *
  1128. * @return HtmlString Список фотографий
  1129. */
  1130. function recentPhotos(int $show = 5): HtmlString
  1131. {
  1132. $photos = Cache::remember('recentPhotos', 1800, static function () use ($show) {
  1133. return Photo::query()
  1134. ->orderByDesc('created_at')
  1135. ->limit($show)
  1136. ->with('files')
  1137. ->get();
  1138. });
  1139.  
  1140. return new HtmlString(view('widgets/_photos', compact('photos')));
  1141. }
  1142.  
  1143. /**
  1144. * Выводит последние темы форума
  1145. *
  1146. * @param int $show Количество последних тем форума
  1147. *
  1148. * @return HtmlString Список тем
  1149. */
  1150. function recentTopics(int $show = 5): HtmlString
  1151. {
  1152. $topics = Cache::remember('recentTopics', 300, static function () use ($show) {
  1153. return Topic::query()
  1154. ->orderByDesc('updated_at')
  1155. ->limit($show)
  1156. ->get();
  1157. });
  1158.  
  1159. return new HtmlString(view('widgets/_topics', compact('topics')));
  1160. }
  1161.  
  1162. /**
  1163. * Выводит последние файлы в загрузках
  1164. *
  1165. * @param int $show Количество последних файлов в загрузках
  1166. *
  1167. * @return HtmlString Список файлов
  1168. */
  1169. function recentDowns(int $show = 5): HtmlString
  1170. {
  1171. $downs = Cache::remember('recentDowns', 600, static function () use ($show) {
  1172. return Down::query()
  1173. ->where('active', 1)
  1174. ->orderByDesc('created_at')
  1175. ->limit($show)
  1176. ->with('category')
  1177. ->get();
  1178. });
  1179.  
  1180. return new HtmlString(view('widgets/_downs', compact('downs')));
  1181. }
  1182.  
  1183. /**
  1184. * Выводит последние статьи в блогах
  1185. *
  1186. * @param int $show Количество последних статей в блогах
  1187. *
  1188. * @return HtmlString Список статей
  1189. */
  1190. function recentArticles(int $show = 5): HtmlString
  1191. {
  1192. $articles = Cache::remember('recentArticles', 600, static function () use ($show) {
  1193. return Article::query()
  1194. ->orderByDesc('created_at')
  1195. ->limit($show)
  1196. ->get();
  1197. });
  1198.  
  1199. return new HtmlString(view('widgets/_articles', compact('articles')));
  1200. }
  1201.  
  1202. /**
  1203. * Выводит последние объявления
  1204. *
  1205. * @param int $show Количество последних объявлений
  1206. *
  1207. * @return HtmlString Список объявлений
  1208. */
  1209. function recentBoards(int $show = 5): HtmlString
  1210. {
  1211. $items = Cache::remember('recentBoards', 600, static function () use ($show) {
  1212. return Item::query()
  1213. ->where('expires_at', '>', SITETIME)
  1214. ->orderByDesc('created_at')
  1215. ->limit($show)
  1216. ->get();
  1217. });
  1218.  
  1219. return new HtmlString(view('widgets/_boards', compact('items')));
  1220. }
  1221.  
  1222. /**
  1223. * Возвращает количество предложений и проблем
  1224. *
  1225. * @return string Количество предложений и проблем
  1226. */
  1227. function statsOffers(): string
  1228. {
  1229. return Cache::remember('offers', 600, static function () {
  1230. $offers = Offer::query()->where('type', 'offer')->count();
  1231. $problems = Offer::query()->where('type', 'issue')->count();
  1232.  
  1233. return $offers . '/' . $problems;
  1234. });
  1235. }
  1236.  
  1237. /**
  1238. * Пересчитывает счетчики
  1239. *
  1240. * @param string $mode сервис счетчиков
  1241. *
  1242. * @return void
  1243. */
  1244. function restatement(string $mode)
  1245. {
  1246. switch ($mode) {
  1247. case 'forums':
  1248. DB::update('update topics set count_posts = (select count(*) from posts where topics.id = posts.topic_id)');
  1249. DB::update('update forums set count_topics = (select count(*) from topics where forums.id = topics.forum_id)');
  1250. DB::update('update forums set count_posts = (select coalesce(sum(count_posts), 0) from topics where forums.id = topics.forum_id)');
  1251. break;
  1252.  
  1253. case 'blogs':
  1254. DB::update('update blogs set count_articles = (select count(*) from articles where blogs.id = articles.category_id)');
  1255. DB::update('update articles set count_comments = (select count(*) from comments where relate_type = "' . Article::$morphName . '" and articles.id = comments.relate_id)');
  1256. break;
  1257.  
  1258. case 'loads':
  1259. DB::update('update loads set count_downs = (select count(*) from downs where loads.id = downs.category_id and active = ?)', [1]);
  1260. DB::update('update downs set count_comments = (select count(*) from comments where relate_type = "' . Down::$morphName . '" and downs.id = comments.relate_id)');
  1261. break;
  1262.  
  1263. case 'news':
  1264. DB::update('update news set count_comments = (select count(*) from comments where relate_type = "' . News::$morphName . '" and news.id = comments.relate_id)');
  1265. break;
  1266.  
  1267. case 'photos':
  1268. DB::update('update photos set count_comments = (select count(*) from comments where relate_type = "' . Photo::$morphName . '" and photos.id = comments.relate_id)');
  1269. break;
  1270.  
  1271. case 'offers':
  1272. DB::update('update offers set count_comments = (select count(*) from comments where relate_type = "' . Offer::$morphName . '" and offers.id = comments.relate_id)');
  1273. break;
  1274.  
  1275. case 'boards':
  1276. DB::update('update boards set count_items = (select count(*) from items where boards.id = items.board_id and items.expires_at > ' . SITETIME . ');');
  1277. break;
  1278.  
  1279. case 'votes':
  1280. DB::update('update votes set count = (select coalesce(sum(result), 0) from voteanswer where votes.id = voteanswer.vote_id)');
  1281. break;
  1282. }
  1283. }
  1284.  
  1285. /**
  1286. * Возвращает количество строк в файле
  1287. *
  1288. * @param string $file Путь к файлу
  1289. *
  1290. * @return int Количество строк
  1291. */
  1292. function counterString(string $file): int
  1293. {
  1294. $countLines = 0;
  1295. if (file_exists($file)) {
  1296. $countLines = count(file($file));
  1297. }
  1298.  
  1299. return $countLines;
  1300. }
  1301.  
  1302. /**
  1303. * Форматирует вывод числа
  1304. *
  1305. * @param int|float $num число
  1306. *
  1307. * @return HtmlString Форматированное число
  1308. */
  1309. function formatNum($num): HtmlString
  1310. {
  1311. if ($num > 0) {
  1312. $data = '<span style="color:#00aa00">+' . $num . '</span>';
  1313. } elseif ($num < 0) {
  1314. $data = '<span style="color:#ff0000">' . $num . '</span>';
  1315. } else {
  1316. $data = '<span>0</span>';
  1317. }
  1318.  
  1319. return new HtmlString($data);
  1320. }
  1321.  
  1322. /**
  1323. * Форматирует вывод числа
  1324. *
  1325. * @param int $num
  1326. *
  1327. * @return string
  1328. */
  1329. function formatShortNum(int $num)
  1330. {
  1331. if (! is_numeric($num)) {
  1332. return '0b';
  1333. }
  1334.  
  1335. if ($num > 1000000000000) {
  1336. return round($num / 1000000000000, 1) . 'T';
  1337. }
  1338.  
  1339. if ($num > 1000000000) {
  1340. return round($num / 1000000000, 1) . 'B';
  1341. }
  1342.  
  1343. if ($num > 1000000) {
  1344. return round($num / 1000000, 1) . 'M';
  1345. }
  1346.  
  1347. if ($num > 1000) {
  1348. return round($num / 1000, 1) . 'K';
  1349. }
  1350.  
  1351. return $num;
  1352. }
  1353.  
  1354. /**
  1355. * Обрабатывает и уменьшает изображение
  1356. *
  1357. * @param string|null $path Путь к изображению
  1358. * @param array $params Параметры изображения
  1359. *
  1360. * @return array Обработанные параметры
  1361. */
  1362. function resizeProcess(?string $path, array $params = []): array
  1363. {
  1364. if (empty($params['alt'])) {
  1365. $params['alt'] = basename($path);
  1366. }
  1367.  
  1368. if (empty($params['class'])) {
  1369. $params['class'] = 'img-fluid';
  1370. }
  1371.  
  1372. if (! file_exists(public_path($path)) || ! is_file(public_path($path))) {
  1373. return [
  1374. 'path' => '/assets/img/images/photo.png',
  1375. 'source' => false,
  1376. 'params' => $params,
  1377. ];
  1378. }
  1379.  
  1380. [$width, $height] = getimagesize(public_path($path));
  1381.  
  1382. if ($width <= setting('previewsize') && $height <= setting('previewsize')) {
  1383. return [
  1384. 'path' => $path,
  1385. 'source' => $path,
  1386. 'params' => $params,
  1387. ];
  1388. }
  1389.  
  1390. $thumb = ltrim(str_replace('/', '_', $path), '_');
  1391.  
  1392. if (! file_exists(public_path('uploads/thumbnails/' . $thumb))) {
  1393. $img = Image::make(public_path($path));
  1394. $img->resize(setting('previewsize'), setting('previewsize'), static function (Constraint $constraint) {
  1395. $constraint->aspectRatio();
  1396. $constraint->upsize();
  1397. });
  1398.  
  1399. $img->save(public_path('uploads/thumbnails/' . $thumb));
  1400. }
  1401.  
  1402. return [
  1403. 'path' => '/uploads/thumbnails/' . $thumb,
  1404. 'source' => $path,
  1405. 'params' => $params,
  1406. ];
  1407. }
  1408.  
  1409. /**
  1410. * Возвращает уменьшенное изображение
  1411. *
  1412. * @param string|null $path Путь к изображению
  1413. * @param array $params Параметры изображения
  1414. *
  1415. * @return HtmlString Уменьшенное изображение
  1416. */
  1417. function resizeImage(?string $path, array $params = []): HtmlString
  1418. {
  1419. $image = resizeProcess($path, $params);
  1420.  
  1421. $strParams = [];
  1422. foreach ($image['params'] as $key => $param) {
  1423. $strParams[] = $key . '="' . check($param) . '"';
  1424. }
  1425.  
  1426. $strParams = implode(' ', $strParams);
  1427.  
  1428. return new HtmlString('<img src="' . $image['path'] . '" data-source="' . $image['source'] . '" ' . $strParams . '>');
  1429. }
  1430.  
  1431. /**
  1432. * Удаляет директорию рекурсивно
  1433. *
  1434. * @param string $dir путь к директории
  1435. *
  1436. * @return void
  1437. */
  1438. function deleteDir(string $dir)
  1439. {
  1440. if (file_exists($dir)) {
  1441. if ($files = glob($dir . '/*')) {
  1442. foreach ($files as $file) {
  1443. is_dir($file) ? deleteDir($file) : unlink($file);
  1444. }
  1445. }
  1446. rmdir($dir);
  1447. }
  1448. }
  1449.  
  1450. /**
  1451. * Удаляет файл
  1452. *
  1453. * @param string $path Путь к файлу
  1454. *
  1455. * @return bool
  1456. */
  1457. function deleteFile(string $path): bool
  1458. {
  1459. if (file_exists($path) && is_file($path)) {
  1460. unlink($path);
  1461. }
  1462.  
  1463. if (in_array(getExtension($path), ['jpg', 'jpeg', 'gif', 'png'], true)) {
  1464. $thumb = ltrim(str_replace([public_path(), '/'], ['', '_'], $path), '_');
  1465. $thumb = public_path('uploads/thumbnails/' . $thumb);
  1466.  
  1467. if (file_exists($thumb) && is_file($thumb)) {
  1468. unlink($thumb);
  1469. }
  1470. }
  1471.  
  1472. return true;
  1473. }
  1474.  
  1475. /**
  1476. * Отправляет уведомление об упоминании в приват
  1477. *
  1478. * @param string $text Текст сообщения
  1479. * @param string $url Путь к странице
  1480. * @param string $title Название страницу
  1481. *
  1482. * @return void
  1483. */
  1484. function sendNotify(string $text, string $url, string $title)
  1485. {
  1486. /*$parseText = preg_replace('|\[quote(.*?)\](.*?)\[/quote\]|s', '', $text);*/
  1487. preg_match_all('/(?<=^|\s|=)@([\w\-]+)/', $text, $matches);
  1488.  
  1489. if (! empty($matches[1])) {
  1490. $login = getUser('login') ?? setting('guestsuser');
  1491. $usersAnswer = array_unique(array_diff($matches[1], [$login]));
  1492.  
  1493. foreach ($usersAnswer as $user) {
  1494. $user = getUserByLogin($user);
  1495.  
  1496. if ($user && $user->notify) {
  1497. $notify = textNotice('notify', compact('login', 'url', 'title', 'text'));
  1498. $user->sendMessage(null, $notify);
  1499. }
  1500. }
  1501. }
  1502. }
  1503.  
  1504. /**
  1505. * Возвращает приватное сообщение
  1506. *
  1507. * @param string $type Тип сообщения
  1508. * @param array $replace Массив заменяемых параметров
  1509. *
  1510. * @return string Сформированный текст
  1511. */
  1512. function textNotice(string $type, array $replace = []): string
  1513. {
  1514. /** @var Notice $message */
  1515. $message = Notice::query()->where('type', $type)->first();
  1516.  
  1517. if (! $message) {
  1518. return __('main.text_missing');
  1519. }
  1520.  
  1521. foreach ($replace as $key => $val) {
  1522. $message->text = str_replace('%' . $key . '%', $val, $message->text);
  1523. }
  1524.  
  1525. return $message->text;
  1526. }
  1527.  
  1528. /**
  1529. * Возвращает блок статистики производительности
  1530. *
  1531. * @return HtmlString|null Статистика производительности
  1532. */
  1533. function performance(): ?HtmlString
  1534. {
  1535. if (isAdmin() && setting('performance')) {
  1536. $queries = getQueryLog();
  1537. $timeQueries = array_sum(array_column($queries, 'time'));
  1538.  
  1539. return new HtmlString(view('app/_performance', compact('queries', 'timeQueries')));
  1540. }
  1541.  
  1542. return null;
  1543. }
  1544.  
  1545. /**
  1546. * Очистка кеш-файлов
  1547. *
  1548. * @param string|array|null $keys
  1549. *
  1550. * @return bool Результат выполнения
  1551. */
  1552. function clearCache($keys = null): bool
  1553. {
  1554. if ($keys) {
  1555. if (!is_array($keys)) {
  1556. $keys = [$keys];
  1557. }
  1558.  
  1559. foreach ($keys as $key) {
  1560. Cache::forget($key);
  1561. }
  1562.  
  1563. return true;
  1564. }
  1565.  
  1566. Cache::flush();
  1567.  
  1568. return true;
  1569. }
  1570.  
  1571. /**
  1572. * Возвращает текущую страницу
  1573. *
  1574. * @param string|null $url
  1575. *
  1576. * @return string|null Текущая страница
  1577. */
  1578. function returnUrl(?string $url = null): ?string
  1579. {
  1580. $request = request();
  1581.  
  1582. if ($request->is('/', 'login', 'register', 'recovery', 'restore', 'ban', 'closed')) {
  1583. return null;
  1584. }
  1585.  
  1586. $query = $request->has('return') ? $request->input('return') : $request->path();
  1587.  
  1588. return '?return=' . urlencode($url ?? '/' . $query);
  1589. }
  1590.  
  1591. /**
  1592. * Saves error logs
  1593. *
  1594. * @param int $code
  1595. * @param string|null $message
  1596. *
  1597. * @return void
  1598. */
  1599. function saveErrorLog(int $code, ?string $message = null)
  1600. {
  1601. $errorCodes = [401, 403, 404, 405, 419, 429, 500, 503, 666];
  1602.  
  1603. if (setting('errorlog') && in_array($code, $errorCodes, true)) {
  1604. Error::query()->create([
  1605. 'code' => $code,
  1606. 'request' => utfSubstr(request()->getRequestUri(), 0, 250),
  1607. 'referer' => utfSubstr(request()->header('referer'), 0, 250),
  1608. 'user_id' => getUser('id'),
  1609. 'message' => utfSubstr($message, 0, 250),
  1610. 'ip' => getIp(),
  1611. 'brow' => getBrowser(),
  1612. 'created_at' => SITETIME,
  1613. ]);
  1614. }
  1615. }
  1616.  
  1617. /**
  1618. * Возвращает ошибку
  1619. *
  1620. * @param string|array $errors ошибки
  1621. *
  1622. * @return HtmlString Сформированный блок с ошибкой
  1623. */
  1624. function showError($errors): HtmlString
  1625. {
  1626. $errors = (array) $errors;
  1627.  
  1628. return new HtmlString(view('app/_error', compact('errors')));
  1629. }
  1630.  
  1631. /**
  1632. * @return HtmlString
  1633. */
  1634. function getCaptcha(): HtmlString
  1635. {
  1636. return new HtmlString(view('app/_captcha'));
  1637. }
  1638.  
  1639. /**
  1640. * Сохраняет flash уведомления
  1641. *
  1642. * @param string $status Статус уведомления
  1643. * @param mixed $message Массив или текст с уведомлениями
  1644. *
  1645. * @return void
  1646. *
  1647. * @deprecated since 10.1 - Use redirect->with('success', 'Message') or session()->flash()
  1648. */
  1649. function setFlash(string $status, $message)
  1650. {
  1651. session()->put('flash.' . $status, $message);
  1652. }
  1653.  
  1654. /**
  1655. * Сохраняет POST данные введенных пользователем
  1656. *
  1657. * @param array $data Массив полей
  1658. *
  1659. * @deprecated since 10.1
  1660. */
  1661. function setInput(array $data)
  1662. {
  1663. session()->flash('input', json_encode($data));
  1664. }
  1665.  
  1666. /**
  1667. * Возвращает значение из POST данных
  1668. *
  1669. * @param string $key Имя поля
  1670. * @param mixed $default
  1671. *
  1672. * @return mixed Сохраненное значение
  1673. *
  1674. * @deprecated since 10.1 - Use old('field', 'default');
  1675. */
  1676. function getInput(string $key, $default = null)
  1677. {
  1678. if (session()->missing('input')) {
  1679. return $default;
  1680. }
  1681.  
  1682. $input = json_decode(session()->get('input', []), true);
  1683.  
  1684. return Arr::get($input, $key, $default);
  1685. }
  1686.  
  1687. /**
  1688. * Подсвечивает блок с полем для ввода сообщения
  1689. *
  1690. * @param string $field Имя поля
  1691. *
  1692. * @return string CSS класс ошибки
  1693. */
  1694. function hasError(string $field): string
  1695. {
  1696. // Новая валидация
  1697. if (session()->has('errors')) {
  1698. /** @var ViewErrorBag $errors */
  1699. $errors = session()->get('errors');
  1700.  
  1701. return $errors->has($field) ? ' is-invalid' : ' is-valid';
  1702. }
  1703.  
  1704. $isValid = session()->has('flash.danger') ? ' is-valid' : '';
  1705.  
  1706. return session()->has('flash.danger.' . $field) ? ' is-invalid' : $isValid;
  1707. }
  1708.  
  1709. /**
  1710. * Возвращает блок с текстом ошибки
  1711. *
  1712. * @param string $field Имя поля
  1713. *
  1714. * @return string|null Блоки ошибки
  1715. */
  1716. function textError(string $field): ?string
  1717. {
  1718. // Новая валидация
  1719. if (session()->has('errors')) {
  1720. /** @var ViewErrorBag $errors */
  1721. $errors = session()->get('errors');
  1722.  
  1723. return $errors->first($field);
  1724. }
  1725.  
  1726. return session()->get('flash.danger.' . $field);
  1727. }
  1728.  
  1729. /**
  1730. * Отправляет уведомления на email
  1731. *
  1732. * @param string $view
  1733. * @param array $data
  1734. *
  1735. * @return bool Результат отправки
  1736. */
  1737. function sendMail(string $view, array $data): bool
  1738. {
  1739. Mail::send($view, $data, function (Message $message) use ($data) {
  1740. $message->subject($data['subject'])
  1741. ->to($data['to'])
  1742. ->from(config('mail.from.address'), config('mail.from.name'));
  1743.  
  1744. if (isset($data['from'])) {
  1745. [$fromEmail, $fromName] = $data['from'];
  1746. $message->replyTo($fromEmail, $fromName);
  1747. }
  1748.  
  1749. if (isset($data['unsubscribe'])) {
  1750. $headers = $message->getHeaders();
  1751. $headers->addTextHeader('List-Unsubscribe', '<' . config('app.url') . '/unsubscribe?key=' . $data['unsubscribe'] . '>');
  1752. }
  1753. });
  1754.  
  1755. return ! Mail::failures();
  1756. }
  1757.  
  1758. /**
  1759. * Возвращает расширение файла
  1760. *
  1761. * @param string $filename Имя файла
  1762. *
  1763. * @return string расширение
  1764. */
  1765. function getExtension(string $filename): string
  1766. {
  1767. return pathinfo($filename, PATHINFO_EXTENSION);
  1768. }
  1769.  
  1770. /**
  1771. * Возвращает имя файла без расширения
  1772. *
  1773. * @param string $filename Имя файла
  1774. *
  1775. * @return string Имя без расширения
  1776. */
  1777. function getBodyName(string $filename): string
  1778. {
  1779. return pathinfo($filename, PATHINFO_FILENAME);
  1780. }
  1781.  
  1782. /**
  1783. * Склоняет числа
  1784. *
  1785. * @param int $num Число
  1786. * @param mixed $forms Массив склоняемых слов (один, два, много)
  1787. *
  1788. * @return string Форматированная строка
  1789. */
  1790. function plural(int $num, $forms): string
  1791. {
  1792. if (! is_array($forms)) {
  1793. $forms = explode(',', $forms);
  1794. }
  1795.  
  1796. if (count($forms) === 1) {
  1797. return $num . ' ' . $forms[0];
  1798. }
  1799.  
  1800. if ($num % 100 > 10 && $num % 100 < 15) {
  1801. return $num . ' ' . $forms[2];
  1802. }
  1803.  
  1804. if ($num % 10 === 1) {
  1805. return $num . ' ' . $forms[0];
  1806. }
  1807.  
  1808. if ($num % 10 > 1 && $num % 10 < 5) {
  1809. return $num . ' ' . $forms[1];
  1810. }
  1811.  
  1812. return $num . ' ' . $forms[2];
  1813. }
  1814.  
  1815. /**
  1816. * Обрабатывает BB-код
  1817. *
  1818. * @param string $text Необработанный текст
  1819. * @param bool $parse Обрабатывать или вырезать код
  1820. *
  1821. * @return HtmlString Обработанный текст
  1822. */
  1823. function bbCode(string $text, bool $parse = true): HtmlString
  1824. {
  1825. $bbCode = new BBCode();
  1826. $checkText = check($text);
  1827.  
  1828. if (! $parse) {
  1829. return new HtmlString($bbCode->clear($checkText));
  1830. }
  1831.  
  1832. $parseText = $bbCode->parse($checkText);
  1833. $parseText = $bbCode->parseStickers($parseText);
  1834.  
  1835. return new HtmlString($parseText);
  1836. }
  1837.  
  1838. /**
  1839. * Определяет IP пользователя
  1840. *
  1841. * @return string IP пользователя
  1842. */
  1843. function getIp(): string
  1844. {
  1845. $cf = new CloudFlare(request());
  1846.  
  1847. return $cf->ip();
  1848. }
  1849.  
  1850. /**
  1851. * Определяет браузер
  1852. *
  1853. * @param string|null $userAgent
  1854. *
  1855. * @return string|null Браузер и версия браузера
  1856. */
  1857. function getBrowser(string $userAgent = null): string
  1858. {
  1859. $browser = new Browser();
  1860. if ($userAgent) {
  1861. $browser->setUserAgent($userAgent);
  1862. }
  1863.  
  1864. $brow = $browser->getBrowser();
  1865. $version = implode('.', array_slice(explode('.', $browser->getVersion()), 0, 2));
  1866.  
  1867. $browser = $version === Browser::VERSION_UNKNOWN ? $brow : $brow . ' ' . $version;
  1868.  
  1869. return mb_substr($browser, 0, 25, 'utf-8');
  1870. }
  1871.  
  1872. /**
  1873. * Является ли пользователь авторизованным
  1874. *
  1875. * @return User|bool
  1876. */
  1877. function checkAuth()
  1878. {
  1879. if (session()->has(['id', 'password'])) {
  1880. $user = getUserById(session()->get('id'));
  1881.  
  1882. if ($user && session()->get('password') === $user->password) {
  1883. return $user;
  1884. }
  1885. }
  1886.  
  1887. return false;
  1888. }
  1889.  
  1890. /**
  1891. * Является ли пользователь администратором
  1892. *
  1893. * @param string|null $level Уровень доступа
  1894. *
  1895. * @return bool Является ли пользователь администратором
  1896. */
  1897. function isAdmin(?string $level = null): bool
  1898. {
  1899. return access($level ?? User::EDITOR);
  1900. }
  1901.  
  1902. /**
  1903. * Имеет ли пользователь доступ по уровню
  1904. *
  1905. * @param string $level Уровень доступа
  1906. *
  1907. * @return bool Разрешен ли доступ
  1908. */
  1909. function access(string $level): bool
  1910. {
  1911. $access = array_flip(User::ALL_GROUPS);
  1912.  
  1913. return getUser()
  1914. && isset($access[$level], $access[getUser('level')])
  1915. && $access[getUser('level')] <= $access[$level];
  1916. }
  1917.  
  1918. /**
  1919. * Возвращает объект пользователя по логину
  1920. *
  1921. * @param string|null $login Логин пользователя
  1922. *
  1923. * @return Builder|Model|null
  1924. */
  1925. function getUserByLogin(?string $login): ?User
  1926. {
  1927. return User::query()->where('login', $login)->first();
  1928. }
  1929.  
  1930. /**
  1931. * Возвращает объект пользователя по id
  1932. *
  1933. * @param int|null $id ID пользователя
  1934. *
  1935. * @return Builder|Model|null
  1936. */
  1937. function getUserById(?int $id): ?User
  1938. {
  1939. return User::query()->find($id);
  1940. }
  1941.  
  1942. /**
  1943. * Возвращает объект пользователя по токену
  1944. *
  1945. * @param string $token Логин пользователя
  1946. *
  1947. * @return Builder|Model|null
  1948. */
  1949. function getUserByToken(string $token): ?User
  1950. {
  1951. return User::query()->where('apikey', $token)->first();
  1952. }
  1953.  
  1954. /**
  1955. * Возвращает объект пользователя по логину или email
  1956. *
  1957. * @param string|null $login Логин или email пользователя
  1958. *
  1959. * @return Builder|Model|null
  1960. */
  1961. function getUserByLoginOrEmail(?string $login): ?User
  1962. {
  1963. $field = strpos($login, '@') ? 'email' : 'login';
  1964.  
  1965. return User::query()->where($field, $login)->first();
  1966. }
  1967.  
  1968. /**
  1969. * Возвращает данные пользователя по ключу
  1970. *
  1971. * @param string|null $key Ключ массива
  1972. *
  1973. * @return User|mixed
  1974. */
  1975. function getUser(?string $key = null)
  1976. {
  1977. static $user;
  1978.  
  1979. if (! $user) {
  1980. $user = checkAuth();
  1981. }
  1982.  
  1983. return $key ? ($user[$key] ?? null) : $user;
  1984. }
  1985.  
  1986. /**
  1987. * Разбивает массив по страницам
  1988. *
  1989. * @param array $items
  1990. * @param int $perPage
  1991. * @param array $appends
  1992. *
  1993. * @return LengthAwarePaginator
  1994. */
  1995. function paginate(array $items, int $perPage, array $appends = []): LengthAwarePaginator
  1996. {
  1997. $currentPage = LengthAwarePaginator::resolveCurrentPage();
  1998. $slice = array_slice($items, $perPage * ($currentPage - 1), $perPage, true);
  1999.  
  2000. $collection = new LengthAwarePaginator($slice, count($items), $perPage);
  2001. $collection->setPath(request()->url());
  2002. $collection->appends($appends);
  2003.  
  2004. return $collection;
  2005. }
  2006.  
  2007. /**
  2008. * Возвращает сформированный код base64 картинки
  2009. *
  2010. * @param string $path Путь к картинке
  2011. * @param array $params Параметры
  2012. *
  2013. * @return HtmlString Сформированный код
  2014. */
  2015. function imageBase64(string $path, array $params = []): HtmlString
  2016. {
  2017. $type = getExtension($path);
  2018. $data = file_get_contents($path);
  2019.  
  2020. if (! isset($params['class'])) {
  2021. $params['class'] = 'img-fluid';
  2022. }
  2023.  
  2024. if (empty($params['alt'])) {
  2025. $params['alt'] = basename($path);
  2026. }
  2027.  
  2028. $strParams = [];
  2029. foreach ($params as $key => $param) {
  2030. $strParams[] = $key . '="' . $param . '"';
  2031. }
  2032.  
  2033. $strParams = implode(' ', $strParams);
  2034.  
  2035. return new HtmlString('<img src="data:image/' . $type . ';base64,' . base64_encode($data) . '"' . $strParams . '>');
  2036. }
  2037.  
  2038. /**
  2039. * Выводит прогресс-бар
  2040. *
  2041. * @param int $percent
  2042. * @param int|float|string|null $title
  2043. *
  2044. * @return HtmlString
  2045. */
  2046. function progressBar(int $percent, $title = null): HtmlString
  2047. {
  2048. if (! $title) {
  2049. $title = $percent . '%';
  2050. }
  2051.  
  2052. return new HtmlString(view('app/_progressbar', compact('percent', 'title')));
  2053. }
  2054.  
  2055. /**
  2056. * Возвращает форматированный список запросов
  2057. *
  2058. * @return array
  2059. */
  2060. function getQueryLog(): array
  2061. {
  2062. $queries = DB::getQueryLog();
  2063.  
  2064. $formattedQueries = [];
  2065.  
  2066. foreach ($queries as $query) {
  2067. foreach ($query['bindings'] as $key => $binding) {
  2068. if (is_string($binding)) {
  2069. $query['bindings'][$key] = ctype_print($binding) ? "'$binding'" : '[binary]';
  2070. }
  2071.  
  2072. $query['bindings'][$key] = $binding ?? 'null';
  2073. }
  2074.  
  2075. $sql = str_replace(['%', '?'], ['%%', '%s'], $query['query']);
  2076. $sql = vsprintf($sql, $query['bindings']);
  2077.  
  2078. $formattedQueries[] = ['query' => $sql, 'time' => $query['time']];
  2079. }
  2080.  
  2081. return $formattedQueries;
  2082. }
  2083.  
  2084. /**
  2085. * Выводит список забаненных ip
  2086. *
  2087. * @param bool $clear Нужно ли сбросить кеш
  2088. *
  2089. * @return array Массив IP
  2090. */
  2091. function ipBan(bool $clear = false): array
  2092. {
  2093. if ($clear) {
  2094. clearCache('ipBan');
  2095. }
  2096.  
  2097. return Cache::rememberForever('ipBan', static function () {
  2098. return Ban::query()->get()->pluck('id', 'ip')->all();
  2099. });
  2100. }
  2101.  
  2102. /**
  2103. * Возвращает настройки сайта по ключу
  2104. *
  2105. * @param string|null $key Ключ массива
  2106. * @param mixed $default Значение по умолчанию
  2107. *
  2108. * @return mixed Данные
  2109. */
  2110. function setting(?string $key = null, $default = null)
  2111. {
  2112. static $settings;
  2113.  
  2114. if (! $settings) {
  2115. $settings = Setting::getSettings();
  2116. }
  2117.  
  2118. return $key ? ($settings[$key] ?? $default) : $settings;
  2119. }
  2120.  
  2121. /**
  2122. * Возвращает имя сайта из ссылки
  2123. *
  2124. * @param string $url Ссылка на сайт
  2125. *
  2126. * @return string Имя сайта
  2127. */
  2128. function siteDomain(string $url): string
  2129. {
  2130. return parse_url(strtolower($url), PHP_URL_HOST);
  2131. }
  2132.  
  2133. /**
  2134. * Получает версию
  2135. *
  2136. * @param string $version
  2137. *
  2138. * @return string
  2139. */
  2140. function parseVersion(string $version): string
  2141. {
  2142. $ver = explode('.', strtok($version, '-'));
  2143.  
  2144. return $ver[0] . '.' . $ver[1] . '.' . ($ver[2] ?? 0);
  2145. }
  2146.  
  2147. /**
  2148. * Проверяет captcha
  2149. *
  2150. * @return bool
  2151. */
  2152. function captchaVerify(): bool
  2153. {
  2154. $request = request();
  2155.  
  2156. if (setting('captcha_type') === 'recaptcha_v2') {
  2157. $recaptcha = new ReCaptcha(setting('recaptcha_private'));
  2158.  
  2159. $response = $recaptcha->setExpectedHostname($_SERVER['SERVER_NAME'])
  2160. ->verify($request->input('g-recaptcha-response'), getIp());
  2161.  
  2162. return $response->isSuccess();
  2163. }
  2164.  
  2165. if (setting('captcha_type') === 'recaptcha_v3') {
  2166. $recaptcha = new ReCaptcha(setting('recaptcha_private'));
  2167.  
  2168. $response = $recaptcha->setExpectedHostname($_SERVER['SERVER_NAME'])
  2169. ->setScoreThreshold(0.5)
  2170. ->verify($request->input('protect'), getIp());
  2171.  
  2172. return $response->isSuccess();
  2173. }
  2174.  
  2175. if (setting('captcha_type') === 'graphical') {
  2176. return strtolower($request->input('protect')) === $request->session()->get('protect');
  2177. }
  2178.  
  2179. return false;
  2180. }
  2181.  
  2182. /**
  2183. * Возвращает уникальное имя
  2184. *
  2185. * @param string|null $extension
  2186. *
  2187. * @return string
  2188. */
  2189. function uniqueName(string $extension = null): string
  2190. {
  2191. if ($extension) {
  2192. $extension = '.' . $extension;
  2193. }
  2194.  
  2195. return str_replace('.', '', uniqid('', true)) . $extension;
  2196. }
  2197.  
  2198. /**
  2199. * Возвращает курсы валют
  2200. *
  2201. * @return HtmlString
  2202. */
  2203. function getCourses(): ?HtmlString
  2204. {
  2205. $courses = Cache::remember('courses', 3600, static function () {
  2206. try {
  2207. $client = new Client(['timeout' => 3.0]);
  2208. $response = $client->get('//www.cbr-xml-daily.ru/daily_json.js');
  2209.  
  2210. $content = json_decode($response->getBody()->getContents(), true);
  2211. } catch (Exception $e) {
  2212. $content = null;
  2213. }
  2214.  
  2215. return $content;
  2216. });
  2217.  
  2218. return new HtmlString(view('app/_courses', compact('courses')));
  2219. }