<?php
use App\Models\AdminAdvert;
use App\Models\PaidAdvert;
use App\Classes\{BBCode, Calendar, Metrika, CloudFlare};
use App\Models\Antimat;
use App\Models\Ban;
use App\Models\Banhist;
use App\Models\BlackList;
use App\Models\Article;
use App\Models\Item;
use App\Models\Load;
use App\Models\Chat;
use App\Models\Counter;
use App\Models\Down;
use App\Models\Guestbook;
use App\Models\Invite;
use App\Models\Error;
use App\Models\News;
use App\Models\Notice;
use App\Models\Offer;
use App\Models\Online;
use App\Models\Photo;
use App\Models\Post;
use App\Models\Advert;
use App\Models\Setting;
use App\Models\Sticker;
use App\Models\Spam;
use App\Models\Topic;
use App\Models\User;
use App\Models\Vote;
use GuzzleHttp\Client;
use Illuminate\Mail\Message;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;
use Illuminate\Support\ViewErrorBag;
use Intervention\Image\Constraint;
use Intervention\Image\ImageManagerStatic as Image;
use ReCaptcha\ReCaptcha;
const ROTOR_VERSION = '10.1';
define('SITETIME', time());
/**
* Форматирует вывод времени из секунд
*
* @param int $time секунды
*
* @return string форматированный вывод
*/
function makeTime(int $time): string
{
$format = $time < 3600 ? 'i:s' : 'H:i:s';
return gmdate($format, $time);
}
/**
* Форматирует время с учетом часовых поясов
*
* @param int|null $timestamp Секунды
* @param string $format Формат времени
* @param bool $original Формат без изменения
*
* @return string Форматированный вывод
*/
function dateFixed(?int $timestamp, string $format = 'd.m.Y / H:i', bool $original = false): string
{
if (! is_numeric($timestamp)) {
$timestamp = SITETIME;
}
$shift = getUser('timezone') * 3600;
$dateStamp = date($format, $timestamp + $shift);
if ($original) {
return $dateStamp;
}
$today = date('d.m.Y', SITETIME + $shift);
$yesterday = date('d.m.Y', strtotime('-1 day', SITETIME + $shift));
$replaces = [
$today => __('main.today'),
$yesterday => __('main.yesterday'),
'January' => __('main.january'),
'February' => __('main.february'),
'March' => __('main.march'),
'April' => __('main.april'),
'May' => __('main.may'),
'June' => __('main.june'),
'July' => __('main.july'),
'August' => __('main.august'),
'September' => __('main.september'),
'October' => __('main.october'),
'November' => __('main.november'),
'December' => __('main.december'),
];
return strtr($dateStamp, $replaces);
}
/**
* Конвертирует строку в кодировку utf-8
*
* @param string $str строка
*
* @return string Конвертированная строка
*/
function winToUtf(string $str): string
{
return mb_convert_encoding($str, 'utf-8', 'windows-1251');
}
/**
* Преобразует строку в нижний регистр
*
* @param string $str строка
*
* @return string Преобразованная строка
*/
function utfLower(string $str): string
{
return mb_strtolower($str, 'utf-8');
}
/**
* Обрезает строку
*
* @param mixed $str Строка
* @param int $start Начало позиции
* @param int|null $length Конец позиции
*
* @return string Обрезанная строка
*/
function utfSubstr($str, int $start, ?int $length = null): string
{
if (! $length) {
$length = utfStrlen($str);
}
return mb_substr($str, $start, $length, 'utf-8');
}
/**
* Возвращает длину строки
*
* @param mixed $str строка
*
* @return int Длина строка
*/
function utfStrlen($str): int
{
return mb_strlen($str, 'utf-8');
}
/**
* Является ли кодировка utf-8
*
* @param string $str строка
*
* @return bool
*/
function isUtf(string $str): bool
{
return mb_check_encoding($str, 'utf-8');
}
/**
* Преобразует специальные символы в HTML-сущности
*
* @param mixed $string Строка или массив строк
* @param bool $doubleEncode Преобразовывать существующие html-сущности
*
* @return array|string Обработанные данные
*/
function check($string, bool $doubleEncode = true)
{
if (is_array($string)) {
foreach ($string as $key => $val) {
$string[$key] = check($val, $doubleEncode);
}
} else {
$string = htmlspecialchars($string, ENT_QUOTES, 'UTF-8', $doubleEncode);
$search = [chr(0), "\x00", "\x1A", chr(226) . chr(128) . chr(174)];
$string = str_replace($search, [], $string);
}
return $string;
}
/**
* Преобразует в положительное число
*
* @param int|string $num число
*
* @return int Обработанные данные
*/
function int($num): int
{
return abs((int) $num);
}
/**
* Преобразует все элементы массива в int
*
* @param array|int|string $numbers массив или число
*
* @return array|null Обработанные данные
*/
function intar($numbers): ?array
{
if ($numbers) {
if (is_array($numbers)) {
$numbers = array_map('intval', $numbers);
} else {
$numbers = [(int) $numbers];
}
}
return $numbers;
}
/**
* Возвращает размер в человеко читаемом формате
*
* @param int $bytes размер в байтах
* @param int $precision кол. символов после запятой
*
* @return string Форматированный вывод размера
*/
function formatSize(int $bytes, int $precision = 2): string
{
$units = ['B', 'Kb', 'Mb', 'Gb', 'Tb'];
$pow = floor(($bytes ? log($bytes) : 0) / log(1000));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, $precision) . $units[$pow];
}
/**
* Возвращает размер файла человеко-читаемом формате
*
* @param string $file Путь к файлу
*
* @return string Размер в читаемом формате
*/
function formatFileSize(string $file): string
{
if (file_exists($file) && is_file($file)) {
return formatSize(filesize($file));
}
return formatSize(0);
}
/**
* Возвращает время в человеко-читаемом формате
*
* @param int $time Кол. секунд timestamp
* @param int $crumbs Кол. элементов
*
* @return string Время в читаемом формате
*/
function formatTime(int $time, int $crumbs = 2): string
{
if ($time < 1) {
return '0';
}
$units = [
__('main.plural_years') => 31536000,
__('main.plural_months') => 2592000,
__('main.plural_days') => 86400,
__('main.plural_hours') => 3600,
__('main.plural_minutes') => 60,
__('main.plural_seconds') => 1,
];
$return = [];
foreach ($units as $unit => $seconds) {
$format = floor($time / $seconds);
$time %= $seconds;
if ($format >= 1) {
$return[] = plural($format, $unit);
}
}
return implode(' ', array_slice($return, 0, $crumbs));
}
/**
* Очищает строку от мата по базе слов
*
* @param string|null $str строка
*
* @return string Обработанная строка
*/
function antimat(?string $str): string
{
return Antimat::replace((string) $str);
}
/**
* Возвращает рейтинг в виде звезд
*
* @param int|float $rating рейтинг
*
* @return HtmlString Преобразованный рейтинг
*/
function ratingVote($rating): HtmlString
{
$rating = round($rating / 0.5) * 0.5;
$full_stars = floor($rating);
$half_stars = ceil($rating - $full_stars);
$empty_stars = 5 - $full_stars - $half_stars;
$output = '<div class="star-rating fa-lg text-danger">';
$output .= str_repeat('<i class="fas fa-star"></i>', $full_stars);
$output .= str_repeat('<i class="fas fa-star-half-alt"></i>', $half_stars);
$output .= str_repeat('<i class="far fa-star"></i>', $empty_stars);
$output .= '( ' . $rating . ' )</div>';
return new HtmlString($output);
}
/**
* Возвращает календарь
*
* @param int $time
*
* @return HtmlString календарь
*/
function getCalendar(int $time = SITETIME): HtmlString
{
$calendar = new Calendar();
return new HtmlString($calendar->getCalendar($time));
}
/**
* Возвращает количество пользователей онлайн по типам
*
* @return array Массив данных
*/
function statsOnline(): array
{
return Cache::remember('online', 60, static function () {
$users = Online::query()
->select('user_id')
->distinct('user_id')
->whereNotNull('user_id')
->get();
$usersCount = $users->count();
$guestsCount = Online::query()->whereNull('user_id')->count();
$total = $usersCount + $guestsCount;
$metrika = new Metrika();
$metrika->getCounter($usersCount + $guestsCount);
return [$usersCount, $guestsCount, $total, $users];
});
}
/**
* Возвращает количество пользователей онлайн
*
* @return HtmlString|null
*/
function showOnline(): ?HtmlString
{
if (setting('onlines')) {
$online = statsOnline();
return new HtmlString(view('app/_online', compact('online')));
}
return null;
}
/**
* Get online widget
*
* @return mixed
*/
function onlineWidget(): HtmlString
{
$online = statsOnline();
return new HtmlString(view('widgets/_online', compact('online')));
}
/**
* Возвращает статистику посещений
*
* @return array статистика посещений
*/
function statsCounter(): array
{
return Cache::remember('counter', 30, static function () {
$counter = Counter::query()->first();
return $counter ? $counter->toArray() : [];
});
}
/**
* Выводит счетчик посещений
*
* @return HtmlString|null
*/
function showCounter(): ?HtmlString
{
$metrika = new Metrika();
$metrika->saveStatistic();
$counter = statsCounter();
if (setting('incount') > 0) {
return new HtmlString(view('app/_counter', compact('counter')));
}
return null;
}
/**
* Возвращает количество пользователей
*
* @return string Количество пользователей
*/
function statsUsers(): string
{
return Cache::remember('statUsers', 1800, static function () {
$startDay = mktime(0, 0, 0, dateFixed(SITETIME, 'n', true));
$stat = User::query()->count();
$new = User::query()->where('created_at', '>', $startDay)->count();
if ($new) {
$stat .= '/+' . $new;
}
return $stat;
});
}
/**
* Возвращает количество администраторов
*
* @return int Количество администраторов
*/
function statsAdmins(): int
{
return Cache::remember('statAdmins', 3600, static function () {
return User::query()->whereIn('level', User::ADMIN_GROUPS)->count();
});
}
/**
* Возвращает количество жалоб
*
* @return int Количество жалоб
*/
function statsSpam(): int
{
return Spam::query()->count();
}
/**
* Возвращает количество забанненых пользователей
*
* @return int Количество забаненных
*/
function statsBanned(): int
{
return User::query()
->where('level', User::BANNED)
->where('timeban', '>', SITETIME)
->count();
}
/**
* Возвращает количество записей в истории банов
*
* @return int Количество записей
*/
function statsBanHist(): int
{
return Banhist::query()->count();
}
/**
* Возвращает количество ожидающих подтверждения регистрации
*
* @return int Количество ожидающих
*/
function statsRegList(): int
{
return User::query()->where('level', User::PENDED)->count();
}
/**
* Возвращает количество забаненных по IP
*
* @return int Количество забаненных
*/
function statsIpBanned(): int
{
return Ban::query()->count();
}
/**
* Возвращает количество фотографий в галерее
*
* @return string Количество фотографий
*/
function statsPhotos(): string
{
return Cache::remember('statPhotos', 900, static function () {
$stat = Photo::query()->count();
$totalNew = Photo::query()->where('created_at', '>', strtotime('-1 day', SITETIME))->count();
return formatShortNum($stat) . ($totalNew ? '/+' . $totalNew : '');
});
}
/**
* Возвращает количество новостей
*
* @return string Количество новостей
*/
function statsNews(): string
{
return Cache::remember('statNews', 300, static function () {
$total = News::query()->count();
$totalNew = News::query()
->where('created_at', '>', strtotime('-1 day', SITETIME))
->count();
return formatShortNum($total) . ($totalNew ? '/+' . $totalNew : '');
});
}
/**
* Возвращает количество записей в черном списке
*
* @return string Количество записей
*/
function statsBlacklist(): string
{
$blacklist = BlackList::query()
->selectRaw('type, count(*) as total')
->groupBy('type')
->pluck('total', 'type')
->all();
$list = $blacklist + ['login' => 0, 'email' => 0, 'domain' => 0];
return $list['login'] . '/' . $list['email'] . '/' . $list['domain'];
}
/**
* Возвращает количество записей в антимате
*
* @return int Количество записей
*/
function statsAntimat(): int
{
return Antimat::query()->count();
}
/**
* Возвращает количество стикеров
*
* @return int Количество стикеров
*/
function statsStickers(): int
{
return Sticker::query()->count();
}
/**
* Возвращает дату последнего сканирования сайта
*
* @return int|string Дата последнего сканирования
*/
function statsChecker()
{
if (file_exists(storage_path('framework/cache/checker.php'))) {
return dateFixed(filemtime(storage_path('framework/cache/checker.php')), 'd.m.Y');
}
return 0;
}
/**
* Возвращает количество приглашений на регистрацию
*
* @return string Количество приглашений
*/
function statsInvite(): string
{
$invited = Invite::query()->where('used', 0)->count();
$usedInvited = Invite::query()->where('used', 1)->count();
return $invited . '/' . $usedInvited;
}
/**
* Возвращает следующею и предыдущую фотографию в галерее
*
* @param int $id Id фотографий
*
* @return array|null Массив данных
*/
function photoNavigation(int $id): ?array
{
if (! $id) {
return null;
}
$next = Photo::query()
->where('id', '>', $id)
->orderBy('id')
->pluck('id')
->first();
$prev = Photo::query()
->where('id', '<', $id)
->orderByDesc('id')
->pluck('id')
->first();
return compact('next', 'prev');
}
/**
* Возвращает количество статей в блогах
*
* @return string Количество статей
*/
function statsBlog(): string
{
return Cache::remember('statArticles', 900, static function () {
$stat = Article::query()->count();
$totalNew = Article::query()->where('created_at', '>', strtotime('-1 day', SITETIME))->count();
return formatShortNum($stat) . ($totalNew ? '/+' . $totalNew : '');
});
}
/**
* Возвращает количество тем и сообщений в форуме
*
* @return string Количество тем и сообщений
*/
function statsForum(): string
{
return Cache::remember('statForums', 600, static function () {
$topics = Topic::query()->count();
$posts = Post::query()->count();
$totalNew = Post::query()
->where('created_at', '>', strtotime('-1 day', SITETIME))
->count();
return formatShortNum($topics) . '/' . formatShortNum($posts) . ($totalNew ? '/+' . $totalNew : '');
});
}
/**
* Возвращает количество сообщений в гостевой книге
*
* @return string Количество сообщений
*/
function statsGuestbook(): string
{
return Cache::remember('statGuestbook', 600, static function () {
$total = Guestbook::query()->count();
$totalNew = Guestbook::query()
->where('created_at', '>', strtotime('-1 day', SITETIME))
->count();
return formatShortNum($total) . ($totalNew ? '/+' . $totalNew : '');
});
}
/**
* Возвращает количество сообщений в админ-чате
*
* @return string Количество сообщений
*/
function statsChat(): string
{
return Cache::remember('statChat', 3600, static function () {
$total = Chat::query()->count();
$totalNew = Chat::query()
->where('created_at', '>', strtotime('-1 day', SITETIME))
->count();
return formatShortNum($total) . ($totalNew ? '/+' . $totalNew : '');
});
}
/**
* Возвращает время последнего сообщения в админ-чате
*
* @return int Время сообщения
*/
function statsNewChat(): int
{
return Chat::query()->max('created_at') ?? 0;
}
/**
* Возвращает количество файлов в загруз-центре
*
* @return string Количество файлов
*/
function statsLoad(): string
{
return Cache::remember('statLoads', 900, static function () {
$totalLoads = Load::query()->sum('count_downs');
$totalNew = Down::query()->where('active', 1)
->where('created_at', '>', strtotime('-1 day', SITETIME))
->count();
return formatShortNum($totalLoads) . ($totalNew ? '/+' . $totalNew : '');
});
}
/**
* Возвращает количество новых файлов
*
* @return int Количество файлов
*/
function statsNewLoad(): int
{
return Down::query()->where('active', 0)->count();
}
/**
* Возвращает количество объявлений
*
* @return string Количество статей
*/
function statsBoard(): string
{
return Cache::remember('statBoards', 900, static function () {
$stat = formatShortNum(Item::query()->where('expires_at', '>', SITETIME)->count());
$totalNew = Item::query()->where('updated_at', '>', strtotime('-1 day', SITETIME))->count();
return formatShortNum($stat) . ($totalNew ? '/+' . $totalNew : '');
});
}
/**
* Обфусцирует email
*
* @param string $email email
*
* @return string Обфусцированный email
*/
function cryptMail(string $email): string
{
$output = '';
$symbols = str_split($email);
foreach ($symbols as $symbol) {
$output .= '&#' . ord($symbol) . ';';
}
return $output;
}
/**
* Частично скрывает email
*
* @param string $email
*
* @return string
*/
function hideMail(string $email): string
{
return preg_replace('/(?<=.).(?=.*@)/u', '*', $email);
}
/**
* Возвращает статистику текущих голосований из кэш-файла
*
* @return string Статистика текущий голосований
*/
function statVotes(): string
{
return Cache::remember('statVotes', 900, static function () {
$votes = Vote::query()
->selectRaw('count(*) AS cnt, coalesce(sum(count), 0) AS sum')
->where('closed', 0)
->first();
if (! $votes) {
$votes->cnt = $votes->sum = 0;
}
return $votes->cnt . '/' . $votes->sum;
});
}
/**
* Возвращает дату последней новости из кэш-файла
*
* @return string Дата последней новости
*/
function statsNewsDate()
{
$newsDate = Cache::remember('statNewsDate', 900, static function () {
/** @var News $news */
$news = News::query()->orderByDesc('created_at')->first();
return $news->created_at ?? 0;
});
return $newsDate ? dateFixed($newsDate, 'd.m.Y') : 0;
}
/**
* Возвращает последние новости
*
* @return HtmlString Новость
*/
function lastNews(): HtmlString
{
$news = null;
if (setting('lastnews') > 0) {
$news = Cache::remember('lastNews', 1800, static function () {
return News::query()
->where('top', 1)
->orderByDesc('created_at')
->limit(setting('lastnews'))
->get();
});
}
return new HtmlString(view('widgets/_news', compact('news')));
}
/**
* Возвращает иконку расширения
*
* @param string $ext Расширение файла
*
* @return HtmlString Иконка
*/
function icons(string $ext): HtmlString
{
switch ($ext) {
case 'php':
$ico = 'file-code';
break;
case 'ppt':
$ico = 'file-powerpoint';
break;
case 'doc':
case 'docx':
$ico = 'file-word';
break;
case 'xls':
case 'xlsx':
$ico = 'file-excel';
break;
case 'txt':
case 'css':
case 'dat':
case 'html':
case 'htm':
$ico = 'file-alt';
break;
case 'wav':
case 'amr':
case 'mp3':
case 'mid':
$ico = 'file-audio';
break;
case 'zip':
case 'rar':
case '7z':
case 'gz':
$ico = 'file-archive';
break;
case '3gp':
case 'mp4':
$ico = 'file-video';
break;
case 'jpg':
case 'jpeg':
case 'bmp':
case 'wbmp':
case 'gif':
case 'png':
$ico = 'file-image';
break;
case 'ttf':
$ico = 'font';
break;
case 'pdf':
$ico = 'file-pdf';
break;
default:
$ico = 'file';
}
return new HtmlString('<i class="far fa-' . $ico . '"></i>');
}
/**
* Перемешивает элементы ассоциативного массива, сохраняя ключи
*
* @param array &$array Исходный массив, переданный по ссылке
*
* @return bool Флаг успешного выполнения операции
*/
function shuffleAssoc(array &$array): bool
{
$keys = array_keys($array);
shuffle($keys);
$new = [];
foreach ($keys as $key) {
$new[$key] = $array[$key];
}
$array = $new;
return true;
}
/**
* Закрывает bb-теги
*
* @param string $html
*
* @return string
*/
function closeTags(string $html): string
{
preg_match_all('#\[([a-z]+)(?:=.*)?(?<![/])\]#iU', $html, $result);
$openTags = $result[1];
preg_match_all('#\[/([a-z]+)\]#iU', $html, $result);
$closedTags = $result[1];
if ($openTags === $closedTags) {
return $html;
}
$diff = array_diff_assoc($openTags, $closedTags);
$tags = array_reverse($diff);
foreach ($tags as $value) {
$html .= '[/' . $value . ']';
}
return $html;
}
/**
* Возвращает обрезанный текст с закрытием тегов
*
* @param string $value
* @param int $words
* @param string $end
*
* @return HtmlString
*/
function bbCodeTruncate(string $value, int $words = 20, string $end = '...'): HtmlString
{
$value = Str::words($value, $words, $end);
$bbText = bbCode(closeTags($value));
return new HtmlString(preg_replace('/\[(.*?)\]/', '', $bbText));
}
/**
* Возвращает обрезанную до заданного количества букв строке
*
* @param HtmlString|string $value Исходная строка
* @param int $limit Максимальное количество символов в результате
* @param string $end
*
* @return string Обрезанная строка
*/
function truncateString($value, int $limit = 100, string $end = '...'): string
{
$value = strip_tags($value);
if (mb_strlen($value, 'utf-8') <= $limit) {
return $value;
}
$string = mb_substr($value, 0, $limit + 1);
if ($lastSpace = mb_strrpos($string, ' ', 0, 'utf-8')) {
$string = mb_substr($string, 0, $lastSpace, 'utf-8');
} else {
$string = mb_substr($string, 0, $limit, 'utf-8');
}
return trim($string) . $end;
}
/**
* Возвращает обрезанную до заданного количества слов строке
*
* @param HtmlString|string $value Исходная строка
* @param int $words Максимальное количество слов в результате
* @param string $end
*
* @return string Обрезанная строка
*/
function truncateWord($value, int $words = 20, string $end = '...'): string
{
$value = strip_tags($value);
return Str::words(trim($value), $words, $end);
}
/**
* Возвращает обрезанную строку с удалением перевода строки
*
* @param HtmlString|string $value
* @param int $words
* @param string $end
*
* @return string
*/
function truncateDescription($value, int $words = 20, string $end = ''): string
{
$value = strip_tags(preg_replace('/\s+/', ' ', $value));
return Str::words(trim($value), $words, $end);
}
/**
* Возвращает код платной рекламы
*
* @param string $place
*
* @return HtmlString|null
*/
function getAdvertPaid(string $place): ?HtmlString
{
$adverts = PaidAdvert::statAdverts();
if (isset($adverts[$place])) {
$links = [];
foreach ($adverts[$place] as $advert) {
$links[] = Arr::random($advert);
}
return new HtmlString(implode('<br>', $links));
}
return null;
}
/**
* Возвращает код админской рекламы
*
* @return HtmlString|null
*/
function getAdvertAdmin(): ?HtmlString
{
$adverts = AdminAdvert::statAdverts();
if ($adverts) {
$result = Arr::random($adverts);
return new HtmlString(view('adverts/_admin_links', compact('result')));
}
return null;
}
/**
* Возвращает код пользовательской рекламы
*
* @return HtmlString|null
*/
function getAdvertUser(): ?HtmlString
{
$adverts = Advert::statAdverts();
if ($adverts) {
$total = count($adverts);
$show = setting('rekusershow') > $total ? $total : setting('rekusershow');
$links = Arr::random($adverts, $show);
$result = implode('<br>', $links);
return new HtmlString(view('adverts/_links', compact('result')));
}
return null;
}
/**
* Выводит последние фотографии
*
* @param int $show Количество последних фотографий
*
* @return HtmlString Список фотографий
*/
function recentPhotos(int $show = 5): HtmlString
{
$photos = Cache::remember('recentPhotos', 1800, static function () use ($show) {
return Photo::query()
->orderByDesc('created_at')
->limit($show)
->with('files')
->get();
});
return new HtmlString(view('widgets/_photos', compact('photos')));
}
/**
* Выводит последние темы форума
*
* @param int $show Количество последних тем форума
*
* @return HtmlString Список тем
*/
function recentTopics(int $show = 5): HtmlString
{
$topics = Cache::remember('recentTopics', 300, static function () use ($show) {
return Topic::query()
->orderByDesc('updated_at')
->limit($show)
->get();
});
return new HtmlString(view('widgets/_topics', compact('topics')));
}
/**
* Выводит последние файлы в загрузках
*
* @param int $show Количество последних файлов в загрузках
*
* @return HtmlString Список файлов
*/
function recentDowns(int $show = 5): HtmlString
{
$downs = Cache::remember('recentDowns', 600, static function () use ($show) {
return Down::query()
->where('active', 1)
->orderByDesc('created_at')
->limit($show)
->with('category')
->get();
});
return new HtmlString(view('widgets/_downs', compact('downs')));
}
/**
* Выводит последние статьи в блогах
*
* @param int $show Количество последних статей в блогах
*
* @return HtmlString Список статей
*/
function recentArticles(int $show = 5): HtmlString
{
$articles = Cache::remember('recentArticles', 600, static function () use ($show) {
return Article::query()
->orderByDesc('created_at')
->limit($show)
->get();
});
return new HtmlString(view('widgets/_articles', compact('articles')));
}
/**
* Выводит последние объявления
*
* @param int $show Количество последних объявлений
*
* @return HtmlString Список объявлений
*/
function recentBoards(int $show = 5): HtmlString
{
$items = Cache::remember('recentBoards', 600, static function () use ($show) {
return Item::query()
->where('expires_at', '>', SITETIME)
->orderByDesc('created_at')
->limit($show)
->get();
});
return new HtmlString(view('widgets/_boards', compact('items')));
}
/**
* Возвращает количество предложений и проблем
*
* @return string Количество предложений и проблем
*/
function statsOffers(): string
{
return Cache::remember('offers', 600, static function () {
$offers = Offer::query()->where('type', 'offer')->count();
$problems = Offer::query()->where('type', 'issue')->count();
return $offers . '/' . $problems;
});
}
/**
* Пересчитывает счетчики
*
* @param string $mode сервис счетчиков
*
* @return void
*/
function restatement(string $mode)
{
switch ($mode) {
case 'forums':
DB::update('update topics set count_posts = (select count(*) from posts where topics.id = posts.topic_id)');
DB::update('update forums set count_topics = (select count(*) from topics where forums.id = topics.forum_id)');
DB::update('update forums set count_posts = (select coalesce(sum(count_posts), 0) from topics where forums.id = topics.forum_id)');
break;
case 'blogs':
DB::update('update blogs set count_articles = (select count(*) from articles where blogs.id = articles.category_id)');
DB::update('update articles set count_comments = (select count(*) from comments where relate_type = "' . Article::$morphName . '" and articles.id = comments.relate_id)');
break;
case 'loads':
DB::update('update loads set count_downs = (select count(*) from downs where loads.id = downs.category_id and active = ?)', [1]);
DB::update('update downs set count_comments = (select count(*) from comments where relate_type = "' . Down::$morphName . '" and downs.id = comments.relate_id)');
break;
case 'news':
DB::update('update news set count_comments = (select count(*) from comments where relate_type = "' . News::$morphName . '" and news.id = comments.relate_id)');
break;
case 'photos':
DB::update('update photos set count_comments = (select count(*) from comments where relate_type = "' . Photo::$morphName . '" and photos.id = comments.relate_id)');
break;
case 'offers':
DB::update('update offers set count_comments = (select count(*) from comments where relate_type = "' . Offer::$morphName . '" and offers.id = comments.relate_id)');
break;
case 'boards':
DB::update('update boards set count_items = (select count(*) from items where boards.id = items.board_id and items.expires_at > ' . SITETIME . ');');
break;
case 'votes':
DB::update('update votes set count = (select coalesce(sum(result), 0) from voteanswer where votes.id = voteanswer.vote_id)');
break;
}
}
/**
* Возвращает количество строк в файле
*
* @param string $file Путь к файлу
*
* @return int Количество строк
*/
function counterString(string $file): int
{
$countLines = 0;
if (file_exists($file)) {
$countLines = count(file($file));
}
return $countLines;
}
/**
* Форматирует вывод числа
*
* @param int|float $num число
*
* @return HtmlString Форматированное число
*/
function formatNum($num): HtmlString
{
if ($num > 0) {
$data = '<span style="color:#00aa00">+' . $num . '</span>';
} elseif ($num < 0) {
$data = '<span style="color:#ff0000">' . $num . '</span>';
} else {
$data = '<span>0</span>';
}
return new HtmlString($data);
}
/**
* Форматирует вывод числа
*
* @param int $num
*
* @return string
*/
function formatShortNum(int $num)
{
if (! is_numeric($num)) {
return '0b';
}
if ($num > 1000000000000) {
return round($num / 1000000000000, 1) . 'T';
}
if ($num > 1000000000) {
return round($num / 1000000000, 1) . 'B';
}
if ($num > 1000000) {
return round($num / 1000000, 1) . 'M';
}
if ($num > 1000) {
return round($num / 1000, 1) . 'K';
}
return $num;
}
/**
* Обрабатывает и уменьшает изображение
*
* @param string|null $path Путь к изображению
* @param array $params Параметры изображения
*
* @return array Обработанные параметры
*/
function resizeProcess(?string $path, array $params = []): array
{
if (empty($params['alt'])) {
$params['alt'] = basename($path);
}
if (empty($params['class'])) {
$params['class'] = 'img-fluid';
}
if (! file_exists(public_path($path)) || ! is_file(public_path($path))) {
return [
'path' => '/assets/img/images/photo.png',
'source' => false,
'params' => $params,
];
}
[$width, $height] = getimagesize(public_path($path));
if ($width <= setting('previewsize') && $height <= setting('previewsize')) {
return [
'path' => $path,
'source' => $path,
'params' => $params,
];
}
$thumb = ltrim(str_replace('/', '_', $path), '_');
if (! file_exists(public_path('uploads/thumbnails/' . $thumb))) {
$img = Image::make(public_path($path));
$img->resize(setting('previewsize'), setting('previewsize'), static function (Constraint $constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
$img->save(public_path('uploads/thumbnails/' . $thumb));
}
return [
'path' => '/uploads/thumbnails/' . $thumb,
'source' => $path,
'params' => $params,
];
}
/**
* Возвращает уменьшенное изображение
*
* @param string|null $path Путь к изображению
* @param array $params Параметры изображения
*
* @return HtmlString Уменьшенное изображение
*/
function resizeImage(?string $path, array $params = []): HtmlString
{
$image = resizeProcess($path, $params);
$strParams = [];
foreach ($image['params'] as $key => $param) {
$strParams[] = $key . '="' . check($param) . '"';
}
$strParams = implode(' ', $strParams);
return new HtmlString('<img src="' . $image['path'] . '" data-source="' . $image['source'] . '" ' . $strParams . '>');
}
/**
* Удаляет директорию рекурсивно
*
* @param string $dir путь к директории
*
* @return void
*/
function deleteDir(string $dir)
{
if (file_exists($dir)) {
if ($files = glob($dir . '/*')) {
foreach ($files as $file) {
is_dir($file) ? deleteDir($file) : unlink($file);
}
}
rmdir($dir);
}
}
/**
* Удаляет файл
*
* @param string $path Путь к файлу
*
* @return bool
*/
function deleteFile(string $path): bool
{
if (file_exists($path) && is_file($path)) {
unlink($path);
}
if (in_array(getExtension($path), ['jpg', 'jpeg', 'gif', 'png'], true)) {
$thumb = ltrim(str_replace([public_path(), '/'], ['', '_'], $path), '_');
$thumb = public_path('uploads/thumbnails/' . $thumb);
if (file_exists($thumb) && is_file($thumb)) {
unlink($thumb);
}
}
return true;
}
/**
* Отправляет уведомление об упоминании в приват
*
* @param string $text Текст сообщения
* @param string $url Путь к странице
* @param string $title Название страницу
*
* @return void
*/
function sendNotify(string $text, string $url, string $title)
{
/*$parseText = preg_replace('|\[quote(.*?)\](.*?)\[/quote\]|s', '', $text);*/
preg_match_all('/(?<=^|\s|=)@([\w\-]+)/', $text, $matches);
if (! empty($matches[1])) {
$login = getUser('login') ?? setting('guestsuser');
$usersAnswer = array_unique(array_diff($matches[1], [$login]));
foreach ($usersAnswer as $user) {
$user = getUserByLogin($user);
if ($user && $user->notify) {
$notify = textNotice('notify', compact('login', 'url', 'title', 'text'));
$user->sendMessage(null, $notify);
}
}
}
}
/**
* Возвращает приватное сообщение
*
* @param string $type Тип сообщения
* @param array $replace Массив заменяемых параметров
*
* @return string Сформированный текст
*/
function textNotice(string $type, array $replace = []): string
{
/** @var Notice $message */
$message = Notice::query()->where('type', $type)->first();
if (! $message) {
return __('main.text_missing');
}
foreach ($replace as $key => $val) {
$message->text = str_replace('%' . $key . '%', $val, $message->text);
}
return $message->text;
}
/**
* Возвращает блок статистики производительности
*
* @return HtmlString|null Статистика производительности
*/
function performance(): ?HtmlString
{
if (isAdmin() && setting('performance')) {
$queries = getQueryLog();
$timeQueries = array_sum(array_column($queries, 'time'));
return new HtmlString(view('app/_performance', compact('queries', 'timeQueries')));
}
return null;
}
/**
* Очистка кеш-файлов
*
* @param string|array|null $keys
*
* @return bool Результат выполнения
*/
function clearCache($keys = null): bool
{
if ($keys) {
if (!is_array($keys)) {
$keys = [$keys];
}
foreach ($keys as $key) {
Cache::forget($key);
}
return true;
}
Cache::flush();
return true;
}
/**
* Возвращает текущую страницу
*
* @param string|null $url
*
* @return string|null Текущая страница
*/
function returnUrl(?string $url = null): ?string
{
$request = request();
if ($request->is('/', 'login', 'register', 'recovery', 'restore', 'ban', 'closed')) {
return null;
}
$query = $request->has('return') ? $request->input('return') : $request->path();
return '?return=' . urlencode($url ?? '/' . $query);
}
/**
* Saves error logs
*
* @param int $code
* @param string|null $message
*
* @return void
*/
function saveErrorLog(int $code, ?string $message = null)
{
$errorCodes = [401, 403, 404, 405, 419, 429, 500, 503, 666];
if (setting('errorlog') && in_array($code, $errorCodes, true)) {
Error::query()->create([
'code' => $code,
'request' => utfSubstr(request()->getRequestUri(), 0, 250),
'referer' => utfSubstr(request()->header('referer'), 0, 250),
'user_id' => getUser('id'),
'message' => utfSubstr($message, 0, 250),
'ip' => getIp(),
'brow' => getBrowser(),
'created_at' => SITETIME,
]);
}
}
/**
* Возвращает ошибку
*
* @param string|array $errors ошибки
*
* @return HtmlString Сформированный блок с ошибкой
*/
function showError($errors): HtmlString
{
$errors = (array) $errors;
return new HtmlString(view('app/_error', compact('errors')));
}
/**
* @return HtmlString
*/
function getCaptcha(): HtmlString
{
return new HtmlString(view('app/_captcha'));
}
/**
* Сохраняет flash уведомления
*
* @param string $status Статус уведомления
* @param mixed $message Массив или текст с уведомлениями
*
* @return void
*
* @deprecated since 10.1 - Use redirect->with('success', 'Message') or session()->flash()
*/
function setFlash(string $status, $message)
{
session()->put('flash.' . $status, $message);
}
/**
* Сохраняет POST данные введенных пользователем
*
* @param array $data Массив полей
*
* @deprecated since 10.1
*/
function setInput(array $data)
{
session()->flash('input', json_encode($data));
}
/**
* Возвращает значение из POST данных
*
* @param string $key Имя поля
* @param mixed $default
*
* @return mixed Сохраненное значение
*
* @deprecated since 10.1 - Use old('field', 'default');
*/
function getInput(string $key, $default = null)
{
if (session()->missing('input')) {
return $default;
}
$input = json_decode(session()->get('input', []), true);
return Arr::get($input, $key, $default);
}
/**
* Подсвечивает блок с полем для ввода сообщения
*
* @param string $field Имя поля
*
* @return string CSS класс ошибки
*/
function hasError(string $field): string
{
// Новая валидация
if (session()->has('errors')) {
/** @var ViewErrorBag $errors */
$errors = session()->get('errors');
return $errors->has($field) ? ' is-invalid' : ' is-valid';
}
$isValid = session()->has('flash.danger') ? ' is-valid' : '';
return session()->has('flash.danger.' . $field) ? ' is-invalid' : $isValid;
}
/**
* Возвращает блок с текстом ошибки
*
* @param string $field Имя поля
*
* @return string|null Блоки ошибки
*/
function textError(string $field): ?string
{
// Новая валидация
if (session()->has('errors')) {
/** @var ViewErrorBag $errors */
$errors = session()->get('errors');
return $errors->first($field);
}
return session()->get('flash.danger.' . $field);
}
/**
* Отправляет уведомления на email
*
* @param string $view
* @param array $data
*
* @return bool Результат отправки
*/
function sendMail(string $view, array $data): bool
{
Mail::send($view, $data, function (Message $message) use ($data) {
$message->subject($data['subject'])
->to($data['to'])
->from(config('mail.from.address'), config('mail.from.name'));
if (isset($data['from'])) {
[$fromEmail, $fromName] = $data['from'];
$message->replyTo($fromEmail, $fromName);
}
if (isset($data['unsubscribe'])) {
$headers = $message->getHeaders();
$headers->addTextHeader('List-Unsubscribe', '<' . config('app.url') . '/unsubscribe?key=' . $data['unsubscribe'] . '>');
}
});
return ! Mail::failures();
}
/**
* Возвращает расширение файла
*
* @param string $filename Имя файла
*
* @return string расширение
*/
function getExtension(string $filename): string
{
return pathinfo($filename, PATHINFO_EXTENSION);
}
/**
* Возвращает имя файла без расширения
*
* @param string $filename Имя файла
*
* @return string Имя без расширения
*/
function getBodyName(string $filename): string
{
return pathinfo($filename, PATHINFO_FILENAME);
}
/**
* Склоняет числа
*
* @param int $num Число
* @param mixed $forms Массив склоняемых слов (один, два, много)
*
* @return string Форматированная строка
*/
function plural(int $num, $forms): string
{
if (! is_array($forms)) {
$forms = explode(',', $forms);
}
if (count($forms) === 1) {
return $num . ' ' . $forms[0];
}
if ($num % 100 > 10 && $num % 100 < 15) {
return $num . ' ' . $forms[2];
}
if ($num % 10 === 1) {
return $num . ' ' . $forms[0];
}
if ($num % 10 > 1 && $num % 10 < 5) {
return $num . ' ' . $forms[1];
}
return $num . ' ' . $forms[2];
}
/**
* Обрабатывает BB-код
*
* @param string $text Необработанный текст
* @param bool $parse Обрабатывать или вырезать код
*
* @return HtmlString Обработанный текст
*/
function bbCode(string $text, bool $parse = true): HtmlString
{
$bbCode = new BBCode();
$checkText = check($text);
if (! $parse) {
return new HtmlString($bbCode->clear($checkText));
}
$parseText = $bbCode->parse($checkText);
$parseText = $bbCode->parseStickers($parseText);
return new HtmlString($parseText);
}
/**
* Определяет IP пользователя
*
* @return string IP пользователя
*/
function getIp(): string
{
$cf = new CloudFlare(request());
return $cf->ip();
}
/**
* Определяет браузер
*
* @param string|null $userAgent
*
* @return string|null Браузер и версия браузера
*/
function getBrowser(string $userAgent = null): string
{
$browser = new Browser();
if ($userAgent) {
$browser->setUserAgent($userAgent);
}
$brow = $browser->getBrowser();
$version = implode('.', array_slice(explode('.', $browser->getVersion()), 0, 2));
$browser = $version === Browser::VERSION_UNKNOWN ? $brow : $brow . ' ' . $version;
return mb_substr($browser, 0, 25, 'utf-8');
}
/**
* Является ли пользователь авторизованным
*
* @return User|bool
*/
function checkAuth()
{
if (session()->has(['id', 'password'])) {
$user = getUserById(session()->get('id'));
if ($user && session()->get('password') === $user->password) {
return $user;
}
}
return false;
}
/**
* Является ли пользователь администратором
*
* @param string|null $level Уровень доступа
*
* @return bool Является ли пользователь администратором
*/
function isAdmin(?string $level = null): bool
{
return access($level ?? User::EDITOR);
}
/**
* Имеет ли пользователь доступ по уровню
*
* @param string $level Уровень доступа
*
* @return bool Разрешен ли доступ
*/
function access(string $level): bool
{
$access = array_flip(User::ALL_GROUPS);
return getUser()
&& isset($access[$level], $access[getUser('level')])
&& $access[getUser('level')] <= $access[$level];
}
/**
* Возвращает объект пользователя по логину
*
* @param string|null $login Логин пользователя
*
* @return Builder|Model|null
*/
function getUserByLogin(?string $login): ?User
{
return User::query()->where('login', $login)->first();
}
/**
* Возвращает объект пользователя по id
*
* @param int|null $id ID пользователя
*
* @return Builder|Model|null
*/
function getUserById(?int $id): ?User
{
return User::query()->find($id);
}
/**
* Возвращает объект пользователя по токену
*
* @param string $token Логин пользователя
*
* @return Builder|Model|null
*/
function getUserByToken(string $token): ?User
{
return User::query()->where('apikey', $token)->first();
}
/**
* Возвращает объект пользователя по логину или email
*
* @param string|null $login Логин или email пользователя
*
* @return Builder|Model|null
*/
function getUserByLoginOrEmail(?string $login): ?User
{
$field = strpos($login, '@') ? 'email' : 'login';
return User::query()->where($field, $login)->first();
}
/**
* Возвращает данные пользователя по ключу
*
* @param string|null $key Ключ массива
*
* @return User|mixed
*/
function getUser(?string $key = null)
{
static $user;
if (! $user) {
$user = checkAuth();
}
return $key ? ($user[$key] ?? null) : $user;
}
/**
* Разбивает массив по страницам
*
* @param array $items
* @param int $perPage
* @param array $appends
*
* @return LengthAwarePaginator
*/
function paginate(array $items, int $perPage, array $appends = []): LengthAwarePaginator
{
$currentPage = LengthAwarePaginator::resolveCurrentPage();
$slice = array_slice($items, $perPage * ($currentPage - 1), $perPage, true);
$collection = new LengthAwarePaginator($slice, count($items), $perPage);
$collection->setPath(request()->url());
$collection->appends($appends);
return $collection;
}
/**
* Возвращает сформированный код base64 картинки
*
* @param string $path Путь к картинке
* @param array $params Параметры
*
* @return HtmlString Сформированный код
*/
function imageBase64(string $path, array $params = []): HtmlString
{
$type = getExtension($path);
$data = file_get_contents($path);
if (! isset($params['class'])) {
$params['class'] = 'img-fluid';
}
if (empty($params['alt'])) {
$params['alt'] = basename($path);
}
$strParams = [];
foreach ($params as $key => $param) {
$strParams[] = $key . '="' . $param . '"';
}
$strParams = implode(' ', $strParams);
return new HtmlString('<img src="data:image/' . $type . ';base64,' . base64_encode($data) . '"' . $strParams . '>');
}
/**
* Выводит прогресс-бар
*
* @param int $percent
* @param int|float|string|null $title
*
* @return HtmlString
*/
function progressBar(int $percent, $title = null): HtmlString
{
if (! $title) {
$title = $percent . '%';
}
return new HtmlString(view('app/_progressbar', compact('percent', 'title')));
}
/**
* Возвращает форматированный список запросов
*
* @return array
*/
function getQueryLog(): array
{
$queries = DB::getQueryLog();
$formattedQueries = [];
foreach ($queries as $query) {
foreach ($query['bindings'] as $key => $binding) {
if (is_string($binding)) {
$query['bindings'][$key] = ctype_print($binding) ? "'$binding'" : '[binary]';
}
$query['bindings'][$key] = $binding ?? 'null';
}
$sql = str_replace(['%', '?'], ['%%', '%s'], $query['query']);
$sql = vsprintf($sql, $query['bindings']);
$formattedQueries[] = ['query' => $sql, 'time' => $query['time']];
}
return $formattedQueries;
}
/**
* Выводит список забаненных ip
*
* @param bool $clear Нужно ли сбросить кеш
*
* @return array Массив IP
*/
function ipBan(bool $clear = false): array
{
if ($clear) {
clearCache('ipBan');
}
return Cache::rememberForever('ipBan', static function () {
return Ban::query()->get()->pluck('id', 'ip')->all();
});
}
/**
* Возвращает настройки сайта по ключу
*
* @param string|null $key Ключ массива
* @param mixed $default Значение по умолчанию
*
* @return mixed Данные
*/
function setting(?string $key = null, $default = null)
{
static $settings;
if (! $settings) {
$settings = Setting::getSettings();
}
return $key ? ($settings[$key] ?? $default) : $settings;
}
/**
* Возвращает имя сайта из ссылки
*
* @param string $url Ссылка на сайт
*
* @return string Имя сайта
*/
function siteDomain(string $url): string
{
return parse_url(strtolower($url), PHP_URL_HOST);
}
/**
* Получает версию
*
* @param string $version
*
* @return string
*/
function parseVersion(string $version): string
{
$ver = explode('.', strtok($version, '-'));
return $ver[0] . '.' . $ver[1] . '.' . ($ver[2] ?? 0);
}
/**
* Проверяет captcha
*
* @return bool
*/
function captchaVerify(): bool
{
$request = request();
if (setting('captcha_type') === 'recaptcha_v2') {
$recaptcha = new ReCaptcha(setting('recaptcha_private'));
$response = $recaptcha->setExpectedHostname($_SERVER['SERVER_NAME'])
->verify($request->input('g-recaptcha-response'), getIp());
return $response->isSuccess();
}
if (setting('captcha_type') === 'recaptcha_v3') {
$recaptcha = new ReCaptcha(setting('recaptcha_private'));
$response = $recaptcha->setExpectedHostname($_SERVER['SERVER_NAME'])
->setScoreThreshold(0.5)
->verify($request->input('protect'), getIp());
return $response->isSuccess();
}
if (setting('captcha_type') === 'graphical') {
return strtolower($request->input('protect')) === $request->session()->get('protect');
}
return false;
}
/**
* Возвращает уникальное имя
*
* @param string|null $extension
*
* @return string
*/
function uniqueName(string $extension = null): string
{
if ($extension) {
$extension = '.' . $extension;
}
return str_replace('.', '', uniqid('', true)) . $extension;
}
/**
* Возвращает курсы валют
*
* @return HtmlString
*/
function getCourses(): ?HtmlString
{
$courses = Cache::remember('courses', 3600, static function () {
try {
$client = new Client(['timeout' => 3.0]);
$response = $client->get('//www.cbr-xml-daily.ru/daily_json.js');
$content = json_decode($response->getBody()->getContents(), true);
} catch (Exception $e) {
$content = null;
}
return $content;
});
return new HtmlString(view('app/_courses', compact('courses')));
}