Compare commits
199 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 | ||
![]() |
fd760cb9ff | ||
![]() |
73047a8155 | ||
![]() |
1a13f93722 | ||
![]() |
4d096d5786 | ||
![]() |
a2309c16d0 | ||
![]() |
a77b4e4ecb | ||
![]() |
c7980386c3 | ||
![]() |
6f84ba979b | ||
![]() |
4bb906c300 | ||
![]() |
9f46e4e302 | ||
![]() |
e18280fbb0 | ||
![]() |
a2410a371a | ||
![]() |
8ade529ff8 | ||
![]() |
e45170cd0e | ||
![]() |
bc4fec2e34 | ||
![]() |
16f22acf61 | ||
![]() |
62e39a8cf5 | ||
![]() |
128692b4c7 | ||
![]() |
59dbdd47b1 | ||
![]() |
9b1008e22b | ||
![]() |
a0521b376c | ||
![]() |
70620c155f | ||
![]() |
257658ca5f | ||
![]() |
48bcd118ad | ||
![]() |
faab11a2fb | ||
![]() |
6f97f2c78d | ||
![]() |
267f126a04 | ||
![]() |
9ccee7c135 | ||
![]() |
461c8c3e60 | ||
![]() |
62bf8a0779 | ||
![]() |
a0e241d548 | ||
![]() |
1c6f7f271b | ||
![]() |
c3a4ceb1c3 | ||
![]() |
09da9ff6a6 | ||
![]() |
3229cd44f5 | ||
![]() |
3710ac1dc4 | ||
![]() |
1f34e1eefc | ||
![]() |
8b13230ef4 | ||
![]() |
676ace3ce7 | ||
![]() |
2c4711f425 | ||
![]() |
9ce0e18f1d | ||
![]() |
33e007b9ce | ||
![]() |
81031a50f0 | ||
![]() |
1e29f0a0ba | ||
![]() |
dacb8a3191 | ||
![]() |
3cf8b82a3d | ||
![]() |
cb20565b96 | ||
![]() |
4e4465e81e | ||
![]() |
4eb77bbc1f | ||
![]() |
62fd33bec2 | ||
![]() |
cc8231c09a | ||
![]() |
8d70b9cae2 | ||
![]() |
6e89a3023e | ||
![]() |
d18ec05c37 | ||
![]() |
5be5ce1271 | ||
![]() |
268ca03d2a | ||
![]() |
40f9b55dfc | ||
![]() |
a828436496 | ||
![]() |
8b829e571a | ||
![]() |
053bfe9b76 | ||
![]() |
421a19f1d5 | ||
![]() |
d0af7e913a | ||
![]() |
046c1dbe92 | ||
![]() |
e726eb1024 | ||
![]() |
ab43a46433 | ||
![]() |
aced07fa48 | ||
![]() |
4a32811d33 | ||
![]() |
1057701d0c | ||
![]() |
fcee58adea | ||
![]() |
80a4ffff6e | ||
![]() |
a313b142be |
246 changed files with 6077 additions and 2544 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>
|
||||
|
@ -14,6 +17,7 @@ AddDefaultCharset UTF-8
|
|||
|
||||
<IfModule !litespeed>
|
||||
RewriteRule ^favicon\.ico$ public/favicon.ico [L]
|
||||
RewriteRule ^apple-touch-icon\.png$ public/apple-touch-icon.png [L]
|
||||
RewriteRule ^robots\.txt$ public/robots.txt [L]
|
||||
|
||||
RewriteRule !^public/ index.php [L]
|
||||
|
@ -28,6 +32,7 @@ AddDefaultCharset UTF-8
|
|||
</IfModule>
|
||||
<IfModule litespeed>
|
||||
RewriteRule ^favicon\.ico$ public/favicon.ico
|
||||
RewriteRule ^apple-touch-icon\.png$ public/apple-touch-icon.png
|
||||
RewriteRule ^robots\.txt$ public/robots.txt
|
||||
|
||||
RewriteRule !^public/ index.php [L]
|
||||
|
|
27
.gitignore
vendored
27
.gitignore
vendored
|
@ -1,15 +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/*
|
||||
/ext/*
|
||||
/public/.htaccess
|
||||
/public/index.php
|
||||
!.gitkeep
|
||||
/public/img/*
|
||||
/public/style/*
|
||||
/public/upload/**/*
|
||||
!/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;
|
||||
|
@ -47,7 +48,7 @@ class FileCache implements CacheInterface
|
|||
/**
|
||||
* Получает данные из кэша по ключу
|
||||
*/
|
||||
public function get($key, $default = null)
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$file = $this->path($key);
|
||||
|
||||
|
@ -71,7 +72,7 @@ class FileCache implements CacheInterface
|
|||
/**
|
||||
* Устанавливает данные в кэш по ключу
|
||||
*/
|
||||
public function set($key, $value, $ttl = null)
|
||||
public function set(string $key, mixed $value, null|int|DateInterval $ttl = null): bool
|
||||
{
|
||||
$file = $this->path($key);
|
||||
|
||||
|
@ -96,7 +97,7 @@ class FileCache implements CacheInterface
|
|||
/**
|
||||
* Удаляет данные по ключу
|
||||
*/
|
||||
public function delete($key)
|
||||
public function delete(string $key): bool
|
||||
{
|
||||
$file = $this->path($key);
|
||||
|
||||
|
@ -115,10 +116,11 @@ class FileCache implements CacheInterface
|
|||
/**
|
||||
* Очищает папку кэша от php файлов (рекурсивно)
|
||||
*/
|
||||
public function clear()
|
||||
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;
|
||||
|
||||
|
@ -132,7 +134,7 @@ class FileCache implements CacheInterface
|
|||
/**
|
||||
* Получает данные по списку ключей
|
||||
*/
|
||||
public function getMultiple($keys, $default = null)
|
||||
public function getMultiple(iterable $keys, mixed $default = null): iterable
|
||||
{
|
||||
$this->validateIterable($keys);
|
||||
|
||||
|
@ -147,7 +149,7 @@ class FileCache implements CacheInterface
|
|||
/**
|
||||
* Устанавливает данные в кэш по списку ключ => значение
|
||||
*/
|
||||
public function setMultiple($values, $ttl = null)
|
||||
public function setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool
|
||||
{
|
||||
$this->validateIterable($keys);
|
||||
|
||||
|
@ -162,7 +164,7 @@ class FileCache implements CacheInterface
|
|||
/**
|
||||
* Удаляет данные по списку ключей
|
||||
*/
|
||||
public function deleteMultiple($keys)
|
||||
public function deleteMultiple(iterable $keys): bool
|
||||
{
|
||||
$this->validateIterable($keys);
|
||||
|
||||
|
@ -177,7 +179,7 @@ class FileCache implements CacheInterface
|
|||
/**
|
||||
* Проверяет кеш на наличие ключа
|
||||
*/
|
||||
public function has($key)
|
||||
public function has(string $key): bool
|
||||
{
|
||||
$file = $this->path($key);
|
||||
|
||||
|
@ -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;
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
<?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)
|
||||
*/
|
||||
/**
|
||||
* based on Container https://github.com/artoodetoo/container
|
||||
* by artoodetoo
|
||||
* based on Container <https://github.com/artoodetoo/container>
|
||||
*
|
||||
* @copyright (c) 2016 artoodetoo <https://github.com/artoodetoo>
|
||||
* @license The MIT License (MIT)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
|
|
@ -226,10 +226,12 @@ class DB
|
|||
case 's':
|
||||
case 'f':
|
||||
$value = [1];
|
||||
|
||||
break;
|
||||
default:
|
||||
$value = [1];
|
||||
$type = 's';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -503,11 +503,19 @@ class Mysql
|
|||
$tmp[] = "{$key}({$val})";
|
||||
}
|
||||
|
||||
$other = [];
|
||||
$stmt = $this->db->query("SHOW VARIABLES LIKE 'character\\_set\\_%'");
|
||||
$other = [];
|
||||
$queries = [
|
||||
"SHOW VARIABLES LIKE 'character\\_set\\_%'",
|
||||
"SHOW VARIABLES LIKE '%max\\_conn%'",
|
||||
"SHOW STATUS LIKE '%\\_conn%'",
|
||||
];
|
||||
|
||||
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
|
||||
$other[$row[0]] = $row[1];
|
||||
foreach ($queries as $query) {
|
||||
$stmt = $this->db->query($query);
|
||||
|
||||
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
|
||||
$other[$row[0]] = $row[1];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
|
|
|
@ -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 : '-';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,6 +127,16 @@ class Image extends File
|
|||
return $result;
|
||||
}
|
||||
|
||||
public function width(): int
|
||||
{
|
||||
return $this->imgDriver->width($this->image);
|
||||
}
|
||||
|
||||
public function height(): int
|
||||
{
|
||||
return $this->imgDriver->height($this->image);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->imgDriver->destroy($this->image);
|
||||
|
|
|
@ -49,6 +49,16 @@ class DefaultDriver
|
|||
return $image;
|
||||
}
|
||||
|
||||
public function width(mixed $image): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function height(mixed $image): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function destroy(mixed $image): void
|
||||
{
|
||||
}
|
||||
|
|
|
@ -162,4 +162,14 @@ class GDDriver extends DefaultDriver
|
|||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function width(mixed $image): int
|
||||
{
|
||||
return \imagesx($image);
|
||||
}
|
||||
|
||||
public function height(mixed $image): int
|
||||
{
|
||||
return \imagesy($image);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,4 +123,14 @@ class ImagickDriver extends DefaultDriver
|
|||
throw new FileException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function width(mixed $imagick): int
|
||||
{
|
||||
return $imagick->getImageWidth();
|
||||
}
|
||||
|
||||
public function height(mixed $imagick): int
|
||||
{
|
||||
return $imagick->getImageHeight();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ use Psr\Log\InvalidArgumentException;
|
|||
use DateTimeZone;
|
||||
use DateTime;
|
||||
use RuntimeException;
|
||||
use Stringable;
|
||||
use Throwable;
|
||||
|
||||
class Log implements LoggerInterface
|
||||
|
@ -44,16 +45,8 @@ class Log implements LoggerInterface
|
|||
|
||||
/**
|
||||
* Logs with an arbitrary level.
|
||||
*
|
||||
* @param mixed $level
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Psr\Log\InvalidArgumentException
|
||||
*/
|
||||
public function log($level, $message, array $context = [])
|
||||
public function log($level, string|Stringable $message, array $context = []): void
|
||||
{
|
||||
if (
|
||||
\is_object($message)
|
||||
|
@ -233,13 +226,8 @@ class Log implements LoggerInterface
|
|||
|
||||
/**
|
||||
* System is unusable.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function emergency($message, array $context = [])
|
||||
public function emergency(string|Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::EMERGENCY, $message, $context);
|
||||
}
|
||||
|
@ -249,13 +237,8 @@ class Log implements LoggerInterface
|
|||
*
|
||||
* Example: Entire website down, database unavailable, etc. This should
|
||||
* trigger the SMS alerts and wake you up.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function alert($message, array $context = [])
|
||||
public function alert(string|Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::ALERT, $message, $context);
|
||||
}
|
||||
|
@ -264,13 +247,8 @@ class Log implements LoggerInterface
|
|||
* Critical conditions.
|
||||
*
|
||||
* Example: Application component unavailable, unexpected exception.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function critical($message, array $context = [])
|
||||
public function critical(string|Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::CRITICAL, $message, $context);
|
||||
}
|
||||
|
@ -278,13 +256,8 @@ class Log implements LoggerInterface
|
|||
/**
|
||||
* Runtime errors that do not require immediate action but should typically
|
||||
* be logged and monitored.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function error($message, array $context = [])
|
||||
public function error(string|Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::ERROR, $message, $context);
|
||||
}
|
||||
|
@ -294,26 +267,16 @@ class Log implements LoggerInterface
|
|||
*
|
||||
* Example: Use of deprecated APIs, poor use of an API, undesirable things
|
||||
* that are not necessarily wrong.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function warning($message, array $context = [])
|
||||
public function warning(string|Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::WARNING, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normal but significant events.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function notice($message, array $context = [])
|
||||
public function notice(string|Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::NOTICE, $message, $context);
|
||||
}
|
||||
|
@ -322,26 +285,16 @@ class Log implements LoggerInterface
|
|||
* Interesting events.
|
||||
*
|
||||
* Example: User logs in, SQL logs.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function info($message, array $context = [])
|
||||
public function info(string|Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::INFO, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed debug information.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function debug($message, array $context = [])
|
||||
public function debug(string|Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::DEBUG, $message, $context);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,65 +5,90 @@
|
|||
* @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
|
||||
* @license The MIT License (MIT)
|
||||
*/
|
||||
/**
|
||||
* based on Dirk <https://github.com/artoodetoo/dirk>
|
||||
*
|
||||
* @copyright (c) 2015 artoodetoo <i.am@artoodetoo.org, https://github.com/artoodetoo>
|
||||
* @license The MIT License (MIT)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ForkBB\Core;
|
||||
|
||||
use R2\Templating\Dirk;
|
||||
use ForkBB\Core\View\Compiler;
|
||||
use ForkBB\Models\Page;
|
||||
use RuntimeException;
|
||||
|
||||
class View extends Dirk
|
||||
class View
|
||||
{
|
||||
public function __construct (string $cache, string $views)
|
||||
{
|
||||
$config = [
|
||||
'views' => $views,
|
||||
'cache' => $cache,
|
||||
'ext' => '.forkbb.php',
|
||||
'echo' => '\\htmlspecialchars((string) %s, \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE, \'UTF-8\')',
|
||||
'separator' => '/',
|
||||
];
|
||||
$this->compilers[] = 'Transformations';
|
||||
protected string $ext = '.forkbb.php';
|
||||
protected string $preFile = '';
|
||||
|
||||
parent::__construct($config);
|
||||
protected ?Compiler $compilerObj;
|
||||
protected string $compilerClass = Compiler::class;
|
||||
|
||||
protected string $cacheDir;
|
||||
protected string $defaultDir;
|
||||
protected string $defaultHash;
|
||||
|
||||
protected array $other = [];
|
||||
protected array $composers = [];
|
||||
protected array $blocks = [];
|
||||
protected array $blockStack = [];
|
||||
protected array $templates = [];
|
||||
|
||||
public function __construct(string|array $config, mixed $views)
|
||||
{
|
||||
if (\is_array($config)) {
|
||||
$this->cacheDir = $config['cache'];
|
||||
$this->defaultDir = $config['defaultDir'];
|
||||
|
||||
if (! empty($config['userDir'])) {
|
||||
$this->addTplDir($config['userDir'], 10);
|
||||
}
|
||||
|
||||
if (! empty($config['composers'])) {
|
||||
foreach ($config['composers'] as $name => $composer) {
|
||||
$this->composer($name, $composer);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($config['compiler'])) {
|
||||
$this->compilerClass = $config['compiler'];
|
||||
}
|
||||
|
||||
if (! empty($config['preFile'])) {
|
||||
$this->preFile = $config['preFile'];
|
||||
}
|
||||
} else {
|
||||
// для rev. 68 и ниже
|
||||
$this->cacheDir = $config;
|
||||
$this->defaultDir = $views;
|
||||
}
|
||||
|
||||
$this->defaultHash = \hash('md5', $this->defaultDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Трансформация скомпилированного шаблона
|
||||
* Добавляет новый каталог шаблонов $pathToDir.
|
||||
* Сортирует список каталогов в соответствии с приоритетом $priority. По убыванию.
|
||||
*/
|
||||
protected function compileTransformations(string $value): string
|
||||
public function addTplDir(string $pathToDir, int $priority): View
|
||||
{
|
||||
if (\str_starts_with($value, '<?xml ')) {
|
||||
$value = \str_replace(' \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE,', ' \\ENT_XML1,', $value);
|
||||
$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];
|
||||
});
|
||||
}
|
||||
|
||||
$perfix = <<<'EOD'
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use function \ForkBB\{__, num, dt, size};
|
||||
|
||||
?>
|
||||
EOD;
|
||||
|
||||
if (false === \strpos($value, '<!-- inline -->')) {
|
||||
return $perfix . $value;
|
||||
}
|
||||
|
||||
return $perfix . \preg_replace_callback(
|
||||
'%<!-- inline -->([^<]*(?:<(?!!-- endinline -->)[^<]*)*+)(?:<!-- endinline -->)?%',
|
||||
function ($matches) {
|
||||
return \preg_replace('%\h*\R\s*%', '', $matches[1]);
|
||||
},
|
||||
$value
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return result of templating
|
||||
* Возвращает отображение страницы $p или null
|
||||
*/
|
||||
public function rendering(Page $p): ?string
|
||||
{
|
||||
|
@ -76,8 +101,10 @@ EOD;
|
|||
$p->prepare();
|
||||
|
||||
$this->templates[] = $p->nameTpl;
|
||||
|
||||
while ($_name = \array_shift($this->templates)) {
|
||||
$this->beginBlock('content');
|
||||
|
||||
foreach ($this->composers as $_cname => $_cdata) {
|
||||
if (\preg_match($_cname, $_name)) {
|
||||
foreach ($_cdata as $_citem) {
|
||||
|
@ -85,7 +112,9 @@ EOD;
|
|||
}
|
||||
}
|
||||
}
|
||||
require($this->prepare($_name));
|
||||
|
||||
require $this->prepare($_name);
|
||||
|
||||
$this->endBlock(true);
|
||||
}
|
||||
|
||||
|
@ -94,29 +123,6 @@ EOD;
|
|||
return $this->block('content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile echos
|
||||
*/
|
||||
protected function compileEchos(string $value): string
|
||||
{
|
||||
$value = \preg_replace_callback(
|
||||
'%(@)?\{\{!\s*(.+?)\s*!\}\}(\r?\n)?%s',
|
||||
function($matches) {
|
||||
$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
|
||||
|
||||
return $matches[1]
|
||||
? \substr($matches[0], 1)
|
||||
: '<?= \\htmlspecialchars((string) '
|
||||
. $this->compileEchoDefaults($matches[2])
|
||||
. ', \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE, \'UTF-8\', false) ?>'
|
||||
. $whitespace;
|
||||
},
|
||||
$value
|
||||
);
|
||||
|
||||
return parent::compileEchos($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправляет HTTP заголовки
|
||||
*/
|
||||
|
@ -128,4 +134,166 @@ EOD;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает отображение шаблона $name
|
||||
*/
|
||||
public function fetch(string $name, array $data = []): string
|
||||
{
|
||||
$this->templates[] = $name;
|
||||
|
||||
if (! empty($data)) {
|
||||
\extract($data);
|
||||
}
|
||||
|
||||
while ($_name = \array_shift($this->templates)) {
|
||||
$this->beginBlock('content');
|
||||
|
||||
foreach ($this->composers as $_cname => $_cdata) {
|
||||
if (\preg_match($_cname, $_name)) {
|
||||
foreach ($_cdata as $_citem) {
|
||||
\extract((\is_callable($_citem) ? $_citem($this) : $_citem) ?: []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require $this->prepare($_name);
|
||||
|
||||
$this->endBlock(true);
|
||||
}
|
||||
|
||||
return $this->block('content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add view composer
|
||||
* @param mixed $name template name or array of names
|
||||
* @param mixed $composer data in the same meaning as for fetch() call, or callable returning such data
|
||||
*/
|
||||
public function composer(string|array $name, mixed $composer): void
|
||||
{
|
||||
if (\is_array($name)) {
|
||||
foreach ($name as $n) {
|
||||
$this->composer($n, $composer);
|
||||
}
|
||||
} else {
|
||||
$p = '~^'
|
||||
. \str_replace('\*', '[^' . $this->separator . ']+', \preg_quote($name, $this->separator . '~'))
|
||||
. '$~';
|
||||
$this->composers[$p][] = $composer;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Подготавливает файл для подключения
|
||||
*/
|
||||
protected function prepare(string $name): string
|
||||
{
|
||||
$st = \preg_replace('%\W%', '-', $name);
|
||||
|
||||
foreach ($this->other as $hash => $cur) {
|
||||
if (\file_exists($tpl = "{$cur[0]}/{$name}{$this->ext}")) {
|
||||
$php = "{$this->cacheDir}/_{$st}-{$hash}.php";
|
||||
|
||||
if (
|
||||
! \file_exists($php)
|
||||
|| \filemtime($tpl) > \filemtime($php)
|
||||
) {
|
||||
$this->create($php, $tpl, $name);
|
||||
}
|
||||
|
||||
return $php;
|
||||
}
|
||||
}
|
||||
|
||||
$hash = $this->defaultHash;
|
||||
$tpl = "{$this->defaultDir}/{$name}{$this->ext}";
|
||||
$php = "{$this->cacheDir}/_{$st}-{$hash}.php";
|
||||
|
||||
if (
|
||||
! \file_exists($php)
|
||||
|| \filemtime($tpl) > \filemtime($php)
|
||||
) {
|
||||
$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, string $name): void
|
||||
{
|
||||
if (empty($this->compilerObj)) {
|
||||
$this->compilerObj = new $this->compilerClass($this->preFile);
|
||||
}
|
||||
|
||||
$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");
|
||||
}
|
||||
|
||||
if (\function_exists('\\opcache_invalidate')) {
|
||||
\opcache_invalidate($php, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Задает родительский шаблон
|
||||
*/
|
||||
protected function extend(string $name): void
|
||||
{
|
||||
$this->templates[] = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает содержимое блока или $default
|
||||
*/
|
||||
protected function block(string $name, string $default = ''): string
|
||||
{
|
||||
return \array_key_exists($name, $this->blocks)
|
||||
? $this->blocks[$name]
|
||||
: $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Задает начало блока
|
||||
*/
|
||||
protected function beginBlock(string $name): void
|
||||
{
|
||||
$this->blockStack[] = $name;
|
||||
|
||||
\ob_start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Задает конец блока
|
||||
*/
|
||||
protected function endBlock(bool $overwrite = false): string
|
||||
{
|
||||
$name = \array_pop($this->blockStack);
|
||||
|
||||
if (
|
||||
$overwrite
|
||||
|| ! \array_key_exists($name, $this->blocks)
|
||||
) {
|
||||
$this->blocks[$name] = \ob_get_clean();
|
||||
} else {
|
||||
$this->blocks[$name] .= \ob_get_clean();
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,79 +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)
|
||||
*/
|
||||
/**
|
||||
* based on Dirk <https://github.com/artoodetoo/dirk>
|
||||
*
|
||||
* @copyright (c) 2015 artoodetoo <i.am@artoodetoo.org, https://github.com/artoodetoo>
|
||||
* @license The MIT License (MIT)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace R2\Templating;
|
||||
namespace ForkBB\Core\View;
|
||||
|
||||
use R2\Templating\PhpEngine;
|
||||
use RuntimeException;
|
||||
|
||||
class Dirk extends PhpEngine
|
||||
class Compiler
|
||||
{
|
||||
|
||||
protected $cache;
|
||||
protected $echoFormat;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$config = \array_replace_recursive(
|
||||
[
|
||||
'ext' => '.blade.php',
|
||||
'cache' => '.',
|
||||
'echo' => '\\htmlspecialchars((string) %s, \\ENT_QUOTES, \'UTF-8\')',
|
||||
],
|
||||
$config
|
||||
);
|
||||
$this->cache = $config['cache'] ?? '.';
|
||||
$this->echoFormat = $config['echo'] ?? '%s';
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
protected $compilers = [
|
||||
protected string $shortID;
|
||||
protected int $loopsCounter = 0;
|
||||
protected array $compilers = [
|
||||
'PrePaste',
|
||||
'Statements',
|
||||
'Comments',
|
||||
'Echos',
|
||||
'Transformations',
|
||||
];
|
||||
protected array $preArray = [];
|
||||
protected string $tplName;
|
||||
|
||||
protected $shortID = '';
|
||||
protected $shortArr = [];
|
||||
|
||||
/**
|
||||
* Prepare file to include
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
protected function prepare(string $name): string
|
||||
public function __construct(string $preFile)
|
||||
{
|
||||
$name = \str_replace('.', '/', $name);
|
||||
$tpl = $this->views . '/' . $name . $this->ext;
|
||||
$sha1 = \sha1($name);
|
||||
$php = $this->cache . '/' . $sha1 . '.php';
|
||||
|
||||
if (
|
||||
! \file_exists($php)
|
||||
|| \filemtime($tpl) > \filemtime($php)
|
||||
! empty($preFile)
|
||||
&& \is_file($preFile)
|
||||
) {
|
||||
$this->shortArr[] = $this->shortID;
|
||||
$this->shortID = \substr($sha1, 0, 4);
|
||||
|
||||
$text = \file_get_contents($tpl);
|
||||
|
||||
foreach ($this->compilers as $type) {
|
||||
$text = $this->{'compile' . $type}($text);
|
||||
}
|
||||
|
||||
\file_put_contents($php, $text);
|
||||
|
||||
$this->shortID = \array_pop($this->shortArr);
|
||||
$this->preArray = include $preFile;
|
||||
}
|
||||
|
||||
return $php;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile Statements that start with "@"
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
* Генерирует php код на основе шаблона из $text
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает операторы начинающиеся с @
|
||||
*/
|
||||
protected function compileStatements(string $value): string
|
||||
{
|
||||
|
@ -91,10 +96,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile comments
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
* Обрабатывает комментарии
|
||||
*/
|
||||
protected function compileComments(string $value): string
|
||||
{
|
||||
|
@ -102,30 +104,29 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile echos
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
* Обрабатывает вывод информации
|
||||
*/
|
||||
protected function compileEchos(string $value): string
|
||||
{
|
||||
// compile escaped echoes
|
||||
// {{! !}}
|
||||
$value = \preg_replace_callback(
|
||||
'%\{\{\{\s*(.+?)\s*\}\}\}(\r?\n)?%s',
|
||||
'%(@)?\{\{![ \t]*+(.+?)[ \t]*!\}\}(\r?\n)?%',
|
||||
function($matches) {
|
||||
$whitespace = empty($matches[2]) ? '' : $matches[2] . $matches[2];
|
||||
$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
|
||||
|
||||
return '<?= \\htmlspecialchars('
|
||||
. $this->compileEchoDefaults($matches[1])
|
||||
. ', \\ENT_QUOTES, \'UTF-8\') ?>'
|
||||
. $whitespace;
|
||||
return $matches[1]
|
||||
? \substr($matches[0], 1)
|
||||
: '<?= \\htmlspecialchars((string) '
|
||||
. $this->compileEchoDefaults($matches[2])
|
||||
. ', \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE, \'UTF-8\', false) ?>'
|
||||
. $whitespace;
|
||||
},
|
||||
$value
|
||||
);
|
||||
|
||||
// compile not escaped echoes
|
||||
// {!! !!}
|
||||
$value = \preg_replace_callback(
|
||||
'%\{\!!\s*(.+?)\s*!!\}(\r?\n)?%s',
|
||||
'%\{\!![ \t]*+(.+?)[ \t]*!!\}(\r?\n)?%',
|
||||
function($matches) {
|
||||
$whitespace = empty($matches[2]) ? '' : $matches[2] . $matches[2];
|
||||
|
||||
|
@ -137,17 +138,18 @@ class Dirk extends PhpEngine
|
|||
$value
|
||||
);
|
||||
|
||||
// compile regular echoes
|
||||
// {{ }}
|
||||
$value = \preg_replace_callback(
|
||||
'%(@)?\{\{\s*(.+?)\s*\}\}(\r?\n)?%s',
|
||||
'%(@)?\{\{(?!!)[ \t]*+(.+?)[ \t]*\}\}(\r?\n)?%',
|
||||
function($matches) {
|
||||
$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
|
||||
|
||||
return $matches[1]
|
||||
? \substr($matches[0], 1)
|
||||
: '<?= '
|
||||
. \sprintf($this->echoFormat, $this->compileEchoDefaults($matches[2]))
|
||||
. ' ?>' . $whitespace;
|
||||
: '<?= \\htmlspecialchars((string) '
|
||||
. $this->compileEchoDefaults($matches[2])
|
||||
. ', \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE, \'UTF-8\') ?>'
|
||||
. $whitespace;
|
||||
},
|
||||
$value
|
||||
);
|
||||
|
@ -156,10 +158,39 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the default values for the echo statement.
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
* Трансформирует скомпилированный шаблон
|
||||
*/
|
||||
protected function compileTransformations(string $value): string
|
||||
{
|
||||
if (\str_starts_with($value, '<?xml ')) {
|
||||
$value = \str_replace(' \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE,', ' \\ENT_XML1,', $value);
|
||||
}
|
||||
|
||||
$perfix = <<<'EOD'
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use function \ForkBB\{__, num, dt, size, url};
|
||||
|
||||
?>
|
||||
EOD;
|
||||
|
||||
if (false === \strpos($value, '<!-- inline -->')) {
|
||||
return $perfix . $value;
|
||||
}
|
||||
|
||||
return $perfix . \preg_replace_callback(
|
||||
'%<!-- inline -->([^<]*(?:<(?!!-- endinline -->)[^<]*)*+)(?:<!-- endinline -->)?%',
|
||||
function ($matches) {
|
||||
return \preg_replace('%\h*\R\s*%', '', $matches[1]);
|
||||
},
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает значение по умолчанию для вывода информации
|
||||
*/
|
||||
public function compileEchoDefaults(string $value): string
|
||||
{
|
||||
|
@ -167,10 +198,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the if statements
|
||||
*
|
||||
* @param string $expression
|
||||
* @return string
|
||||
* @if()
|
||||
*/
|
||||
protected function compileIf(string $expression): string
|
||||
{
|
||||
|
@ -186,10 +214,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the else-if statements
|
||||
*
|
||||
* @param string $expression
|
||||
* @return string
|
||||
* @elseif()
|
||||
*/
|
||||
protected function compileElseif(string $expression): string
|
||||
{
|
||||
|
@ -197,9 +222,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the else statements
|
||||
*
|
||||
* @return string
|
||||
* @else
|
||||
*/
|
||||
protected function compileElse(): string
|
||||
{
|
||||
|
@ -207,9 +230,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the end-if statements
|
||||
*
|
||||
* @return string
|
||||
* @endif
|
||||
*/
|
||||
protected function compileEndif(): string
|
||||
{
|
||||
|
@ -217,10 +238,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the isset statements
|
||||
*
|
||||
* @param string $expression
|
||||
* @return string
|
||||
* @isset()
|
||||
*/
|
||||
protected function compileIsset(string $expression): string
|
||||
{
|
||||
|
@ -228,9 +246,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the end-isset statements
|
||||
*
|
||||
* @return string
|
||||
* @endisset
|
||||
*/
|
||||
protected function compileEndisset(): string
|
||||
{
|
||||
|
@ -238,9 +254,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the end-empty statements
|
||||
*
|
||||
* @return string
|
||||
* @endempty
|
||||
*/
|
||||
protected function compileEndempty(): string
|
||||
{
|
||||
|
@ -248,10 +262,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the unless statements
|
||||
*
|
||||
* @param string $expression
|
||||
* @return string
|
||||
* @unless()
|
||||
*/
|
||||
protected function compileUnless(string $expression): string
|
||||
{
|
||||
|
@ -259,9 +270,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the end unless statements
|
||||
*
|
||||
* @return string
|
||||
* @endunless
|
||||
*/
|
||||
protected function compileEndunless(): string
|
||||
{
|
||||
|
@ -269,10 +278,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the for statements
|
||||
*
|
||||
* @param string $expression
|
||||
* @return string
|
||||
* @for()
|
||||
*/
|
||||
protected function compileFor(string $expression): string
|
||||
{
|
||||
|
@ -280,22 +286,15 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the end-for statements
|
||||
*
|
||||
* @return string
|
||||
* @endfor
|
||||
*/
|
||||
protected function compileEndfor(): string
|
||||
{
|
||||
return "<?php endfor; ?>";
|
||||
}
|
||||
|
||||
protected $loopsCounter = 0;
|
||||
|
||||
/**
|
||||
* Compile the foreach statements
|
||||
*
|
||||
* @param string $expression
|
||||
* @return string
|
||||
* @foreach()
|
||||
*/
|
||||
protected function compileForeach(string $expression): string
|
||||
{
|
||||
|
@ -307,9 +306,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the end-for-each statements
|
||||
*
|
||||
* @return string
|
||||
* @endforeach
|
||||
*/
|
||||
protected function compileEndforeach(): string
|
||||
{
|
||||
|
@ -318,16 +315,16 @@ class Dirk extends PhpEngine
|
|||
return "<?php endforeach; ?>";
|
||||
}
|
||||
|
||||
/**
|
||||
* @iteration
|
||||
*/
|
||||
protected function compileIteration(): string
|
||||
{
|
||||
return "((int) \$__iter{$this->shortID}_{$this->loopsCounter})";
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the forelse statements
|
||||
*
|
||||
* @param string $expression
|
||||
* @return string
|
||||
* @forelse()
|
||||
*/
|
||||
protected function compileForelse(string $expression): string
|
||||
{
|
||||
|
@ -339,11 +336,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the end-forelse statements
|
||||
* Compile the empty statements
|
||||
*
|
||||
* @param string $expression
|
||||
* @return string
|
||||
* @empty / @empty()
|
||||
*/
|
||||
protected function compileEmpty(string $expression): string
|
||||
{
|
||||
|
@ -362,9 +355,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the end-forelse statements
|
||||
*
|
||||
* @return string
|
||||
* @endforelse
|
||||
*/
|
||||
protected function compileEndforelse(): string
|
||||
{
|
||||
|
@ -372,10 +363,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the while statements
|
||||
*
|
||||
* @param string $expression
|
||||
* @return string
|
||||
* @while()
|
||||
*/
|
||||
protected function compileWhile(string $expression): string
|
||||
{
|
||||
|
@ -383,9 +371,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the end-while statements
|
||||
*
|
||||
* @return string
|
||||
* @endwhile
|
||||
*/
|
||||
protected function compileEndwhile(): string
|
||||
{
|
||||
|
@ -393,10 +379,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the extends statements
|
||||
*
|
||||
* @param string $expression
|
||||
* @return string
|
||||
* @extends()
|
||||
*/
|
||||
protected function compileExtends(string $expression): string
|
||||
{
|
||||
|
@ -411,10 +394,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the include statements
|
||||
*
|
||||
* @param string $expression
|
||||
* @return string
|
||||
* @include()
|
||||
*/
|
||||
protected function compileInclude(string $expression): string
|
||||
{
|
||||
|
@ -429,10 +409,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the yield statements
|
||||
*
|
||||
* @param string $expression
|
||||
* @return string
|
||||
* @yield()
|
||||
*/
|
||||
protected function compileYield(string $expression): string
|
||||
{
|
||||
|
@ -440,10 +417,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the section statements
|
||||
*
|
||||
* @param string $expression
|
||||
* @return string
|
||||
* @section()
|
||||
*/
|
||||
protected function compileSection(string $expression): string
|
||||
{
|
||||
|
@ -451,9 +425,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the end-section statements
|
||||
*
|
||||
* @return string
|
||||
* @endsection
|
||||
*/
|
||||
protected function compileEndsection(): string
|
||||
{
|
||||
|
@ -461,9 +433,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the show statements
|
||||
*
|
||||
* @return string
|
||||
* @show()
|
||||
*/
|
||||
protected function compileShow(): string
|
||||
{
|
||||
|
@ -471,9 +441,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the append statements
|
||||
*
|
||||
* @return string
|
||||
* @append
|
||||
*/
|
||||
protected function compileAppend(): string
|
||||
{
|
||||
|
@ -481,9 +449,7 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the stop statements
|
||||
*
|
||||
* @return string
|
||||
* @stop
|
||||
*/
|
||||
protected function compileStop(): string
|
||||
{
|
||||
|
@ -491,20 +457,24 @@ class Dirk extends PhpEngine
|
|||
}
|
||||
|
||||
/**
|
||||
* Compile the overwrite statements
|
||||
*
|
||||
* @return string
|
||||
* @overwrite
|
||||
*/
|
||||
protected function compileOverwrite(): string
|
||||
{
|
||||
return "<?php \$this->endBlock(true); ?>";
|
||||
}
|
||||
|
||||
/**
|
||||
* @switch()
|
||||
*/
|
||||
protected function compileSwitch(string $expression): string
|
||||
{
|
||||
return "<?php switch {$expression}: ?>";
|
||||
}
|
||||
|
||||
/**
|
||||
* @case()
|
||||
*/
|
||||
protected function compileCase(string $expression): string
|
||||
{
|
||||
$expression = \substr($expression, 1, -1);
|
||||
|
@ -512,18 +482,43 @@ class Dirk extends PhpEngine
|
|||
return "<?php case {$expression}: ?>";
|
||||
}
|
||||
|
||||
/**
|
||||
* @default
|
||||
*/
|
||||
protected function compileDefault(): string
|
||||
{
|
||||
return "<?php default: ?>";
|
||||
}
|
||||
|
||||
/**
|
||||
* @endswitch
|
||||
*/
|
||||
protected function compileEndswitch(): string
|
||||
{
|
||||
return "<?php endswitch; ?>";
|
||||
}
|
||||
|
||||
/**
|
||||
* @break
|
||||
*/
|
||||
protected function compileBreak(): string
|
||||
{
|
||||
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;
|
||||
|
|
|
@ -22,6 +22,10 @@ class Delete extends Method
|
|||
public function delete(int ...$ids): BanList
|
||||
{
|
||||
if (! empty($ids)) {
|
||||
if (\count($ids) > 1) {
|
||||
\sort($ids, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':ids' => $ids,
|
||||
];
|
||||
|
|
|
@ -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'])) {
|
||||
|
|
|
@ -29,6 +29,7 @@ class Save extends Method
|
|||
if (! isset($list[$id]['search_for'], $list[$id]['replace_with'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('' === \trim($list[$id]['search_for'])) {
|
||||
if ($id > 0) {
|
||||
$forDel[] = $id;
|
||||
|
@ -60,9 +61,14 @@ class Save extends Method
|
|||
$this->c->DB->exec($query, $vars);
|
||||
}
|
||||
}
|
||||
|
||||
if ($forDel) {
|
||||
if (\count($forDel) > 1) {
|
||||
\sort($forDel, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':del' => $forDel
|
||||
':del' => $forDel,
|
||||
];
|
||||
$query = 'DELETE
|
||||
FROM ::censoring
|
||||
|
|
|
@ -79,14 +79,13 @@ class Save extends Method
|
|||
$this->c->DB->exec($query, $vars);
|
||||
|
||||
$query = 'INSERT INTO ::config (conf_name, conf_value)
|
||||
SELECT ?s:name, ?s:value
|
||||
FROM ::groups
|
||||
SELECT tmp.*
|
||||
FROM (SELECT ?s:name AS f1, ?s:value AS f2) AS tmp
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ::config
|
||||
WHERE conf_name=?s:name
|
||||
)
|
||||
LIMIT 1';
|
||||
)';
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
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;
|
||||
|
||||
}
|
||||
}
|
|
@ -30,18 +30,15 @@ class CalcStat extends Method
|
|||
];
|
||||
$query = 'SELECT COUNT(t.id)
|
||||
FROM ::topics AS t
|
||||
WHERE t.forum_id=?i:fid AND t.moved_to!=0';
|
||||
WHERE t.forum_id=?i:fid';
|
||||
|
||||
$moved = (int) $this->c->DB->query($query, $vars)->fetchColumn();
|
||||
$this->model->num_topics = (int) $this->c->DB->query($query, $vars)->fetchColumn();
|
||||
|
||||
$query = 'SELECT COUNT(t.id) as num_topics, SUM(t.num_replies) as num_replies
|
||||
$query = 'SELECT SUM(t.num_replies + 1)
|
||||
FROM ::topics AS t
|
||||
WHERE t.forum_id=?i:fid AND t.moved_to=0';
|
||||
|
||||
$result = $this->c->DB->query($query, $vars)->fetch();
|
||||
|
||||
$this->model->num_topics = $result['num_topics'] + $moved;
|
||||
$this->model->num_posts = $result['num_topics'] + $result['num_replies'];
|
||||
$this->model->num_posts = (int) $this->c->DB->query($query, $vars)->fetchColumn();
|
||||
|
||||
$query = 'SELECT t.last_post, t.last_post_id, t.last_poster, t.last_poster_id, t.subject as last_topic
|
||||
FROM ::topics AS t
|
||||
|
|
|
@ -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)');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,6 +84,10 @@ class Delete extends Action
|
|||
}
|
||||
|
||||
if ($forums) {
|
||||
if (\count($forums) > 1) {
|
||||
\ksort($forums, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$this->c->subscriptions->unsubscribe(...$forums);
|
||||
|
||||
foreach ($forums as $forum) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -125,9 +125,9 @@ class LoadTree extends Action
|
|||
$query = 'SELECT t.forum_id, t.last_post
|
||||
FROM ::topics AS t
|
||||
LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND mot.tid=t.id)
|
||||
WHERE t.forum_id IN(?ai:forums)
|
||||
AND t.last_post>?i:max
|
||||
WHERE t.forum_id IN (?ai:forums)
|
||||
AND t.moved_to=0
|
||||
AND t.last_post>?i:max
|
||||
AND (mot.mt_last_visit IS NULL OR t.last_post>mot.mt_last_visit)';
|
||||
|
||||
$stmt = $this->c->DB->query($query, $vars);
|
||||
|
|
|
@ -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
|
||||
|
@ -89,6 +89,7 @@ class Refresh extends Action
|
|||
$sub[] = $id;
|
||||
$all = \array_merge($this->createList($list, $id), $all);
|
||||
}
|
||||
|
||||
if (0 === $parent) {
|
||||
if (empty($sub)) {
|
||||
return [];
|
||||
|
@ -99,6 +100,11 @@ class Refresh extends Action
|
|||
}
|
||||
|
||||
$all = \array_merge($sub, $all);
|
||||
|
||||
if (\count($all) > 1) {
|
||||
\sort($all, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$list[$parent]['subforums'] = $sub ?: null;
|
||||
$list[$parent]['descendants'] = $all ?: null;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -98,7 +98,8 @@ class Online extends Model
|
|||
$users = [];
|
||||
$guests = [];
|
||||
$bots = [];
|
||||
$needClean = false;
|
||||
$upUsers = [];
|
||||
$delGuests = [];
|
||||
|
||||
if ($detail) {
|
||||
$query = 'SELECT o.user_id, o.ident, o.logged, o.o_position, o.o_name
|
||||
|
@ -116,16 +117,10 @@ class Online extends Model
|
|||
// посетитель уже не онлайн (или почти не онлайн)
|
||||
if ($cur['logged'] < $tOnline) {
|
||||
if ($cur['logged'] < $tVisit) {
|
||||
$needClean = true;
|
||||
|
||||
if ($cur['user_id'] > 0) {
|
||||
$this->c->users->updateLastVisit(
|
||||
$this->c->users->create([
|
||||
'id' => $cur['user_id'],
|
||||
'group_id' => FORK_GROUP_MEMBER,
|
||||
'logged' => $cur['logged'],
|
||||
])
|
||||
);
|
||||
$upUsers[$cur['user_id']] = $cur['logged'];
|
||||
} else {
|
||||
$delGuests[] = $cur['ident'];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,12 +156,41 @@ class Online extends Model
|
|||
}
|
||||
|
||||
// удаление просроченных посетителей
|
||||
if ($needClean) {
|
||||
if ($upUsers) {
|
||||
if (\count($upUsers) > 1) {
|
||||
\ksort($upUsers, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
foreach ($upUsers as $id => $logged) {
|
||||
$this->c->users->updateLastVisit(
|
||||
$this->c->users->create([
|
||||
'id' => $id,
|
||||
'group_id' => FORK_GROUP_MEMBER,
|
||||
'logged' => $logged,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':visit' => $tVisit,
|
||||
':ids' => \array_keys($upUsers),
|
||||
];
|
||||
$query = 'DELETE FROM ::online
|
||||
WHERE logged<?i:visit';
|
||||
WHERE user_id IN (?ai:ids)';
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
}
|
||||
|
||||
// удаление просроченных гостей
|
||||
if ($delGuests) {
|
||||
if (\count($delGuests) > 1) {
|
||||
\sort($delGuests, \SORT_STRING);
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':idents' => $delGuests,
|
||||
];
|
||||
$query = 'DELETE FROM ::online
|
||||
WHERE user_id=0 AND ident IN (?as:idents)';
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
}
|
||||
|
@ -235,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)');
|
||||
|
@ -62,7 +61,7 @@ class Delete extends Method
|
|||
}
|
||||
|
||||
$users[$arg->id] = $arg;
|
||||
$isUser = 1;
|
||||
$isUser = 1;
|
||||
} elseif ($arg instanceof PPost) {
|
||||
if (! $arg->parent instanceof PTopic) {
|
||||
throw new RuntimeException('Bad ppost');
|
||||
|
@ -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)');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,6 +84,10 @@ class Delete extends Method
|
|||
}
|
||||
|
||||
if ($topics) {
|
||||
if (\count($topics) > 1) {
|
||||
\ksort($topics, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
|
||||
foreach ($topics as $topic) {
|
||||
|
@ -103,6 +104,10 @@ class Delete extends Method
|
|||
}
|
||||
|
||||
if ($posts) {
|
||||
if (\count($posts) > 1) {
|
||||
\ksort($posts, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$calcTopics = [];
|
||||
|
||||
foreach ($posts as $post) {
|
||||
|
@ -123,6 +128,10 @@ class Delete extends Method
|
|||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
|
||||
if (\count($calcTopics) > 1) {
|
||||
\ksort($calcTopics, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
foreach ($calcTopics as $topic) {
|
||||
$this->model->update(Cnst::PTOPIC, $topic->calcStat());
|
||||
}
|
||||
|
@ -184,6 +193,10 @@ class Delete extends Method
|
|||
$uids[$row['target_id']] = $row['target_id'];
|
||||
}
|
||||
|
||||
if (\count($ids) > 1) {
|
||||
\sort($ids, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$this->deletePTopics($ids);
|
||||
|
||||
foreach ($this->c->users->loadByIds($uids) as $user) {
|
||||
|
@ -193,6 +206,10 @@ class Delete extends Method
|
|||
}
|
||||
}
|
||||
|
||||
if (\count($calcUsers) > 1) {
|
||||
\ksort($calcUsers, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
foreach ($calcUsers as $user) {
|
||||
$this->model->recalculate($user);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -69,6 +69,14 @@ abstract class Page extends Model
|
|||
$this->fDescription = $container->config->o_board_desc;
|
||||
$this->fRootLink = $container->Router->link('Index');
|
||||
|
||||
$this->mDescription = $this->c->config->s_meta_desc;
|
||||
|
||||
if (! empty($this->c->config->a_og_image['file'])) {
|
||||
$this->mOgImage = $this->c->PUBLIC_URL . '/img/og/' . $this->c->config->a_og_image['file'];
|
||||
$this->mOgImageX = $this->c->config->a_og_image['width'] ?? null;
|
||||
$this->mOgImageY = $this->c->config->a_og_image['height'] ?? null;
|
||||
}
|
||||
|
||||
if (1 === $container->config->b_announcement) {
|
||||
$this->fAnnounce = $container->config->o_announcement_message;
|
||||
}
|
||||
|
@ -304,7 +312,11 @@ abstract class Page extends Model
|
|||
1 === $this->c->config->b_maintenance
|
||||
&& $this->user->isAdmin
|
||||
) {
|
||||
$this->fIswev = [FORK_MESS_WARN, ['Maintenance mode enabled', $this->c->Router->link('AdminMaintenance')]];
|
||||
if ($this->c->MAINTENANCE_OFF) {
|
||||
$this->fIswev = [FORK_MESS_ERR, ['Maintenance mode enabled off', $this->c->Router->link('AdminMaintenance')]];
|
||||
} else {
|
||||
$this->fIswev = [FORK_MESS_WARN, ['Maintenance mode enabled', $this->c->Router->link('AdminMaintenance')]];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -354,20 +366,6 @@ abstract class Page extends Model
|
|||
*/
|
||||
protected function getpageHeaders(): array
|
||||
{
|
||||
if ($this->canonical) {
|
||||
$this->pageHeader('canonical', 'link', 0, [
|
||||
'rel' => 'canonical',
|
||||
'href' => $this->canonical,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->robots) {
|
||||
$this->pageHeader('robots', 'meta', 11000, [
|
||||
'name' => 'robots',
|
||||
'content' => $this->robots,
|
||||
]);
|
||||
}
|
||||
|
||||
if (1 === $this->user->g_search) {
|
||||
$this->pageHeader('opensearch', 'link', 0, [
|
||||
'rel' => 'search',
|
||||
|
@ -378,11 +376,7 @@ abstract class Page extends Model
|
|||
}
|
||||
|
||||
\uasort($this->pageHeaders, function (array $a, array $b) {
|
||||
if ($a['weight'] === $b['weight']) {
|
||||
return 0;
|
||||
} else {
|
||||
return $a['weight'] > $b['weight'] ? -1 : 1;
|
||||
}
|
||||
return $b['weight'] <=> $a['weight'];
|
||||
});
|
||||
|
||||
return $this->pageHeaders;
|
||||
|
|
|
@ -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],
|
||||
|
@ -931,8 +947,9 @@ class Install extends Admin
|
|||
],
|
||||
'PRIMARY KEY' => ['id'],
|
||||
'INDEXES' => [
|
||||
'topic_id_idx' => ['topic_id'],
|
||||
'multi_idx' => ['poster_id', 'topic_id', 'posted'],
|
||||
'topic_id_idx' => ['topic_id'],
|
||||
'multi_idx' => ['poster_id', 'topic_id', 'posted'],
|
||||
'editor_id_idx' => ['editor_id'],
|
||||
],
|
||||
'ENGINE' => $this->DBEngine,
|
||||
];
|
||||
|
@ -1051,10 +1068,10 @@ class Install extends Admin
|
|||
],
|
||||
'PRIMARY KEY' => ['id'],
|
||||
'INDEXES' => [
|
||||
'forum_id_idx' => ['forum_id'],
|
||||
'moved_to_idx' => ['moved_to'],
|
||||
'multi_2_idx' => ['forum_id', 'sticky', 'last_post'],
|
||||
'last_post_idx' => ['last_post'],
|
||||
'first_post_id_idx' => ['first_post_id'],
|
||||
'multi_1_idx' => ['moved_to', 'forum_id', 'num_replies', 'last_post'],
|
||||
],
|
||||
'ENGINE' => $this->DBEngine,
|
||||
];
|
||||
|
@ -1581,6 +1598,8 @@ class Install extends Admin
|
|||
'i_search_ttl' => 900,
|
||||
'b_ant_hidden_ch' => 1,
|
||||
'b_ant_use_js' => 0,
|
||||
's_meta_desc' => '',
|
||||
'a_og_image' => \json_encode([], FORK_JSON_ENCODE),
|
||||
];
|
||||
|
||||
foreach ($forkConfig as $name => $value) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -106,7 +106,7 @@ class Maintenance extends Admin
|
|||
*/
|
||||
protected function formRebuild(): array
|
||||
{
|
||||
return [
|
||||
$form = [
|
||||
'action' => $this->c->Router->link('AdminMaintenanceRebuild'),
|
||||
'hidden' => [
|
||||
'token' => $this->c->Csrf->create('AdminMaintenanceRebuild'),
|
||||
|
@ -162,6 +162,24 @@ class Maintenance extends Admin
|
|||
],
|
||||
];
|
||||
|
||||
if (
|
||||
1 !== $this->c->config->b_maintenance
|
||||
|| $this->c->MAINTENANCE_OFF
|
||||
) {
|
||||
$form['sets']['maintenance-only'] = [
|
||||
'inform' => [
|
||||
[
|
||||
'html' => '- - -',
|
||||
],
|
||||
[
|
||||
'message' => 'Maintenance only',
|
||||
],
|
||||
],
|
||||
];
|
||||
$form['btns']['rebuild']['disabled'] = true;
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -217,6 +235,13 @@ class Maintenance extends Admin
|
|||
*/
|
||||
public function rebuild(array $args, string $method): Page
|
||||
{
|
||||
if (
|
||||
1 !== $this->c->config->b_maintenance
|
||||
|| $this->c->MAINTENANCE_OFF
|
||||
) {
|
||||
return $this->c->Message->message('Maintenance only');
|
||||
}
|
||||
|
||||
$this->c->Lang->load('validator');
|
||||
$this->c->Lang->load('admin_maintenance');
|
||||
|
||||
|
@ -306,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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace ForkBB\Models\Pages\Admin;
|
||||
|
||||
use ForkBB\Core\Image;
|
||||
use ForkBB\Core\Validator;
|
||||
use ForkBB\Models\Page;
|
||||
use ForkBB\Models\Pages\Admin;
|
||||
|
@ -43,6 +44,7 @@ class Options extends Admin
|
|||
'token' => 'token:AdminOptions',
|
||||
'o_board_title' => 'required|string:trim|max:255',
|
||||
'o_board_desc' => 'exist|string:trim,empty|max:65000 bytes|html',
|
||||
's_meta_desc' => 'exist|string:trim,empty|max:255',
|
||||
'o_default_timezone' => [
|
||||
'required',
|
||||
'string:trim',
|
||||
|
@ -113,6 +115,8 @@ class Options extends Admin
|
|||
'b_poll_guest' => 'required|integer|in:0,1',
|
||||
'b_pm' => 'required|integer|in:0,1',
|
||||
'b_oauth_allow' => 'required|integer|in:0,1',
|
||||
'upload_og_image' => 'image',
|
||||
'delete_og_image' => 'checkbox',
|
||||
])->addAliases([
|
||||
])->addArguments([
|
||||
])->addMessages([
|
||||
|
@ -121,20 +125,56 @@ class Options extends Admin
|
|||
'o_webmaster_email' => 'Invalid webmaster e-mail message',
|
||||
]);
|
||||
|
||||
$valid = $v->validation($_POST);
|
||||
$valid = $v->validation($_FILES + $_POST);
|
||||
$data = $v->getData();
|
||||
|
||||
if (empty($data['changeSmtpPassword'])) {
|
||||
unset($data['o_smtp_pass']);
|
||||
}
|
||||
|
||||
unset($data['changeSmtpPassword'], $data['token']);
|
||||
unset($data['changeSmtpPassword'], $data['token'], $data['upload_og_image'], $data['delete_og_image']);
|
||||
|
||||
foreach ($data as $attr => $value) {
|
||||
$config->$attr = $value;
|
||||
}
|
||||
|
||||
if ($valid) {
|
||||
if (
|
||||
$v->delete_og_image
|
||||
|| $v->upload_og_image instanceof Image
|
||||
) {
|
||||
$folder = $this->c->DIR_PUBLIC . '/img/og/';
|
||||
|
||||
$this->deleteOgImage($folder);
|
||||
|
||||
$config->a_og_image = [];
|
||||
}
|
||||
|
||||
if ($v->upload_og_image instanceof Image) {
|
||||
$path = $folder . $this->c->Secury->randomPass(8) . '.webp';
|
||||
|
||||
$result = $v->upload_og_image
|
||||
->rename(true)
|
||||
->rewrite(false)
|
||||
->setQuality($this->c->config->i_avatars_quality ?? 75)
|
||||
->toFile($path);
|
||||
|
||||
if (true === $result) {
|
||||
$config->a_og_image = [
|
||||
'file' => $v->upload_og_image->name() . '.' . $v->upload_og_image->ext(),
|
||||
'width' => $v->upload_og_image->width(),
|
||||
'height' => $v->upload_og_image->height(),
|
||||
];
|
||||
} else {
|
||||
$config->a_og_image = [];
|
||||
|
||||
$this->c->Log->warning('og:image Failed image processing', [
|
||||
'user' => $this->user->fLog(),
|
||||
'error' => $v->upload_og_image->error(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$config->save();
|
||||
|
||||
return $this->c->Redirect->page('AdminOptions')->message('Options updated redirect', FORK_MESS_SUCC);
|
||||
|
@ -152,6 +192,20 @@ class Options extends Admin
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет текущую картинку Open Graph
|
||||
*/
|
||||
protected function deleteOgImage(string $folder): void
|
||||
{
|
||||
if (! empty($this->c->config->a_og_image['file'])) {
|
||||
$path = $folder . $this->c->config->a_og_image['file'];
|
||||
|
||||
if (\is_file($path)) {
|
||||
\unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Дополнительная проверка времени online
|
||||
*/
|
||||
|
@ -201,12 +255,13 @@ class Options extends Admin
|
|||
protected function formEdit(Config $config): array
|
||||
{
|
||||
$form = [
|
||||
'action' => $this->c->Router->link('AdminOptions'),
|
||||
'hidden' => [
|
||||
'action' => $this->c->Router->link('AdminOptions'),
|
||||
'hidden' => [
|
||||
'token' => $this->c->Csrf->create('AdminOptions'),
|
||||
],
|
||||
'sets' => [],
|
||||
'btns' => [
|
||||
'enctype' => 'multipart/form-data',
|
||||
'sets' => [],
|
||||
'btns' => [
|
||||
'save' => [
|
||||
'type' => 'submit',
|
||||
'value' => __('Save changes'),
|
||||
|
@ -218,6 +273,10 @@ class Options extends Admin
|
|||
$langs = $this->c->Func->getNameLangs();
|
||||
$styles = $this->c->Func->getStyles();
|
||||
|
||||
if (isset($config->a_og_image['file'])) {
|
||||
$this->ogImageUrl = $this->c->PUBLIC_URL . '/img/og/' . $config->a_og_image['file'];
|
||||
}
|
||||
|
||||
$form['sets']['essentials'] = [
|
||||
'legend' => 'Essentials subhead',
|
||||
'fields' => [
|
||||
|
@ -236,6 +295,13 @@ class Options extends Admin
|
|||
'caption' => 'Board desc label',
|
||||
'help' => 'Board desc help',
|
||||
],
|
||||
's_meta_desc' => [
|
||||
'type' => 'text',
|
||||
'maxlength' => '255',
|
||||
'value' => $config->s_meta_desc,
|
||||
'caption' => 'Meta desc label',
|
||||
'help' => 'Meta desc help',
|
||||
],
|
||||
'o_default_timezone' => [
|
||||
'type' => 'select',
|
||||
'options' => $this->createTimeZoneOptions(),
|
||||
|
@ -257,6 +323,23 @@ class Options extends Admin
|
|||
'caption' => 'Default style label',
|
||||
'help' => 'Default style help',
|
||||
],
|
||||
'a_og_image' => [
|
||||
'type' => empty($this->ogImageUrl) ? 'str' : 'yield',
|
||||
'caption' => 'Og image label',
|
||||
'value' => empty($this->ogImageUrl) ? __('Not uploaded') : 'og:image',
|
||||
'help' => empty($this->ogImageUrl) ? null : ['Og image help', $config->a_og_image['width'], $config->a_og_image['height']],
|
||||
],
|
||||
'delete_og_image' => [
|
||||
'type' => 'checkbox',
|
||||
'label' => 'Delete og image',
|
||||
'checked' => false,
|
||||
],
|
||||
'upload_og_image' => [
|
||||
'type' => 'file',
|
||||
'caption' => 'New og image label',
|
||||
'help' => 'New og image help',
|
||||
'accept' => 'image/*',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
|
|
|
@ -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 = 67;
|
||||
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) {
|
||||
|
@ -871,4 +875,195 @@ class Update extends Admin
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* rev.67 to rev.68
|
||||
*/
|
||||
protected function stageNumber67(array $args): ?int
|
||||
{
|
||||
$config = $this->c->config;
|
||||
|
||||
$config->s_meta_desc ??= '';
|
||||
$config->a_og_image ??= [];
|
||||
|
||||
$config->save();
|
||||
|
||||
$this->c->DB->addIndex('::posts', 'editor_id_idx', ['editor_id']);
|
||||
|
||||
$this->c->DB->dropIndex('::topics', 'forum_id_idx');
|
||||
$this->c->DB->dropIndex('::topics', 'moved_to_idx');
|
||||
$this->c->DB->addIndex('::topics', 'multi_1_idx', ['moved_to', 'forum_id', 'num_replies', 'last_post']);
|
||||
$this->c->DB->addIndex('::topics', 'multi_2_idx', ['forum_id', 'sticky', 'last_post']);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* rev.68 to rev.69
|
||||
*/
|
||||
protected function stageNumber68(array $args): ?int
|
||||
{
|
||||
$coreConfig = new CoreConfig($this->configFile);
|
||||
|
||||
$coreConfig->add(
|
||||
'shared=>View',
|
||||
[
|
||||
'class' => '\\ForkBB\\Core\\View::class',
|
||||
'config' => [
|
||||
'cache' => '\'%DIR_CACHE%\'',
|
||||
'defaultDir' => '\'%DIR_VIEWS%/_default\'',
|
||||
'userDir' => '\'%DIR_VIEWS%/_user\'',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$coreConfig->save();
|
||||
|
||||
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',
|
||||
|
@ -416,6 +416,13 @@ class View extends Users
|
|||
*/
|
||||
public function recalculate(array $args, string $method): Page
|
||||
{
|
||||
if (
|
||||
1 !== $this->c->config->b_maintenance
|
||||
|| $this->c->MAINTENANCE_OFF
|
||||
) {
|
||||
return $this->c->Message->message('Maintenance only');
|
||||
}
|
||||
|
||||
$v = $this->c->Validator->reset()
|
||||
->addValidators([
|
||||
])->addRules([
|
||||
|
@ -435,7 +442,12 @@ class View extends Users
|
|||
);
|
||||
}
|
||||
|
||||
if (\function_exists('\\set_time_limit')) {
|
||||
\set_time_limit(0);
|
||||
}
|
||||
|
||||
$this->c->users->updateCountPosts();
|
||||
$this->c->users->updateCountTopics();
|
||||
|
||||
return $this->c->Redirect->page('AdminUsers')->message('Updated the number of users posts redirect', FORK_MESS_SUCC);
|
||||
}
|
||||
|
@ -470,6 +482,20 @@ class View extends Users
|
|||
],
|
||||
];
|
||||
|
||||
if (
|
||||
1 !== $this->c->config->b_maintenance
|
||||
|| $this->c->MAINTENANCE_OFF
|
||||
) {
|
||||
$form['sets']['maintenance-only'] = [
|
||||
'inform' => [
|
||||
[
|
||||
'message' => 'Maintenance only',
|
||||
],
|
||||
],
|
||||
];
|
||||
$form['btns']['recalculate']['disabled'] = true;
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,6 +120,7 @@ class Auth extends Page
|
|||
$this->nameTpl = 'login';
|
||||
$this->onlinePos = 'login';
|
||||
$this->onlineDetail = null;
|
||||
$this->canonical = $this->c->Router->link('Login');
|
||||
$this->robots = 'noindex';
|
||||
$this->titles = 'Login';
|
||||
$this->regLink = 1 === $this->c->config->b_regs_allow ? $this->c->Router->link('Register') : null;
|
||||
|
@ -205,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) {
|
||||
|
@ -363,6 +364,7 @@ class Auth extends Page
|
|||
$this->nameTpl = 'passphrase_reset';
|
||||
$this->onlinePos = 'passphrase_reset';
|
||||
$this->onlineDetail = null;
|
||||
$this->canonical = $this->c->Router->link('Forget');
|
||||
$this->robots = 'noindex';
|
||||
$this->titles = 'Passphrase reset';
|
||||
$this->form = $this->formForget($v->email ?? $email);
|
||||
|
|
|
@ -72,7 +72,7 @@ class Delete extends Page
|
|||
|
||||
$this->nameTpl = 'post';
|
||||
$this->onlinePos = 'topic-' . $topic->id;
|
||||
$this->canonical = $post->linkDelete;
|
||||
// $this->canonical = $post->linkDelete;
|
||||
$this->robots = 'noindex';
|
||||
$this->formTitle = $deleteTopic ? 'Delete topic' : 'Delete post';
|
||||
$this->crumbs = $this->crumbs($this->formTitle, $topic);
|
||||
|
|
|
@ -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
|
||||
|
@ -129,7 +130,7 @@ class Edit extends Page
|
|||
|
||||
$this->nameTpl = 'post';
|
||||
$this->onlinePos = 'topic-' . $topic->id;
|
||||
$this->canonical = $post->linkEdit;
|
||||
// $this->canonical = $post->linkEdit;
|
||||
$this->robots = 'noindex';
|
||||
$this->formTitle = $firstPost ? 'Edit topic' : 'Edit post';
|
||||
$this->crumbs = $this->crumbs($this->formTitle, $topic);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,8 @@ abstract class Profile extends Page
|
|||
$this->nameTpl = 'profile';
|
||||
$this->onlinePos = 'profile-' . $this->curUser->id; // ????
|
||||
|
||||
$this->mDescription = __(['mDescription for %s', $this->curUser->username]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -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'] = [
|
||||
|
@ -391,11 +396,13 @@ class View extends Profile
|
|||
];
|
||||
}
|
||||
|
||||
$form['sets']['private'] = [
|
||||
'class' => ['data'],
|
||||
'legend' => 'Private information',
|
||||
'fields' => $fields,
|
||||
];
|
||||
if (! empty($fields)) {
|
||||
$form['sets']['private'] = [
|
||||
'class' => ['data'],
|
||||
'legend' => 'Private information',
|
||||
'fields' => $fields,
|
||||
];
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
|
|
@ -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 = [];
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ class Rules extends Page
|
|||
$this->nameTpl = 'rules';
|
||||
$this->onlinePos = 'rules';
|
||||
$this->onlineDetail = null;
|
||||
$this->canonical = $this->c->Router->link('Register');
|
||||
$this->robots = 'noindex';
|
||||
$this->crumbs = $this->crumbs(
|
||||
'Forum rules',
|
||||
|
|
|
@ -143,7 +143,7 @@ class Search extends Page
|
|||
$this->nameTpl = 'search';
|
||||
$this->onlinePos = 'search';
|
||||
$this->onlineDetail = null;
|
||||
$this->canonical = $this->c->Router->link('Search');
|
||||
$this->canonical = $this->c->Router->link($advanced ? 'SearchAdvanced' : 'Search');
|
||||
$this->robots = 'noindex';
|
||||
$this->form = $advanced ? $this->formSearchAdvanced($v) : $this->formSearch($v);
|
||||
$this->crumbs = $this->crumbs();
|
||||
|
@ -334,7 +334,6 @@ class Search extends Page
|
|||
if (! $search->prepare($query)) {
|
||||
$v->addError([$search->queryError, $search->queryText]);
|
||||
} else {
|
||||
|
||||
if ($this->c->search->execute($v, $this->listOfIndexes, $flood)) {
|
||||
$flood = false;
|
||||
|
||||
|
@ -384,7 +383,9 @@ class Search extends Page
|
|||
if (! empty(\array_diff($forums, $this->listOfIndexes))) {
|
||||
$v->addError('The :alias contains an invalid value');
|
||||
}
|
||||
|
||||
\sort($forums, SORT_NUMERIC);
|
||||
|
||||
$forums = \implode('.', $forums);
|
||||
}
|
||||
|
||||
|
@ -500,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)');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,6 +52,10 @@ class Delete extends Action
|
|||
throw new InvalidArgumentException('Expected only Poll(s) or Topic(s)');
|
||||
}
|
||||
|
||||
if (\count($tids) > 1) {
|
||||
\sort($tids, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':tids' => $tids,
|
||||
];
|
||||
|
|
|
@ -43,9 +43,9 @@ class Save extends Action
|
|||
SET qna_text=?s:qna
|
||||
WHERE tid=?i:tid AND question_id=?i:qid AND field_id=?i:fid';
|
||||
$queryD1 = 'DELETE FROM ::poll
|
||||
WHERE tid=?i:tid AND question_id IN(?ai:qids)';
|
||||
WHERE tid=?i:tid AND question_id IN (?ai:qids)';
|
||||
$queryD2 = 'DELETE FROM ::poll
|
||||
WHERE tid=?i:tid AND question_id=?i:qid AND field_id IN(?ai:fid)';
|
||||
WHERE tid=?i:tid AND question_id=?i:qid AND field_id IN (?ai:fid)';
|
||||
|
||||
$modified = false;
|
||||
$oldQuestion = $old->question;
|
||||
|
|
|
@ -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)');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,17 +133,17 @@ class Delete extends Action
|
|||
|
||||
$parents = $this->c->topics->loadByIds($tids, false);
|
||||
|
||||
$query = 'UPDATE ::posts
|
||||
SET editor_id=0
|
||||
WHERE editor_id IN (?ai:users)';
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
|
||||
$query = 'DELETE
|
||||
FROM ::posts
|
||||
WHERE poster_id IN (?ai:users)';
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
|
||||
$query = 'UPDATE ::posts
|
||||
SET editor_id=0
|
||||
WHERE editor_id IN (?ai:users)';
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
}
|
||||
|
||||
if ($forums) {
|
||||
|
@ -161,13 +158,19 @@ class Delete extends Action
|
|||
|
||||
$uidsUpdate = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
$query = 'DELETE
|
||||
FROM ::posts
|
||||
WHERE topic_id IN (
|
||||
SELECT id
|
||||
FROM ::topics
|
||||
WHERE forum_id IN (?ai:forums)
|
||||
)';
|
||||
$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',
|
||||
|
||||
default => 'DELETE
|
||||
FROM ::posts
|
||||
WHERE topic_id IN (
|
||||
SELECT id
|
||||
FROM ::topics
|
||||
WHERE forum_id IN (?ai:forums)
|
||||
)',
|
||||
};
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
}
|
||||
|
@ -191,6 +194,10 @@ class Delete extends Action
|
|||
}
|
||||
|
||||
if ($pids) {
|
||||
if (\count($pids) > 1) {
|
||||
\sort($pids, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':posts' => $pids,
|
||||
];
|
||||
|
@ -202,15 +209,24 @@ class Delete extends Action
|
|||
}
|
||||
|
||||
if ($parents) {
|
||||
if (\count($parents) > 1) {
|
||||
\ksort($parents, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$topics = $parents;
|
||||
$parents = [];
|
||||
|
||||
foreach ($topics as $topic) {
|
||||
$parents[$topic->parent->id] = $topic->parent;
|
||||
|
||||
$this->c->topics->update($topic->calcStat());
|
||||
}
|
||||
|
||||
if (! $forums) {
|
||||
if (\count($parents) > 1) {
|
||||
\ksort($parents, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
foreach ($parents as $forum) {
|
||||
$this->c->forums->update($forum->calcStat());
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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';
|
||||
|
||||
$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 t.forum_id IN(?ai:forums)
|
||||
ORDER BY p.id DESC
|
||||
LIMIT 50';
|
||||
|
||||
} else {
|
||||
throw new InvalidArgumentException('Expected Topic or Forum');
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -68,19 +68,25 @@ class ActionT extends Method
|
|||
|
||||
break;
|
||||
case 'topics_with_your_posts':
|
||||
/*
|
||||
$query = 'SELECT t.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
|
||||
GROUP BY t.id
|
||||
ORDER BY t.last_post DESC';
|
||||
*/
|
||||
// упрощенный запрос для больших форумов, дополнительная обработка ниже
|
||||
$query = 'SELECT DISTINCT t.id, t.last_post
|
||||
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;
|
||||
case 'topics':
|
||||
$query = 'SELECT t.id
|
||||
FROM ::topics AS t
|
||||
INNER JOIN ::posts AS p ON t.first_post_id=p.id
|
||||
WHERE t.forum_id IN (?ai:forums) AND t.moved_to=0 AND p.poster_id=?i:uid
|
||||
WHERE t.forum_id IN (?ai:forums) AND t.moved_to=0 AND t.poster_id=?i:uid
|
||||
ORDER BY t.first_post_id DESC'; // t.last_post
|
||||
|
||||
break;
|
||||
|
@ -90,8 +96,8 @@ class ActionT extends Method
|
|||
LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND mot.tid=t.id)
|
||||
LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:uid AND mof.fid=t.forum_id)
|
||||
WHERE t.forum_id IN (?ai:forums)
|
||||
AND t.last_post>?i:max
|
||||
AND t.moved_to=0
|
||||
AND t.last_post>?i:max
|
||||
AND (mot.mt_last_visit IS NULL OR t.last_post>mot.mt_last_visit)
|
||||
AND (mof.mf_mark_all_read IS NULL OR t.last_post>mof.mf_mark_all_read)
|
||||
ORDER BY t.last_post DESC';
|
||||
|
@ -126,7 +132,15 @@ class ActionT extends Method
|
|||
':max' => \max((int) $this->c->user->last_visit, (int) $this->c->user->u_mark_all_read),
|
||||
];
|
||||
|
||||
$list = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
|
||||
if ('topics_with_your_posts' === $action) {
|
||||
$list = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
\arsort($list, \SORT_NUMERIC);
|
||||
|
||||
$list = \array_keys($list);
|
||||
} else {
|
||||
$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_topics);
|
||||
|
|
|
@ -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,16 +23,16 @@ 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)');
|
||||
}
|
||||
|
||||
$uids = [];
|
||||
$forums = [];
|
||||
$topics = [];
|
||||
$posts = [];
|
||||
$fids = [];
|
||||
$tids = [];
|
||||
$pids = [];
|
||||
$isUser = 0;
|
||||
$isForum = 0;
|
||||
$isTopic = 0;
|
||||
|
@ -55,15 +54,15 @@ class Delete extends Method
|
|||
throw new RuntimeException('Forum unavailable');
|
||||
}
|
||||
|
||||
$forums[$arg->id] = $arg;
|
||||
$isForum = 1;
|
||||
$fids[$arg->id] = $arg->id;
|
||||
$isForum = 1;
|
||||
} elseif ($arg instanceof Topic) {
|
||||
if (! $arg->parent instanceof Forum) {
|
||||
throw new RuntimeException('Parent unavailable');
|
||||
}
|
||||
|
||||
$topics[$arg->id] = $arg;
|
||||
$isTopic = 1;
|
||||
$tids[$arg->id] = $arg->id;
|
||||
$isTopic = 1;
|
||||
} elseif ($arg instanceof Post) {
|
||||
if (
|
||||
! $arg->parent instanceof Topic
|
||||
|
@ -72,10 +71,8 @@ class Delete extends Method
|
|||
throw new RuntimeException('Parents unavailable');
|
||||
}
|
||||
|
||||
$posts[$arg->id] = $arg;
|
||||
$isPost = 1;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Expected User(s), Forum(s), Topic(s) or Post(s)');
|
||||
$pids[$arg->id] = $arg->id;
|
||||
$isPost = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,45 +84,67 @@ class Delete extends Method
|
|||
$vars = [
|
||||
':users' => $uids,
|
||||
];
|
||||
$query = 'DELETE
|
||||
FROM ::search_matches
|
||||
WHERE post_id IN (
|
||||
SELECT p.id
|
||||
FROM ::posts AS p
|
||||
WHERE p.poster_id IN (?ai:users)
|
||||
)';
|
||||
$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',
|
||||
|
||||
default => 'DELETE
|
||||
FROM ::search_matches
|
||||
WHERE post_id IN (
|
||||
SELECT p.id
|
||||
FROM ::posts AS p
|
||||
WHERE p.poster_id IN (?ai:users)
|
||||
)',
|
||||
};
|
||||
}
|
||||
|
||||
if ($forums) {
|
||||
if ($fids) {
|
||||
$vars = [
|
||||
':forums' => \array_keys($forums),
|
||||
':forums' => $fids,
|
||||
];
|
||||
$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)
|
||||
)';
|
||||
$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',
|
||||
|
||||
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 ($topics) {
|
||||
if ($tids) {
|
||||
$vars = [
|
||||
':topics' => \array_keys($topics),
|
||||
':topics' => $tids,
|
||||
];
|
||||
$query = 'DELETE
|
||||
FROM ::search_matches
|
||||
WHERE post_id IN (
|
||||
SELECT p.id
|
||||
FROM ::posts AS p
|
||||
WHERE p.topic_id IN (?ai:topics)
|
||||
)';
|
||||
$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',
|
||||
|
||||
default => 'DELETE
|
||||
FROM ::search_matches
|
||||
WHERE post_id IN (
|
||||
SELECT p.id
|
||||
FROM ::posts AS p
|
||||
WHERE p.topic_id IN (?ai:topics)
|
||||
)',
|
||||
};
|
||||
}
|
||||
|
||||
if ($posts) {
|
||||
if ($pids) {
|
||||
if (\count($pids) > 1) {
|
||||
\sort($pids, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':posts' => \array_keys($posts),
|
||||
':posts' => $pids,
|
||||
];
|
||||
$query = 'DELETE
|
||||
FROM ::search_matches
|
||||
|
|
|
@ -80,7 +80,7 @@ class Index extends Method
|
|||
];
|
||||
$query = 'SELECT sw.word
|
||||
FROM ::search_words AS sw
|
||||
WHERE sw.word IN(?as:words)';
|
||||
WHERE sw.word IN (?as:words)';
|
||||
|
||||
$oldWords = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
|
||||
$newWords = \array_diff($allWords, $oldWords);
|
||||
|
@ -106,6 +106,10 @@ class Index extends Method
|
|||
continue;
|
||||
}
|
||||
|
||||
if (\count($list) > 1) {
|
||||
\sort($list, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':pid' => $post->id,
|
||||
':subj' => 's' === $key ? 1 : 0,
|
||||
|
@ -113,7 +117,7 @@ class Index extends Method
|
|||
];
|
||||
$query = 'DELETE
|
||||
FROM ::search_matches
|
||||
WHERE word_id IN(?ai:ids) AND post_id=?i:pid AND subject_match=?i:subj';
|
||||
WHERE word_id IN (?ai:ids) AND post_id=?i:pid AND subject_match=?i:subj';
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
}
|
||||
|
@ -132,7 +136,7 @@ class Index extends Method
|
|||
$query = 'INSERT INTO ::search_matches (post_id, word_id, subject_match)
|
||||
SELECT ?i:pid, id, ?i:subj
|
||||
FROM ::search_words
|
||||
WHERE word IN(?as:words)';
|
||||
WHERE word IN (?as:words)';
|
||||
|
||||
$this->c->DB->exec($query, $vars);
|
||||
}
|
||||
|
|
|
@ -19,8 +19,14 @@ class TruncateIndex extends Method
|
|||
*/
|
||||
public function truncateIndex(): void
|
||||
{
|
||||
if ($this->c->DB->inTransaction()) {
|
||||
$this->c->DB->commit();
|
||||
}
|
||||
|
||||
$this->c->DB->truncateTable('::search_cache');
|
||||
$this->c->DB->truncateTable('::search_matches');
|
||||
$this->c->DB->truncateTable('::search_words');
|
||||
|
||||
$this->c->DB->beginTransaction();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,14 +89,13 @@ class Subscription extends Model
|
|||
|
||||
if (! empty($this->forums)) {
|
||||
$query = 'INSERT INTO ::forum_subscriptions (user_id, forum_id)
|
||||
SELECT ?i:uid, ?i:id
|
||||
FROM ::groups
|
||||
SELECT tmp.*
|
||||
FROM (SELECT ?i:uid AS f1, ?i:id AS f2) AS tmp
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ::forum_subscriptions
|
||||
WHERE user_id=?i:uid AND forum_id=?i:id
|
||||
)
|
||||
LIMIT 1';
|
||||
)';
|
||||
|
||||
foreach ($this->forums as $id) {
|
||||
$vars[':id'] = $id;
|
||||
|
@ -107,14 +106,13 @@ class Subscription extends Model
|
|||
|
||||
if (! empty($this->topics)) {
|
||||
$query = 'INSERT INTO ::topic_subscriptions (user_id, topic_id)
|
||||
SELECT ?i:uid, ?i:id
|
||||
FROM ::groups
|
||||
SELECT tmp.*
|
||||
FROM (SELECT ?i:uid AS f1, ?i:id AS f2) AS tmp
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ::topic_subscriptions
|
||||
WHERE user_id=?i:uid AND topic_id=?i:id
|
||||
)
|
||||
LIMIT 1';
|
||||
)';
|
||||
|
||||
foreach ($this->topics as $id) {
|
||||
$vars[':id'] = $id;
|
||||
|
@ -143,20 +141,23 @@ class Subscription extends Model
|
|||
$where[':uid'] = 'user_id=?i:uid';
|
||||
$vars[':uid'] = \reset($this->users);
|
||||
} else {
|
||||
$where[':uid'] = 'user_id IN(?ai:uid)';
|
||||
$where[':uid'] = 'user_id IN (?ai:uid)';
|
||||
$vars[':uid'] = $this->users;
|
||||
}
|
||||
}
|
||||
|
||||
$all = empty($this->forums) && empty($this->topics);
|
||||
|
||||
if ($all || ! empty($this->forums)) {
|
||||
if (
|
||||
$all
|
||||
|| ! empty($this->forums)
|
||||
) {
|
||||
if (! empty($this->forums)) {
|
||||
if (1 === \count($this->forums)) {
|
||||
$where[':id'] = 'forum_id=?i:id';
|
||||
$vars[':id'] = \reset($this->forums);
|
||||
} else {
|
||||
$where[':id'] = 'forum_id IN(?ai:id)';
|
||||
$where[':id'] = 'forum_id IN (?ai:id)';
|
||||
$vars[':id'] = $this->forums;
|
||||
}
|
||||
}
|
||||
|
@ -170,13 +171,16 @@ class Subscription extends Model
|
|||
|
||||
unset($where[':id'], $vars[':id']);
|
||||
|
||||
if ($all || ! empty($this->topics)) {
|
||||
if (
|
||||
$all
|
||||
|| ! empty($this->topics)
|
||||
) {
|
||||
if (! empty($this->topics)) {
|
||||
if (1 === \count($this->topics)) {
|
||||
$where[':id'] = 'topic_id=?i:id';
|
||||
$vars[':id'] = \reset($this->topics);
|
||||
} else {
|
||||
$where[':id'] = 'topic_id IN(?ai:id)';
|
||||
$where[':id'] = 'topic_id IN (?ai:id)';
|
||||
$vars[':id'] = $this->topics;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,10 @@ class Access extends Action
|
|||
}
|
||||
|
||||
if (! empty($ids)) {
|
||||
if (\count($ids) > 1) {
|
||||
\sort($ids, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$vars = [
|
||||
':ids' => $ids,
|
||||
':closed' => $open ? 0 : 1,
|
||||
|
|
|
@ -53,7 +53,7 @@ class CalcStat extends Method
|
|||
];
|
||||
$query = 'SELECT p.id, p.poster, p.poster_id, p.posted, p.edited
|
||||
FROM ::posts AS p
|
||||
WHERE p.id IN(?ai:ids)';
|
||||
WHERE p.id IN (?ai:ids)';
|
||||
|
||||
$result = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_UNIQUE);
|
||||
|
||||
|
|
|
@ -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)');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,12 +103,15 @@ class Delete extends Action
|
|||
FROM ::topics AS t
|
||||
WHERE t.poster_id IN (?ai:users)';
|
||||
|
||||
$tids = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
|
||||
$topics = $this->manager->loadByIds($tids, false);
|
||||
$tids = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
foreach ($topics as $topic) {
|
||||
$parents[$topic->parent->id] = $topic->parent;
|
||||
}
|
||||
# $topics = $this->manager->loadByIds($tids, false);
|
||||
#
|
||||
# foreach ($topics as $topic) {
|
||||
# $parents[$topic->parent->id] = $topic->parent;
|
||||
# }
|
||||
|
||||
$this->delete(...($this->manager->loadByIds($tids, false)));
|
||||
}
|
||||
|
||||
$this->c->posts->delete(...$args);
|
||||
|
@ -149,6 +149,10 @@ class Delete extends Action
|
|||
throw new RuntimeException('Bad topic');
|
||||
}
|
||||
|
||||
if (\count($topics) > 1) {
|
||||
\ksort($topics, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$this->c->subscriptions->unsubscribe(...$topics);
|
||||
$this->c->polls->delete(...$topics);
|
||||
|
||||
|
@ -175,6 +179,10 @@ class Delete extends Action
|
|||
}
|
||||
|
||||
if ($parents) {
|
||||
if (\count($parents) > 1) {
|
||||
\ksort($parents, \SORT_NUMERIC);
|
||||
}
|
||||
|
||||
foreach ($parents as $forum) {
|
||||
$this->c->forums->update($forum->calcStat());
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue