forkbb/app/Models/Page.php
Visman aeb1e97d09 Add media.js
My modification for fluxbb and punbb with some changes.
2023-07-28 21:55:09 +07:00

627 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* This file is part of the ForkBB <https://github.com/forkbb>.
*
* @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
* @license The MIT License (MIT)
*/
declare(strict_types=1);
namespace ForkBB\Models;
use ForkBB\Core\Container;
use ForkBB\Models\Model;
use function \ForkBB\__;
abstract class Page extends Model
{
const FI_INDEX = 'index';
const FI_USERS = 'userlist';
const FI_RULES = 'rules';
const FI_SRCH = 'search';
const FI_REG = 'register';
const FI_LOGIN = 'login';
const FI_PROFL = 'profile';
const FI_PM = 'pm';
const FI_ADMIN = 'admin';
const FI_LGOUT = 'logout';
/**
* Заголовки страницы
*/
protected array $pageHeaders = [];
/**
* Http заголовки
*/
protected array $httpHeaders = [];
public function __construct(Container $container)
{
parent::__construct($container);
$container->Lang->load('common');
$formats = $container->DATE_FORMATS;
$formats[0] = __($formats[0]);
$formats[1] = __($formats[1]);
$container->DATE_FORMATS = $formats;
$formats = $container->TIME_FORMATS;
$formats[0] = __($formats[0]);
$formats[1] = __($formats[1]);
$container->TIME_FORMATS = $formats;
$this->fIndex = self::FI_INDEX; # string Указатель на активный пункт навигации
$this->httpStatus = 200; # int HTTP статус ответа для данной страницы
# $this->nameTpl = null; # null|string Имя шаблона
# $this->titles = []; # array Массив титула страницы | setTitles()
# $this->fIswev = []; # array Массив info, success, warning, error, validation информации
# $this->onlinePos = ''; # null|string Позиция для таблицы онлайн текущего пользователя
$this->onlineDetail = false; # null|bool Формировать данные по посетителям online или нет
$this->onlineFilter = true; # bool Посетители только по текущей странице или по всем
# $this->robots = ''; # string Переменная для meta name="robots"
# $this->canonical = ''; # string Переменная для link rel="canonical"
$this->hhsLevel = 'common'; # string Ключ для $c->HTTP_HEADERS (для вывода заголовков HTTP из конфига)
$this->fTitle = $container->config->o_board_title;
$this->fDescription = $container->config->o_board_desc;
$this->fRootLink = $container->Router->link('Index');
if (1 === $container->config->b_announcement) {
$this->fAnnounce = $container->config->o_announcement_message;
}
// передача текущего юзера и его правил в шаблон
$this->user = $this->c->user;
$this->userRules = $this->c->userRules;
$this->pageHeader('mainStyle', 'link', 10000, [
'rel' => 'stylesheet',
'type' => 'text/css',
'href' => $this->publicLink("/style/{$this->user->style}/style.css"),
]);
$now = \gmdate('D, d M Y H:i:s') . ' GMT';
$this //->header('Cache-Control', 'no-cache, no-store, must-revalidate')
->header('Cache-Control', 'private, no-cache')
->header('Content-Type', 'text/html; charset=utf-8')
->header('Date', $now)
->header('Last-Modified', $now)
->header('Expires', $now);
}
/**
* Подготовка страницы к отображению
*/
public function prepare(): void
{
// вспышка нового личного сообщения
if (
null !== $this->onlinePos
&& ! $this->user->isGuest
&& 1 === $this->user->u_pm_flash
) {
$this->fPMFlash = true;
$this->user->u_pm_flash = 0;
$this->c->users->update($this->user);
}
$this->pageHeader('commonJS', 'script', 10000, [
'src' => $this->publicLink('/js/common.js'),
]);
if ($this->useMediaJS) {
$this->pageHeader('mediaJS', 'script', 10000, [
'src' => $this->publicLink('/js/media.js'),
]);
}
$this->boardNavigation();
$this->iswevMessages();
}
/**
* Задает массивы главной навигации форума
*/
protected function boardNavigation(): void
{
$r = $this->c->Router;
$navUser = [];
$navGen = [
self::FI_INDEX => [
$r->link('Index'),
'Index',
'Home page',
],
];
if (
1 === $this->user->g_read_board
&& $this->userRules->viewUsers
) {
$navGen[self::FI_USERS] = [
$r->link('Userlist'),
'User list',
'List of users',
];
}
if (
1 === $this->c->config->b_rules
&& 1 === $this->user->g_read_board
&& (
! $this->user->isGuest
|| 1 === $this->c->config->b_regs_allow
)
) {
$navGen[self::FI_RULES] = [
$r->link('Rules'),
'Rules',
'Board rules',
];
}
if (
1 === $this->user->g_read_board
&& 1 === $this->user->g_search
) {
$sub = [];
$sub['latest'] = [
$r->link(
'SearchAction',
[
'action' => 'latest_active_topics',
]
),
'Latest active topics',
'Find latest active topics',
];
if (! $this->user->isGuest) {
$sub['with-your-posts'] = [
$r->link(
'SearchAction',
[
'action' => 'topics_with_your_posts',
]
),
'Topics with your posts',
'Find topics with your posts',
];
}
$sub['unanswered'] = [
$r->link(
'SearchAction',
[
'action' => 'unanswered_topics',
]
),
'Unanswered topics',
'Find unanswered topics',
];
$navGen[self::FI_SRCH] = [
$r->link('Search'),
'Search',
'Search topics and posts',
$sub
];
}
if ($this->user->isGuest) {
if (1 === $this->c->config->b_regs_allow) {
$navUser[self::FI_REG] = [
$r->link('Register'),
'Register',
'Register',
];
}
$navUser[self::FI_LOGIN] = [
$r->link('Login'),
'Login',
'Login',
];
} else {
$navUser[self::FI_PROFL] = [
$this->user->link,
['User %s', $this->user->username],
'Your profile',
];
if ($this->user->usePM) {
$tmpPM = [];
if (1 !== $this->user->u_pm) {
$tmpPM[] = 'pmoff';
}
if ($this->user->u_pm_num_new > 0) {
$tmpPM[] = 'pmnew';
}
$navUser[self::FI_PM] = [
$r->link('PM'),
$this->user->u_pm_num_new > 0 ? ['PM %s', $this->user->u_pm_num_new] : 'PM',
'Private messages',
null,
$tmpPM ?: null,
];
}
if ($this->user->isAdmMod) {
$navUser[self::FI_ADMIN] = [
$r->link('Admin'),
'Admin',
'Administration functions',
];
}
$navUser[self::FI_LGOUT] = [
$r->link('Logout'),
'Logout',
'Logout',
];
}
if (
1 === $this->user->g_read_board
&& '' != $this->c->config->o_additional_navlinks
) {
// position|name|link[|id]\n
if (\preg_match_all('%^(\d+)\|([^\|\n\r]+)\|([^\|\n\r]+)(?:\|([^\|\n\r]+))?%m', $this->c->config->o_additional_navlinks . "\n", $matches)) {
$k = \count($matches[0]);
for ($i = 0; $i < $k; ++$i) {
if (empty($matches[4][$i])) {
$matches[4][$i] = 'extra' . $i;
}
if (isset($navGen[$matches[4][$i]])) {
$navGen[$matches[4][$i]] = [$matches[3][$i], $matches[2][$i], $matches[2][$i]];
} else {
$navGen = \array_merge(
\array_slice($navGen, 0, (int) $matches[1][$i]),
[$matches[4][$i] => [$matches[3][$i], $matches[2][$i], $matches[2][$i]]],
\array_slice($navGen, (int) $matches[1][$i])
);
}
}
}
}
$this->fNavigation = $navGen;
$this->fNavigationUser = $navUser;
}
/**
* Задает вывод различных сообщений по условиям
*/
protected function iswevMessages(): void
{
if (
1 === $this->c->config->b_maintenance
&& $this->user->isAdmin
) {
$this->fIswev = [FORK_MESS_WARN, ['Maintenance mode enabled', $this->c->Router->link('AdminMaintenance')]];
}
if (
$this->user->isAdmMod
&& $this->user->last_report_id < $this->c->reports->lastId()
) {
$this->fIswev = [FORK_MESS_INFO, ['New reports', $this->c->Router->link('AdminReports')]];
}
}
/**
* Возвращает title страницы
* $this->pageTitle
*/
protected function getpageTitle(array $titles = []): string
{
if (empty($titles)) {
$titles = $this->titles;
}
$titles[] = ['%s', $this->c->config->o_board_title];
return \implode(__('Title separator'), \array_map('\\ForkBB\\__', $titles));
}
/**
* Задает/получает заголовок страницы
*/
public function pageHeader(string $name, string $type, int $weight = 0, array $values = null): mixed
{
if (null === $values) {
return $this->pageHeaders["{$name}_{$type}"] ?? null;
} else {
$this->pageHeaders["{$name}_{$type}"] = [
'weight' => $weight,
'type' => $type,
'values' => $values
];
return $this;
}
}
/**
* Возвращает массива заголовков страницы
* $this->pageHeaders
*/
protected function getpageHeaders(): array
{
if ($this->canonical) {
$this->pageHeader('canonical', 'link', 0, [
'rel' => 'canonical',
'href' => $this->canonical,
]);
}
if ($this->robots) {
$this->pageHeader('robots', 'meta', 11000, [
'name' => 'robots',
'content' => $this->robots,
]);
}
if (1 === $this->user->g_search) {
$this->pageHeader('opensearch', 'link', 0, [
'rel' => 'search',
'type' => 'application/opensearchdescription+xml',
'href' => $this->c->Router->link('OpenSearch'),
'title' => $this->fTitle,
]);
}
\uasort($this->pageHeaders, function (array $a, array $b) {
if ($a['weight'] === $b['weight']) {
return 0;
} else {
return $a['weight'] > $b['weight'] ? -1 : 1;
}
});
return $this->pageHeaders;
}
/**
* Добавляет/заменяет/удаляет HTTP заголовок
*/
public function header(string $header, ?string $value, bool $replace = true): Page
{
$key = \strtolower($header);
if (\str_starts_with($key, 'http/')) {
$key = 'http/';
$replace = true;
if (
! empty($_SERVER['SERVER_PROTOCOL'])
&& \in_array($_SERVER['SERVER_PROTOCOL'], ['HTTP/1.1', 'HTTP/2', 'HTTP/3'], true)
) {
$header = $_SERVER['SERVER_PROTOCOL'];
}
} else {
$header .= ':';
}
if (null === $value) {
unset($this->httpHeaders[$key]);
} elseif (
true === $replace
|| empty($this->httpHeaders[$key])
) {
$this->httpHeaders[$key] = [
["{$header} {$value}", $replace],
];
} else {
$this->httpHeaders[$key][] = ["{$header} {$value}", $replace];
}
return $this;
}
/**
* Возвращает HTTP заголовки страницы
* $this->httpHeaders
*/
protected function gethttpHeaders(): array
{
foreach ($this->c->HTTP_HEADERS[$this->hhsLevel] as $header => $value) {
if (
'Content-Security-Policy' === $header
&& (
$this->needUnsafeInlineStyle
|| (
$this->c->isInit('Parser')
&& $this->c->Parser->inlineStyle()
)
)
) {
$value = $this->addUnsafeInline($value);
}
$this->header($header, $value);
}
$this->httpStatus();
return $this->httpHeaders;
}
/**
* Добавляет в заголовок (Content-Security-Policy) значение unsafe-inline для style-src
*/
protected function addUnsafeInline(string $header): string
{
if (\preg_match('%style\-src([^;]+)%', $header, $matches)) {
if (false === \strpos($matches[1], 'unsafe-inline')) {
return \str_replace($matches[0], "{$matches[0]} 'unsafe-inline'", $header);
} else {
return $header;
}
}
if (\preg_match('%default\-src([^;]+)%', $header, $matches)) {
if (false === \strpos($matches[1], 'unsafe-inline')) {
return "{$header};style-src{$matches[1]} 'unsafe-inline'";
} else {
return "{$header};style-src{$matches[1]}";
}
}
return "{$header};style-src 'self' 'unsafe-inline'";
}
/**
* Устанавливает HTTP статус страницы
*/
protected function httpStatus(): Page
{
$list = [
302 => '302 Moved Temporarily',
400 => '400 Bad Request',
403 => '403 Forbidden',
404 => '404 Not Found',
405 => '405 Method Not Allowed',
501 => '501 Not Implemented',
503 => '503 Service Unavailable',
];
if (isset($list[$this->httpStatus])) {
$this->header('HTTP/1.0', $list[$this->httpStatus]);
}
return $this;
}
/**
* Дописывает в массив титула страницы новый элемент
* $this->titles = ...
*/
public function settitles(string|array $value): void
{
$attr = $this->getModelAttr('titles', []);
$attr[] = $value;
$this->setModelAttr('titles', $attr);
}
/**
* Добавление новой ошибки
* $this->fIswev = ...
*/
public function setfIswev(array $value): void
{
$attr = $this->getModelAttr('fIswev', []);
if (
isset($value[0], $value[1])
&& \is_string($value[0])
&& 2 === \count($value)
) {
$attr[$value[0]][] = $value[1];
} else {
$attr = \array_merge_recursive($attr, $value); // ???? добавить проверку?
}
$this->setModelAttr('fIswev', $attr) ;
}
/**
* Возвращает массив хлебных крошек
* Заполняет массив титула страницы
*/
protected function crumbs(mixed ...$crumbs): array
{
$result = [];
$active = true;
$ext = null;
foreach ($crumbs as $crumb) {
// модель
if ($crumb instanceof Model) {
do {
$name = $crumb->name ?? 'NO NAME';
if (! \is_array($name)) {
$name = ['%s', $name];
}
$result[] = [$crumb, $name, $active, $ext];
$active = null;
$this->titles = $name;
if ($crumb->page > 1) {
$this->titles = ['Page %s', $crumb->page];
}
if ($crumb->linkCrumbExt) {
$ext = [$crumb->linkCrumbExt, '#'];
} else {
$ext = null;
}
$crumb = $crumb->parent;
} while (
$crumb instanceof Model
&& null !== $crumb->parent
);
// ссылка (передана массивом)
} elseif (\is_array($crumb)) {
$result[] = [$crumb[0], $crumb[1], $active, $ext];
$this->titles = $crumb[1];
// строка
} else {
$result[] = [null, (string) $crumb, $active, $ext];
$this->titles = (string) $crumb;
}
$active = null;
$ext = null;
}
// главная страница
$result[] = [$this->c->Router->link('Index'), 'Index', $active, $ext];
return \array_reverse($result);
}
/**
* Возвращает url для $path заданного в каталоге public
* Если для файла name.ext есть файл name.min.ext, то url возвращается для него
* Ведущий слеш обязателен O_o
*/
public function publicLink(string $path, bool $returnEmpty = false): string
{
if (\preg_match('%^(.+)(\.[^.\\/]++)$%D', $path, $matches)) {
$variants = [
$matches[1] . '.min' => $matches[2],
$matches[1] => $matches[2],
];
} else {
$variants = [
$path => '',
];
}
foreach ($variants as $start => $end) {
$fullPath = $this->c->DIR_PUBLIC . $start . $end;
if (\is_file($fullPath)) {
if ('' === $end) {
return $this->c->PUBLIC_URL . $start;
} else {
$time = \filemtime($fullPath) ?: '0';
return $this->c->PUBLIC_URL . $start . '.v.' . $time . $end;
}
}
}
return $returnEmpty ? '' : $this->c->PUBLIC_URL . $path;
}
}