Compare commits
128 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e88121236c | ||
![]() |
13598f350e | ||
![]() |
46714487d4 | ||
![]() |
7530f5fee0 | ||
![]() |
8ec4676535 | ||
![]() |
6917fc8e53 | ||
![]() |
f53b6965dc | ||
![]() |
6687175940 | ||
![]() |
46df93ef9c | ||
![]() |
d091b7e30d | ||
![]() |
db591e53fa | ||
![]() |
766e79b8ed | ||
![]() |
af6fd98f09 | ||
![]() |
f3976236ec | ||
![]() |
ef8e34c861 | ||
![]() |
a5c27acb2a | ||
![]() |
70e196eefd | ||
![]() |
fe532b6990 | ||
![]() |
85e2c3ed29 | ||
![]() |
3299b641a7 | ||
![]() |
b7001bc83c | ||
![]() |
baaecfcea7 | ||
![]() |
164e0f8653 | ||
![]() |
56160a3a94 | ||
![]() |
e98a014f24 | ||
![]() |
5dc6ecfa23 | ||
![]() |
4bd6f93161 | ||
![]() |
568e119a79 | ||
![]() |
06486e890b | ||
![]() |
3d6501ac7c | ||
![]() |
b39197b70e | ||
![]() |
26cd5d3c17 | ||
![]() |
47882fb1d1 | ||
![]() |
c2be23603a | ||
![]() |
fadf4098ba | ||
![]() |
1ac5847399 | ||
![]() |
b3d238c1cd | ||
![]() |
7ef1e68af7 | ||
![]() |
d10d8aa2c9 | ||
![]() |
032301df17 | ||
![]() |
113df48a3c | ||
![]() |
a0eb7a0e27 | ||
![]() |
1d57ade40f | ||
![]() |
fabc46f8ee | ||
![]() |
2c87b98d24 | ||
![]() |
2feaff7b5c | ||
![]() |
956a2b2d67 | ||
![]() |
5ebf9eb3f6 | ||
![]() |
c648a52651 | ||
![]() |
d4969ae009 | ||
![]() |
40563b4ffc | ||
![]() |
5e1e956de6 | ||
![]() |
7a2efd3bd5 | ||
![]() |
f40602fd82 | ||
![]() |
223efdfb8f | ||
![]() |
ced3c7cd15 | ||
![]() |
4495f64268 | ||
![]() |
0e8e5cd87a | ||
![]() |
2e64177610 | ||
![]() |
432a441a2c | ||
![]() |
5a3ad9d33e | ||
![]() |
2ddd7796b0 | ||
![]() |
9f1d781beb | ||
![]() |
d5eec724d6 | ||
![]() |
acaad2db29 | ||
![]() |
8e3c74367e | ||
![]() |
e6d66f7e0a | ||
![]() |
4f2c637134 | ||
![]() |
c5f2aa0a97 | ||
![]() |
5ac2b20ff2 | ||
![]() |
414e3d9717 | ||
![]() |
25691fa3af | ||
![]() |
c0bb06dc13 | ||
![]() |
4bde2ad136 | ||
![]() |
1d917f0151 | ||
![]() |
d64b016637 | ||
![]() |
1c7d91b643 | ||
![]() |
da79516766 | ||
![]() |
441c17de3c | ||
![]() |
5a439cb932 | ||
![]() |
7344002a3a | ||
![]() |
4b48914996 | ||
![]() |
a104d0b6b3 | ||
![]() |
a6f39ae72a | ||
![]() |
2c94e1bc69 | ||
![]() |
b3afd2b87f | ||
![]() |
dff71bcace | ||
![]() |
95aa1fa7bf | ||
![]() |
3be1dc4181 | ||
![]() |
e3f28a6a14 | ||
![]() |
bbb8f87cec | ||
![]() |
327c5cfb1a | ||
![]() |
b57a66f0cf | ||
![]() |
55ba708c61 | ||
![]() |
2d59bbf92e | ||
![]() |
f4e5ba2b5f | ||
![]() |
8aaf85b610 | ||
![]() |
3891d8fced | ||
![]() |
0cf80df852 | ||
![]() |
3b2dadd87a | ||
![]() |
46e402e452 | ||
![]() |
2cd5513c48 | ||
![]() |
16d62d4bc2 | ||
![]() |
e42fc8d9f0 | ||
![]() |
5c39cdbddf | ||
![]() |
e096fa6965 | ||
![]() |
9cfd336e7f | ||
![]() |
427e6790d4 | ||
![]() |
fed15d3243 | ||
![]() |
3be22c5961 | ||
![]() |
863a7e50c7 | ||
![]() |
d51ac30d0c | ||
![]() |
3ddc2c0940 | ||
![]() |
9bf55098a1 | ||
![]() |
c15e89a2d2 | ||
![]() |
5de4b88f1a | ||
![]() |
d1acaf15a3 | ||
![]() |
ec9e3704d8 | ||
![]() |
5e40fc4b3e | ||
![]() |
71dda154a5 | ||
![]() |
c84ac5938f | ||
![]() |
568ff292f5 | ||
![]() |
ba2f6a0461 | ||
![]() |
537b51d879 | ||
![]() |
304a1d720f | ||
![]() |
dd4cca2680 | ||
![]() |
bc3cbca43c | ||
![]() |
e045a4c481 |
147 changed files with 4735 additions and 654 deletions
|
@ -4,6 +4,9 @@
|
|||
#
|
||||
AddDefaultCharset UTF-8
|
||||
|
||||
#Options +FollowSymLinks # For extensions with symlinks
|
||||
##Options -FollowSymLinks +SymLinksIfOwnerMatch # or this (more security(?), more checks(!!!))
|
||||
|
||||
<IfModule mod_autoindex.c>
|
||||
Options -Indexes
|
||||
</IfModule>
|
||||
|
|
27
.gitignore
vendored
27
.gitignore
vendored
|
@ -1,17 +1,32 @@
|
|||
/_*
|
||||
/.htaccess
|
||||
/index.php
|
||||
/app/config/main.php
|
||||
/app/config/_*
|
||||
/app/config/db/*
|
||||
/app/cache/**/*.php
|
||||
/app/cache/**/*.lock
|
||||
/app/cache/**/*.tmp
|
||||
/app/config/ext/*
|
||||
/app/cache/*
|
||||
/app/log/*
|
||||
/public/img/avatars/*
|
||||
/public/img/og/*
|
||||
/ext/*
|
||||
/public/.htaccess
|
||||
/public/index.php
|
||||
/public/img/*
|
||||
/public/style/*
|
||||
/public/upload/**/*
|
||||
/public/style/ForkBB_old/*
|
||||
!/public/img/sm/big_smile.png
|
||||
!/public/img/sm/cool.png
|
||||
!/public/img/sm/hmm.png
|
||||
!/public/img/sm/lol.png
|
||||
!/public/img/sm/mad.png
|
||||
!/public/img/sm/neutral.png
|
||||
!/public/img/sm/roll.png
|
||||
!/public/img/sm/sad.png
|
||||
!/public/img/sm/smile.png
|
||||
!/public/img/sm/tongue.png
|
||||
!/public/img/sm/wink.png
|
||||
!/public/img/sm/yikes.png
|
||||
!/public/style/font/*
|
||||
!/public/style/ForkBB/*
|
||||
!/public/style/sc/*
|
||||
!/public/upload/index.html
|
||||
!.gitkeep
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017-2023 Visman (mio.visman@yandex.ru)
|
||||
Copyright (c) 2017-2023 Visman (mio.visman@yandex.ru, https://github.com/MioVisman)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
@ -46,7 +46,6 @@ class Primary
|
|||
$confChange = [
|
||||
'multiple' => [
|
||||
'CtrlRouting' => \ForkBB\Controllers\Update::class,
|
||||
|
||||
'AdminUpdate' => \ForkBB\Models\Pages\Admin\Update::class,
|
||||
],
|
||||
];
|
||||
|
|
|
@ -29,6 +29,13 @@ class Routing
|
|||
$config = $this->c->config;
|
||||
$r = $this->c->Router;
|
||||
|
||||
$r->add(
|
||||
$r::GET,
|
||||
'/sitemap{id:\d*}.xml',
|
||||
'Sitemap:view',
|
||||
'Sitemap'
|
||||
);
|
||||
|
||||
// регистрация/вход/выход
|
||||
if ($user->isGuest) {
|
||||
// вход
|
||||
|
@ -98,8 +105,8 @@ class Routing
|
|||
|
||||
// OAuth
|
||||
if (
|
||||
$user->isAdmin
|
||||
|| 1 === $config->b_oauth_allow
|
||||
1 === $config->b_oauth_allow
|
||||
|| $user->isAdmin
|
||||
) {
|
||||
$r->add(
|
||||
$r::GET,
|
||||
|
@ -399,18 +406,30 @@ class Routing
|
|||
'Topic:viewPost',
|
||||
'ViewPost'
|
||||
);
|
||||
$r->add(
|
||||
$r::DUO,
|
||||
'/post/{id|i:[1-9]\d*}/edit',
|
||||
'Edit:edit',
|
||||
'EditPost'
|
||||
);
|
||||
$r->add(
|
||||
$r::DUO,
|
||||
'/post/{id|i:[1-9]\d*}/delete',
|
||||
'Delete:delete',
|
||||
'DeletePost'
|
||||
);
|
||||
|
||||
if (! $user->isGuest) {
|
||||
$r->add(
|
||||
$r::DUO,
|
||||
'/post/{id|i:[1-9]\d*}/edit',
|
||||
'Edit:edit',
|
||||
'EditPost'
|
||||
);
|
||||
$r->add(
|
||||
$r::DUO,
|
||||
'/post/{id|i:[1-9]\d*}/delete',
|
||||
'Delete:delete',
|
||||
'DeletePost'
|
||||
);
|
||||
}
|
||||
|
||||
if ($user->isAdmin) {
|
||||
$r->add(
|
||||
$r::DUO,
|
||||
'/post/{id|i:[1-9]\d*}/change',
|
||||
'Edit:change',
|
||||
'ChangeAnD'
|
||||
);
|
||||
}
|
||||
|
||||
// сигналы (репорты)
|
||||
if (
|
||||
|
@ -620,7 +639,6 @@ class Routing
|
|||
'Moderate:action',
|
||||
'Moderate'
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// только админ
|
||||
|
@ -829,6 +847,18 @@ class Routing
|
|||
'AdminAntispam:view',
|
||||
'AdminAntispam'
|
||||
);
|
||||
$r->add(
|
||||
$r::GET,
|
||||
'/admin/extensions',
|
||||
'AdminExtensions:info',
|
||||
'AdminExtensions'
|
||||
);
|
||||
$r->add(
|
||||
$r::PST,
|
||||
'/admin/extensions/action',
|
||||
'AdminExtensions:action',
|
||||
'AdminExtensionsAction'
|
||||
);
|
||||
}
|
||||
|
||||
$uri = $_SERVER['REQUEST_URI'];
|
||||
|
|
|
@ -16,6 +16,7 @@ use Psr\SimpleCache\InvalidArgumentException;
|
|||
use DateInterval;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use FilesystemIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use RegexIterator;
|
||||
|
@ -117,8 +118,9 @@ class FileCache implements CacheInterface
|
|||
*/
|
||||
public function clear(): bool
|
||||
{
|
||||
$dir = new RecursiveDirectoryIterator($this->cacheDir, RecursiveDirectoryIterator::SKIP_DOTS);
|
||||
$iterator = new RecursiveIteratorIterator($dir);
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($this->cacheDir, FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
$files = new RegexIterator($iterator, '%\.(?:php|tmp)$%i', RegexIterator::MATCH);
|
||||
$result = true;
|
||||
|
||||
|
@ -224,8 +226,6 @@ class FileCache implements CacheInterface
|
|||
{
|
||||
if (\function_exists('\\opcache_invalidate')) {
|
||||
\opcache_invalidate($file, true);
|
||||
} elseif (\function_exists('\\apc_delete_file')) {
|
||||
\apc_delete_file($file);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -172,6 +172,7 @@ class Config
|
|||
switch ($type) {
|
||||
case 'ZERO':
|
||||
$type = 'NEW';
|
||||
|
||||
break;
|
||||
case 'NEW':
|
||||
case '=>':
|
||||
|
@ -180,6 +181,7 @@ class Config
|
|||
$value_before = $other;
|
||||
$other = '';
|
||||
$type = 'VALUE';
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new ForkException('Config array cannot be parsed (3)');
|
||||
|
@ -219,6 +221,7 @@ class Config
|
|||
case 'VALUE':
|
||||
case 'VALUE_OR_KEY':
|
||||
$type = 'NEW';
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new ForkException('Config array cannot be parsed (6)');
|
||||
|
@ -234,6 +237,7 @@ class Config
|
|||
$value = null;
|
||||
$value_before = '';
|
||||
$type = '=>';
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new ForkException('Config array cannot be parsed (7)');
|
||||
|
@ -251,7 +255,8 @@ class Config
|
|||
case 'VALUE_OR_KEY':
|
||||
case 'VALUE':
|
||||
case '=>':
|
||||
$other .= $token;
|
||||
$other .= $token;
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new ForkException('Config array cannot be parsed (8)');
|
||||
|
@ -291,9 +296,11 @@ class Config
|
|||
}
|
||||
|
||||
$type = 'VALUE_OR_KEY';
|
||||
|
||||
break;
|
||||
case '=>':
|
||||
$type = 'VALUE';
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new ForkException('Config array cannot be parsed (10)');
|
||||
|
@ -311,11 +318,11 @@ class Config
|
|||
protected function isFormat(mixed $data): bool
|
||||
{
|
||||
return \is_array($data)
|
||||
&& \array_key_exists('value', $data)
|
||||
&& \array_key_exists('value_before', $data)
|
||||
&& \array_key_exists('value_after', $data)
|
||||
&& \array_key_exists('key_before', $data)
|
||||
&& \array_key_exists('key_after', $data);
|
||||
&& \array_key_exists('value', $data)
|
||||
&& \array_key_exists('value_before', $data)
|
||||
&& \array_key_exists('value_after', $data)
|
||||
&& \array_key_exists('key_before', $data)
|
||||
&& \array_key_exists('key_after', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -429,6 +436,7 @@ class Config
|
|||
return false;
|
||||
} else {
|
||||
$result = $config[$key];
|
||||
|
||||
unset($config[$key]);
|
||||
|
||||
return $result;
|
||||
|
|
|
@ -226,10 +226,12 @@ class DB
|
|||
case 's':
|
||||
case 'f':
|
||||
$value = [1];
|
||||
|
||||
break;
|
||||
default:
|
||||
$value = [1];
|
||||
$type = 's';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -67,8 +67,8 @@ class ErrorHandler
|
|||
\set_error_handler([$this, 'errorHandler'], \E_ALL);
|
||||
\set_exception_handler([$this, 'exceptionHandler']);
|
||||
\register_shutdown_function([$this, 'shutdownHandler']);
|
||||
|
||||
\ob_start();
|
||||
|
||||
$this->obLevel = \ob_get_level();
|
||||
}
|
||||
|
||||
|
@ -198,6 +198,7 @@ class ErrorHandler
|
|||
if (isset($error['exception'])) {
|
||||
$context['exception'] = $error['exception'];
|
||||
}
|
||||
|
||||
$context['headers'] = false;
|
||||
|
||||
$this->c->Log->{$method}($this->message($error), $context);
|
||||
|
@ -279,15 +280,19 @@ EOT;
|
|||
switch ($type) {
|
||||
case 'boolean':
|
||||
$type = $arg ? 'true' : 'false';
|
||||
|
||||
break;
|
||||
case 'array':
|
||||
$type .= '(' . \count($arg) . ')';
|
||||
|
||||
break;
|
||||
case 'resource':
|
||||
$type = \get_resource_type($arg);
|
||||
|
||||
break;
|
||||
case 'object':
|
||||
$type .= '{' . \get_class($arg) . '}';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -296,8 +301,8 @@ EOT;
|
|||
}
|
||||
}
|
||||
$line .= ')';
|
||||
$line = $this->e(\str_replace($this->hidePath, '...', $line));
|
||||
|
||||
$line = $this->e(\str_replace($this->hidePath, '...', $line));
|
||||
echo "<li>{$line}</li>";
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ use ForkBB\Core\File;
|
|||
use ForkBB\Core\Image;
|
||||
use ForkBB\Core\Image\DefaultDriver;
|
||||
use ForkBB\Core\Exceptions\FileException;
|
||||
use Transliterator;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
|
@ -55,6 +56,11 @@ class Files
|
|||
*/
|
||||
protected array $tmpFiles = [];
|
||||
|
||||
/**
|
||||
* Для кэширования транслитератора
|
||||
*/
|
||||
protected Transliterator|false|null $transl = null;
|
||||
|
||||
/**
|
||||
* Список mime типов считающихся картинками
|
||||
*/
|
||||
|
@ -971,18 +977,17 @@ class Files
|
|||
*/
|
||||
public function filterName(string $name): string
|
||||
{
|
||||
$name = \transliterator_transliterate(
|
||||
"Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; Lower();",
|
||||
$name
|
||||
);
|
||||
|
||||
$name = \trim(\preg_replace(['%[^\w-]+%', '%_+%'], ['-', '_'], $name), '-_');
|
||||
|
||||
if (! isset($name[0])) {
|
||||
$name = (string) \time();
|
||||
if (null === $this->transl) {
|
||||
$this->transl = Transliterator::create('Any-Latin;Latin-ASCII;Lower();') ?? false;
|
||||
}
|
||||
|
||||
return $name;
|
||||
if ($this->transl instanceof Transliterator) {
|
||||
$name = $this->transl->transliterate($name);
|
||||
}
|
||||
|
||||
$name = \trim(\preg_replace(['%[^\w]+%', '%_+%'], ['-', '_'], $name), '-_');
|
||||
|
||||
return isset($name[0]) ? $name : (string) \time();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1095,32 +1100,16 @@ class Files
|
|||
protected function uploadFile(array $file, bool $isUploaded = true): ?File
|
||||
{
|
||||
if (\UPLOAD_ERR_OK !== $file['error']) {
|
||||
switch ($file['error']) {
|
||||
case \UPLOAD_ERR_INI_SIZE:
|
||||
$this->error = 'The uploaded file exceeds the upload_max_filesize';
|
||||
break;
|
||||
case \UPLOAD_ERR_FORM_SIZE:
|
||||
$this->error = 'The uploaded file exceeds the MAX_FILE_SIZE';
|
||||
break;
|
||||
case \UPLOAD_ERR_PARTIAL:
|
||||
$this->error = 'The uploaded file was only partially uploaded';
|
||||
break;
|
||||
case \UPLOAD_ERR_NO_FILE:
|
||||
$this->error = 'No file was uploaded';
|
||||
break;
|
||||
case \UPLOAD_ERR_NO_TMP_DIR:
|
||||
$this->error = 'Missing a temporary folder';
|
||||
break;
|
||||
case \UPLOAD_ERR_CANT_WRITE:
|
||||
$this->error = 'Failed to write file to disk';
|
||||
break;
|
||||
case \UPLOAD_ERR_EXTENSION:
|
||||
$this->error = 'A PHP extension stopped the file upload';
|
||||
break;
|
||||
default:
|
||||
$this->error = 'Unknown upload error';
|
||||
break;
|
||||
}
|
||||
$this->error = match ($file['error']) {
|
||||
\UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize',
|
||||
\UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE',
|
||||
\UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded',
|
||||
\UPLOAD_ERR_NO_FILE => 'No file was uploaded',
|
||||
\UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder',
|
||||
\UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
|
||||
\UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload',
|
||||
default => 'Unknown upload error',
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -1148,16 +1137,22 @@ class Files
|
|||
$ext = \mb_strtolower(\substr($file['name'], $pos + 1), 'UTF-8');
|
||||
}
|
||||
|
||||
$imageExt = $this->imageExt($file['tmp_name']);
|
||||
$mimeType = $this->mimeType($file['tmp_name']);
|
||||
|
||||
if (\is_string($imageExt)) {
|
||||
if (! isset($this->mimeToExt[$mimeType])) {
|
||||
$this->error = "Unknown mime type of the file: {$mimeType}";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($this->imageType[$mimeType])) {
|
||||
if ($file['size'] > $this->maxImgSize) {
|
||||
$this->error = 'The image too large';
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$ext = $imageExt;
|
||||
$ext = $this->imageType[$mimeType];
|
||||
$className = Image::class;
|
||||
} else {
|
||||
if ($file['size'] > $this->maxFileSize) {
|
||||
|
@ -1169,14 +1164,6 @@ class Files
|
|||
$className = File::class;
|
||||
}
|
||||
|
||||
$mimeType = $this->mimeType($file['tmp_name']);
|
||||
|
||||
if (! isset($this->mimeToExt[$mimeType])) {
|
||||
$this->error = "Unknown mime type of the file: {$mimeType}";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$realExt = $this->mimeToExt[$mimeType];
|
||||
|
||||
if (false === \strpos("/{$realExt}/", "/{$ext}/")) {
|
||||
|
@ -1201,21 +1188,32 @@ class Files
|
|||
}
|
||||
|
||||
/**
|
||||
* Получает файл по внешней ссылке
|
||||
* Получает файл по внешней ссылке или из строки data:...;base64,...
|
||||
*/
|
||||
public function uploadFromLink(string $url): ?File
|
||||
{
|
||||
$cmpn = \parse_url($url);
|
||||
if (\preg_match('%^data:(.*?);base64,%', $url, $matches)) {
|
||||
$name = '';
|
||||
$type = $matches[1];
|
||||
$offset = \strlen($matches[0]);
|
||||
} else {
|
||||
$cmpn = \parse_url($url);
|
||||
|
||||
if (
|
||||
! isset($cmpn['scheme'], $cmpn['host'], $cmpn['path'])
|
||||
|| ! \in_array($cmpn['scheme'], ['https', 'http'], true)
|
||||
) {
|
||||
$this->error = 'Bad url';
|
||||
if (
|
||||
! isset($cmpn['scheme'], $cmpn['host'], $cmpn['path'])
|
||||
|| ! \in_array($cmpn['scheme'], ['https', 'http'], true)
|
||||
) {
|
||||
$this->error = 'Bad url';
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = \basename($cmpn['path']) ?: '';
|
||||
$type = '';
|
||||
$offset = 0;
|
||||
}
|
||||
|
||||
|
||||
$tmpName = $this->c->DIR_CACHE . '/' . $this->c->Secury->randomPass(32) . '.tmp';
|
||||
|
||||
$this->addTmpFile($tmpName);
|
||||
|
@ -1230,7 +1228,21 @@ class Files
|
|||
|
||||
$result = null;
|
||||
|
||||
if (\extension_loaded('curl')) {
|
||||
if ($offset > 0) {
|
||||
$content = \base64_decode(\substr($url, $offset), true);
|
||||
|
||||
if (false === $content) {
|
||||
$this->error = 'Bad base64';
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = (bool) @\fwrite($tmpFile, $content);
|
||||
|
||||
if (false === $result) {
|
||||
$this->error = "Failed fwrite() to temp file";
|
||||
}
|
||||
} elseif (\extension_loaded('curl')) {
|
||||
$result = $this->curlAction($url, $tmpFile);
|
||||
} elseif (\filter_var(\ini_get('allow_url_fopen'), \FILTER_VALIDATE_BOOL)) {
|
||||
$result = $this->streamAction($url, $tmpFile);
|
||||
|
@ -1242,8 +1254,8 @@ class Files
|
|||
return $this->uploadFile(
|
||||
[
|
||||
'tmp_name' => $tmpName,
|
||||
'name' => \basename($cmpn['path']) ?: '',
|
||||
'type' => '',
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'error' => \UPLOAD_ERR_OK,
|
||||
'size' => \filesize($tmpName),
|
||||
],
|
||||
|
@ -1259,7 +1271,7 @@ class Files
|
|||
/**
|
||||
* Переменные конфига подключения
|
||||
*/
|
||||
protected int $actMaxRedir = 10;
|
||||
protected int $actMaxRedir = 5;
|
||||
protected float $actTimeout = 15.0;
|
||||
protected string $actUAgent = 'ForkBB downloader (%s)';
|
||||
protected array $actHeader = [
|
||||
|
@ -1279,6 +1291,8 @@ class Files
|
|||
return false;
|
||||
}
|
||||
|
||||
\curl_setopt($ch, \CURLOPT_PROTOCOLS, \CURLPROTO_HTTPS | \CURLPROTO_HTTP);
|
||||
\curl_setopt($ch, \CURLOPT_REDIR_PROTOCOLS, \CURLPROTO_HTTPS);
|
||||
\curl_setopt($ch, \CURLOPT_HTTPGET, true);
|
||||
\curl_setopt($ch, \CURLOPT_HEADER, false);
|
||||
\curl_setopt($ch, \CURLOPT_HTTPHEADER, $this->actHeader);
|
||||
|
|
|
@ -11,6 +11,9 @@ declare(strict_types=1);
|
|||
namespace ForkBB\Core;
|
||||
|
||||
use ForkBB\Core\Container;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Transliterator;
|
||||
use function \ForkBB\__;
|
||||
|
||||
class Func
|
||||
|
@ -30,8 +33,29 @@ class Func
|
|||
*/
|
||||
protected ?array $nameLangs = null;
|
||||
|
||||
/**
|
||||
* Смещение времени для текущего пользователя
|
||||
*/
|
||||
protected ?int $offset = null;
|
||||
|
||||
/**
|
||||
* Копия $this->c->FRIENDLY_URL
|
||||
*/
|
||||
protected array $fUrl;
|
||||
|
||||
/**
|
||||
* Для кэширования транслитератора
|
||||
*/
|
||||
protected Transliterator|false|null $transl = null;
|
||||
|
||||
/**
|
||||
* Массив подмены символов перед/вместо траслитератор(ом/а)
|
||||
*/
|
||||
protected ?array $translArray = null;
|
||||
|
||||
public function __construct(protected Container $c)
|
||||
{
|
||||
$this->fUrl = $c->isInit('FRIENDLY_URL') ? $c->FRIENDLY_URL : [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -254,4 +278,80 @@ class Func
|
|||
|
||||
return \array_keys($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает смещение в секундах для часовой зоны текущего пользователя или 0
|
||||
*/
|
||||
public function offset(): int
|
||||
{
|
||||
if (null !== $this->offset) {
|
||||
return $this->offset;
|
||||
} elseif (\in_array($this->c->user->timezone, DateTimeZone::listIdentifiers(), true)) {
|
||||
$dateTimeZone = new DateTimeZone($this->c->user->timezone);
|
||||
$dateTime = new DateTime('now', $dateTimeZone);
|
||||
|
||||
return $this->offset = $dateTime->getOffset();
|
||||
} else {
|
||||
return $this->offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Переводит метку времени в дату-время с учетом/без учета часового пояса пользователя
|
||||
*/
|
||||
public function timeToDate(int $timestamp, bool $useOffset = true): string
|
||||
{
|
||||
return \gmdate('Y-m-d\TH:i:s', $timestamp + ($useOffset ? $this->offset() : 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Переводит дату-время в метку времени с учетом/без учета часового пояса пользователя
|
||||
*/
|
||||
public function dateToTime(string $date, bool $useOffset = true): int|false
|
||||
{
|
||||
$timestamp = \strtotime("{$date} UTC");
|
||||
|
||||
if (! \is_int($timestamp)) {
|
||||
return false;
|
||||
} elseif ($useOffset) {
|
||||
return $timestamp - $this->offset();
|
||||
} else {
|
||||
return $timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует строку в соотвествии с правилами FRIENDLY_URL
|
||||
*/
|
||||
public function friendly(string $str): string
|
||||
{
|
||||
if (! empty($this->fUrl['translit'])) {
|
||||
if (! empty($this->fUrl['file'])) {
|
||||
$this->translArray ??= include "{$this->c->DIR_CONFIG}/{$this->fUrl['file']}";
|
||||
|
||||
$str = \strtr($str, $this->translArray);
|
||||
}
|
||||
|
||||
if (
|
||||
\is_string($this->fUrl['translit'])
|
||||
&& \preg_match('%[\x80-\xFF]%', $str)
|
||||
) {
|
||||
$this->transl ??= Transliterator::create($this->fUrl['translit']) ?? false;
|
||||
|
||||
if ($this->transl instanceof Transliterator) {
|
||||
$str = $this->transl->transliterate($str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (true === $this->fUrl['lowercase']) {
|
||||
$str = \mb_strtolower($str, 'UTF-8');
|
||||
}
|
||||
|
||||
if (true === $this->fUrl['WtoHyphen']) {
|
||||
$str = \trim(\preg_replace(['%[^\w]+%u', '%_+%'], ['-', '_'], $str), '-_');
|
||||
}
|
||||
|
||||
return isset($str[0]) ? $str : '-';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||
namespace ForkBB\Core;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use FilesystemIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use RegexIterator;
|
||||
|
@ -76,8 +77,9 @@ class LogViewer
|
|||
|
||||
protected function getFileList(): array
|
||||
{
|
||||
$dir = new RecursiveDirectoryIterator($this->dir, RecursiveDirectoryIterator::SKIP_DOTS);
|
||||
$iterator = new RecursiveIteratorIterator($dir);
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($this->dir, FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
$files = new RegexIterator($iterator, $this->namePattern, RegexIterator::MATCH);
|
||||
$result = [];
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ class Mail
|
|||
|
||||
public function __construct(string $host, string $user, #[SensitiveParameter] string $pass, int $ssl, string $eol, protected Container $c)
|
||||
{
|
||||
if ('' != $host) {
|
||||
if ('' !== $host) {
|
||||
$hp = \explode(':', $host, 2);
|
||||
|
||||
if (
|
||||
|
@ -103,11 +103,10 @@ class Mail
|
|||
return false;
|
||||
}
|
||||
|
||||
|
||||
if ($strict) {
|
||||
if (true === $strict) {
|
||||
$level = $this->c->ErrorHandler->logOnly(\E_WARNING);
|
||||
|
||||
if ($ip) {
|
||||
if (\is_string($ip)) {
|
||||
$mx = \checkdnsrr($ip, 'MX'); // ipv6 в пролёте :(
|
||||
} else {
|
||||
$mx = \dns_get_record($domainASCII, \DNS_MX);
|
||||
|
|
|
@ -149,7 +149,9 @@ class Router
|
|||
'page' !== $name
|
||||
|| 1 !== $args[$name]
|
||||
) {
|
||||
$data['{' . $name . '}'] = \rawurlencode(\str_replace($this->subSearch, $this->subRepl, (string) $args[$name]));
|
||||
$data['{' . $name . '}'] = \is_integer($args[$name])
|
||||
? (string) $args[$name]
|
||||
: \rawurlencode(\str_replace($this->subSearch, $this->subRepl, (string) $args[$name]));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
@ -232,7 +234,12 @@ class Router
|
|||
}
|
||||
|
||||
$pos = \strpos($uri, '/', 1);
|
||||
$base = false === $pos ? $uri : \substr($uri, 0, $pos);
|
||||
|
||||
if (false === $pos) {
|
||||
$base = isset($this->dynamic[$uri]) ? $uri : '/';
|
||||
} else {
|
||||
$base = \substr($uri, 0, $pos);
|
||||
}
|
||||
|
||||
if (isset($this->dynamic[$base])) {
|
||||
foreach ($this->dynamic[$base] as $pattern => $data) {
|
||||
|
|
|
@ -140,13 +140,7 @@ class Validator
|
|||
public function addRules(array $list): Validator
|
||||
{
|
||||
foreach ($list as $field => $raw) {
|
||||
$rules = [];
|
||||
$suffix = null;
|
||||
|
||||
// правило для элементов массива
|
||||
if (\strpos($field, '.') > 0) {
|
||||
list($field, $suffix) = \explode('.', $field, 2);
|
||||
}
|
||||
$rules = [];
|
||||
|
||||
if (! \is_array($raw)) {
|
||||
$raw = \explode('|', $raw);
|
||||
|
@ -172,23 +166,44 @@ class Validator
|
|||
}
|
||||
}
|
||||
|
||||
if (
|
||||
'array' === $name
|
||||
&& ! \is_array($rule)
|
||||
) {
|
||||
$rule = [];
|
||||
}
|
||||
|
||||
$rules[$name] = $rule ?? '';
|
||||
}
|
||||
|
||||
if (isset($suffix)) {
|
||||
if (
|
||||
isset($this->rules[$field]['array'])
|
||||
&& ! \is_array($this->rules[$field]['array'])
|
||||
) {
|
||||
$this->rules[$field]['array'] = [];
|
||||
if (\strpos($field, '.') > 0) {
|
||||
$fields = \explode('.', $field);
|
||||
$n = \count($fields);
|
||||
$start = true;
|
||||
$r = &$this->rules;
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (true === $start) {
|
||||
$this->fields[$field] = $field;
|
||||
$start = false;
|
||||
}
|
||||
|
||||
if (--$n) {
|
||||
if (! isset($r[$field]['array'])) {
|
||||
$r[$field]['array'] = [];
|
||||
}
|
||||
|
||||
$r = &$r[$field]['array'];
|
||||
} else {
|
||||
$r[$field] = $rules;
|
||||
}
|
||||
}
|
||||
|
||||
$this->rules[$field]['array'][$suffix] = $rules;
|
||||
unset ($r);
|
||||
} else {
|
||||
$this->rules[$field] = $rules;
|
||||
$this->rules[$field] = $rules;
|
||||
$this->fields[$field] = $field;
|
||||
}
|
||||
|
||||
$this->fields[$field] = $field;
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
@ -639,35 +654,32 @@ class Validator
|
|||
if ('' === $name) {
|
||||
$result = $this->checkValue($value, $rules, $field);
|
||||
} else {
|
||||
if (! \preg_match('%^([^.]+)(?:\.(.+))?$%', $name, $matches)) {
|
||||
if (false !== \strpos($name, '.')) {
|
||||
throw new RuntimeException("Bad path '{$name}'");
|
||||
}
|
||||
|
||||
$key = $matches[1];
|
||||
$name = $matches[2] ?? '';
|
||||
|
||||
if (
|
||||
'*' === $key
|
||||
'*' === $name
|
||||
&& \is_array($value)
|
||||
) {
|
||||
foreach ($value as $i => $cur) {
|
||||
$this->recArray($value[$i], $result[$i], $name, $rules, $field);
|
||||
$this->recArray($value[$i], $result[$i], '', $rules, $field);
|
||||
}
|
||||
} elseif (
|
||||
'*' !== $key
|
||||
'*' !== $name
|
||||
&& \is_array($value)
|
||||
&& \array_key_exists($key, $value)
|
||||
&& \array_key_exists($name, $value)
|
||||
) {
|
||||
$this->recArray($value[$key], $result[$key], $name, $rules, $field);
|
||||
$this->recArray($value[$name], $result[$name], '', $rules, $field);
|
||||
} elseif (isset($rules['required'])) {
|
||||
$tmp1 = null;
|
||||
$tmp2 = null;
|
||||
$this->recArray($tmp1, $tmp2, $name, $rules, $field);
|
||||
} elseif ('*' === $key) {
|
||||
$this->recArray($tmp1, $tmp2, '', $rules, $field);
|
||||
} elseif ('*' === $name) {
|
||||
$result = []; // ???? а может там не отсутствие элемента, а не array?
|
||||
} else {
|
||||
$value[$key] = null;
|
||||
$this->recArray($value[$key], $result[$key], $name, $rules, $field);
|
||||
$value[$name] = null;
|
||||
$this->recArray($value[$name], $result[$name], '', $rules, $field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -956,16 +968,21 @@ class Validator
|
|||
protected function vDate(Validator $v, mixed $value): ?string
|
||||
{
|
||||
if ($this->noValue($value)) {
|
||||
$value = null;
|
||||
} elseif (
|
||||
! \is_string($value)
|
||||
|| false === \strtotime("{$value} UTC")
|
||||
) {
|
||||
$v->addError('The :alias does not contain a date');
|
||||
|
||||
$value = \is_scalar($value) ? (string) $value : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value;
|
||||
if (\is_string($value)) {
|
||||
$timestamp = $this->c->Func->dateToTime($value);
|
||||
} else {
|
||||
$timestamp = false;
|
||||
}
|
||||
|
||||
if (false === $timestamp) {
|
||||
$v->addError('The :alias does not contain a date');
|
||||
} elseif ($timestamp < 0) {
|
||||
$v->addError('The :alias contains time before start of Unix');
|
||||
}
|
||||
|
||||
return \is_scalar($value) ? (string) $value : null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,11 +23,12 @@ use RuntimeException;
|
|||
class View
|
||||
{
|
||||
protected string $ext = '.forkbb.php';
|
||||
protected string $preFile = '';
|
||||
|
||||
protected ?Compiler $compilerObj;
|
||||
protected string $compilerClass = Compiler::class;
|
||||
|
||||
protected string $cache;
|
||||
protected string $cacheDir;
|
||||
protected string $defaultDir;
|
||||
protected string $defaultHash;
|
||||
|
||||
|
@ -40,11 +41,11 @@ class View
|
|||
public function __construct(string|array $config, mixed $views)
|
||||
{
|
||||
if (\is_array($config)) {
|
||||
$this->cache = $config['cache'];
|
||||
$this->cacheDir = $config['cache'];
|
||||
$this->defaultDir = $config['defaultDir'];
|
||||
|
||||
if (! empty($config['userDir'])) {
|
||||
$this->other[\hash('md5', $config['userDir'])] = [$config['userDir'], 10];
|
||||
$this->addTplDir($config['userDir'], 10);
|
||||
}
|
||||
|
||||
if (! empty($config['composers'])) {
|
||||
|
@ -56,15 +57,36 @@ class View
|
|||
if (! empty($config['compiler'])) {
|
||||
$this->compilerClass = $config['compiler'];
|
||||
}
|
||||
|
||||
if (! empty($config['preFile'])) {
|
||||
$this->preFile = $config['preFile'];
|
||||
}
|
||||
} else {
|
||||
// для rev. 68 и ниже
|
||||
$this->cache = $config;
|
||||
$this->defaultDir = $views;
|
||||
$this->cacheDir = $config;
|
||||
$this->defaultDir = $views;
|
||||
}
|
||||
|
||||
$this->defaultHash = \hash('md5', $this->defaultDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавляет новый каталог шаблонов $pathToDir.
|
||||
* Сортирует список каталогов в соответствии с приоритетом $priority. По убыванию.
|
||||
*/
|
||||
public function addTplDir(string $pathToDir, int $priority): View
|
||||
{
|
||||
$this->other[\hash('md5', $pathToDir)] = [$pathToDir, $priority];
|
||||
|
||||
if (\count($this->other) > 1) {
|
||||
\uasort($this->other, function (array $a, array $b) {
|
||||
return $b[1] <=> $a[1];
|
||||
});
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает отображение страницы $p или null
|
||||
*/
|
||||
|
@ -171,13 +193,13 @@ class View
|
|||
|
||||
foreach ($this->other as $hash => $cur) {
|
||||
if (\file_exists($tpl = "{$cur[0]}/{$name}{$this->ext}")) {
|
||||
$php = "{$this->cache}/_{$st}-{$hash}.php";
|
||||
$php = "{$this->cacheDir}/_{$st}-{$hash}.php";
|
||||
|
||||
if (
|
||||
! \file_exists($php)
|
||||
|| \filemtime($tpl) > \filemtime($php)
|
||||
) {
|
||||
$this->create($php, $tpl);
|
||||
$this->create($php, $tpl, $name);
|
||||
}
|
||||
|
||||
return $php;
|
||||
|
@ -186,28 +208,38 @@ class View
|
|||
|
||||
$hash = $this->defaultHash;
|
||||
$tpl = "{$this->defaultDir}/{$name}{$this->ext}";
|
||||
$php = "{$this->cache}/_{$st}-{$hash}.php";
|
||||
$php = "{$this->cacheDir}/_{$st}-{$hash}.php";
|
||||
|
||||
if (
|
||||
! \file_exists($php)
|
||||
|| \filemtime($tpl) > \filemtime($php)
|
||||
) {
|
||||
$this->create($php, $tpl);
|
||||
$this->create($php, $tpl, $name);
|
||||
}
|
||||
|
||||
return $php;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет файлы кэша для шаблона $name
|
||||
*/
|
||||
public function delete(string $name): void
|
||||
{
|
||||
$st = \preg_replace('%\W%', '-', $name);
|
||||
|
||||
\array_map('\\unlink', \glob("{$this->cacheDir}/_{$st}-*.php"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует $php файл на основе шаблона $tpl
|
||||
*/
|
||||
protected function create(string $php, string $tpl): void
|
||||
protected function create(string $php, string $tpl, string $name): void
|
||||
{
|
||||
if (empty($this->compilerObj)) {
|
||||
$this->compilerObj = new $this->compilerClass();
|
||||
$this->compilerObj = new $this->compilerClass($this->preFile);
|
||||
}
|
||||
|
||||
$text = $this->compilerObj->create(\file_get_contents($tpl), \hash('fnv1a32', $tpl));
|
||||
$text = $this->compilerObj->create($name, \file_get_contents($tpl), \hash('fnv1a32', $tpl));
|
||||
|
||||
if (false === \file_put_contents($php, $text, \LOCK_EX)) {
|
||||
throw new RuntimeException("Failed to write {$php} file");
|
||||
|
|
|
@ -22,23 +22,33 @@ class Compiler
|
|||
{
|
||||
protected string $shortID;
|
||||
protected int $loopsCounter = 0;
|
||||
protected array $compilers = [
|
||||
protected array $compilers = [
|
||||
'PrePaste',
|
||||
'Statements',
|
||||
'Comments',
|
||||
'Echos',
|
||||
'Transformations',
|
||||
];
|
||||
protected array $preArray = [];
|
||||
protected string $tplName;
|
||||
|
||||
public function __construct()
|
||||
public function __construct(string $preFile)
|
||||
{
|
||||
if (
|
||||
! empty($preFile)
|
||||
&& \is_file($preFile)
|
||||
) {
|
||||
$this->preArray = include $preFile;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует php код на основе шаблона из $text
|
||||
*/
|
||||
public function create(string $text, string $hash): string
|
||||
public function create(string $name, string $text, string $hash): string
|
||||
{
|
||||
$this->shortID = $hash;
|
||||
$this->tplName = $name;
|
||||
|
||||
foreach ($this->compilers as $type) {
|
||||
$text = $this->{'compile' . $type}($text);
|
||||
|
@ -47,6 +57,26 @@ class Compiler
|
|||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает предварительную подстановку кода в шаблон
|
||||
*/
|
||||
protected function compilePrePaste(string $value): string
|
||||
{
|
||||
$pre = $this->preArray[$this->tplName] ?? null;
|
||||
|
||||
return \preg_replace_callback(
|
||||
'%^[ \t]*+<!-- PRE (\w+) -->[ \t]*(?:\r?\n)?%m',
|
||||
function($match) use ($pre) {
|
||||
if (isset($pre[$match[1]])) {
|
||||
return \rtrim($pre[$match[1]]) . "\n";
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает операторы начинающиеся с @
|
||||
*/
|
||||
|
@ -80,7 +110,7 @@ class Compiler
|
|||
{
|
||||
// {{! !}}
|
||||
$value = \preg_replace_callback(
|
||||
'%(@)?\{\{!\s*(.+?)\s*!\}\}(\r?\n)?%s',
|
||||
'%(@)?\{\{![ \t]*+(.+?)[ \t]*!\}\}(\r?\n)?%',
|
||||
function($matches) {
|
||||
$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
|
||||
|
||||
|
@ -96,7 +126,7 @@ class Compiler
|
|||
|
||||
// {!! !!}
|
||||
$value = \preg_replace_callback(
|
||||
'%\{\!!\s*(.+?)\s*!!\}(\r?\n)?%s',
|
||||
'%\{\!![ \t]*+(.+?)[ \t]*!!\}(\r?\n)?%',
|
||||
function($matches) {
|
||||
$whitespace = empty($matches[2]) ? '' : $matches[2] . $matches[2];
|
||||
|
||||
|
@ -110,7 +140,7 @@ class Compiler
|
|||
|
||||
// {{ }}
|
||||
$value = \preg_replace_callback(
|
||||
'%(@)?\{\{\s*(.+?)\s*\}\}(\r?\n)?%s',
|
||||
'%(@)?\{\{(?!!)[ \t]*+(.+?)[ \t]*\}\}(\r?\n)?%',
|
||||
function($matches) {
|
||||
$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
|
||||
|
||||
|
@ -141,7 +171,7 @@ class Compiler
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use function \ForkBB\{__, num, dt, size};
|
||||
use function \ForkBB\{__, num, dt, size, url};
|
||||
|
||||
?>
|
||||
EOD;
|
||||
|
@ -475,4 +505,20 @@ EOD;
|
|||
{
|
||||
return "<?php break; ?>";
|
||||
}
|
||||
|
||||
/**
|
||||
* @php
|
||||
*/
|
||||
protected function compilePhp(): string
|
||||
{
|
||||
return "<?php";
|
||||
}
|
||||
|
||||
/**
|
||||
* @endphp
|
||||
*/
|
||||
protected function compileEndphp(): string
|
||||
{
|
||||
return " ?>";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -182,31 +182,23 @@ class Attachments extends Model
|
|||
]);
|
||||
}
|
||||
|
||||
switch ($this->c->DB->getType()) {
|
||||
case 'mysql':
|
||||
$query = "INSERT IGNORE INTO {$table} (id, pid)
|
||||
VALUES (?i:id, ?i:pid)";
|
||||
$query = match ($this->c->DB->getType()) {
|
||||
'mysql' => "INSERT IGNORE INTO {$table} (id, pid)
|
||||
VALUES (?i:id, ?i:pid)",
|
||||
|
||||
break;
|
||||
case 'sqlite':
|
||||
case 'pgsql':
|
||||
$query = "INSERT INTO {$table} (id, pid)
|
||||
VALUES (?i:id, ?i:pid)
|
||||
ON CONFLICT(id, pid) DO NOTHING";
|
||||
'sqlite', 'pgsql' => "INSERT INTO {$table} (id, pid)
|
||||
VALUES (?i:id, ?i:pid)
|
||||
ON CONFLICT(id, pid) DO NOTHING",
|
||||
|
||||
break;
|
||||
default:
|
||||
$query = "INSERT INTO {$table} (id, pid)
|
||||
SELECT tmp.*
|
||||
FROM (SELECT ?i:id AS f1, ?i:pid AS f2) AS tmp
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM {$table}
|
||||
WHERE id=?i:id AND pid=?i:pid
|
||||
)";
|
||||
|
||||
break;
|
||||
}
|
||||
default => "INSERT INTO {$table} (id, pid)
|
||||
SELECT tmp.*
|
||||
FROM (SELECT ?i:id AS f1, ?i:pid AS f2) AS tmp
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM {$table}
|
||||
WHERE id=?i:id AND pid=?i:pid
|
||||
)",
|
||||
};
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$vars = [
|
||||
|
|
|
@ -66,8 +66,6 @@ class BBCodeList extends Model
|
|||
{
|
||||
if (\function_exists('\\opcache_invalidate')) {
|
||||
\opcache_invalidate($this->fileCache, true);
|
||||
} elseif (\function_exists('\\apc_delete_file')) {
|
||||
\apc_delete_file($this->fileCache);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
|
|
@ -41,11 +41,6 @@ class Categories extends Manager
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getList(): array
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
public function set($key, $value): Manager
|
||||
{
|
||||
if (! isset($value['cat_name'], $value['disp_position'])) {
|
||||
|
|
237
app/Models/Extension/Extension.php
Normal file
237
app/Models/Extension/Extension.php
Normal file
|
@ -0,0 +1,237 @@
|
|||
<?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\Extension;
|
||||
|
||||
use ForkBB\Models\Model;
|
||||
use RuntimeException;
|
||||
|
||||
class Extension extends Model
|
||||
{
|
||||
const NOT_INSTALLED = 0;
|
||||
const DISABLED = 4;
|
||||
const DISABLED_DOWN = 5;
|
||||
const DISABLED_UP = 6;
|
||||
const ENABLED = 8;
|
||||
const ENABLED_DOWN = 9;
|
||||
const ENABLED_UP = 10;
|
||||
const CRASH = 12;
|
||||
|
||||
/**
|
||||
* Ключ модели для контейнера
|
||||
*/
|
||||
protected string $cKey = 'Extension';
|
||||
|
||||
protected array $prepareData;
|
||||
|
||||
protected function getdispalyName(): string
|
||||
{
|
||||
return $this->dbData['extra']['display-name'] ?? $this->fileData['extra']['display-name'];
|
||||
}
|
||||
|
||||
protected function getversion(): string
|
||||
{
|
||||
return $this->dbData['version'] ?? $this->fileData['version'];
|
||||
}
|
||||
|
||||
protected function getfileVersion(): string
|
||||
{
|
||||
return $this->fileData['version'] ?? '-';
|
||||
}
|
||||
|
||||
protected function getname(): string
|
||||
{
|
||||
return $this->dbData['name'] ?? $this->fileData['name'];
|
||||
}
|
||||
|
||||
protected function getid(): string
|
||||
{
|
||||
return 'ext-' . \trim(\preg_replace('%\W+%', '-', $this->name), '-');
|
||||
}
|
||||
|
||||
protected function getdescription(): string
|
||||
{
|
||||
return $this->dbData['description'] ?? $this->fileData['description'];
|
||||
}
|
||||
|
||||
protected function gettime(): ?string
|
||||
{
|
||||
return $this->dbData['time'] ?? $this->fileData['time'];
|
||||
}
|
||||
|
||||
protected function gethomepage(): ?string
|
||||
{
|
||||
return $this->dbData['homepage'] ?? $this->fileData['homepage'];
|
||||
}
|
||||
|
||||
protected function getlicense(): ?string
|
||||
{
|
||||
return $this->dbData['license'] ?? $this->fileData['license'];
|
||||
}
|
||||
|
||||
protected function getrequirements(): array
|
||||
{
|
||||
return $this->dbData['extra']['requirements'] ?? $this->fileData['extra']['requirements'];
|
||||
}
|
||||
|
||||
protected function getauthors(): array
|
||||
{
|
||||
return $this->dbData['authors'] ?? $this->fileData['authors'];
|
||||
}
|
||||
|
||||
protected function getstatus(): int
|
||||
{
|
||||
if (null === $this->dbStatus) {
|
||||
return self::NOT_INSTALLED;
|
||||
} elseif (empty($this->fileData['version'])) {
|
||||
return self::CRASH;
|
||||
}
|
||||
|
||||
switch (
|
||||
\version_compare($this->fileData['version'], $this->dbData['version'])
|
||||
+ 4 * (1 === $this->dbStatus)
|
||||
) {
|
||||
case -1:
|
||||
return self::DISABLED_DOWN;
|
||||
case 0:
|
||||
return self::DISABLED;
|
||||
case 1:
|
||||
return self::DISABLED_UP;
|
||||
case 3:
|
||||
return self::ENABLED_DOWN;
|
||||
case 4:
|
||||
return self::ENABLED;
|
||||
case 5:
|
||||
return self::ENABLED_UP;
|
||||
default:
|
||||
throw new RuntimeException("Error in {$this->name} extension status");
|
||||
}
|
||||
}
|
||||
|
||||
protected function getcanInstall(): bool
|
||||
{
|
||||
return self::NOT_INSTALLED === $this->status;
|
||||
}
|
||||
|
||||
protected function getcanUninstall(): bool
|
||||
{
|
||||
return \in_array($this->status, [self::DISABLED, self::DISABLED_DOWN, self::DISABLED_UP], true);
|
||||
}
|
||||
|
||||
protected function getcanUpdate(): bool
|
||||
{
|
||||
return \in_array($this->status, [self::DISABLED_UP, self::ENABLED_UP], true);
|
||||
}
|
||||
|
||||
protected function getcanDowndate(): bool
|
||||
{
|
||||
return \in_array($this->status, [self::DISABLED_DOWN, self::ENABLED_DOWN], true);
|
||||
}
|
||||
|
||||
protected function getcanEnable(): bool
|
||||
{
|
||||
return self::DISABLED === $this->status;
|
||||
}
|
||||
|
||||
protected function getcanDisable(): bool
|
||||
{
|
||||
return \in_array($this->status, [self::ENABLED, self::ENABLED_DOWN, self::ENABLED_UP, self::CRASH], true);
|
||||
}
|
||||
|
||||
public function prepare(): bool|string|array
|
||||
{
|
||||
$this->prepareData = [];
|
||||
|
||||
if ($this->fileData['extra']['templates']) {
|
||||
foreach ($this->fileData['extra']['templates'] as $cur) {
|
||||
switch($cur['type']) {
|
||||
case 'pre':
|
||||
if (empty($cur['name'])) {
|
||||
return 'PRE name not found';
|
||||
} elseif (empty($cur['file'])) {
|
||||
return ['Template file \'%s\' not found', $cur['file']];
|
||||
}
|
||||
|
||||
$path = $this->fileData['path'] . '/' . \ltrim($cur['file'], '\\/');
|
||||
|
||||
if (
|
||||
$this->c->Files->isBadPath($path)
|
||||
|| ! \is_file($path)
|
||||
) {
|
||||
return ['Template file \'%s\' not found', $cur['file']];
|
||||
}
|
||||
|
||||
$data = \file_get_contents($path);
|
||||
|
||||
foreach (\explode(',', $cur['template']) as $template) {
|
||||
$this->prepareData['templates']['pre'][$template][$cur['name']][] = [
|
||||
'priority' => $cur['priority'] ?: 0,
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
return 'Invalid template type';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->fileData['extra']['symlinks']) {
|
||||
foreach ($this->fileData['extra']['symlinks'] as $cur) {
|
||||
switch($cur['type']) {
|
||||
case 'public':
|
||||
if (
|
||||
empty($cur['target'])
|
||||
|| empty($cur['link'])
|
||||
|| $this->c->Files->isBadPath($cur['target'])
|
||||
|| $this->c->Files->isBadPath($cur['link'])
|
||||
) {
|
||||
return 'Bad symlink';
|
||||
}
|
||||
|
||||
$target = $this->fileData['path'] . '/' . \trim($cur['target'], '\\/');
|
||||
|
||||
if (
|
||||
! \is_file($target)
|
||||
&& ! \is_dir($target)
|
||||
) {
|
||||
return ['Target \'%s\' not found', $cur['target']];
|
||||
}
|
||||
|
||||
$link = $this->c->DIR_PUBLIC . '/' . \trim($cur['link'], '\\/');
|
||||
|
||||
if (
|
||||
! \is_link($link)
|
||||
&& (
|
||||
\is_file($link)
|
||||
|| \is_dir($link)
|
||||
)
|
||||
) {
|
||||
return ['Link \'%s\' already exists', $cur['link']];
|
||||
}
|
||||
|
||||
$this->prepareData['symlinks'][$target] = $link;
|
||||
|
||||
break;
|
||||
default:
|
||||
return 'Invalid symlink type';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function prepareData(): array
|
||||
{
|
||||
return $this->prepareData;
|
||||
}
|
||||
}
|
605
app/Models/Extension/Extensions.php
Normal file
605
app/Models/Extension/Extensions.php
Normal file
|
@ -0,0 +1,605 @@
|
|||
<?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\Extension;
|
||||
|
||||
use ForkBB\Models\Extension\Extension;
|
||||
use ForkBB\Models\Manager;
|
||||
use FilesystemIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use RegexIterator;
|
||||
use RuntimeException;
|
||||
|
||||
class Extensions extends Manager
|
||||
{
|
||||
/**
|
||||
* Ключ модели для контейнера
|
||||
*/
|
||||
protected string $cKey = 'Extensions';
|
||||
|
||||
/**
|
||||
* Список отсканированных папок
|
||||
*/
|
||||
protected array $folders = [];
|
||||
|
||||
/**
|
||||
* Текст ошибки
|
||||
*/
|
||||
protected string|array $error = '';
|
||||
|
||||
protected string $commonFile;
|
||||
protected string $preFile;
|
||||
|
||||
/**
|
||||
* Возвращает action (или свойство) по его имени
|
||||
*/
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
return 'error' === $name ? $this->error : parent::__get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализирует менеджер
|
||||
*/
|
||||
public function init(): Extensions
|
||||
{
|
||||
$this->commonFile = $this->c->DIR_CONFIG . '/ext/common.php';
|
||||
$this->preFile = $this->c->DIR_CONFIG . '/ext/pre.php';
|
||||
|
||||
$this->fromDB();
|
||||
|
||||
$list = $this->scan($this->c->DIR_EXT);
|
||||
|
||||
$this->fromList($this->prepare($list));
|
||||
|
||||
\uasort($this->repository, function (Extension $a, Extension $b) {
|
||||
return $a->dispalyName <=> $b->dispalyName;
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает в репозиторий из БД список расширений
|
||||
*/
|
||||
protected function fromDB(): void
|
||||
{
|
||||
$query = 'SELECT ext_name, ext_status, ext_data
|
||||
FROM ::extensions
|
||||
ORDER BY ext_name';
|
||||
|
||||
$stmt = $this->c->DB->query($query);
|
||||
|
||||
while ($row = $stmt->fetch()) {
|
||||
$model = $this->c->ExtensionModel->setModelAttrs([
|
||||
'name' => $row['ext_name'],
|
||||
'dbStatus' => $row['ext_status'],
|
||||
'dbData' => \json_decode($row['ext_data'], true, 512, \JSON_THROW_ON_ERROR),
|
||||
]);
|
||||
|
||||
$this->set($row['ext_name'], $model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Заполняет массив данными из файлов composer.json
|
||||
*/
|
||||
protected function scan(string $folder, array $result = []): array
|
||||
{
|
||||
$folder = \rtrim($folder, '\\/');
|
||||
|
||||
if (
|
||||
empty($folder)
|
||||
|| ! \is_dir($folder)
|
||||
) {
|
||||
throw new RuntimeException("Not a directory: {$folder}");
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
$files = new RegexIterator($iterator, '%[\\\\/]composer\.json$%i', RegexIterator::MATCH);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$data = \file_get_contents($file->getPathname());
|
||||
|
||||
if (\is_string($data)) {
|
||||
$data = \json_decode($data, true);
|
||||
}
|
||||
|
||||
$result[$file->getPath()] = $data;
|
||||
}
|
||||
|
||||
$this->folders[] = $folder;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Подготавливает данные для моделей
|
||||
*/
|
||||
protected function prepare(array $files): array
|
||||
{
|
||||
$v = clone $this->c->Validator;
|
||||
$v = $v->reset()
|
||||
->addValidators([
|
||||
])->addRules([
|
||||
'name' => 'required|string|regex:%^[a-z0-9](?:[_.-]?[a-z0-9]+)*/[a-z0-9](?:[_.-]?[a-z0-9]+)*$%',
|
||||
'type' => 'required|string|in:forkbb-extension',
|
||||
'description' => 'required|string',
|
||||
'homepage' => 'string',
|
||||
'version' => 'required|string',
|
||||
'time' => 'string',
|
||||
'license' => 'string',
|
||||
'authors' => 'required|array',
|
||||
'authors.*.name' => 'required|string',
|
||||
'authors.*.email' => 'string',
|
||||
'authors.*.homepage' => 'string',
|
||||
'authors.*.role' => 'string',
|
||||
'autoload.psr-4' => 'array',
|
||||
'autoload.psr-4.*' => 'required|string',
|
||||
'require' => 'array',
|
||||
'extra' => 'required|array',
|
||||
'extra.display-name' => 'required|string',
|
||||
'extra.requirements' => 'array',
|
||||
'extra.symlinks' => 'array',
|
||||
'extra.symlinks.*.type' => 'required|string|in:public',
|
||||
'extra.symlinks.*.target' => 'required|string',
|
||||
'extra.symlinks.*.link' => 'required|string',
|
||||
'extra.templates' => 'array',
|
||||
'extra.templates.*.type' => 'required|string|in:pre',
|
||||
'extra.templates.*.template' => 'required|string',
|
||||
'extra.templates.*.name' => 'string',
|
||||
'extra.templates.*.priority' => 'integer',
|
||||
'extra.templates.*.file' => 'string',
|
||||
])->addAliases([
|
||||
])->addArguments([
|
||||
])->addMessages([
|
||||
]);
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($files as $path => $file) {
|
||||
$context = null;
|
||||
|
||||
if (! \is_array($file)) {
|
||||
$context = [
|
||||
'errors' => ['Bad json'],
|
||||
];
|
||||
} elseif (! $v->validation($file)) {
|
||||
$context = [
|
||||
'errors' => \array_map('\\ForkBB\__', $v->getErrorsWithoutType()),
|
||||
];
|
||||
}
|
||||
|
||||
if (null === $context) {
|
||||
$data = $v->getData(true);
|
||||
$data['path'] = $path;
|
||||
$result[$v->name] = $data;
|
||||
} else {
|
||||
$context['headers'] = false;
|
||||
$path = \preg_replace('%^.+((?:[\\\\/]+[^\\\\/]+){3})$%', '$1', $path);
|
||||
|
||||
$this->c->Log->debug("Extension: Bad structure for {$path}", $context);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Дополняет репозиторий данными из файлов composer.json
|
||||
*/
|
||||
protected function fromList(array $list): void
|
||||
{
|
||||
foreach ($list as $name => $data) {
|
||||
$model = $this->get($name);
|
||||
|
||||
if (! $model instanceof Extension) {
|
||||
$model = $this->c->ExtensionModel->setModelAttrs([
|
||||
'name' => $name,
|
||||
'fileData' => $data,
|
||||
]);
|
||||
|
||||
$this->set($name, $model);
|
||||
} else {
|
||||
$model->setModelAttr('fileData', $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает расширение
|
||||
*/
|
||||
public function install(Extension $ext): bool
|
||||
{
|
||||
if (true !== $ext->canInstall) {
|
||||
$this->error = 'Invalid action';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $ext->prepare();
|
||||
|
||||
if (true !== $result) {
|
||||
$this->error = $result;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':name' => $ext->name,
|
||||
':data' => \json_encode($ext->fileData, FORK_JSON_ENCODE),
|
||||
];
|
||||
$query = 'INSERT INTO ::extensions (ext_name, ext_status, ext_data)
|
||||
VALUES(?s:name, 1, ?s:data)';
|
||||
|
||||
$ext->setModelAttrs([
|
||||
'name' => $ext->name,
|
||||
'dbStatus' => 1,
|
||||
'dbData' => $ext->fileData,
|
||||
'fileData' => $ext->fileData,
|
||||
]);
|
||||
|
||||
if (true !== $this->updateCommon($ext)) {
|
||||
$this->error = 'An error occurred in updateCommon';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->setSymlinks($ext);
|
||||
$this->updateIndividual();
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет расширение
|
||||
*/
|
||||
public function uninstall(Extension $ext): bool
|
||||
{
|
||||
if (true !== $ext->canUninstall) {
|
||||
$this->error = 'Invalid action';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldStatus = $ext->dbStatus;
|
||||
|
||||
$vars = [
|
||||
':name' => $ext->name,
|
||||
];
|
||||
$query = 'DELETE
|
||||
FROM ::extensions
|
||||
WHERE ext_name=?s:name';
|
||||
|
||||
$ext->setModelAttrs([
|
||||
'name' => $ext->name,
|
||||
'dbStatus' => null,
|
||||
'dbData' => null,
|
||||
'fileData' => $ext->fileData,
|
||||
]);
|
||||
|
||||
$this->removeSymlinks($ext);
|
||||
|
||||
if (true !== $this->updateCommon($ext)) {
|
||||
$this->error = 'An error occurred in updateCommon';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($oldStatus) {
|
||||
$this->updateIndividual();
|
||||
}
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет расширение
|
||||
*/
|
||||
public function update(Extension $ext): bool
|
||||
{
|
||||
if (true === $ext->canUpdate) {
|
||||
return $this->updown($ext);
|
||||
} else {
|
||||
$this->error = 'Invalid action';
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет расширение
|
||||
*/
|
||||
public function downdate(Extension $ext): bool
|
||||
{
|
||||
if (true === $ext->canDowndate) {
|
||||
return $this->updown($ext);
|
||||
} else {
|
||||
$this->error = 'Invalid action';
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function updown(Extension $ext): bool
|
||||
{
|
||||
$oldStatus = $ext->dbStatus;
|
||||
$result = $ext->prepare();
|
||||
|
||||
if (true !== $result) {
|
||||
$this->error = $result;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':name' => $ext->name,
|
||||
':data' => \json_encode($ext->fileData, FORK_JSON_ENCODE),
|
||||
];
|
||||
$query = 'UPDATE ::extensions SET ext_data=?s:data
|
||||
WHERE ext_name=?s:name';
|
||||
|
||||
$ext->setModelAttrs([
|
||||
'name' => $ext->name,
|
||||
'dbStatus' => $ext->dbStatus,
|
||||
'dbData' => $ext->fileData,
|
||||
'fileData' => $ext->fileData,
|
||||
]);
|
||||
|
||||
if ($oldStatus) {
|
||||
$this->removeSymlinks($ext);
|
||||
}
|
||||
|
||||
if (true !== $this->updateCommon($ext)) {
|
||||
$this->error = 'An error occurred in updateCommon';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($oldStatus) {
|
||||
$this->setSymlinks($ext);
|
||||
$this->updateIndividual();
|
||||
}
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Включает расширение
|
||||
*/
|
||||
public function enable(Extension $ext): bool
|
||||
{
|
||||
if (true !== $ext->canEnable) {
|
||||
$this->error = 'Invalid action';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':name' => $ext->name,
|
||||
];
|
||||
$query = 'UPDATE ::extensions SET ext_status=1
|
||||
WHERE ext_name=?s:name';
|
||||
|
||||
$ext->setModelAttrs([
|
||||
'name' => $ext->name,
|
||||
'dbStatus' => 1,
|
||||
'dbData' => $ext->dbData,
|
||||
'fileData' => $ext->fileData,
|
||||
]);
|
||||
|
||||
$this->setSymlinks($ext);
|
||||
$this->updateIndividual();
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Выключает расширение
|
||||
*/
|
||||
public function disable(Extension $ext): bool
|
||||
{
|
||||
if (true !== $ext->canDisable) {
|
||||
$this->error = 'Invalid action';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':name' => $ext->name,
|
||||
];
|
||||
$query = 'UPDATE ::extensions SET ext_status=0
|
||||
WHERE ext_name=?s:name';
|
||||
|
||||
$ext->setModelAttrs([
|
||||
'name' => $ext->name,
|
||||
'dbStatus' => 0,
|
||||
'dbData' => $ext->dbData,
|
||||
'fileData' => $ext->fileData,
|
||||
]);
|
||||
|
||||
$this->removeSymlinks($ext);
|
||||
$this->updateIndividual();
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает данные из файла с общими данными по расширениям
|
||||
*/
|
||||
protected function loadDataFromFile(string $file): array
|
||||
{
|
||||
if (\is_file($file)) {
|
||||
return include $file;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет файл с общими данными по расширениям
|
||||
*/
|
||||
protected function updateCommon(Extension $ext): bool
|
||||
{
|
||||
$data = $this->loadDataFromFile($this->commonFile);
|
||||
|
||||
if ($ext::NOT_INSTALLED === $ext->status) {
|
||||
unset($data[$ext->name]);
|
||||
} else {
|
||||
$data[$ext->name] = $ext->prepareData();
|
||||
}
|
||||
|
||||
return $this->putData($this->commonFile, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Записывает данные в указанный файл
|
||||
*/
|
||||
protected function putData(string $file, mixed $data): bool
|
||||
{
|
||||
$content = "<?php\n\nreturn " . \var_export($data, true) . ";\n";
|
||||
|
||||
if (false === \file_put_contents($file, $content, \LOCK_EX)) {
|
||||
return false;
|
||||
} else {
|
||||
if (\function_exists('\\opcache_invalidate')) {
|
||||
\opcache_invalidate($file, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет индивидуальные файлы с данными по расширениям
|
||||
*/
|
||||
protected function updateIndividual(): bool
|
||||
{
|
||||
$oldPre = $this->loadDataFromFile($this->preFile);
|
||||
$templates = [];
|
||||
$commonData = $this->loadDataFromFile($this->commonFile);
|
||||
$pre = [];
|
||||
$newPre = [];
|
||||
|
||||
// выделение данных
|
||||
foreach ($this->repository as $ext) {
|
||||
if (1 !== $ext->dbStatus) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($commonData[$ext->name]['templates']['pre'])) {
|
||||
$pre = \array_merge_recursive($pre, $commonData[$ext->name]['templates']['pre']);
|
||||
}
|
||||
}
|
||||
|
||||
// PRE-данные шаблонов
|
||||
foreach ($pre as $template => $names) {
|
||||
$templates[$template] = $template;
|
||||
|
||||
foreach ($names as $name => $list) {
|
||||
\uasort($list, function (array $a, array $b) {
|
||||
return $b['priority'] <=> $a['priority'];
|
||||
});
|
||||
|
||||
$result = '';
|
||||
|
||||
foreach ($list as $value) {
|
||||
$result .= $value['data'];
|
||||
}
|
||||
|
||||
$newPre[$template][$name] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
$this->putData($this->preFile, $newPre);
|
||||
|
||||
// удаление скомпилированных шаблонов
|
||||
foreach (\array_merge($this->diffPre($oldPre, $newPre), $this->diffPre($newPre, $oldPre)) as $template) {
|
||||
$this->c->View->delete($template);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Вычисляет расхождение для PRE-данных
|
||||
*/
|
||||
protected function diffPre(array $a, array $b): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($a as $template => $names) {
|
||||
if (! isset($b[$template])) {
|
||||
$result[$template] = $template;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($names as $name => $value) {
|
||||
if (
|
||||
! isset($b[$template][$name])
|
||||
|| $value !== $b[$template][$name]
|
||||
) {
|
||||
$result[$template] = $template;
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает симлинки для расширения
|
||||
*/
|
||||
protected function setSymlinks(Extension $ext): bool
|
||||
{
|
||||
$data = $this->loadDataFromFile($this->commonFile);
|
||||
$symlinks = $data[$ext->name]['symlinks'] ?? [];
|
||||
|
||||
foreach ($symlinks as $target => $link) {
|
||||
\symlink($target, $link);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет симлинки расширения
|
||||
*/
|
||||
protected function removeSymlinks(Extension $ext): bool
|
||||
{
|
||||
$data = $this->loadDataFromFile($this->commonFile);
|
||||
$symlinks = $data[$ext->name]['symlinks'] ?? [];
|
||||
|
||||
foreach ($symlinks as $target => $link) {
|
||||
if (\is_link($link)) {
|
||||
\is_file($link) ? \unlink($link) : \rmdir($link);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
|||
namespace ForkBB\Models\Forum;
|
||||
|
||||
use ForkBB\Models\Action;
|
||||
use ForkBB\Models\DataModel;
|
||||
use ForkBB\Models\Forum\Forum;
|
||||
use ForkBB\Models\User\User;
|
||||
use InvalidArgumentException;
|
||||
|
@ -22,7 +21,7 @@ class Delete extends Action
|
|||
/**
|
||||
* Удаляет раздел(ы)
|
||||
*/
|
||||
public function delete(DataModel ...$args): void
|
||||
public function delete(Forum|User ...$args): void
|
||||
{
|
||||
if (empty($args)) {
|
||||
throw new InvalidArgumentException('No arguments, expected User(s) or Forum(s)');
|
||||
|
@ -43,7 +42,7 @@ class Delete extends Action
|
|||
$uids[$arg->id] = $arg->id;
|
||||
$isUser = 1;
|
||||
} elseif ($arg instanceof Forum) {
|
||||
if (! $this->c->forums->get($arg->id) instanceof Forum) {
|
||||
if (! $this->manager->get($arg->id) instanceof Forum) {
|
||||
throw new RuntimeException('Forum unavailable');
|
||||
}
|
||||
|
||||
|
@ -54,8 +53,6 @@ class Delete extends Action
|
|||
foreach (\array_keys($arg->descendants) as $id) { //???? а если не админ?
|
||||
$all[$id] = true;
|
||||
}
|
||||
} else {
|
||||
throw new InvalidArgumentException('Expected User(s) or Forum(s)');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ class Forum extends DataModel
|
|||
|
||||
return null;
|
||||
} else {
|
||||
return $this->c->forums->get($this->parent_forum_id);
|
||||
return $this->manager->get($this->parent_forum_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,6 +47,14 @@ class Forum extends DataModel
|
|||
return $this->forum_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает название для формирования URL
|
||||
*/
|
||||
protected function getfriendly(): ?string
|
||||
{
|
||||
return isset($this->friendly_name[0]) ? $this->friendly_name : $this->forum_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Статус возможности создания новой темы
|
||||
*/
|
||||
|
@ -93,7 +101,7 @@ class Forum extends DataModel
|
|||
|
||||
if (\is_array($attr)) {
|
||||
foreach ($attr as $id) {
|
||||
$sub[$id] = $this->c->forums->get($id);
|
||||
$sub[$id] = $this->manager->get($id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,7 +118,7 @@ class Forum extends DataModel
|
|||
|
||||
if (\is_array($attr)) {
|
||||
foreach ($attr as $id) {
|
||||
$all[$id] = $this->c->forums->get($id);
|
||||
$all[$id] = $this->manager->get($id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,7 +137,7 @@ class Forum extends DataModel
|
|||
'Forum',
|
||||
[
|
||||
'id' => $this->id,
|
||||
'name' => $this->forum_name,
|
||||
'name' => $this->friendly,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -261,7 +269,7 @@ class Forum extends DataModel
|
|||
'User',
|
||||
[
|
||||
'id' => $id,
|
||||
'name' => $cur,
|
||||
'name' => $this->c->Func->friendly($cur),
|
||||
]
|
||||
)
|
||||
: null,
|
||||
|
@ -352,7 +360,7 @@ class Forum extends DataModel
|
|||
}
|
||||
}
|
||||
|
||||
$attr = $this->c->forums->create([
|
||||
$attr = $this->manager->create([
|
||||
'num_topics' => $numT,
|
||||
'num_posts' => $numP,
|
||||
'last_post' => $time,
|
||||
|
@ -391,7 +399,7 @@ class Forum extends DataModel
|
|||
'Forum',
|
||||
[
|
||||
'id' => $this->id,
|
||||
'name' => $this->forum_name,
|
||||
'name' => $this->friendly,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -446,26 +454,14 @@ class Forum extends DataModel
|
|||
*/
|
||||
protected function createIdsList(int $rows = null, int $offset = null): void
|
||||
{
|
||||
switch ($this->sort_by) {
|
||||
case 1:
|
||||
$sortBy = 't.posted DESC';
|
||||
break;
|
||||
case 2:
|
||||
$sortBy = 't.subject ASC';
|
||||
break;
|
||||
case 4:
|
||||
$sortBy = 't.last_post ASC';
|
||||
break;
|
||||
case 5:
|
||||
$sortBy = 't.posted ASC';
|
||||
break;
|
||||
case 6:
|
||||
$sortBy = 't.subject DESC';
|
||||
break;
|
||||
default:
|
||||
$sortBy = 't.last_post DESC';
|
||||
break;
|
||||
}
|
||||
$sortBy = match ($this->sort_by) {
|
||||
1 => 't.posted DESC',
|
||||
2 => 't.subject',
|
||||
4 => 't.last_post',
|
||||
5 => 't.posted',
|
||||
6 => 't.subject DESC',
|
||||
default => 't.last_post DESC',
|
||||
};
|
||||
|
||||
$vars = [
|
||||
':fid' => $this->id,
|
||||
|
|
|
@ -34,7 +34,7 @@ class Forums extends Manager
|
|||
*/
|
||||
public function create(array $attrs = []): Forum
|
||||
{
|
||||
return $this->c->ForumModel->setModelAttrs($attrs);
|
||||
return $this->c->ForumModel->setManager($this)->setModelAttrs($attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -37,7 +37,7 @@ class Refresh extends Action
|
|||
$vars = [
|
||||
':gid' => $gid,
|
||||
];
|
||||
$query = 'SELECT f.cat_id, c.cat_name, f.id, f.forum_name, f.redirect_url, f.parent_forum_id,
|
||||
$query = 'SELECT f.cat_id, c.cat_name, f.id, f.forum_name, f.friendly_name, f.redirect_url, f.parent_forum_id,
|
||||
f.moderators, f.no_sum_mess, f.disp_position, f.sort_by, fp.post_topics, fp.post_replies
|
||||
FROM ::categories AS c
|
||||
INNER JOIN ::forums AS f ON c.id=f.cat_id
|
||||
|
|
|
@ -44,7 +44,7 @@ class UpdateUsername extends Action
|
|||
$isMod = true;
|
||||
$forum->modAdd($user); // переименование модератора
|
||||
|
||||
$this->c->forums->update($forum);
|
||||
$this->manager->update($forum);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,11 +33,6 @@ class Groups extends Manager
|
|||
return $this->c->GroupModel->setModelAttrs($attrs);
|
||||
}
|
||||
|
||||
public function getList(): array
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузка списка групп
|
||||
*/
|
||||
|
|
|
@ -50,7 +50,7 @@ class Manager
|
|||
$x = \ord($name);
|
||||
|
||||
if ($x > 90 || $x < 65) {
|
||||
return null;
|
||||
return 'repository' === $name ? $this->repository : null;
|
||||
} else {
|
||||
$key = $this->cKey . '/' . \lcfirst($name);
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||
namespace ForkBB\Models;
|
||||
|
||||
use ForkBB\Core\Container;
|
||||
use ForkBB\Models\Manager;
|
||||
|
||||
class Model
|
||||
{
|
||||
|
@ -34,6 +35,11 @@ class Model
|
|||
*/
|
||||
protected array $zDepend = [];
|
||||
|
||||
/**
|
||||
* Текущий Manager для модели
|
||||
*/
|
||||
protected Manager $manager;
|
||||
|
||||
public function __construct(protected Container $c)
|
||||
{
|
||||
}
|
||||
|
@ -165,4 +171,14 @@ class Model
|
|||
|
||||
return $this->c->$key->setModel($this)->$name(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Объявление менеджера
|
||||
*/
|
||||
public function setManager(Manager $manager): Model
|
||||
{
|
||||
$this->manager = $manager;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ class Info extends Method
|
|||
'User',
|
||||
[
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
'name' => $this->c->Func->friendly($name),
|
||||
]
|
||||
)
|
||||
: null,
|
||||
|
|
|
@ -259,31 +259,23 @@ class Online extends Model
|
|||
WHERE user_id=?i:id';
|
||||
}
|
||||
} else {
|
||||
switch ($this->c->DB->getType()) {
|
||||
case 'mysql':
|
||||
$query = 'INSERT IGNORE INTO ::online (user_id, ident, logged, o_position, o_name)
|
||||
VALUES (?i:id, ?s:ident, ?i:logged, ?s:pos, ?s:name)';
|
||||
$query = match ($this->c->DB->getType()) {
|
||||
'mysql' => 'INSERT IGNORE INTO ::online (user_id, ident, logged, o_position, o_name)
|
||||
VALUES (?i:id, ?s:ident, ?i:logged, ?s:pos, ?s:name)',
|
||||
|
||||
break;
|
||||
case 'sqlite':
|
||||
case 'pgsql':
|
||||
$query = 'INSERT INTO ::online (user_id, ident, logged, o_position, o_name)
|
||||
VALUES (?i:id, ?s:ident, ?i:logged, ?s:pos, ?s:name)
|
||||
ON CONFLICT(user_id, ident) DO NOTHING';
|
||||
'sqlite', 'pgsql' => 'INSERT INTO ::online (user_id, ident, logged, o_position, o_name)
|
||||
VALUES (?i:id, ?s:ident, ?i:logged, ?s:pos, ?s:name)
|
||||
ON CONFLICT(user_id, ident) DO NOTHING',
|
||||
|
||||
break;
|
||||
default:
|
||||
$query = 'INSERT INTO ::online (user_id, ident, logged, o_position, o_name)
|
||||
SELECT tmp.*
|
||||
FROM (SELECT ?i:id AS f1, ?s:ident AS f2, ?i:logged AS f3, ?s:pos AS f4, ?s:name AS f5) AS tmp
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ::online
|
||||
WHERE user_id=?i:id AND ident=?s:ident
|
||||
)';
|
||||
|
||||
break;
|
||||
}
|
||||
default => 'INSERT INTO ::online (user_id, ident, logged, o_position, o_name)
|
||||
SELECT tmp.*
|
||||
FROM (SELECT ?i:id AS f1, ?s:ident AS f2, ?i:logged AS f3, ?s:pos AS f4, ?s:name AS f5) AS tmp
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ::online
|
||||
WHERE user_id=?i:id AND ident=?s:ident
|
||||
)',
|
||||
};
|
||||
}
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
|
|
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
|||
namespace ForkBB\Models\PM;
|
||||
|
||||
use ForkBB\Models\Method;
|
||||
use ForkBB\Models\DataModel;
|
||||
use ForkBB\Models\PM\Cnst;
|
||||
use ForkBB\Models\PM\PPost;
|
||||
use ForkBB\Models\PM\PTopic;
|
||||
|
@ -41,7 +40,7 @@ class Delete extends Method
|
|||
}
|
||||
}
|
||||
|
||||
public function delete(DataModel ...$args): void
|
||||
public function delete(PPost|PTopic|User ...$args): void
|
||||
{
|
||||
if (empty($args)) {
|
||||
throw new InvalidArgumentException('No arguments, expected User(s), PPost(s) or PTopic(s)');
|
||||
|
@ -77,8 +76,6 @@ class Delete extends Method
|
|||
|
||||
$topics[$arg->id] = $arg;
|
||||
$isTopic = 1;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Expected User(s), PPost(s) or PTopic(s)');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,17 +11,15 @@ declare(strict_types=1);
|
|||
namespace ForkBB\Models\PM;
|
||||
|
||||
use ForkBB\Models\Method;
|
||||
use ForkBB\Models\DataModel;
|
||||
use ForkBB\Models\PM\Cnst;
|
||||
use ForkBB\Models\PM\PPost;
|
||||
use ForkBB\Models\PM\PTopic;
|
||||
use ForkBB\Models\PM\PRnd;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
class Save extends Method
|
||||
{
|
||||
public function update(DataModel $model): DataModel
|
||||
public function update(PPost|PTopic $model): PPost|PTopic
|
||||
{
|
||||
if ($model->id < 1) {
|
||||
throw new RuntimeException('The model does not have ID');
|
||||
|
@ -31,8 +29,6 @@ class Save extends Method
|
|||
$table = 'pm_posts';
|
||||
} elseif ($model instanceof PTopic) {
|
||||
$table = 'pm_topics';
|
||||
} else {
|
||||
throw new InvalidArgumentException('Bad model');
|
||||
}
|
||||
|
||||
$modified = $model->getModified();
|
||||
|
@ -69,7 +65,7 @@ class Save extends Method
|
|||
return $model;
|
||||
}
|
||||
|
||||
public function insert(DataModel $model): int
|
||||
public function insert(PPost|PTopic $model): int
|
||||
{
|
||||
if (null !== $model->id) {
|
||||
throw new RuntimeException('The model has ID');
|
||||
|
@ -79,8 +75,6 @@ class Save extends Method
|
|||
$table = 'pm_posts';
|
||||
} elseif ($model instanceof PTopic) {
|
||||
$table = 'pm_topics';
|
||||
} else {
|
||||
throw new InvalidArgumentException('Bad model');
|
||||
}
|
||||
|
||||
$attrs = $model->getModelAttrs();
|
||||
|
|
|
@ -82,6 +82,7 @@ abstract class Admin extends Page
|
|||
'uploads' => [$r->link('AdminUploads'), 'Uploads'],
|
||||
'antispam' => [$r->link('AdminAntispam'), 'Antispam'],
|
||||
'logs' => [$r->link('AdminLogs'), 'Logs'],
|
||||
'extensions' => [$r->link('AdminExtensions'), 'Extensions'],
|
||||
'maintenance' => [$r->link('AdminMaintenance'), 'Maintenance'],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -174,16 +174,16 @@ class Bans extends Admin
|
|||
];
|
||||
$fields['s_expire_1'] = [
|
||||
'class' => ['bstart'],
|
||||
'type' => 'text',
|
||||
'maxlength' => '100',
|
||||
'type' => 'datetime-local',
|
||||
'value' => $data['s_expire_1'] ?? null,
|
||||
'caption' => 'Expire date label',
|
||||
'step' => '1',
|
||||
];
|
||||
$fields['s_expire_2'] = [
|
||||
'class' => ['bend'],
|
||||
'type' => 'text',
|
||||
'maxlength' => '100',
|
||||
'type' => 'datetime-local',
|
||||
'value' => $data['s_expire_2'] ?? null,
|
||||
'step' => '1',
|
||||
];
|
||||
$fields[] = [
|
||||
'type' => 'endwrap',
|
||||
|
@ -288,11 +288,11 @@ class Bans extends Admin
|
|||
'value' => $data['message'] ?? null,
|
||||
];
|
||||
$fields['expire'] = [
|
||||
'type' => 'text',
|
||||
'maxlength' => '100',
|
||||
'type' => 'datetime-local',
|
||||
'caption' => 'Expire date label',
|
||||
'help' => 'Expire date help',
|
||||
'value' => $data['expire'] ?? null,
|
||||
'step' => '1',
|
||||
];
|
||||
$form['sets']['ban-exp'] = [
|
||||
'legend' => 'Message expiry subhead',
|
||||
|
@ -333,7 +333,7 @@ class Bans extends Admin
|
|||
$key = $matches[2];
|
||||
|
||||
if (\is_string($value)) {
|
||||
$value = \strtotime($value . ' UTC');
|
||||
$value = $this->c->Func->dateToTime($value);
|
||||
}
|
||||
} elseif (\is_string($value)) {
|
||||
$type = 'LIKE';
|
||||
|
@ -468,7 +468,7 @@ class Bans extends Admin
|
|||
'class' => empty($ban['expire']) ? ['result', 'expire', 'no-data'] : ['result', 'expire'],
|
||||
'type' => 'str',
|
||||
'caption' => 'Results expire head',
|
||||
'value' => empty($ban['expire']) ? '' : dt($ban['expire'], true),
|
||||
'value' => empty($ban['expire']) ? '' : dt($ban['expire']),
|
||||
];
|
||||
$fields["l{$number}-message"] = [
|
||||
'class' => '' == $ban['message'] ? ['result', 'message', 'no-data'] : ['result', 'message'],
|
||||
|
@ -615,7 +615,7 @@ class Bans extends Admin
|
|||
}
|
||||
|
||||
$ban = $data[$id];
|
||||
$ban['expire'] = empty($ban['expire']) ? '' : \date('Y-m-d', $ban['expire']);
|
||||
$ban['expire'] = empty($ban['expire']) ? '' : $this->c->Func->timeToDate($ban['expire']);
|
||||
$userList = [
|
||||
$this->c->users->create(['username' => $ban['username']]),
|
||||
];
|
||||
|
@ -664,7 +664,7 @@ class Bans extends Admin
|
|||
$action = $isNew ? 'insert' : 'update';
|
||||
$id = $isNew ? null : $args['id'];
|
||||
$message = (string) $v->message;
|
||||
$expire = empty($v->expire) ? 0 : \strtotime($v->expire . ' UTC');
|
||||
$expire = empty($v->expire) ? 0 : $this->c->Func->dateToTime($v->expire);
|
||||
|
||||
if ($this->banCount < 1) {
|
||||
$userList = [false];
|
||||
|
@ -834,7 +834,7 @@ class Bans extends Admin
|
|||
null !== $expire
|
||||
&& '' !== \trim($expire)
|
||||
) {
|
||||
if (\strtotime($expire . ' UTC') - \time() < 86400) {
|
||||
if ($this->c->Func->dateToTime($expire) - \time() < 86400) {
|
||||
$v->addError('Invalid date message');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ class Categories extends Admin
|
|||
$v = $this->c->Validator->reset()
|
||||
->addRules([
|
||||
'token' => 'token:AdminCategories',
|
||||
'form' => 'required|array',
|
||||
'form.*.cat_name' => 'required|string:trim|max:80',
|
||||
'form.*.disp_position' => 'required|integer|min:0|max:9999999999',
|
||||
'new' => 'exist|string:trim|max:80'
|
||||
|
@ -85,7 +86,7 @@ class Categories extends Admin
|
|||
],
|
||||
];
|
||||
|
||||
foreach ($this->c->categories->getList() as $key => $row) {
|
||||
foreach ($this->c->categories->repository as $key => $row) {
|
||||
$fields = [];
|
||||
$fields["form[{$key}][cat_name]"] = [
|
||||
'class' => ['name', 'category'],
|
||||
|
|
|
@ -29,8 +29,9 @@ class Censoring extends Admin
|
|||
->addRules([
|
||||
'token' => 'token:AdminCensoring',
|
||||
'b_censoring' => 'required|integer|in:0,1',
|
||||
'form.*.search_for' => 'string:trim|max:60',
|
||||
'form.*.replace_with' => 'string:trim|max:60',
|
||||
'form' => 'required|array',
|
||||
'form.*.search_for' => 'exist|string:trim|max:60',
|
||||
'form.*.replace_with' => 'exist|string:trim|max:60',
|
||||
])->addAliases([
|
||||
])->addArguments([
|
||||
])->addMessages([
|
||||
|
|
84
app/Models/Pages/Admin/Extensions.php
Normal file
84
app/Models/Pages/Admin/Extensions.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?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\Pages\Admin;
|
||||
|
||||
use ForkBB\Models\Extension\Extension;
|
||||
use ForkBB\Models\Page;
|
||||
use ForkBB\Models\Pages\Admin;
|
||||
use Throwable;
|
||||
use function \ForkBB\__;
|
||||
|
||||
class Extensions extends Admin
|
||||
{
|
||||
/**
|
||||
* Подготавливает данные для шаблона
|
||||
*/
|
||||
public function info(array $args, string $method): Page
|
||||
{
|
||||
$this->c->Lang->load('admin_extensions');
|
||||
|
||||
$this->nameTpl = 'admin/extensions';
|
||||
$this->aIndex = 'extensions';
|
||||
$this->extensions = $this->c->extensions->repository;
|
||||
$this->actionLink = $this->c->Router->link('AdminExtensionsAction');
|
||||
$this->formsToken = $this->c->Csrf->create('AdminExtensionsAction');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function action(array $args, string $method): Page
|
||||
{
|
||||
$this->c->Lang->load('admin_extensions');
|
||||
|
||||
$v = $this->c->Validator->reset()
|
||||
->addRules([
|
||||
'token' => 'token:AdminExtensionsAction',
|
||||
'name' => 'required|string',
|
||||
'confirm' => 'required|string|in:1',
|
||||
'install' => 'string',
|
||||
'uninstall' => 'string',
|
||||
'update' => 'string',
|
||||
'downdate' => 'string',
|
||||
'enable' => 'string',
|
||||
'disable' => 'string',
|
||||
])->addAliases([
|
||||
])->addMessages([
|
||||
'confirm' => [FORK_MESS_WARN, 'No confirm redirect'],
|
||||
])->addArguments([
|
||||
]);
|
||||
|
||||
if (! $v->validation($_POST)) {
|
||||
$message = $this->c->Message;
|
||||
$message->fIswev = $v->getErrors();
|
||||
|
||||
return $message->message('');
|
||||
}
|
||||
|
||||
$ext = $this->c->extensions->get($v->name);
|
||||
|
||||
if (! $ext instanceof Extension) {
|
||||
return $this->c->Message->message('Extension not found');
|
||||
}
|
||||
|
||||
$actions = $v->getData(false, ['token', 'name', 'confirm']);
|
||||
$action = \array_key_first($actions);
|
||||
|
||||
if (empty($action)) {
|
||||
return $this->c->Message->message('Invalid action');
|
||||
}
|
||||
|
||||
if (true !== $this->c->extensions->{$action}($ext)) {
|
||||
return $this->c->Message->message($this->c->extensions->error);
|
||||
}
|
||||
|
||||
return $this->c->Redirect->page('AdminExtensions', ['#' => $ext->id])->message("Redirect {$action}", FORK_MESS_SUCC);
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ class Forums extends Admin
|
|||
protected function calcList(Forum $forum): void
|
||||
{
|
||||
$cid = null;
|
||||
$categories = $this->c->categories->getList();
|
||||
$categories = $this->c->categories->repository;
|
||||
$options = [
|
||||
['', __('Not selected')],
|
||||
];
|
||||
|
@ -102,6 +102,7 @@ class Forums extends Admin
|
|||
$v = $this->c->Validator->reset()
|
||||
->addRules([
|
||||
'token' => 'token:AdminForums',
|
||||
'form' => 'required|array',
|
||||
'form.*.disp_position' => 'required|integer|min:0|max:9999999999',
|
||||
])->addAliases([
|
||||
])->addArguments([
|
||||
|
@ -362,10 +363,11 @@ class Forums extends Admin
|
|||
->addRules([
|
||||
'token' => 'token:' . $marker,
|
||||
'forum_name' => 'required|string:trim|max:80',
|
||||
'friendly_name' => 'string:trim|max:80|regex:%^[\w-]*$%',
|
||||
'forum_desc' => 'exist|string:trim|max:65000 bytes|html',
|
||||
'parent' => 'required|integer|in:' . \implode(',', $this->listOfIndexes),
|
||||
'sort_by' => 'required|integer|in:0,1,2,4,5,6',
|
||||
'redirect_url' => 'string:trim|max:255', //???? это поле может быть отключено в форме
|
||||
'redirect_url' => 'string:trim|max:255|regex:%^(?:https?://.+)?$%', //???? это поле может быть отключено в форме
|
||||
'no_sum_mess' => 'required|integer|in:0,1',
|
||||
'perms.*.read_forum' => 'checkbox',
|
||||
'perms.*.post_replies' => 'checkbox',
|
||||
|
@ -379,11 +381,12 @@ class Forums extends Admin
|
|||
|
||||
$valid = $v->validation($_POST);
|
||||
|
||||
$forum->forum_name = $v->forum_name;
|
||||
$forum->forum_desc = $v->forum_desc;
|
||||
$forum->sort_by = $v->sort_by;
|
||||
$forum->redirect_url = $v->redirect_url ?? '';
|
||||
$forum->no_sum_mess = $v->no_sum_mess;
|
||||
$forum->forum_name = $v->forum_name;
|
||||
$forum->friendly_name = \trim($v->friendly_name, '_-');
|
||||
$forum->forum_desc = $v->forum_desc;
|
||||
$forum->sort_by = $v->sort_by;
|
||||
$forum->redirect_url = $v->redirect_url ?? '';
|
||||
$forum->no_sum_mess = $v->no_sum_mess;
|
||||
|
||||
if ($v->parent > 0) {
|
||||
$forum->parent_forum_id = $v->parent;
|
||||
|
@ -462,6 +465,13 @@ class Forums extends Admin
|
|||
'caption' => 'Forum name label',
|
||||
'required' => true,
|
||||
],
|
||||
'friendly_name' => [
|
||||
'type' => 'text',
|
||||
'maxlength' => '80',
|
||||
'value' => $forum->friendly_name,
|
||||
'caption' => 'Friendly name label',
|
||||
'help' => 'Friendly name help',
|
||||
],
|
||||
'forum_desc' => [
|
||||
'type' => 'textarea',
|
||||
'value' => $forum->forum_desc,
|
||||
|
|
|
@ -33,7 +33,7 @@ class Groups extends Admin
|
|||
$notForNew = [FORK_GROUP_ADMIN];
|
||||
$notForDefault = [FORK_GROUP_ADMIN, FORK_GROUP_MOD, FORK_GROUP_GUEST];
|
||||
|
||||
foreach ($this->c->groups->getList() as $key => $group) {
|
||||
foreach ($this->c->groups->repository as $key => $group) {
|
||||
$groupsList[$key] = [$group->g_title, $group->linkEdit, $group->linkDelete];
|
||||
|
||||
if (! \in_array($group->g_id, $notForNew, true)) {
|
||||
|
|
|
@ -114,9 +114,12 @@ class Install extends Admin
|
|||
$folders = [
|
||||
$this->c->DIR_CONFIG,
|
||||
$this->c->DIR_CONFIG . '/db',
|
||||
$this->c->DIR_CONFIG . '/ext',
|
||||
$this->c->DIR_CACHE,
|
||||
$this->c->DIR_CACHE . '/polls',
|
||||
$this->c->DIR_PUBLIC . '/img/avatars',
|
||||
$this->c->DIR_PUBLIC . '/upload',
|
||||
$this->c->DIR_LOG,
|
||||
];
|
||||
|
||||
foreach ($folders as $folder) {
|
||||
|
@ -805,6 +808,18 @@ class Install extends Admin
|
|||
];
|
||||
$this->c->DB->createTable('::config', $schema);
|
||||
|
||||
// extensions
|
||||
$schema = [
|
||||
'FIELDS' => [
|
||||
'ext_name' => ['VARCHAR(190)', false, ''],
|
||||
'ext_status' => ['TINYINT', false, 0],
|
||||
'ext_data' => ['TEXT', false],
|
||||
],
|
||||
'PRIMARY KEY' => ['ext_name'],
|
||||
'ENGINE' => $this->DBEngine,
|
||||
];
|
||||
$this->c->DB->createTable('::extensions', $schema);
|
||||
|
||||
// forum_perms
|
||||
$schema = [
|
||||
'FIELDS' => [
|
||||
|
@ -824,6 +839,7 @@ class Install extends Admin
|
|||
'FIELDS' => [
|
||||
'id' => ['SERIAL', false],
|
||||
'forum_name' => ['VARCHAR(80)', false, 'New forum'],
|
||||
'friendly_name' => ['VARCHAR(80)', false, ''],
|
||||
'forum_desc' => ['TEXT', false],
|
||||
'redirect_url' => ['VARCHAR(255)', false, ''],
|
||||
'moderators' => ['TEXT', false],
|
||||
|
|
|
@ -175,7 +175,7 @@ class Logs extends Admin
|
|||
$data = $this->c->LogViewer->parse($path);
|
||||
|
||||
foreach ($data as &$cur) {
|
||||
$cur['context'] = \print_r($cur['context'], true);
|
||||
$cur['context'] = \preg_replace('%^\s*Array\s*\(\n(.+)\n\)\s*$%s', '$1', \print_r($cur['context'], true));
|
||||
}
|
||||
|
||||
unset($cur);
|
||||
|
|
|
@ -331,6 +331,10 @@ class Maintenance extends Admin
|
|||
throw new RuntimeException('Unable to clear cache');
|
||||
}
|
||||
|
||||
if (\function_exists('\\opcache_reset')) {
|
||||
\opcache_reset();
|
||||
}
|
||||
|
||||
return $this->c->Redirect->page('AdminMaintenance')->message('Clear cache redirect', FORK_MESS_SUCC);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@ class Providers extends Admin
|
|||
],
|
||||
];
|
||||
|
||||
foreach ($this->c->providers->init()->list() as $provider) {
|
||||
foreach ($this->c->providers->init()->repository as $provider) {
|
||||
$fields = [];
|
||||
$fields["name-{$provider->name}"] = [
|
||||
'class' => ['name', 'provider'],
|
||||
|
|
|
@ -25,7 +25,7 @@ class Update extends Admin
|
|||
{
|
||||
const PHP_MIN = '8.0.0';
|
||||
const REV_MIN_FOR_UPDATE = 53;
|
||||
const LATEST_REV_WITH_DB_CHANGES = 68;
|
||||
const LATEST_REV_WITH_DB_CHANGES = 72;
|
||||
const LOCK_NAME = 'lock_update';
|
||||
const LOCK_TTL = 1800;
|
||||
const CONFIG_FILE = 'main.php';
|
||||
|
@ -360,6 +360,10 @@ class Update extends Admin
|
|||
|
||||
do {
|
||||
if (\method_exists($this, 'stageNumber' . $stage)) {
|
||||
if (\function_exists('\\set_time_limit')) {
|
||||
\set_time_limit(0);
|
||||
}
|
||||
|
||||
$start = $this->{'stageNumber' . $stage}($args);
|
||||
|
||||
if (null === $start) {
|
||||
|
@ -917,4 +921,149 @@ class Update extends Admin
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* rev.69 to rev.70
|
||||
*/
|
||||
protected function stageNumber69(array $args): ?int
|
||||
{
|
||||
$coreConfig = new CoreConfig($this->configFile);
|
||||
|
||||
$coreConfig->add(
|
||||
'shared=>%DIR_EXT%',
|
||||
'\'%DIR_ROOT%/ext\'',
|
||||
'%DIR_VIEWS%'
|
||||
);
|
||||
|
||||
$coreConfig->add(
|
||||
'multiple=>ExtensionModel',
|
||||
'\\ForkBB\\Models\\Extension\\Extension::class',
|
||||
'DBMapModel'
|
||||
);
|
||||
|
||||
$coreConfig->add(
|
||||
'multiple=>ExtensionManager',
|
||||
'\\ForkBB\\Models\\Extension\\Extensions::class',
|
||||
'ExtensionModel'
|
||||
);
|
||||
|
||||
$coreConfig->add(
|
||||
'shared=>extensions',
|
||||
'\'@ExtensionManager:init\'',
|
||||
'attachments'
|
||||
);
|
||||
|
||||
$coreConfig->add(
|
||||
'multiple=>AdminExtensions',
|
||||
'\\ForkBB\\Models\\Pages\\Admin\\Extensions::class',
|
||||
'AdminAntispam'
|
||||
);
|
||||
|
||||
$coreConfig->add(
|
||||
'shared=>View=>config=>preFile',
|
||||
'\'%DIR_CONFIG%/ext/pre.php\''
|
||||
);
|
||||
|
||||
$coreConfig->save();
|
||||
|
||||
// extensions
|
||||
$schema = [
|
||||
'FIELDS' => [
|
||||
'ext_name' => ['VARCHAR(190)', false, ''],
|
||||
'ext_status' => ['TINYINT', false, 0],
|
||||
'ext_data' => ['TEXT', false],
|
||||
],
|
||||
'PRIMARY KEY' => ['ext_name'],
|
||||
];
|
||||
$this->c->DB->createTable('::extensions', $schema);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* rev.70 to rev.71
|
||||
*/
|
||||
protected function stageNumber70(array $args): ?int
|
||||
{
|
||||
$coreConfig = new CoreConfig($this->configFile);
|
||||
|
||||
$coreConfig->add(
|
||||
'FRIENDLY_URL',
|
||||
[
|
||||
'lowercase' => 'false',
|
||||
'translit' => '\'\'',
|
||||
'WtoHyphen' => 'false',
|
||||
],
|
||||
'TIME_FORMATS'
|
||||
);
|
||||
|
||||
$coreConfig->save();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* rev.71 to rev.72
|
||||
*/
|
||||
protected function stageNumber71(array $args): ?int
|
||||
{
|
||||
switch ($args['start'] ?? 1) {
|
||||
case 3:
|
||||
$f = $this->c->FRIENDLY_URL;
|
||||
|
||||
if (
|
||||
! empty($f['lowercase'])
|
||||
|| ! empty($f['translit'])
|
||||
|| ! empty($f['WtoHyphen'])
|
||||
) {
|
||||
|
||||
$names = $this->c->DB->query('SELECT id, forum_name FROM ::forums WHERE redirect_url=\'\' ORDER BY id')->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
$query = 'UPDATE ::forums SET friendly_name=?s:name WHERE id=?i:id';
|
||||
|
||||
foreach ($names as $id => $name) {
|
||||
$vars = [
|
||||
':id' => $id,
|
||||
':name' => \mb_substr($this->c->Func->friendly($name), 0, 80, 'UTF-8'),
|
||||
];
|
||||
$this->c->DB->exec($query, $vars);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
case 2:
|
||||
$this->c->DB->addField('::forums', 'friendly_name', 'VARCHAR(80)', false, '', null, 'forum_name');
|
||||
|
||||
return 3;
|
||||
default:
|
||||
$coreConfig = new CoreConfig($this->configFile);
|
||||
|
||||
$coreConfig->add(
|
||||
'FRIENDLY_URL=>file',
|
||||
'\'translit.default.php\'',
|
||||
'WtoHyphen'
|
||||
);
|
||||
|
||||
$coreConfig->save();
|
||||
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* rev.72 to rev.73
|
||||
*/
|
||||
protected function stageNumber72(array $args): ?int
|
||||
{
|
||||
$coreConfig = new CoreConfig($this->configFile);
|
||||
|
||||
$coreConfig->add(
|
||||
'multiple=>Sitemap',
|
||||
'\\ForkBB\\Models\\Pages\\Sitemap::class',
|
||||
'Misc'
|
||||
);
|
||||
|
||||
$coreConfig->save();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -220,7 +220,7 @@ class Action extends Users
|
|||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($this->c->groups->getList() as $id => $group) {
|
||||
foreach ($this->c->groups->repository as $id => $group) {
|
||||
$list[$id] = $group->g_title;
|
||||
}
|
||||
|
||||
|
|
|
@ -215,7 +215,7 @@ class Result extends Users
|
|||
$key = $matches[2];
|
||||
|
||||
if (\is_string($value)) {
|
||||
$value = \strtotime($value . ' UTC');
|
||||
$value = $this->c->Func->dateToTime($value);
|
||||
}
|
||||
} elseif (\is_string($value)) {
|
||||
$type = 'LIKE';
|
||||
|
|
|
@ -27,7 +27,7 @@ class View extends Users
|
|||
0 => __('Unverified users'),
|
||||
];
|
||||
|
||||
foreach ($this->c->groups->getList() as $group) {
|
||||
foreach ($this->c->groups->repository as $group) {
|
||||
if (! $group->groupGuest) {
|
||||
$groups[$group->g_id] = $group->g_title;
|
||||
}
|
||||
|
@ -272,16 +272,16 @@ class View extends Users
|
|||
];
|
||||
$fields['last_post_1'] = [
|
||||
'class' => ['bstart'],
|
||||
'type' => 'text',
|
||||
'maxlength' => '100',
|
||||
'type' => 'datetime-local',
|
||||
'value' => $data['last_post_1'] ?? null,
|
||||
'caption' => 'Last post label',
|
||||
'step' => '1',
|
||||
];
|
||||
$fields['last_post_2'] = [
|
||||
'class' => ['bend'],
|
||||
'type' => 'text',
|
||||
'maxlength' => '100',
|
||||
'type' => 'datetime-local',
|
||||
'value' => $data['last_post_2'] ?? null,
|
||||
'step' => '1',
|
||||
];
|
||||
$fields[] = [
|
||||
'type' => 'endwrap',
|
||||
|
@ -292,16 +292,16 @@ class View extends Users
|
|||
];
|
||||
$fields['last_visit_1'] = [
|
||||
'class' => ['bstart'],
|
||||
'type' => 'text',
|
||||
'maxlength' => '100',
|
||||
'type' => 'datetime-local',
|
||||
'value' => $data['last_visit_1'] ?? null,
|
||||
'caption' => 'Last visit label',
|
||||
'step' => '1',
|
||||
];
|
||||
$fields['last_visit_2'] = [
|
||||
'class' => ['bend'],
|
||||
'type' => 'text',
|
||||
'maxlength' => '100',
|
||||
'type' => 'datetime-local',
|
||||
'value' => $data['last_visit_2'] ?? null,
|
||||
'step' => '1',
|
||||
];
|
||||
$fields[] = [
|
||||
'type' => 'endwrap',
|
||||
|
@ -312,16 +312,16 @@ class View extends Users
|
|||
];
|
||||
$fields['registered_1'] = [
|
||||
'class' => ['bstart'],
|
||||
'type' => 'text',
|
||||
'maxlength' => '100',
|
||||
'type' => 'datetime-local',
|
||||
'value' => $data['registered_1'] ?? null,
|
||||
'caption' => 'Registered label',
|
||||
'step' => '1',
|
||||
];
|
||||
$fields['registered_2'] = [
|
||||
'class' => ['bend'],
|
||||
'type' => 'text',
|
||||
'maxlength' => '100',
|
||||
'type' => 'datetime-local',
|
||||
'value' => $data['registered_2'] ?? null,
|
||||
'step' => '1',
|
||||
];
|
||||
$fields[] = [
|
||||
'type' => 'endwrap',
|
||||
|
|
|
@ -206,7 +206,7 @@ class Auth extends Page
|
|||
/**
|
||||
* Проверка пользователя по базе
|
||||
*/
|
||||
public function vLoginCheck(Validator $v, #[SensitiveParameter] string $password ): string
|
||||
public function vLoginCheck(Validator $v, #[SensitiveParameter] string $password): string
|
||||
{
|
||||
if (empty($v->getErrors())) {
|
||||
if ($this->loginWithForm) {
|
||||
|
|
|
@ -17,6 +17,7 @@ use ForkBB\Models\Pages\PostValidatorTrait;
|
|||
use ForkBB\Models\Poll\Poll;
|
||||
use ForkBB\Models\Post\Post;
|
||||
use ForkBB\Models\Topic\Topic;
|
||||
use ForkBB\Models\User\User;
|
||||
use function \ForkBB\__;
|
||||
|
||||
class Edit extends Page
|
||||
|
@ -314,4 +315,199 @@ class Edit extends Page
|
|||
$this->c->polls->insert($poll);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Изменение автора и даты
|
||||
*/
|
||||
public function change(array $args, string $method): Page
|
||||
{
|
||||
$post = $this->c->posts->load($args['id']);
|
||||
|
||||
if (
|
||||
! $post instanceof Post
|
||||
|| ! $post->canEdit
|
||||
) {
|
||||
return $this->c->Message->message('Bad request');
|
||||
}
|
||||
|
||||
$topic = $post->parent;
|
||||
$firstPost = $post->id === $topic->first_post_id;
|
||||
$lastPost = $post->id === $topic->last_post_id;
|
||||
|
||||
$this->c->Lang->load('post');
|
||||
$this->c->Lang->load('validator');
|
||||
|
||||
if ('POST' === $method) {
|
||||
$v = $this->c->Validator->reset()
|
||||
->addValidators([
|
||||
'username_check' => [$this, 'vUsernameCheck'],
|
||||
])->addRules([
|
||||
'token' => 'token:ChangeAnD',
|
||||
'username' => 'required|string|username_check',
|
||||
'posted' => 'required|date',
|
||||
'confirm' => 'checkbox',
|
||||
'change_and' => 'required|string',
|
||||
])->addAliases([
|
||||
'username' => 'Username',
|
||||
'posted' => 'Posted',
|
||||
])->addArguments([
|
||||
'token' => $args,
|
||||
'username.username_check' => $post->user,
|
||||
]);
|
||||
|
||||
if ($v->validation($_POST)) {
|
||||
if ('1' !== $v->confirm) {
|
||||
return $this->c->Redirect->url($post->link)->message('No confirm redirect', FORK_MESS_WARN);
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
$upPost = false;
|
||||
|
||||
// изменить имя автора
|
||||
if (
|
||||
$this->newUser instanceof User
|
||||
&& $this->newUser->id !== $post->user->id
|
||||
) {
|
||||
if (! $post->user->isGuest) {
|
||||
$ids[] = $post->user->id;
|
||||
}
|
||||
|
||||
if (! $this->newUser->isGuest) {
|
||||
$ids[] = $this->newUser->id;
|
||||
}
|
||||
|
||||
$post->poster = $this->newUser->username;
|
||||
$post->poster_id = $this->newUser->id;
|
||||
$upPost = true;
|
||||
}
|
||||
|
||||
$posted = $this->c->Func->dateToTime($v->posted);
|
||||
|
||||
// изменит время создания
|
||||
if (\abs($post->posted - $posted) >= 60) {
|
||||
$post->posted = $posted;
|
||||
$upPost = true;
|
||||
}
|
||||
|
||||
if ($upPost) {
|
||||
$post->edited = \time();
|
||||
$post->editor = $this->user->username;
|
||||
$post->editor_id = $this->user->id;
|
||||
|
||||
$this->c->posts->update($post);
|
||||
|
||||
if (
|
||||
$firstPost
|
||||
|| $lastPost
|
||||
) {
|
||||
$topic->calcStat();
|
||||
$this->c->topics->update($topic);
|
||||
|
||||
if ($lastPost) {
|
||||
$topic->parent->calcStat();
|
||||
$this->c->forums->update($topic->parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($ids) {
|
||||
$this->c->users->updateCountPosts(...$ids);
|
||||
|
||||
if ($firstPost) {
|
||||
$this->c->users->updateCountTopics(...$ids);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->c->Redirect->url($post->link)->message('Change redirect', FORK_MESS_SUCC);
|
||||
}
|
||||
|
||||
$this->fIswev = $v->getErrors();
|
||||
|
||||
$data = [
|
||||
'username' => $v->username ?: $post->poster,
|
||||
'posted' => $v->posted ?: $this->c->Func->timeToDate($post->posted),
|
||||
];
|
||||
} else {
|
||||
$data = [
|
||||
'username' => $post->poster,
|
||||
'posted' => $this->c->Func->timeToDate($post->posted),
|
||||
];
|
||||
}
|
||||
|
||||
$this->nameTpl = 'post';
|
||||
$this->onlinePos = 'topic-' . $topic->id;
|
||||
$this->robots = 'noindex';
|
||||
$this->formTitle = $firstPost ? 'Change AnD topic' : 'Change AnD post';
|
||||
$this->crumbs = $this->crumbs($this->formTitle, $topic);
|
||||
$this->form = $this->formAuthorAndDate($data, $args);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function vUsernameCheck(Validator $v, string $username, $attr, User $user): string
|
||||
{
|
||||
if ($username !== $user->username) {
|
||||
$newUser = $this->c->users->loadByName($username, true);
|
||||
|
||||
if ($newUser instanceof User) {
|
||||
$username = $newUser->username;
|
||||
$this->newUser = $newUser;
|
||||
} else {
|
||||
$v->addError(['User %s does not exist', $username]);
|
||||
}
|
||||
}
|
||||
|
||||
return $username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает данные для построения формы изменения автора поста и времени создания
|
||||
*/
|
||||
protected function formAuthorAndDate(array $data, array $args): ?array
|
||||
{
|
||||
if (! $this->user->isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'action' => $this->c->Router->link('ChangeAnD', $args),
|
||||
'hidden' => [
|
||||
'token' => $this->c->Csrf->create('ChangeAnD', $args),
|
||||
],
|
||||
'sets' => [
|
||||
'author-and-date' => [
|
||||
'fields' => [
|
||||
'username'=> [
|
||||
'type' => 'text',
|
||||
'minlength' => $this->c->USERNAME['min'],
|
||||
'maxlength' => $this->c->USERNAME['max'],
|
||||
'caption' => 'Username',
|
||||
'required' => true,
|
||||
'pattern' => $this->c->USERNAME['jsPattern'],
|
||||
'value' => $data['username'] ?? null,
|
||||
'autofocus' => true,
|
||||
],
|
||||
'posted'=> [
|
||||
'type' => 'datetime-local',
|
||||
'caption' => 'Posted',
|
||||
'required' => true,
|
||||
'value' => $data['posted'] ?? null,
|
||||
'step' => '1',
|
||||
],
|
||||
'confirm' => [
|
||||
'type' => 'checkbox',
|
||||
'label' => 'Confirm action',
|
||||
'checked' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'btns' => [
|
||||
'change_and' => [
|
||||
'type' => 'submit',
|
||||
'value' => __('Change'),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ class Forum extends Page
|
|||
'Forum',
|
||||
[
|
||||
'id' => $args['id'],
|
||||
'name' => $forum->forum_name,
|
||||
'name' => $forum->friendly,
|
||||
'page' => $forum->page,
|
||||
]
|
||||
);
|
||||
|
@ -68,7 +68,10 @@ class Forum extends Page
|
|||
$this->formMod = $this->formMod($forum);
|
||||
}
|
||||
|
||||
if ($this->c->config->i_feed_type > 0) {
|
||||
if (
|
||||
$this->c->config->i_feed_type > 0
|
||||
&& $forum->num_posts > 0
|
||||
) {
|
||||
$feedType = 2 === $this->c->config->i_feed_type ? 'atom' : 'rss';
|
||||
|
||||
$this->pageHeader('feed', 'link', 0, [
|
||||
|
@ -122,6 +125,11 @@ class Forum extends Page
|
|||
'type' => 'submit',
|
||||
'value' => __('Merge'),
|
||||
],
|
||||
'link' => [
|
||||
'class' => ['origin'],
|
||||
'type' => 'submit',
|
||||
'value' => __('Link btn'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
|
@ -151,7 +159,7 @@ class Forum extends Page
|
|||
'Forum',
|
||||
[
|
||||
'id' => $forum->id,
|
||||
'name' => $forum->forum_name,
|
||||
'name' => $forum->friendly,
|
||||
'page' => $forum->page,
|
||||
'#' => "topic-{$topic->id}",
|
||||
]
|
||||
|
|
|
@ -31,7 +31,7 @@ class Index extends Page
|
|||
'User',
|
||||
[
|
||||
'id' => $this->c->stats->userLast['id'],
|
||||
'name' => $this->c->stats->userLast['username'],
|
||||
'name' => $this->c->Func->friendly($this->c->stats->userLast['username']),
|
||||
]
|
||||
)
|
||||
: null,
|
||||
|
|
|
@ -39,6 +39,7 @@ class Moderate extends Page
|
|||
'unstick' => self::INTOPIC + self::TOTOPIC,
|
||||
'stick' => self::INTOPIC + self::TOTOPIC,
|
||||
'split' => self::INTOPIC,
|
||||
'link' => self::INFORUM,
|
||||
];
|
||||
|
||||
public function __construct(Container $container)
|
||||
|
@ -128,6 +129,12 @@ class Moderate extends Page
|
|||
&& \count($v->ids) < 2
|
||||
) {
|
||||
$v->addError('Not enough topics selected');
|
||||
// управление перенаправлениями
|
||||
} elseif (
|
||||
'link' === $action
|
||||
&& \count($v->ids) > 1
|
||||
) {
|
||||
$v->addError('Only one topic is permissible');
|
||||
// перенос тем или разделение постов
|
||||
} elseif (
|
||||
'move' === $action
|
||||
|
@ -168,9 +175,11 @@ class Moderate extends Page
|
|||
'step' => 'required|integer|min:1',
|
||||
'forum' => 'required|integer|min:1|max:9999999999',
|
||||
'topic' => 'integer|min:1|max:9999999999',
|
||||
'page' => 'integer|min:1',
|
||||
'page' => 'integer|min:1|max:9999999999',
|
||||
'ids' => 'required|array',
|
||||
'ids.*' => 'required|integer|min:1|max:9999999999',
|
||||
'forums' => 'array',
|
||||
'forums.*' => 'integer|min:1|max:9999999999', // ????
|
||||
'confirm' => 'integer',
|
||||
'redirect' => 'integer',
|
||||
'subject' => 'string:trim,spaces|min:1|max:70',
|
||||
|
@ -184,6 +193,7 @@ class Moderate extends Page
|
|||
'unstick' => 'string',
|
||||
'stick' => 'string',
|
||||
'split' => 'string',
|
||||
'link' => 'string',
|
||||
'action' => 'action_process',
|
||||
])->addAliases([
|
||||
])->addArguments([
|
||||
|
@ -209,8 +219,6 @@ class Moderate extends Page
|
|||
return $this->c->Message->message('No permission', true, 403);
|
||||
}
|
||||
|
||||
$page = $v->page ?? 1;
|
||||
|
||||
if ($v->topic) {
|
||||
$this->curTopic = $this->c->topics->load($v->topic);
|
||||
|
||||
|
@ -257,7 +265,7 @@ class Moderate extends Page
|
|||
[
|
||||
'id' => $this->curTopic->id,
|
||||
'name' => $this->curTopic->name,
|
||||
'page' => $page,
|
||||
'page' => $v->page,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
|
@ -276,8 +284,8 @@ class Moderate extends Page
|
|||
'Forum',
|
||||
[
|
||||
'id' => $this->curForum->id,
|
||||
'name' => $this->curForum->forum_name,
|
||||
'page' => $page,
|
||||
'name' => $this->curForum->friendly,
|
||||
'page' => $v->page,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -391,9 +399,11 @@ class Moderate extends Page
|
|||
if (1 === $v->confirm) {
|
||||
if (true === $this->processAsPosts) {
|
||||
$this->c->posts->delete(...$objects);
|
||||
|
||||
$message = 'Delete post redirect';
|
||||
} else {
|
||||
$this->c->topics->delete(...$objects);
|
||||
|
||||
$message = 'Delete topic redirect';
|
||||
}
|
||||
|
||||
|
@ -424,6 +434,7 @@ class Moderate extends Page
|
|||
case 2:
|
||||
if (1 === $v->confirm) {
|
||||
$forum = $this->c->forums->get($v->destination);
|
||||
|
||||
$this->c->topics->move(1 === $v->redirect, $forum, ...$topics);
|
||||
|
||||
return $this->c->Redirect->url($this->curForum->link)->message(['Move topic redirect', $this->numObj], FORK_MESS_SUCC);
|
||||
|
@ -499,6 +510,7 @@ class Moderate extends Page
|
|||
if (1 === $v->confirm) {
|
||||
foreach ($topics as $topic) {
|
||||
$topic->sticky = 0;
|
||||
|
||||
$this->c->topics->update($topic);
|
||||
}
|
||||
|
||||
|
@ -529,6 +541,7 @@ class Moderate extends Page
|
|||
if (1 === $v->confirm) {
|
||||
foreach ($topics as $topic) {
|
||||
$topic->sticky = 1;
|
||||
|
||||
$this->c->topics->update($topic);
|
||||
}
|
||||
|
||||
|
@ -557,8 +570,8 @@ class Moderate extends Page
|
|||
$newTopic = $this->c->topics->create();
|
||||
$newTopic->subject = $v->subject;
|
||||
$newTopic->forum_id = $v->forum;
|
||||
$this->c->topics->insert($newTopic);
|
||||
|
||||
$this->c->topics->insert($newTopic);
|
||||
$this->c->posts->move(false, $newTopic, ...$posts);
|
||||
|
||||
return $this->c->Redirect->url($this->curForum->link)->message('Split posts redirect', FORK_MESS_SUCC);
|
||||
|
@ -570,6 +583,157 @@ class Moderate extends Page
|
|||
}
|
||||
}
|
||||
|
||||
protected function actionLink(array $topics, Validator $v): Page
|
||||
{
|
||||
$topic = \array_pop($topics);
|
||||
|
||||
if ($topic->moved_to) {
|
||||
return $this->c->Message->message('Need full topic for this operation');
|
||||
}
|
||||
|
||||
$links = $this->c->topics->loadLinks($topic);
|
||||
$ft = [];
|
||||
|
||||
foreach ($links as $link) {
|
||||
$ft[$link->parent->id][] = $link;
|
||||
}
|
||||
|
||||
switch ($v->step) {
|
||||
case 1:
|
||||
$this->formTitle = 'Control of redirects title';
|
||||
$this->crumbs = $this->crumbs($this->formTitle, 'Moderate', $this->curForum);
|
||||
$this->form = $this->formLinks($topic, $ft, $v);
|
||||
|
||||
return $this;
|
||||
case 2:
|
||||
$root = $this->c->forums->get(0);
|
||||
|
||||
if ($root instanceof Forum) {
|
||||
$selected = $v->forums ?: [];
|
||||
$delLinks = [];
|
||||
|
||||
foreach ($this->c->forums->depthList($root, 0) as $forum) {
|
||||
if ($forum->redirect_url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// создать тему-перенаправление
|
||||
if (
|
||||
empty($ft[$forum->id])
|
||||
&& \in_array($forum->id, $selected, true)
|
||||
) {
|
||||
$rTopic = $this->c->topics->create();
|
||||
$rTopic->poster = $topic->poster;
|
||||
$rTopic->poster_id = $topic->poster_id;
|
||||
$rTopic->subject = $topic->subject;
|
||||
$rTopic->posted = $topic->posted;
|
||||
$rTopic->last_post = $topic->last_post;
|
||||
$rTopic->moved_to = $topic->moved_to ?: $topic->id;
|
||||
$rTopic->forum_id = $forum->id;
|
||||
|
||||
$this->c->topics->insert($rTopic);
|
||||
$this->c->forums->update($forum->calcStat());
|
||||
// удалить тему(ы)-перенаправление
|
||||
} elseif (
|
||||
! empty($ft[$forum->id])
|
||||
&& ! \in_array($forum->id, $selected, true)
|
||||
) {
|
||||
foreach ($ft[$forum->id] as $link) {
|
||||
$delLinks[] = $link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($delLinks) {
|
||||
$this->c->topics->delete(...$delLinks);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->c->Redirect->url($topic->linkCrumbExt)->message('Redirects changed redirect', FORK_MESS_SUCC);
|
||||
default:
|
||||
return $this->c->Message->message('Bad request');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Подготавливает массив данных для формы управления переадресацией
|
||||
*/
|
||||
protected function formLinks(Topic $topic, array $ft, Validator $v): array
|
||||
{
|
||||
$form = [
|
||||
'action' => $this->c->Router->link('Moderate'),
|
||||
'hidden' => [
|
||||
'token' => $this->c->Csrf->create('Moderate'),
|
||||
'step' => $v->step + 1,
|
||||
'forum' => $v->forum,
|
||||
'ids' => $v->ids,
|
||||
],
|
||||
'sets' => [
|
||||
'info' => [
|
||||
'inform' => [
|
||||
[
|
||||
'html' => __(['Topic «%s»', $topic->name]),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'btns' => [
|
||||
'link' => [
|
||||
'type' => 'submit',
|
||||
'value' => __('Change btn'),
|
||||
],
|
||||
'cancel' => [
|
||||
'type' => 'submit',
|
||||
'value' => __('Cancel'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$root = $this->c->forums->get(0);
|
||||
|
||||
if ($root instanceof Forum) {
|
||||
$list = $this->c->forums->depthList($root, 0);
|
||||
$cid = null;
|
||||
|
||||
foreach ($list as $forum) {
|
||||
if ($cid !== $forum->cat_id) {
|
||||
$form['sets']["category{$forum->cat_id}-info"] = [
|
||||
'inform' => [
|
||||
[
|
||||
'message' => $forum->cat_name,
|
||||
],
|
||||
],
|
||||
];
|
||||
$cid = $forum->cat_id;
|
||||
}
|
||||
|
||||
$fields = [];
|
||||
$fields["name{$forum->id}"] = [
|
||||
'class' => ['modforum', 'name', 'depth' . $forum->depth],
|
||||
'type' => 'label',
|
||||
'value' => $forum->forum_name,
|
||||
'caption' => 'Forum label',
|
||||
'for' => "forums[{$forum->id}]",
|
||||
];
|
||||
$fields["forums[{$forum->id}]"] = [
|
||||
'class' => ['modforum', 'moderator'],
|
||||
'type' => 'checkbox',
|
||||
'value' => $forum->id,
|
||||
'checked' => ! empty($ft[$forum->id]),
|
||||
'disabled' => ! empty($forum->redirect_url),
|
||||
'caption' => 'Redir label',
|
||||
];
|
||||
$form['sets']["forum{$forum->id}"] = [
|
||||
'class' => $topic->parent->id === $forum->id ? ['modforum', 'current'] : ['modforum'],
|
||||
'legend' => $forum->cat_name . ' / ' . $forum->forum_name,
|
||||
'fields' => $fields,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Подготавливает массив данных для формы подтверждения
|
||||
*/
|
||||
|
@ -582,6 +746,7 @@ class Moderate extends Page
|
|||
'step' => $v->step + 1,
|
||||
'forum' => $v->forum,
|
||||
'ids' => $v->ids,
|
||||
'page' => $v->page ?? 1,
|
||||
],
|
||||
'sets' => [],
|
||||
'btns' => [],
|
||||
|
|
|
@ -31,7 +31,7 @@ abstract class AbstractPM extends Page
|
|||
$this->fIndex = self::FI_PM;
|
||||
$this->onlinePos = 'pm';
|
||||
$this->robots = 'noindex, nofollow';
|
||||
$this->hhsLevel = 'secure';
|
||||
// $this->hhsLevel = 'secure';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -156,11 +156,11 @@ abstract class AbstractPM extends Page
|
|||
$name = \substr($pms->second, 1, -1);
|
||||
}
|
||||
|
||||
switch ($pms->area) {
|
||||
case Cnst::ACTION_NEW: $m = ['New messages with %s', $name]; break;
|
||||
case Cnst::ACTION_CURRENT: $m = ['My talks with %s', $name]; break;
|
||||
case Cnst::ACTION_ARCHIVE: $m = ['Archive messages with %s', $name]; break;
|
||||
}
|
||||
$m = match ($pms->area) {
|
||||
Cnst::ACTION_NEW => ['New messages with %s', $name],
|
||||
Cnst::ACTION_CURRENT => ['My talks with %s', $name],
|
||||
Cnst::ACTION_ARCHIVE => ['Archive messages with %s', $name],
|
||||
};
|
||||
} else {
|
||||
if ($this->targetUser instanceof User) {
|
||||
$crumbs[] = [
|
||||
|
@ -177,11 +177,11 @@ abstract class AbstractPM extends Page
|
|||
];
|
||||
}
|
||||
|
||||
switch ($pms->area) {
|
||||
case Cnst::ACTION_NEW: $m = 'New messages'; break;
|
||||
case Cnst::ACTION_CURRENT: $m = 'My talks'; break;
|
||||
case Cnst::ACTION_ARCHIVE: $m = 'Archive messages'; break;
|
||||
}
|
||||
$m = match ($pms->area) {
|
||||
Cnst::ACTION_NEW => 'New messages',
|
||||
Cnst::ACTION_CURRENT => 'My talks',
|
||||
Cnst::ACTION_ARCHIVE => 'Archive messages',
|
||||
};
|
||||
}
|
||||
|
||||
$crumbs[] = [
|
||||
|
|
|
@ -38,12 +38,14 @@ class Poll extends Page
|
|||
->addValidators([
|
||||
])->addRules([
|
||||
'token' => 'token:Poll',
|
||||
'poll_vote' => 'required|array',
|
||||
'poll_vote.*.*' => 'required|integer',
|
||||
'vote' => 'required|string',
|
||||
])->addAliases([
|
||||
])->addArguments([
|
||||
'token' => $args,
|
||||
])->addMessages([
|
||||
'poll_vote' => 'The poll structure is broken',
|
||||
'poll_vote.*.*' => 'The poll structure is broken',
|
||||
]);
|
||||
|
||||
|
|
|
@ -85,12 +85,7 @@ class Post extends Page
|
|||
}
|
||||
|
||||
$this->nameTpl = 'post';
|
||||
$this->canonical = $this->c->Router->link(
|
||||
'NewTopic',
|
||||
[
|
||||
'id' => $forum->id,
|
||||
]
|
||||
);
|
||||
$this->canonical = $forum->linkCreateTopic;
|
||||
$this->robots = 'noindex';
|
||||
$this->formTitle = 'Post new topic';
|
||||
$this->crumbs = $this->crumbs($this->formTitle, $forum);
|
||||
|
@ -157,12 +152,7 @@ class Post extends Page
|
|||
}
|
||||
|
||||
$this->nameTpl = 'post';
|
||||
$this->canonical = $this->c->Router->link(
|
||||
'NewReply',
|
||||
[
|
||||
'id' => $topic->id,
|
||||
]
|
||||
);
|
||||
$this->canonical = $topic->linkReply;
|
||||
$this->robots = 'noindex';
|
||||
$this->formTitle = 'Post a reply';
|
||||
$this->crumbs = $this->crumbs($this->formTitle, $topic);
|
||||
|
|
|
@ -10,6 +10,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace ForkBB\Models\Pages;
|
||||
|
||||
use ForkBB\Core\Image;
|
||||
use ForkBB\Core\Validator;
|
||||
use ForkBB\Models\Model;
|
||||
use function \ForkBB\__;
|
||||
|
@ -103,10 +104,7 @@ trait PostValidatorTrait
|
|||
{
|
||||
$this->c->Lang->load('validator');
|
||||
|
||||
// обработка вложений + хак с добавление вложений в сообщение на лету
|
||||
if (\is_string($attMessage = $this->attachmentsProc($marker, $args))) {
|
||||
$_POST['message'] .= $attMessage;
|
||||
}
|
||||
$this->attachmentsProc($marker, $args);
|
||||
|
||||
$notPM = $this->fIndex !== self::FI_PM;
|
||||
|
||||
|
@ -297,55 +295,83 @@ trait PostValidatorTrait
|
|||
/**
|
||||
* Обрабатывает загруженные файлы
|
||||
*/
|
||||
protected function attachmentsProc(string $marker, array $args): ?string
|
||||
protected function attachmentsProc(string $marker, array $args): void
|
||||
{
|
||||
if (! $this->userRules->useUpload) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
$v = $this->c->Validator->reset()
|
||||
->addValidators([
|
||||
'check_attach' => [$this, 'vCheckAttach'],
|
||||
'check_attach' => [$this, 'vCheckAttach'],
|
||||
])->addRules([
|
||||
'token' => 'token:' . $marker,
|
||||
'attachments' => "file:multiple|max:{$this->user->g_up_size_kb}|check_attach",
|
||||
'token' => 'token:' . $marker,
|
||||
'message' => 'string:trim',
|
||||
'attachments' => "file:multiple|max:{$this->user->g_up_size_kb}|check_attach",
|
||||
])->addAliases([
|
||||
'attachments' => 'Attachments',
|
||||
'attachments' => 'Attachments',
|
||||
])->addArguments([
|
||||
'token' => $args,
|
||||
'token' => $args,
|
||||
])->addMessages([
|
||||
]);
|
||||
|
||||
if (! $v->validation($_FILES + $_POST)) {
|
||||
$this->fIswev = $v->getErrors();
|
||||
|
||||
return null;
|
||||
} elseif (! \is_array($v->attachments)) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
$result = "\n";
|
||||
$calc = false;
|
||||
$calc = false;
|
||||
|
||||
foreach ($v->attachments as $file) {
|
||||
$data = $this->c->attachments->addFile($file);
|
||||
// костыль с конвертацией картинок из base64 в файлы
|
||||
$_POST['message'] = \preg_replace_callback(
|
||||
'%\[img\](data:[\w/.+-]*+;base64,[a-zA-Z0-9/+=]++)\[/img\]%',
|
||||
function ($matches) use ($calc) {
|
||||
$file = $this->c->Files->uploadFromLink($matches[1]);
|
||||
|
||||
if (\is_array($data)) {
|
||||
$name = $file->name();
|
||||
$calc = true;
|
||||
|
||||
if ($data['image']) {
|
||||
$result .= "[img]{$data['url']}[/img]\n"; // ={$name}
|
||||
} else {
|
||||
$result .= "[url={$data['url']}]{$name}[/url]\n";
|
||||
if (! $file instanceof Image) {
|
||||
return $this->c->Files->error() ?? 'Bad image';
|
||||
}
|
||||
|
||||
$data = $this->c->attachments->addFile($file);
|
||||
|
||||
if (\is_array($data)) {
|
||||
$calc = true;
|
||||
|
||||
return "[img]{$data['url']}[/img]";
|
||||
} else {
|
||||
return 'Bad file';
|
||||
}
|
||||
},
|
||||
(string) $v->message
|
||||
);
|
||||
|
||||
if (\is_array($v->attachments)) {
|
||||
$result = '';
|
||||
|
||||
foreach ($v->attachments as $file) {
|
||||
$data = $this->c->attachments->addFile($file);
|
||||
|
||||
if (\is_array($data)) {
|
||||
$name = $file->name();
|
||||
$calc = true;
|
||||
|
||||
if ($data['image']) {
|
||||
$result .= "\n[img]{$data['url']}[/img]"; // ={$name}
|
||||
} else {
|
||||
$result .= "\n[url={$data['url']}]{$name}[/url]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// костыль с добавление вложений в сообщение на лету
|
||||
if ('' !== $result) {
|
||||
$_POST['message'] .= $result;
|
||||
}
|
||||
}
|
||||
|
||||
if ($calc) {
|
||||
$this->c->attachments->recalculate($this->user);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,16 +132,17 @@ class Mod extends Profile
|
|||
$fields = [];
|
||||
$fields["name{$forum->id}"] = [
|
||||
'class' => ['modforum', 'name', 'depth' . $forum->depth],
|
||||
'type' => 'str',
|
||||
'type' => 'label',
|
||||
'value' => $forum->forum_name,
|
||||
'caption' => 'Forum label',
|
||||
'for' => "moderator[{$forum->id}]",
|
||||
];
|
||||
$fields["moderator[{$forum->id}]"] = [
|
||||
'class' => ['modforum', 'moderator'],
|
||||
'type' => 'checkbox',
|
||||
'value' => $forum->id,
|
||||
'checked' => isset($this->curForums[$forum->id]) && $this->curUser->isModerator($forum),
|
||||
'disabled' => ! isset($this->curForums[$forum->id]) || '' != $this->curForums[$forum->id]->redirect_url,
|
||||
'disabled' => ! isset($this->curForums[$forum->id]) || ! empty($this->curForums[$forum->id]->redirect_url),
|
||||
'caption' => 'Moderator label',
|
||||
];
|
||||
$form['sets']["forum{$forum->id}"] = [
|
||||
|
|
|
@ -52,7 +52,15 @@ class Search extends Profile
|
|||
|
||||
if ($v->validation($_POST)) {
|
||||
if (! empty($v->follow)) {
|
||||
$unfollow = \array_diff(\array_keys($this->curForums), $v->follow);
|
||||
$unfollow = [];
|
||||
|
||||
foreach ($this->curForums as $id => $forum) {
|
||||
if (empty($forum->redirect_url)) {
|
||||
$unfollow[$id] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
$unfollow = \array_diff($unfollow, $v->follow);
|
||||
|
||||
\sort($unfollow, \SORT_NUMERIC);
|
||||
|
||||
|
@ -176,16 +184,17 @@ class Search extends Profile
|
|||
$fields = [];
|
||||
$fields["name{$forum->id}"] = [
|
||||
'class' => ['modforum', 'name', 'depth' . $forum->depth],
|
||||
'type' => 'str',
|
||||
'type' => 'label',
|
||||
'value' => $forum->forum_name,
|
||||
'caption' => 'Forum label',
|
||||
'for' => "follow[{$forum->id}]",
|
||||
];
|
||||
$fields["follow[{$forum->id}]"] = [
|
||||
'class' => ['modforum', 'moderator'],
|
||||
'type' => 'checkbox',
|
||||
'value' => $forum->id,
|
||||
'checked' => ! isset($this->curUnfollowed[$forum->id]),
|
||||
'disabled' => '' != $this->curForums[$forum->id]->redirect_url,
|
||||
'disabled' => ! empty($this->curForums[$forum->id]->redirect_url),
|
||||
'caption' => 'Follow label',
|
||||
];
|
||||
$form['sets']["forum{$forum->id}"] = [
|
||||
|
|
|
@ -257,7 +257,10 @@ class View extends Profile
|
|||
];
|
||||
|
||||
if ($this->curUser->last_post > 0) {
|
||||
if (1 === $this->user->g_search) {
|
||||
if (
|
||||
1 === $this->user->g_search
|
||||
&& ! $this->user->isBot
|
||||
) {
|
||||
$fields['posts'] = [
|
||||
'class' => ['pline'],
|
||||
'type' => 'link',
|
||||
|
@ -271,6 +274,7 @@ class View extends Profile
|
|||
]
|
||||
),
|
||||
'title' => __('Show posts'),
|
||||
'rel' => 'nofollow',
|
||||
];
|
||||
$fields['topics'] = [
|
||||
'class' => ['pline'],
|
||||
|
@ -285,6 +289,7 @@ class View extends Profile
|
|||
]
|
||||
),
|
||||
'title' => __('Show topics'),
|
||||
'rel' => 'nofollow',
|
||||
];
|
||||
} elseif ($this->userRules->showPostCount) {
|
||||
$fields['posts'] = [
|
||||
|
|
|
@ -30,20 +30,11 @@ trait RegLogTrait
|
|||
|
||||
$this->c->Lang->load('admin_providers');
|
||||
|
||||
switch ($type) {
|
||||
case 'reg':
|
||||
$message = 'Sign up with %s';
|
||||
|
||||
break;
|
||||
case 'add':
|
||||
$message = 'From %s';
|
||||
|
||||
break;
|
||||
default:
|
||||
$message = 'Sign in with %s';
|
||||
|
||||
break;
|
||||
}
|
||||
$message = match ($type) {
|
||||
'reg' => 'Sign up with %s',
|
||||
'add' => 'From %s',
|
||||
default => 'Sign in with %s',
|
||||
};
|
||||
|
||||
$btns = [];
|
||||
|
||||
|
|
|
@ -501,7 +501,10 @@ class Search extends Page
|
|||
case 'topics':
|
||||
case 'topics_subscriptions':
|
||||
case 'forums_subscriptions':
|
||||
if (! isset($uid)) {
|
||||
if (
|
||||
! isset($uid)
|
||||
|| $this->user->isBot
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
196
app/Models/Pages/Sitemap.php
Normal file
196
app/Models/Pages/Sitemap.php
Normal file
|
@ -0,0 +1,196 @@
|
|||
<?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\Pages;
|
||||
|
||||
use ForkBB\Models\Page;
|
||||
use ForkBB\Models\Forum\Forum;
|
||||
use ForkBB\Models\Forum\Forums;
|
||||
use ForkBB\Models\Group\Group;
|
||||
|
||||
class Sitemap extends Page
|
||||
{
|
||||
|
||||
public array $sitemap = [];
|
||||
|
||||
/**
|
||||
* Вывод sitemap
|
||||
*/
|
||||
public function view(array $args): Page
|
||||
{
|
||||
$this->nameTpl = 'sitemap';
|
||||
$this->onlinePos = 'sitemap';
|
||||
$this->onlineDetail = null;
|
||||
|
||||
$gGroup = $this->c->groups->get(FORK_GROUP_GUEST);
|
||||
$forums = $this->c->ForumManager->init($gGroup);
|
||||
$max = 50000;
|
||||
|
||||
if (1 === $gGroup->g_read_board) {
|
||||
$result = match ($args['id']) {
|
||||
null => $this->sitemap($forums, $gGroup, $max),
|
||||
'0' => $this->sitemap0($forums, $gGroup, $max),
|
||||
'00' => $this->sitemap00($forums, $gGroup, $max),
|
||||
default => $this->sitemapN($forums, $gGroup, $max, $args['id']),
|
||||
};
|
||||
}
|
||||
|
||||
$d = \number_format(\microtime(true) - $this->c->START, 3);
|
||||
|
||||
$this->c->Log->debug("{$this->nameTpl} : {$args['id']} : time = {$d}", [
|
||||
'user' => $this->user->fLog(),
|
||||
'headers' => true,
|
||||
]);
|
||||
|
||||
if (empty($this->sitemap)) {
|
||||
return $this->c->Message->message('Bad request');
|
||||
} else {
|
||||
$this->header('Content-type', 'application/xml; charset=utf-8');
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
protected function sitemap(Forums $forums, Group $gGroup, int $max): bool
|
||||
{
|
||||
foreach ($forums->loadTree(0)->descendants as $forum) {
|
||||
if ($forum->last_post > 0) {
|
||||
$this->sitemap[$this->c->Router->link('Sitemap', ['id' => $forum->id])] = $forum->last_post;
|
||||
}
|
||||
}
|
||||
|
||||
if (1 === $gGroup->g_view_users) {
|
||||
$this->sitemap[$this->c->Router->link('Sitemap', ['id' => '0'])] = null;
|
||||
}
|
||||
|
||||
$this->sitemap[$this->c->Router->link('Sitemap', ['id' => '00'])] = null;
|
||||
|
||||
$this->nameTpl = 'sitemap_index';
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function sitemap00(Forums $forums, Group $gGroup, int $max): bool
|
||||
{
|
||||
$this->sitemap[$this->c->Router->link('Index')] = null;
|
||||
|
||||
--$max;
|
||||
|
||||
if (
|
||||
1 === $this->c->config->b_rules
|
||||
&& 1 === $this->c->config->b_regs_allow
|
||||
) {
|
||||
$this->sitemap[$this->c->Router->link('Rules')] = null;
|
||||
|
||||
--$max;
|
||||
}
|
||||
|
||||
$dtd = $this->c->config->i_disp_topics_default;
|
||||
|
||||
foreach ($forums->loadTree(0)->descendants as $forum) {
|
||||
if ($forum->last_post > 0) {
|
||||
$pages = (int) \ceil(($forum->num_topics ?: 1) / $dtd);
|
||||
$page = 1;
|
||||
|
||||
for (; $max > 0 && $page <= $pages; --$max, ++$page) {
|
||||
$this->sitemap[$this->c->Router->link(
|
||||
'Forum',
|
||||
[
|
||||
'id' => $forum->id,
|
||||
'name' => $forum->friendly,
|
||||
'page' => $page,
|
||||
]
|
||||
)] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function sitemap0(Forums $forums, Group $gGroup, int $max): bool
|
||||
{
|
||||
if (1 !== $gGroup->g_view_users) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':max' => $max,
|
||||
];
|
||||
$query = 'SELECT u.id, u.username
|
||||
FROM ::users AS u
|
||||
WHERE u.last_post!=0
|
||||
ORDER BY u.id DESC
|
||||
LIMIT ?i:max';
|
||||
|
||||
$stmt = $this->c->DB->query($query, $vars);
|
||||
|
||||
while ($cur = $stmt->fetch()) {
|
||||
$name = $this->c->Func->friendly($cur['username']);
|
||||
|
||||
$this->sitemap[$this->c->Router->link(
|
||||
'User',
|
||||
[
|
||||
'id' => $cur['id'],
|
||||
'name' => $name,
|
||||
]
|
||||
)] = null;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function sitemapN(Forums $forums, Group $gGroup, int $max, string $raw): bool
|
||||
{
|
||||
if (! \preg_match('%^[1-9]\d*$%', $raw)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$id = (int) $raw;
|
||||
$forum = $forums->get($id);
|
||||
|
||||
if (! $forum instanceof Forum) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dpd = $this->c->config->i_disp_posts_default;
|
||||
|
||||
$vars = [
|
||||
':fid' => $forum->id,
|
||||
];
|
||||
$query = 'SELECT t.id, t.subject, t.last_post, t.num_replies
|
||||
FROM ::topics AS t
|
||||
WHERE t.moved_to=0 AND t.forum_id=?i:fid
|
||||
ORDER BY t.last_post DESC';
|
||||
|
||||
$stmt = $this->c->DB->query($query, $vars);
|
||||
|
||||
while ($cur = $stmt->fetch()) {
|
||||
$name = $this->c->Func->friendly($cur['subject']);
|
||||
$page = (int) \ceil(($cur['num_replies'] + 1) / $dpd);
|
||||
$last = $cur['last_post'];
|
||||
|
||||
for (; $max > 0 && $page > 0; --$max, --$page) {
|
||||
$this->sitemap[$this->c->Router->link(
|
||||
'Topic',
|
||||
[
|
||||
'id' => $cur['id'],
|
||||
'name' => $name,
|
||||
'page' => $page,
|
||||
]
|
||||
)] = $last;
|
||||
|
||||
$last = null;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -140,8 +140,8 @@ class Topic extends Page
|
|||
'Topic',
|
||||
[
|
||||
'id' => $topic->id,
|
||||
'name' => $topic->name,
|
||||
'page' => $topic->page
|
||||
'name' => $this->c->Func->friendly($topic->name),
|
||||
'page' => $topic->page,
|
||||
]
|
||||
);
|
||||
$this->model = $topic;
|
||||
|
|
|
@ -27,7 +27,7 @@ class Userlist extends Page
|
|||
'all' => __('All users'),
|
||||
];
|
||||
|
||||
foreach ($this->c->groups->getList() as $group) {
|
||||
foreach ($this->c->groups->repository as $group) {
|
||||
if (! $group->groupGuest) {
|
||||
$list[$group->g_id] = $group->g_title;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
|||
namespace ForkBB\Models\Poll;
|
||||
|
||||
use ForkBB\Models\Action;
|
||||
use ForkBB\Models\DataModel;
|
||||
use ForkBB\Models\Forum\Forum;
|
||||
use ForkBB\Models\Poll\Poll;
|
||||
use ForkBB\Models\Topic\Topic;
|
||||
|
@ -23,7 +22,7 @@ class Delete extends Action
|
|||
/**
|
||||
* Удаление индекса
|
||||
*/
|
||||
public function delete(DataModel ...$args): void
|
||||
public function delete(Poll|Topic ...$args): void
|
||||
{
|
||||
if (empty($args)) {
|
||||
throw new InvalidArgumentException('No arguments, expected Poll(s) or Topic(s)');
|
||||
|
@ -46,8 +45,6 @@ class Delete extends Action
|
|||
|
||||
$tids[$arg->id] = $arg->id;
|
||||
$isTopic = 1;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Expected Poll(s) or Topic(s)');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
|||
namespace ForkBB\Models\Post;
|
||||
|
||||
use ForkBB\Models\Action;
|
||||
use ForkBB\Models\DataModel;
|
||||
use ForkBB\Models\Forum\Forum;
|
||||
use ForkBB\Models\Post\Post;
|
||||
use ForkBB\Models\Topic\Topic;
|
||||
|
@ -25,7 +24,7 @@ class Delete extends Action
|
|||
/**
|
||||
* Удаляет сообщение(я)
|
||||
*/
|
||||
public function delete(DataModel ...$args): void
|
||||
public function delete(Forum|Post|Topic|User ...$args): void
|
||||
{
|
||||
if (empty($args)) {
|
||||
throw new InvalidArgumentException('No arguments, expected User(s), Forum(s), Topic(s) or Post(s)');
|
||||
|
@ -85,8 +84,6 @@ class Delete extends Action
|
|||
if ($arg->poster_id > 0) {
|
||||
$uidsUpdate[$arg->poster_id] = $arg->poster_id;
|
||||
}
|
||||
} else {
|
||||
throw new InvalidArgumentException('Expected User(s), Forum(s), Topic(s) or Post(s)');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,24 +158,19 @@ class Delete extends Action
|
|||
|
||||
$uidsUpdate = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
switch ($this->c->DB->getType()) {
|
||||
case 'mysql':
|
||||
$query = 'DELETE p
|
||||
FROM ::posts AS p, ::topics AS t
|
||||
WHERE t.forum_id IN (?ai:forums) AND p.topic_id=t.id';
|
||||
$query = match ($this->c->DB->getType()) {
|
||||
'mysql' => 'DELETE p
|
||||
FROM ::posts AS p, ::topics AS t
|
||||
WHERE t.forum_id IN (?ai:forums) AND p.topic_id=t.id',
|
||||
|
||||
break;
|
||||
default:
|
||||
$query = 'DELETE
|
||||
FROM ::posts
|
||||
WHERE topic_id IN (
|
||||
SELECT id
|
||||
FROM ::topics
|
||||
WHERE forum_id IN (?ai:forums)
|
||||
)';
|
||||
|
||||
break;
|
||||
}
|
||||
default => 'DELETE
|
||||
FROM ::posts
|
||||
WHERE topic_id IN (
|
||||
SELECT id
|
||||
FROM ::topics
|
||||
WHERE forum_id IN (?ai:forums)
|
||||
)',
|
||||
};
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
}
|
||||
|
|
|
@ -11,19 +11,16 @@ declare(strict_types=1);
|
|||
namespace ForkBB\Models\Post;
|
||||
|
||||
use ForkBB\Models\Action;
|
||||
use ForkBB\Models\DataModel;
|
||||
use ForkBB\Models\Topic\Topic;
|
||||
use ForkBB\Models\Forum\Forum;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use PDO;
|
||||
|
||||
class Feed extends Action
|
||||
{
|
||||
/**
|
||||
* Загружает данные для feed
|
||||
*/
|
||||
public function Feed(DataModel $model): array
|
||||
public function Feed(Forum|Topic $model): array
|
||||
{
|
||||
if ($model instanceof Topic) {
|
||||
if (0 !== $model->moved_to) {
|
||||
|
@ -54,16 +51,28 @@ class Feed extends Action
|
|||
$vars = [
|
||||
':forums' => $ids,
|
||||
];
|
||||
$query = 'SELECT p.id as pid, p.poster as username, p.poster_id as uid, p.message as content,
|
||||
p.hide_smilies, p.posted, p.edited, t.id as tid, t.subject as topic_name, t.forum_id as fid
|
||||
$query = 'SELECT p.id
|
||||
FROM ::posts AS p
|
||||
INNER JOIN ::topics AS t ON t.id=p.topic_id
|
||||
WHERE t.forum_id IN (?ai:forums)
|
||||
ORDER BY p.id DESC
|
||||
LIMIT 50';
|
||||
|
||||
} else {
|
||||
throw new InvalidArgumentException('Expected Topic or Forum');
|
||||
$ids = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':ids' => $ids,
|
||||
];
|
||||
$query = 'SELECT p.id as pid, p.poster as username, p.poster_id as uid, p.message as content,
|
||||
p.hide_smilies, p.posted, p.edited, t.id as tid, t.subject as topic_name, t.forum_id as fid
|
||||
FROM ::posts AS p
|
||||
INNER JOIN ::topics AS t ON t.id=p.topic_id
|
||||
WHERE p.id IN (?ai:ids)
|
||||
ORDER BY p.id DESC';
|
||||
}
|
||||
|
||||
return $this->c->DB->query($query, $vars)->fetchAll();
|
||||
|
|
|
@ -194,6 +194,19 @@ class Post extends DataModel
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ссылка на страницу редактирования автора и даты
|
||||
*/
|
||||
protected function getlinkAnD(): string
|
||||
{
|
||||
return $this->c->Router->link(
|
||||
'ChangeAnD',
|
||||
[
|
||||
'id' => $this->id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Статус возможности ответа с цитированием
|
||||
*/
|
||||
|
|
|
@ -11,12 +11,10 @@ declare(strict_types=1);
|
|||
namespace ForkBB\Models\Post;
|
||||
|
||||
use ForkBB\Models\Action;
|
||||
use ForkBB\Models\Model;
|
||||
use ForkBB\Models\Post\Post;
|
||||
use ForkBB\Models\Search\Search;
|
||||
use ForkBB\Models\Topic\Topic;
|
||||
use PDO;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
class View extends Action
|
||||
|
@ -24,15 +22,8 @@ class View extends Action
|
|||
/**
|
||||
* Возвращает список сообщений
|
||||
*/
|
||||
public function view(Model $arg, bool $review = false): array
|
||||
public function view(Search|Topic $arg, bool $review = false): array
|
||||
{
|
||||
if (
|
||||
! $arg instanceof Topic
|
||||
&& ! $arg instanceof Search
|
||||
) {
|
||||
throw new InvalidArgumentException('Expected Topic or Search');
|
||||
}
|
||||
|
||||
if (
|
||||
empty($arg->idsList)
|
||||
|| ! \is_array($arg->idsList)
|
||||
|
|
|
@ -267,7 +267,9 @@ abstract class Driver extends Model
|
|||
break;
|
||||
}
|
||||
|
||||
\curl_setopt($ch, \CURLOPT_MAXREDIRS, 10);
|
||||
\curl_setopt($ch, \CURLOPT_PROTOCOLS, \CURLPROTO_HTTPS | \CURLPROTO_HTTP);
|
||||
\curl_setopt($ch, \CURLOPT_REDIR_PROTOCOLS, \CURLPROTO_HTTPS);
|
||||
\curl_setopt($ch, \CURLOPT_MAXREDIRS, 5);
|
||||
\curl_setopt($ch, \CURLOPT_TIMEOUT, 10);
|
||||
\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true);
|
||||
\curl_setopt($ch, \CURLOPT_HEADER, false);
|
||||
|
|
|
@ -83,14 +83,6 @@ class Providers extends Manager
|
|||
return $driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возращает список созданных провайдеров
|
||||
*/
|
||||
public function list(): array
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возращает список имён активных провайдеров
|
||||
*/
|
||||
|
|
|
@ -31,49 +31,66 @@ class ActionP extends Method
|
|||
return [];
|
||||
}
|
||||
|
||||
$query = null;
|
||||
|
||||
switch ($action) {
|
||||
case 'search':
|
||||
$list = $this->model->queryIds;
|
||||
|
||||
$this->model->numPages = (int) \ceil(($this->model->count($list) ?: 1) / $this->c->user->disp_posts);
|
||||
|
||||
break;
|
||||
case 'posts':
|
||||
$query = 'SELECT p.id
|
||||
$vars = [
|
||||
':forums' => $forums,
|
||||
':uid' => $uid,
|
||||
];
|
||||
$query = 'SELECT COUNT(p.id)
|
||||
FROM ::posts AS p
|
||||
INNER JOIN ::topics AS t ON t.id=p.topic_id
|
||||
WHERE p.poster_id=?i:uid AND t.forum_id IN (?ai:forums)
|
||||
ORDER BY p.posted DESC';
|
||||
WHERE p.poster_id=?i:uid AND t.forum_id IN (?ai:forums)';
|
||||
|
||||
$count = (int) $this->c->DB->query($query, $vars)->fetchColumn();
|
||||
|
||||
$this->model->numPages = (int) \ceil(($count ?: 1) / $this->c->user->disp_posts);
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException('Unknown action: ' . $action);
|
||||
}
|
||||
|
||||
if (null !== $query) {
|
||||
$vars = [
|
||||
':forums' => $forums,
|
||||
':uid' => $uid,
|
||||
];
|
||||
|
||||
$list = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
$this->model->numPages = (int) \ceil(($this->model->count($list) ?: 1) / $this->c->user->disp_posts);
|
||||
|
||||
// нет такой страницы в результате поиска
|
||||
if (! $this->model->hasPage()) {
|
||||
return false;
|
||||
// результат пуст
|
||||
} elseif (empty($list)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$this->model->idsList = $this->model->slice(
|
||||
$list,
|
||||
($this->model->page - 1) * $this->c->user->disp_posts,
|
||||
(int) $this->c->user->disp_posts
|
||||
);
|
||||
switch ($action) {
|
||||
case 'search':
|
||||
// результат пуст
|
||||
if (empty($list)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$this->model->idsList = $this->model->slice(
|
||||
$list,
|
||||
($this->model->page - 1) * $this->c->user->disp_posts,
|
||||
(int) $this->c->user->disp_posts
|
||||
);
|
||||
|
||||
break;
|
||||
case 'posts':
|
||||
$vars[':offset'] = ($this->model->page - 1) * $this->c->user->disp_posts;
|
||||
$vars[':rows'] = (int) $this->c->user->disp_posts;
|
||||
|
||||
$query = 'SELECT p.id
|
||||
FROM ::posts AS p
|
||||
INNER JOIN ::topics AS t ON t.id=p.topic_id
|
||||
WHERE p.poster_id=?i:uid AND t.forum_id IN (?ai:forums)
|
||||
ORDER BY p.posted DESC
|
||||
LIMIT ?i:rows OFFSET ?i:offset';
|
||||
|
||||
$this->model->idsList = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $this->c->posts->view($this->model);
|
||||
}
|
||||
|
|
|
@ -78,8 +78,8 @@ class ActionT extends Method
|
|||
*/
|
||||
// упрощенный запрос для больших форумов, дополнительная обработка ниже
|
||||
$query = 'SELECT DISTINCT t.id, t.last_post
|
||||
FROM forum_topics AS t
|
||||
INNER JOIN forum_posts AS p ON t.id=p.topic_id
|
||||
FROM ::topics AS t
|
||||
INNER JOIN ::posts AS p ON t.id=p.topic_id
|
||||
WHERE t.forum_id IN (?ai:forums) AND t.moved_to=0 AND p.poster_id=?i:uid';
|
||||
|
||||
break;
|
||||
|
|
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
|||
namespace ForkBB\Models\Search;
|
||||
|
||||
use ForkBB\Models\Method;
|
||||
use ForkBB\Models\DataModel;
|
||||
use ForkBB\Models\Forum\Forum;
|
||||
use ForkBB\Models\Post\Post;
|
||||
use ForkBB\Models\Topic\Topic;
|
||||
|
@ -24,7 +23,7 @@ class Delete extends Method
|
|||
/**
|
||||
* Удаление индекса
|
||||
*/
|
||||
public function delete(DataModel ...$args): void
|
||||
public function delete(Forum|Post|Topic|User ...$args): void
|
||||
{
|
||||
if (empty($args)) {
|
||||
throw new InvalidArgumentException('No arguments, expected User(s), Forum(s), Topic(s) or Post(s)');
|
||||
|
@ -74,8 +73,6 @@ class Delete extends Method
|
|||
|
||||
$pids[$arg->id] = $arg->id;
|
||||
$isPost = 1;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Expected User(s), Forum(s), Topic(s) or Post(s)');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,76 +84,58 @@ class Delete extends Method
|
|||
$vars = [
|
||||
':users' => $uids,
|
||||
];
|
||||
$query = match ($this->c->DB->getType()) {
|
||||
'mysql' => 'DELETE sm
|
||||
FROM ::search_matches AS sm, ::posts AS p
|
||||
WHERE p.poster_id IN (?ai:users) AND sm.post_id=p.id',
|
||||
|
||||
switch ($this->c->DB->getType()) {
|
||||
case 'mysql':
|
||||
$query = 'DELETE sm
|
||||
FROM ::search_matches AS sm, ::posts AS p
|
||||
WHERE p.poster_id IN (?ai:users) AND sm.post_id=p.id';
|
||||
|
||||
break;
|
||||
default:
|
||||
$query = 'DELETE
|
||||
FROM ::search_matches
|
||||
WHERE post_id IN (
|
||||
SELECT p.id
|
||||
FROM ::posts AS p
|
||||
WHERE p.poster_id IN (?ai:users)
|
||||
)';
|
||||
|
||||
break;
|
||||
}
|
||||
default => 'DELETE
|
||||
FROM ::search_matches
|
||||
WHERE post_id IN (
|
||||
SELECT p.id
|
||||
FROM ::posts AS p
|
||||
WHERE p.poster_id IN (?ai:users)
|
||||
)',
|
||||
};
|
||||
}
|
||||
|
||||
if ($fids) {
|
||||
$vars = [
|
||||
':forums' => $fids,
|
||||
];
|
||||
$query = match ($this->c->DB->getType()) {
|
||||
'mysql' => 'DELETE sm
|
||||
FROM ::search_matches AS sm, ::posts AS p, ::topics AS t
|
||||
WHERE t.forum_id IN (?ai:forums) AND p.topic_id=t.id AND sm.post_id=p.id',
|
||||
|
||||
switch ($this->c->DB->getType()) {
|
||||
case 'mysql':
|
||||
$query = 'DELETE sm
|
||||
FROM ::search_matches AS sm, ::posts AS p, ::topics AS t
|
||||
WHERE t.forum_id IN (?ai:forums) AND p.topic_id=t.id AND sm.post_id=p.id';
|
||||
|
||||
break;
|
||||
default:
|
||||
$query = 'DELETE
|
||||
FROM ::search_matches
|
||||
WHERE post_id IN (
|
||||
SELECT p.id
|
||||
FROM ::posts AS p
|
||||
INNER JOIN ::topics AS t ON t.id=p.topic_id
|
||||
WHERE t.forum_id IN (?ai:forums)
|
||||
)';
|
||||
|
||||
break;
|
||||
}
|
||||
default => 'DELETE
|
||||
FROM ::search_matches
|
||||
WHERE post_id IN (
|
||||
SELECT p.id
|
||||
FROM ::posts AS p
|
||||
INNER JOIN ::topics AS t ON t.id=p.topic_id
|
||||
WHERE t.forum_id IN (?ai:forums)
|
||||
)',
|
||||
};
|
||||
}
|
||||
|
||||
if ($tids) {
|
||||
$vars = [
|
||||
':topics' => $tids,
|
||||
];
|
||||
$query = match ($this->c->DB->getType()) {
|
||||
'mysql' => 'DELETE sm
|
||||
FROM ::search_matches AS sm, ::posts AS p
|
||||
WHERE p.topic_id IN (?ai:topics) AND sm.post_id=p.id',
|
||||
|
||||
switch ($this->c->DB->getType()) {
|
||||
case 'mysql':
|
||||
$query = 'DELETE sm
|
||||
FROM ::search_matches AS sm, ::posts AS p
|
||||
WHERE p.topic_id IN (?ai:topics) AND sm.post_id=p.id';
|
||||
|
||||
break;
|
||||
default:
|
||||
$query = 'DELETE
|
||||
FROM ::search_matches
|
||||
WHERE post_id IN (
|
||||
SELECT p.id
|
||||
FROM ::posts AS p
|
||||
WHERE p.topic_id IN (?ai:topics)
|
||||
)';
|
||||
|
||||
break;
|
||||
}
|
||||
default => 'DELETE
|
||||
FROM ::search_matches
|
||||
WHERE post_id IN (
|
||||
SELECT p.id
|
||||
FROM ::posts AS p
|
||||
WHERE p.topic_id IN (?ai:topics)
|
||||
)',
|
||||
};
|
||||
}
|
||||
|
||||
if ($pids) {
|
||||
|
|
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
|||
namespace ForkBB\Models\Topic;
|
||||
|
||||
use ForkBB\Models\Action;
|
||||
use ForkBB\Models\DataModel;
|
||||
use ForkBB\Models\Forum\Forum;
|
||||
use ForkBB\Models\Topic\Topic;
|
||||
use ForkBB\Models\User\User;
|
||||
|
@ -24,7 +23,7 @@ class Delete extends Action
|
|||
/**
|
||||
* Удаляет тему(ы)
|
||||
*/
|
||||
public function delete(DataModel ...$args): void
|
||||
public function delete(Forum|Topic|User ...$args): void
|
||||
{
|
||||
if (empty($args)) {
|
||||
throw new InvalidArgumentException('No arguments, expected User(s), Forum(s) or Topic(s)');
|
||||
|
@ -69,8 +68,6 @@ class Delete extends Action
|
|||
$topics[$arg->id] = $arg;
|
||||
$parents[$arg->parent->id] = $arg->parent;
|
||||
$isTopic = 1;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Expected User(s), Forum(s) or Topic(s)');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,14 +58,14 @@ class Load extends Action
|
|||
];
|
||||
$query = $this->getSql('t.id=?i:tid', true);
|
||||
|
||||
$data = $this->c->DB->query($query, $vars)->fetch();
|
||||
$row = $this->c->DB->query($query, $vars)->fetch();
|
||||
|
||||
// тема отсутствует или недоступна
|
||||
if (empty($data)) {
|
||||
if (empty($row)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$topic = $this->manager->create($data);
|
||||
$topic = $this->manager->create($row);
|
||||
$forum = $topic->parent;
|
||||
|
||||
if ($forum instanceof Forum) {
|
||||
|
@ -75,8 +75,6 @@ class Load extends Action
|
|||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,4 +115,36 @@ class Load extends Action
|
|||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает список тем при открытие которых идет переадресация на тему c указанным id
|
||||
*/
|
||||
public function loadLinks(int $id): array
|
||||
{
|
||||
if ($id < 1) {
|
||||
throw new InvalidArgumentException('Expected a positive topic id');
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':id' => $id,
|
||||
];
|
||||
$query = 'SELECT *
|
||||
FROM ::topics
|
||||
WHERE moved_to=?i:id
|
||||
ORDER BY id';
|
||||
|
||||
$stmt = $this->c->DB->query($query, $vars);
|
||||
|
||||
$result = [];
|
||||
|
||||
while ($row = $stmt->fetch()) {
|
||||
$topic = $this->manager->create($row);
|
||||
|
||||
if ($topic->parent instanceof Forum) {
|
||||
$result[] = $topic;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ class Move extends Action
|
|||
if ($topic->parent === $toForum) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($redirect) {
|
||||
$rTopic = $this->c->topics->create();
|
||||
$rTopic->poster = $topic->poster;
|
||||
|
@ -40,7 +41,7 @@ class Move extends Action
|
|||
// $rTopic->last_post_id = $topic->last_post_id;
|
||||
// $rTopic->last_poster = $topic->last_poster;
|
||||
// $rTopic->last_poster_id = $topic->last_poster_id;
|
||||
$rTopic->moved_to = $topic->id;
|
||||
$rTopic->moved_to = $topic->moved_to ?: $topic->id;
|
||||
$rTopic->forum_id = $topic->forum_id;
|
||||
|
||||
$this->c->topics->insert($rTopic);
|
||||
|
|
|
@ -101,7 +101,7 @@ class Topic extends DataModel
|
|||
'Topic',
|
||||
[
|
||||
'id' => $this->moved_to ?: $this->id,
|
||||
'name' => $this->name,
|
||||
'name' => $this->c->Func->friendly($this->name),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -329,7 +329,7 @@ class Topic extends DataModel
|
|||
'Topic',
|
||||
[
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'name' => $this->c->Func->friendly($this->name),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ class Topics extends Manager
|
|||
return $this->get($id);
|
||||
} else {
|
||||
$topic = $this->Load->load($id);
|
||||
|
||||
$this->set($id, $topic);
|
||||
|
||||
return $topic;
|
||||
|
@ -57,6 +58,7 @@ class Topics extends Manager
|
|||
} else {
|
||||
$result[$id] = null;
|
||||
$data[] = $id;
|
||||
|
||||
$this->set($id, null);
|
||||
}
|
||||
}
|
||||
|
@ -68,6 +70,28 @@ class Topics extends Manager
|
|||
foreach ($this->Load->loadByIds($data, $full) as $topic) {
|
||||
if ($topic instanceof Topic) {
|
||||
$result[$topic->id] = $topic;
|
||||
|
||||
$this->set($topic->id, $topic);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает список тем при открытие которых идет переадресация на текущую тему
|
||||
*/
|
||||
public function loadLinks(int|Topic $arg): array
|
||||
{
|
||||
$id = \is_int($arg) ? $arg : (int) $arg->id;
|
||||
$result = [];
|
||||
|
||||
foreach ($this->Load->loadLinks($id) as $topic) {
|
||||
if ($this->isset($topic->id)) {
|
||||
$result[$topic->id] = $this->get($topic->id);
|
||||
} else {
|
||||
$result[$topic->id] = $topic;
|
||||
|
||||
$this->set($topic->id, $topic);
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +113,7 @@ class Topics extends Manager
|
|||
public function insert(Topic $topic): int
|
||||
{
|
||||
$id = $this->Save->insert($topic);
|
||||
|
||||
$this->set($id, $topic);
|
||||
|
||||
return $id;
|
||||
|
|
|
@ -11,12 +11,10 @@ declare(strict_types=1);
|
|||
namespace ForkBB\Models\Topic;
|
||||
|
||||
use ForkBB\Models\Action;
|
||||
use ForkBB\Models\Model;
|
||||
use ForkBB\Models\Forum\Forum;
|
||||
use ForkBB\Models\Search\Search;
|
||||
use ForkBB\Models\Topic\Topic;
|
||||
use PDO;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
class View extends Action
|
||||
|
@ -24,14 +22,12 @@ class View extends Action
|
|||
/**
|
||||
* Возвращает список тем
|
||||
*/
|
||||
public function view(Model $arg): array
|
||||
public function view(Forum|Search $arg): array
|
||||
{
|
||||
if ($arg instanceof Forum) {
|
||||
$full = false;
|
||||
} elseif ($arg instanceof Search) {
|
||||
$full = true;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Expected Forum or Search');
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
|
@ -168,7 +168,7 @@ class User extends DataModel
|
|||
'User',
|
||||
[
|
||||
'id' => $this->id,
|
||||
'name' => $this->username,
|
||||
'name' => $this->c->Func->friendly($this->username),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -42,8 +42,7 @@ define('FORK_GEN_FEM', 2);
|
|||
|
||||
define('FORK_JSON_ENCODE', \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR);
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$loader = require __DIR__ . '/../vendor/autoload.php';
|
||||
$errorHandler = new ErrorHandler();
|
||||
|
||||
if (\is_file(__DIR__ . '/config/main.php')) {
|
||||
|
@ -54,6 +53,8 @@ if (\is_file(__DIR__ . '/config/main.php')) {
|
|||
throw new RuntimeException('Application is not configured');
|
||||
}
|
||||
|
||||
$c->autoloader = $loader;
|
||||
|
||||
$errorHandler->setContainer($c);
|
||||
|
||||
require __DIR__ . '/functions.php';
|
||||
|
@ -69,7 +70,7 @@ if (
|
|||
$c->BASE_URL = \str_replace('https://', 'http://', $c->BASE_URL);
|
||||
}
|
||||
|
||||
$c->FORK_REVISION = 69;
|
||||
$c->FORK_REVISION = 73;
|
||||
$c->START = $forkStart;
|
||||
$c->PUBLIC_URL = $c->BASE_URL . $forkPublicPrefix;
|
||||
|
||||
|
|
0
app/config/ext/.gitkeep
Normal file
0
app/config/ext/.gitkeep
Normal file
|
@ -60,6 +60,12 @@ return [
|
|||
],
|
||||
'DATE_FORMATS' => ['Y-m-d', 'd M Y', 'Y-m-d', 'Y-d-m', 'd-m-Y', 'm-d-Y', 'M j Y', 'jS M Y'],
|
||||
'TIME_FORMATS' => ['H:i:s', 'H:i', 'H:i:s', 'H:i', 'g:i:s a', 'g:i a'],
|
||||
'FRIENDLY_URL' => [
|
||||
'lowercase' => true,
|
||||
'translit' => true, // 'Any-Latin;Latin-ASCII;',
|
||||
'WtoHyphen' => true,
|
||||
'file' => 'translit.default.php',
|
||||
],
|
||||
|
||||
'forConfig' => [
|
||||
'o_default_lang' => 'en',
|
||||
|
|
|
@ -42,7 +42,7 @@ return [
|
|||
],
|
||||
// Разрешенные атрибуты тегов / Allowed tag attributes
|
||||
'cfgAllowTagParams' => [
|
||||
['a', ['class', 'title', 'href']],
|
||||
['a', ['class', 'title', 'href', 'rel']],
|
||||
['abbr', ['class']],
|
||||
['address', ['class']],
|
||||
['article', ['class']],
|
||||
|
@ -110,7 +110,7 @@ return [
|
|||
// [тег, атрибут, значение, перезапись существующего атрибута]
|
||||
// [tag, attribute, value, overwrite existing attribute]
|
||||
'cfgSetTagParamDefault' => [
|
||||
['a', 'rel', 'ugc', true],
|
||||
// ['a', 'rel', 'ugc', false],
|
||||
['img', 'alt', 'image', false],
|
||||
['img', 'loading', 'lazy', true],
|
||||
],
|
||||
|
|
|
@ -74,6 +74,12 @@ return [
|
|||
],
|
||||
'DATE_FORMATS' => ['Y-m-d', 'd M Y', 'Y-m-d', 'Y-d-m', 'd-m-Y', 'm-d-Y', 'M j Y', 'jS M Y'],
|
||||
'TIME_FORMATS' => ['H:i:s', 'H:i', 'H:i:s', 'H:i', 'g:i:s a', 'g:i a'],
|
||||
'FRIENDLY_URL' => [
|
||||
'lowercase' => true,
|
||||
'translit' => true, // 'Any-Latin;Latin-ASCII;',
|
||||
'WtoHyphen' => true,
|
||||
'file' => 'translit.default.php',
|
||||
],
|
||||
|
||||
'shared' => [
|
||||
'%DIR_ROOT%' => \realpath(__DIR__ . '/../..'),
|
||||
|
@ -84,6 +90,7 @@ return [
|
|||
'%DIR_LANG%' => '%DIR_APP%/lang',
|
||||
'%DIR_LOG%' => '%DIR_APP%/log',
|
||||
'%DIR_VIEWS%' => '%DIR_APP%/templates',
|
||||
'%DIR_EXT%' => '%DIR_ROOT%/ext',
|
||||
|
||||
'DB' => [
|
||||
'class' => \ForkBB\Core\DB::class,
|
||||
|
@ -109,6 +116,7 @@ return [
|
|||
'cache' => '%DIR_CACHE%',
|
||||
'defaultDir' => '%DIR_VIEWS%/_default',
|
||||
'userDir' => '%DIR_VIEWS%/_user',
|
||||
'preFile' => '%DIR_CONFIG%/ext/pre.php',
|
||||
],
|
||||
],
|
||||
'Router' => [
|
||||
|
@ -185,6 +193,7 @@ return [
|
|||
],
|
||||
'providerUser' => \ForkBB\Models\ProviderUser\ProviderUser::class,
|
||||
'attachments' => \ForkBB\Models\Attachment\Attachments::class,
|
||||
'extensions' => '@ExtensionManager:init',
|
||||
|
||||
'Csrf' => [
|
||||
'class' => \ForkBB\Core\Csrf::class,
|
||||
|
@ -363,6 +372,7 @@ return [
|
|||
'Ban' => \ForkBB\Models\Pages\Ban::class,
|
||||
'Debug' => \ForkBB\Models\Pages\Debug::class,
|
||||
'Misc' => \ForkBB\Models\Pages\Misc::class,
|
||||
'Sitemap' => \ForkBB\Models\Pages\Sitemap::class,
|
||||
'Moderate' => \ForkBB\Models\Pages\Moderate::class,
|
||||
'Report' => \ForkBB\Models\Pages\Report::class,
|
||||
'Email' => \ForkBB\Models\Pages\Email::class,
|
||||
|
@ -409,6 +419,7 @@ return [
|
|||
'AdminLogs' => \ForkBB\Models\Pages\Admin\Logs::class,
|
||||
'AdminUploads' => \ForkBB\Models\Pages\Admin\Uploads::class,
|
||||
'AdminAntispam' => \ForkBB\Models\Pages\Admin\Antispam::class,
|
||||
'AdminExtensions' => \ForkBB\Models\Pages\Admin\Extensions::class,
|
||||
|
||||
'AdminListModel' => \ForkBB\Models\AdminList\AdminList::class,
|
||||
'BanListModel' => \ForkBB\Models\BanList\BanList::class,
|
||||
|
@ -417,6 +428,8 @@ return [
|
|||
'CensorshipModel' => \ForkBB\Models\Censorship\Censorship::class,
|
||||
'ConfigModel' => \ForkBB\Models\Config\Config::class,
|
||||
'DBMapModel' => \ForkBB\Models\DBMap\DBMap::class,
|
||||
'ExtensionModel' => \ForkBB\Models\Extension\Extension::class,
|
||||
'ExtensionManager' => \ForkBB\Models\Extension\Extensions::class,
|
||||
'ForumModel' => \ForkBB\Models\Forum\Forum::class,
|
||||
'ForumManager' => \ForkBB\Models\Forum\Forums::class,
|
||||
'GroupModel' => \ForkBB\Models\Group\Group::class,
|
||||
|
|
1121
app/config/translit.default.php
Normal file
1121
app/config/translit.default.php
Normal file
File diff suppressed because it is too large
Load diff
|
@ -11,8 +11,6 @@ declare(strict_types=1);
|
|||
namespace ForkBB;
|
||||
|
||||
use ForkBB\Core\Container;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
|
@ -105,13 +103,7 @@ function dt(int $arg, bool $dateOnly = false, string $dateFormat = null, string
|
|||
}
|
||||
|
||||
if (null === $offset) {
|
||||
if (\in_array($c->user->timezone, DateTimeZone::listIdentifiers(), true)) {
|
||||
$dateTimeZone = new DateTimeZone($c->user->timezone);
|
||||
$dateTime = new DateTime('now', $dateTimeZone);
|
||||
$offset = $dateTime->getOffset();
|
||||
} else {
|
||||
$offset = 0;
|
||||
}
|
||||
$offset = $c->Func->offset();
|
||||
}
|
||||
|
||||
$arg += $offset;
|
||||
|
|
|
@ -86,3 +86,6 @@ msgstr "Antispam"
|
|||
|
||||
msgid "Maintenance only"
|
||||
msgstr "Available only in maintenance mode."
|
||||
|
||||
msgid "Extensions"
|
||||
msgstr "Extensions"
|
||||
|
|
133
app/lang/en/admin_extensions.po
Normal file
133
app/lang/en/admin_extensions.po
Normal file
|
@ -0,0 +1,133 @@
|
|||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Project-Id-Version: ForkBB\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: en\n"
|
||||
|
||||
msgid "Details"
|
||||
msgstr "Details:"
|
||||
|
||||
msgid "Name"
|
||||
msgstr "Name of package"
|
||||
|
||||
msgid "Description"
|
||||
msgstr "Description"
|
||||
|
||||
msgid "Release date"
|
||||
msgstr "Release date"
|
||||
|
||||
msgid "Homepage"
|
||||
msgstr "Homepage"
|
||||
|
||||
msgid "Licence"
|
||||
msgstr "Licence"
|
||||
|
||||
msgid "Requirements"
|
||||
msgstr "Requirements:"
|
||||
|
||||
msgid "Authors"
|
||||
msgstr "Author(s):"
|
||||
|
||||
msgid "php"
|
||||
msgstr "PHP version"
|
||||
|
||||
msgid "forkbb"
|
||||
msgstr "ForkBB revision"
|
||||
|
||||
msgid "Not installed"
|
||||
msgstr "Not installed"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Disabled"
|
||||
|
||||
msgid "Disabled, package changed"
|
||||
msgstr "Disabled, package changed"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "Enabled"
|
||||
|
||||
msgid "Enabled, package changed"
|
||||
msgstr "Enabled, but package changed!"
|
||||
|
||||
msgid "Crash"
|
||||
msgstr "Crash, package not found!"
|
||||
|
||||
msgid "Install_"
|
||||
msgstr "Install"
|
||||
|
||||
msgid "Uninstall_"
|
||||
msgstr "Uninstall"
|
||||
|
||||
msgid "Enable_"
|
||||
msgstr "Enable"
|
||||
|
||||
msgid "Disable_"
|
||||
msgstr "Disable"
|
||||
|
||||
msgid "Update_"
|
||||
msgstr "Update"
|
||||
|
||||
msgid "Downdate_"
|
||||
msgstr "Downdate"
|
||||
|
||||
msgid "Package version"
|
||||
msgstr "Package version"
|
||||
|
||||
msgid "Extension not found"
|
||||
msgstr "Extension not found."
|
||||
|
||||
msgid "Invalid action"
|
||||
msgstr "Invalid action."
|
||||
|
||||
msgid "Redirect install"
|
||||
msgstr "The extension is installed."
|
||||
|
||||
msgid "Redirect uninstall"
|
||||
msgstr "The extension has been uninstalled."
|
||||
|
||||
msgid "Redirect update"
|
||||
msgstr "The extension has been updated."
|
||||
|
||||
msgid "Redirect downdate"
|
||||
msgstr "The extension version has been downgraded."
|
||||
|
||||
msgid "Redirect enable"
|
||||
msgstr "The extension is enabled."
|
||||
|
||||
msgid "Redirect disable"
|
||||
msgstr "The extension is disabled."
|
||||
|
||||
msgid "Invalid template type"
|
||||
msgstr "Invalid template type."
|
||||
|
||||
msgid "PRE name not found"
|
||||
msgstr "PRE name not found."
|
||||
|
||||
msgid "Template file '%s' not found"
|
||||
msgstr "Template file '%s' not found."
|
||||
|
||||
msgid "An error occurred in updateCommon"
|
||||
msgstr "An error occurred in updateCommon."
|
||||
|
||||
msgid "Empty"
|
||||
msgstr "Empty"
|
||||
|
||||
msgid "Invalid symlink type"
|
||||
msgstr "Invalid symlink type."
|
||||
|
||||
msgid "Bad symlink"
|
||||
msgstr "Bad symlink."
|
||||
|
||||
msgid "Target '%s' not found"
|
||||
msgstr "Target '%s' not found."
|
||||
|
||||
msgid "Link '%s' already exists"
|
||||
msgstr "Link '%s' already exists."
|
|
@ -134,3 +134,9 @@ msgstr "If YES, then new messages from users in this forum will increase their c
|
|||
|
||||
msgid "<span></span>"
|
||||
msgstr "<span></span>"
|
||||
|
||||
msgid "Friendly name label"
|
||||
msgstr "Name for URL"
|
||||
|
||||
msgid "Friendly name help"
|
||||
msgstr "A string identifier on the basis of which the url to this forum will be generated. May consist of Latin letters, numbers, underscores and hyphens."
|
||||
|
|
|
@ -270,3 +270,27 @@ msgstr "First, please <a href=\"%s\">confirm</a> your email address."
|
|||
|
||||
msgid "Poll cannot be attached"
|
||||
msgstr "Topic with a poll cannot be attached to another topic."
|
||||
|
||||
msgid "Link btn"
|
||||
msgstr "Redirect"
|
||||
|
||||
msgid "Only one topic is permissible"
|
||||
msgstr "Only one topic should be selected to manage links to it."
|
||||
|
||||
msgid "Control of redirects title"
|
||||
msgstr "Control of redirects topics"
|
||||
|
||||
msgid "Need full topic for this operation"
|
||||
msgstr "You need to choose a full-fledged topic for this operation."
|
||||
|
||||
msgid "Forum label"
|
||||
msgstr "Forum"
|
||||
|
||||
msgid "Redir label"
|
||||
msgstr "Set redirect topic"
|
||||
|
||||
msgid "Change btn"
|
||||
msgstr "Change"
|
||||
|
||||
msgid "Redirects changed redirect"
|
||||
msgstr "Redirects topics changed."
|
||||
|
|
|
@ -83,3 +83,24 @@ msgstr "%1$s (%2$s max size)"
|
|||
|
||||
msgid "Attachments"
|
||||
msgstr "Attachments"
|
||||
|
||||
msgid "Change author and date"
|
||||
msgstr "Change author and date"
|
||||
|
||||
msgid "Change"
|
||||
msgstr "Change"
|
||||
|
||||
msgid "Posted"
|
||||
msgstr "Posted"
|
||||
|
||||
msgid "Change AnD topic"
|
||||
msgstr "Change author of topic and its date"
|
||||
|
||||
msgid "Change AnD post"
|
||||
msgstr "Change author of post and its date"
|
||||
|
||||
msgid "User %s does not exist"
|
||||
msgstr "User '%s' does not exist."
|
||||
|
||||
msgid "Change redirect"
|
||||
msgstr "Changes done."
|
||||
|
|
|
@ -127,3 +127,6 @@ msgstr "%1$s (%2$s max size)"
|
|||
|
||||
msgid "Attachments"
|
||||
msgstr "Attachments"
|
||||
|
||||
msgid "Change author and date"
|
||||
msgstr "Change author and date"
|
||||
|
|
|
@ -218,3 +218,6 @@ msgstr "Invalid passphrase."
|
|||
|
||||
msgid "Javascript disabled or bot"
|
||||
msgstr "Your browser most likely has javascript disabled or you are a robot :)"
|
||||
|
||||
msgid "The :alias contains time before start of Unix"
|
||||
msgstr "The :alias field contains the time before the start of the Unix epoch."
|
||||
|
|
|
@ -86,3 +86,6 @@ msgstr "Антиспам"
|
|||
|
||||
msgid "Maintenance only"
|
||||
msgstr "Доступно только в режиме обслуживания."
|
||||
|
||||
msgid "Extensions"
|
||||
msgstr "Расширения"
|
||||
|
|
133
app/lang/ru/admin_extensions.po
Normal file
133
app/lang/ru/admin_extensions.po
Normal file
|
@ -0,0 +1,133 @@
|
|||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"Project-Id-Version: ForkBB\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: ru\n"
|
||||
|
||||
msgid "Details"
|
||||
msgstr "Подробности:"
|
||||
|
||||
msgid "Name"
|
||||
msgstr "Имя пакета"
|
||||
|
||||
msgid "Description"
|
||||
msgstr "Описание"
|
||||
|
||||
msgid "Release date"
|
||||
msgstr "Дата выпуска"
|
||||
|
||||
msgid "Homepage"
|
||||
msgstr "Домашняя страница"
|
||||
|
||||
msgid "Licence"
|
||||
msgstr "Лицензия"
|
||||
|
||||
msgid "Requirements"
|
||||
msgstr "Требования:"
|
||||
|
||||
msgid "Authors"
|
||||
msgstr "Автор(ы):"
|
||||
|
||||
msgid "php"
|
||||
msgstr "Версия PHP"
|
||||
|
||||
msgid "forkbb"
|
||||
msgstr "Ревизия ForkBB"
|
||||
|
||||
msgid "Not installed"
|
||||
msgstr "Не установлено"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Выключено"
|
||||
|
||||
msgid "Disabled, package changed"
|
||||
msgstr "Выключено, пакет изменен"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "Включено"
|
||||
|
||||
msgid "Enabled, package changed"
|
||||
msgstr "Включено, но пакет изменен!"
|
||||
|
||||
msgid "Crash"
|
||||
msgstr "Сломано, пакет не найден!"
|
||||
|
||||
msgid "Install_"
|
||||
msgstr "Установить"
|
||||
|
||||
msgid "Uninstall_"
|
||||
msgstr "Удалить"
|
||||
|
||||
msgid "Enable_"
|
||||
msgstr "Включить"
|
||||
|
||||
msgid "Disable_"
|
||||
msgstr "Выключить"
|
||||
|
||||
msgid "Update_"
|
||||
msgstr "Обновить"
|
||||
|
||||
msgid "Downdate_"
|
||||
msgstr "Откатить"
|
||||
|
||||
msgid "Package version"
|
||||
msgstr "Версия пакета"
|
||||
|
||||
msgid "Extension not found"
|
||||
msgstr "Расширение не найдено."
|
||||
|
||||
msgid "Invalid action"
|
||||
msgstr "Недопустимое действие."
|
||||
|
||||
msgid "Redirect install"
|
||||
msgstr "Расширение установлено."
|
||||
|
||||
msgid "Redirect uninstall"
|
||||
msgstr "Расширение деинсталлировано."
|
||||
|
||||
msgid "Redirect update"
|
||||
msgstr "Расширение обновлено."
|
||||
|
||||
msgid "Redirect downdate"
|
||||
msgstr "Версия расширения понижена."
|
||||
|
||||
msgid "Redirect enable"
|
||||
msgstr "Расширение включено."
|
||||
|
||||
msgid "Redirect disable"
|
||||
msgstr "Расширение выключено."
|
||||
|
||||
msgid "Invalid template type"
|
||||
msgstr "Неверный тип шаблона."
|
||||
|
||||
msgid "PRE name not found"
|
||||
msgstr "PRE-имя не найдено."
|
||||
|
||||
msgid "Template file '%s' not found"
|
||||
msgstr "Файл шаблона '%s' не найден."
|
||||
|
||||
msgid "An error occurred in updateCommon"
|
||||
msgstr "Возникла ошибка в updateCommon."
|
||||
|
||||
msgid "Empty"
|
||||
msgstr "Пусто"
|
||||
|
||||
msgid "Invalid symlink type"
|
||||
msgstr "Неверный тип символической ссылки."
|
||||
|
||||
msgid "Bad symlink"
|
||||
msgstr "Плохая символическая ссылка."
|
||||
|
||||
msgid "Target '%s' not found"
|
||||
msgstr "Target '%s' отсутствует."
|
||||
|
||||
msgid "Link '%s' already exists"
|
||||
msgstr "Link '%s' уже существует."
|
|
@ -134,3 +134,9 @@ msgstr "Если ДА, то новые сообщения пользовател
|
|||
|
||||
msgid "<span></span>"
|
||||
msgstr "<span></span>"
|
||||
|
||||
msgid "Friendly name label"
|
||||
msgstr "Имя для URL"
|
||||
|
||||
msgid "Friendly name help"
|
||||
msgstr "Строковый идентификатор на основе которого будет сформирован адрес указывающий на данный раздел. Может состоять из латинских букв, цифр, знаков подчеркивания и дефисов."
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue