Compare commits

...

128 commits

Author SHA1 Message Date
Visman
e88121236c Minor changes 2023-11-22 23:21:30 +07:00
Visman
13598f350e Use current Forums 2023-11-22 23:05:08 +07:00
Visman
46714487d4 Use the current Forums manager in the Forum model
Before this commit, the container->forums manager was used, which caused a logical error when deleting messages if there were hidden forums.
2023-11-22 13:51:52 +07:00
Visman
7530f5fee0 Models\Model.: Add setManager() method to pass the current Manager to the model 2023-11-22 13:48:33 +07:00
Visman
8ec4676535 Fix 2 for #29
:)
2023-11-22 00:40:16 +07:00
Visman
6917fc8e53 Fix for #29 2023-11-22 00:37:45 +07:00
Visman
f53b6965dc Change debug info 2023-11-20 14:45:59 +07:00
Visman
6687175940 Update sccontent.css 2023-11-20 12:42:11 +07:00
Visman
46df93ef9c Change the method for receiving all user messages
The previous method increased memory consumption as the number of user posts increased.
2023-11-19 23:38:20 +07:00
Visman
d091b7e30d Update style for links 2023-11-19 12:55:32 +07:00
Visman
db591e53fa Set :focus-visible only for a and .f-btn 2023-11-18 19:04:54 +07:00
Visman
766e79b8ed Close links to posts/topics of users for visitors identified as bots 2023-11-18 19:01:54 +07:00
Visman
af6fd98f09 Style: Add :focus-visible 2023-11-18 17:52:41 +07:00
Visman
f3976236ec Update style for links 2023-11-18 17:18:28 +07:00
Visman
ef8e34c861 Pages\Forum: Display feed link only for forums that have posts 2023-11-18 12:28:08 +07:00
Visman
a5c27acb2a Post\Feed: Fix for empty list 2023-11-18 12:00:44 +07:00
Visman
70e196eefd Apply the same type of check for redirect_url 2023-11-17 21:28:10 +07:00
Visman
fe532b6990 Admin\Forums: Add regex for redirect_url 2023-11-17 21:11:29 +07:00
Visman
85e2c3ed29 Skip forums-redirect when selecting unfollow 2023-11-17 18:53:52 +07:00
Visman
3299b641a7 Inline not use max-width 2023-11-17 18:51:37 +07:00
Visman
b7001bc83c link 2023-11-17 18:51:02 +07:00
Visman
baaecfcea7 Delete apc_delete_file() 2023-11-15 20:48:50 +07:00
Visman
164e0f8653 Downgrade CSP status for PM from secury to common 2023-11-15 17:15:31 +07:00
Visman
56160a3a94 Extensions: Add Log->debug 2023-11-15 14:11:09 +07:00
Visman
e98a014f24 Change form validation rules for arrays 2023-11-14 20:51:42 +07:00
Visman
5dc6ecfa23 Core\Validator: Add support for multidimensional rules arrays 2023-11-14 20:21:51 +07:00
Visman
4bd6f93161 Core\Func: Fix FRIENDLY URL for update 2023-11-14 08:46:17 +07:00
Visman
568e119a79 Extensions: fix call to set/remove symlinks 2023-11-13 23:12:35 +07:00
Visman
06486e890b Fix button 2023-11-13 23:11:06 +07:00
Visman
3d6501ac7c Update .dist.htaccess 2023-11-13 20:34:48 +07:00
Visman
b39197b70e Extensions: Add symlinks support
https://github.com/forkbb/forkbb/issues/13
Example: 642109d7c0
2023-11-13 20:13:40 +07:00
Visman
26cd5d3c17 Update .gitignore 2023-11-13 18:03:41 +07:00
Visman
47882fb1d1 Update readme.md 2023-11-12 15:33:26 +07:00
Visman
c2be23603a Pages\Profile\View: Add nofollow for post/topics links 2023-11-11 21:34:33 +07:00
Visman
fadf4098ba Revert "Update Page.php"
This reverts commit b3d238c1cd.
2023-11-11 21:16:45 +07:00
Visman
1ac5847399 Fix slow slow request for feed 2023-11-11 21:09:32 +07:00
Visman
b3d238c1cd Update Page.php 2023-11-11 20:27:37 +07:00
Visman
7ef1e68af7 Add a little rigor to Curl 2023-11-11 19:12:45 +07:00
Visman
d10d8aa2c9 Fix OAuth
fix 9cfd336e7f
2023-11-10 17:24:11 +07:00
Visman
032301df17 Update readme.md 2023-11-10 11:46:58 +07:00
Visman
113df48a3c Up rev 2023-11-10 11:45:54 +07:00
Visman
a0eb7a0e27 Create Sitemap page 2023-11-10 11:44:53 +07:00
Visman
1d57ade40f Pages\Misc: Change sitemap() method 2023-11-10 10:21:25 +07:00
Visman
fabc46f8ee Add rel="nofollow" to topic and forum templates 2023-11-09 22:11:16 +07:00
Visman
2c87b98d24 Pages\Misc: Change sitemap() method 2023-11-09 20:24:07 +07:00
Visman
2feaff7b5c Take into account in the online list the person who accessed the sitemap 2023-11-08 20:29:18 +07:00
Visman
956a2b2d67 Add sitemap.xml for test 2023-11-08 20:05:16 +07:00
Visman
5ebf9eb3f6 Core\Router: Fix for dynamic files in the forum root
Example: /sitemap.xml, /sitemap1.xml, /sitemap542.xml
2023-11-08 19:58:04 +07:00
Visman
c648a52651 Revert "Core\Router: Fix for dynamic files in the forum root"
This reverts commit d4969ae009.
2023-11-08 19:29:14 +07:00
Visman
d4969ae009 Core\Router: Fix for dynamic files in the forum root
Example: /sitemap.xml, /sitemap1.xml, /sitemap542.xml
2023-11-08 16:28:33 +07:00
Visman
40563b4ffc Admin\Maintenance: Resets the contents of the opcode cache after clearing the engine cache
Is it necessary to do this?
2023-11-07 15:58:00 +07:00
Visman
5e1e956de6 Core\Func: Change friendly() method 2023-11-07 15:52:28 +07:00
Visman
7a2efd3bd5 Change default transliteration
With these initial settings, only the character substitution array from translit.default.php will be used for transliteration. The Transliterator class will not be used.
https://forkbb.ru/post/280#p280
2023-11-06 22:22:28 +07:00
Visman
f40602fd82 Up rev 2023-11-04 23:06:39 +07:00
Visman
223efdfb8f Use friendly_name in code 2023-11-04 22:23:12 +07:00
Visman
ced3c7cd15 Add friendly_name field to ::forums table 2023-11-04 21:38:02 +07:00
Visman
4495f64268 Add character substitution file for transliteration 2023-11-04 19:36:25 +07:00
Visman
0e8e5cd87a Fix for #27 2023-11-04 18:25:58 +07:00
Visman
2e64177610 Core\Router: Do less calculations in link() method 2023-11-03 15:44:40 +07:00
Visman
432a441a2c Minor change 2023-11-03 13:32:25 +07:00
Visman
5a3ad9d33e Core\Files: Optimize the filterName() method 2023-11-03 13:32:13 +07:00
Visman
2ddd7796b0 Core\Func: Optimize the friendly() method
Removed the transliterator_transliterate() function since it initializes a new Transliterator every time.
Currently, Transliterator is initialized once with the rules set, and then only the transliterate() method is used.
2023-11-03 12:08:23 +07:00
Visman
9f1d781beb Remove repeated hyphens 2023-11-02 19:38:59 +07:00
Visman
d5eec724d6 Up rev 2023-11-02 19:02:05 +07:00
Visman
acaad2db29 Fix for the previous 2023-11-02 18:56:07 +07:00
Visman
8e3c74367e Add settings for friendly url
https://forkbb.ru/post/280#p280
2023-11-02 18:44:32 +07:00
Visman
e6d66f7e0a Core\Files: Change file name transliteration 2023-11-02 16:58:26 +07:00
Visman
4f2c637134 Admin\Logs: Clear context 2023-11-01 09:40:07 +07:00
Visman
c5f2aa0a97 config\jevix.default: fix rel attribute in a tag
#22
2023-10-31 19:58:34 +07:00
Visman
5ac2b20ff2 Replace base64 images in messages to uploaded files
For users who have permission to upload files.
2023-10-30 20:08:59 +07:00
Visman
414e3d9717 Core\Files: change the verification logic and extend the uploadFromLink() method 2023-10-30 19:05:25 +07:00
Visman
25691fa3af Update form.forkbb.php 2023-10-30 12:58:19 +07:00
Visman
c0bb06dc13 Templates: use php/endphp + if(empty()) to empty() 2023-10-30 12:42:03 +07:00
Visman
4bde2ad136 templates\extensions: fix + change for empty list 2023-10-29 20:31:59 +07:00
Visman
1d917f0151 View\Compiler: add @php and @endphp 2023-10-29 20:31:00 +07:00
Visman
d64b016637 Update readme.md 2023-10-29 19:08:40 +07:00
Visman
1c7d91b643 Update admin.po 2023-10-28 22:03:32 +07:00
Visman
da79516766
Merge pull request #25 from forkbb/Extensions
Extensions
2023-10-28 21:46:07 +07:00
Visman
441c17de3c Minor edits 2023-10-28 21:37:35 +07:00
Visman
5a439cb932 Add multi 'template' for one PRE 'name' 2023-10-24 21:09:41 +07:00
Visman
7344002a3a Add PRE points to templates 2023-10-24 21:01:38 +07:00
Visman
4b48914996 Update Admin\Install page 2023-10-23 20:54:05 +07:00
Visman
a104d0b6b3 Update main config 2023-10-23 20:53:45 +07:00
Visman
a6f39ae72a Add more checks and return by anchor 2023-10-23 20:52:16 +07:00
Visman
2c94e1bc69 Update Extension\Extensions 2023-10-23 00:37:04 +07:00
Visman
b3afd2b87f Another teaspoon of code for the extension system 2 2023-10-22 18:45:08 +07:00
Visman
dff71bcace Another teaspoon of code for the extension system 2023-10-19 22:48:18 +07:00
Visman
95aa1fa7bf Add Admin\Extensions page 2023-10-17 21:35:26 +07:00
Visman
3be1dc4181 Update Extension model 2023-10-17 21:34:03 +07:00
Visman
e3f28a6a14 Update style for summary 2023-10-17 16:36:44 +07:00
Visman
bbb8f87cec View\Compile: Add url 2023-10-17 16:35:40 +07:00
Visman
327c5cfb1a Update .gitignore 2023-10-16 18:10:09 +07:00
Visman
b57a66f0cf Models\Manager: Add access to the repository property 2023-10-16 18:07:40 +07:00
Visman
55ba708c61 Models\Manager: Add access to the repository property 2023-10-16 18:06:04 +07:00
Visman
2d59bbf92e Update config 2023-10-15 21:39:35 +07:00
Visman
f4e5ba2b5f Initial sketch of a manager 2023-10-15 20:22:23 +07:00
Visman
8aaf85b610 Add ::extensions table 2023-10-15 17:31:46 +07:00
Visman
3891d8fced Minor changes 2023-10-14 22:34:24 +07:00
Visman
0cf80df852 Core\Validator: Fix addRules() method for array 2023-10-14 22:32:59 +07:00
Visman
3b2dadd87a Minor changes 2023-10-14 22:31:33 +07:00
Visman
46e402e452 Core\Validator: Fix addRules() method for array 2023-10-14 22:28:16 +07:00
Visman
2cd5513c48 Add folder for extensions 2023-10-12 21:07:21 +07:00
Visman
16d62d4bc2 Pass Composer autoloader to Container 2023-10-12 20:50:07 +07:00
Visman
e42fc8d9f0 View\Compiler: Edit regular expressions 2023-10-11 00:19:16 +07:00
Visman
5c39cdbddf Core\View: Add code pre-insertion to template when compiling
Needed for a plugin system.
2023-10-10 23:29:55 +07:00
Visman
e096fa6965 Core\View: Add delete() method
Probably needed for a plugin system.
2023-10-10 16:32:59 +07:00
Visman
9cfd336e7f Minor changes 2023-10-10 16:31:35 +07:00
Visman
427e6790d4 Admin\Update: add set_time_limit(0) for stage 2023-10-05 20:42:22 +07:00
Visman
fed15d3243 Deny search bots access to the pages for creating a new topic/reply if guests have permission to do these actions 2023-10-04 22:53:28 +07:00
Visman
3be22c5961 Fix router: guests always cannot edit and delete posts 2023-10-04 22:28:16 +07:00
Visman
863a7e50c7 Fix color a:hover, a:focus for SCEditor iframe 2023-10-04 21:24:12 +07:00
Visman
d51ac30d0c Core\View: Add addTplDir() method 2023-10-04 19:28:30 +07:00
Visman
3ddc2c0940 Pages\Moderate: Take page in the second step 2023-10-03 23:15:04 +07:00
Visman
9bf55098a1 Control of redirects topics
#22
2023-10-03 18:22:30 +07:00
Visman
c15e89a2d2 Update form in Profile\Mod page 2023-10-01 19:46:37 +07:00
Visman
5de4b88f1a Add label type to form 2023-10-01 19:42:50 +07:00
Visman
d1acaf15a3 Admin\Bans: Change the format for displaying the ban end date in ban search results 2023-09-29 13:29:54 +07:00
Visman
ec9e3704d8 Admin\Users: Use user time zone for last_post, last_visit and registered 2023-09-28 20:29:16 +07:00
Visman
5e40fc4b3e Admin\Bans: Use user time zone for ban end time 2023-09-28 17:21:39 +07:00
Visman
71dda154a5 Consider user time zone in Validator\date 2023-09-28 15:55:33 +07:00
Visman
c84ac5938f Set step for Firefox bug 2023-09-28 15:54:17 +07:00
Visman
568ff292f5 Move methods to Core\Func
timeToDate() and dateToTime(()
2023-09-28 15:40:39 +07:00
Visman
ba2f6a0461 Add confirm 2023-09-28 14:36:51 +07:00
Visman
537b51d879 Add for admins to edit the author and date of post
You can only change it to the user, not to the guest.
The time of a post does not affect its position in the topic (the display order is by id).
The time of the first post affects the creation time of the topic.
2023-09-27 21:07:20 +07:00
Visman
304a1d720f Cnage Validator - date
Add error: The :alias field contains the time before the start of the Unix epoch.
2023-09-27 19:04:08 +07:00
Visman
dd4cca2680 The link to the topic left for the link to the topic must point to the original topic 2023-09-24 15:29:12 +07:00
Visman
bc3cbca43c Union types 2023-09-21 19:32:19 +07:00
Visman
e045a4c481 Replace switch() to match() 2023-09-21 17:50:37 +07:00
147 changed files with 4735 additions and 654 deletions

View file

@ -4,6 +4,9 @@
#
AddDefaultCharset UTF-8
#Options +FollowSymLinks # For extensions with symlinks
##Options -FollowSymLinks +SymLinksIfOwnerMatch # or this (more security(?), more checks(!!!))
<IfModule mod_autoindex.c>
Options -Indexes
</IfModule>

27
.gitignore vendored
View file

@ -1,17 +1,32 @@
/_*
/.htaccess
/index.php
/app/config/main.php
/app/config/_*
/app/config/db/*
/app/cache/**/*.php
/app/cache/**/*.lock
/app/cache/**/*.tmp
/app/config/ext/*
/app/cache/*
/app/log/*
/public/img/avatars/*
/public/img/og/*
/ext/*
/public/.htaccess
/public/index.php
/public/img/*
/public/style/*
/public/upload/**/*
/public/style/ForkBB_old/*
!/public/img/sm/big_smile.png
!/public/img/sm/cool.png
!/public/img/sm/hmm.png
!/public/img/sm/lol.png
!/public/img/sm/mad.png
!/public/img/sm/neutral.png
!/public/img/sm/roll.png
!/public/img/sm/sad.png
!/public/img/sm/smile.png
!/public/img/sm/tongue.png
!/public/img/sm/wink.png
!/public/img/sm/yikes.png
!/public/style/font/*
!/public/style/ForkBB/*
!/public/style/sc/*
!/public/upload/index.html
!.gitkeep

View file

@ -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

View file

@ -46,7 +46,6 @@ class Primary
$confChange = [
'multiple' => [
'CtrlRouting' => \ForkBB\Controllers\Update::class,
'AdminUpdate' => \ForkBB\Models\Pages\Admin\Update::class,
],
];

View file

@ -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'];

View file

@ -16,6 +16,7 @@ use Psr\SimpleCache\InvalidArgumentException;
use DateInterval;
use DateTime;
use DateTimeZone;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
@ -117,8 +118,9 @@ class FileCache implements CacheInterface
*/
public function clear(): bool
{
$dir = new RecursiveDirectoryIterator($this->cacheDir, RecursiveDirectoryIterator::SKIP_DOTS);
$iterator = new RecursiveIteratorIterator($dir);
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->cacheDir, FilesystemIterator::SKIP_DOTS)
);
$files = new RegexIterator($iterator, '%\.(?:php|tmp)$%i', RegexIterator::MATCH);
$result = true;
@ -224,8 +226,6 @@ class FileCache implements CacheInterface
{
if (\function_exists('\\opcache_invalidate')) {
\opcache_invalidate($file, true);
} elseif (\function_exists('\\apc_delete_file')) {
\apc_delete_file($file);
}
}

View 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;

View file

@ -226,10 +226,12 @@ class DB
case 's':
case 'f':
$value = [1];
break;
default:
$value = [1];
$type = 's';
break;
}

View file

@ -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>";
}

View file

@ -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);

View file

@ -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 : '-';
}
}

View file

@ -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 = [];

View file

@ -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);

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -23,11 +23,12 @@ use RuntimeException;
class View
{
protected string $ext = '.forkbb.php';
protected string $preFile = '';
protected ?Compiler $compilerObj;
protected string $compilerClass = Compiler::class;
protected string $cache;
protected string $cacheDir;
protected string $defaultDir;
protected string $defaultHash;
@ -40,11 +41,11 @@ class View
public function __construct(string|array $config, mixed $views)
{
if (\is_array($config)) {
$this->cache = $config['cache'];
$this->cacheDir = $config['cache'];
$this->defaultDir = $config['defaultDir'];
if (! empty($config['userDir'])) {
$this->other[\hash('md5', $config['userDir'])] = [$config['userDir'], 10];
$this->addTplDir($config['userDir'], 10);
}
if (! empty($config['composers'])) {
@ -56,15 +57,36 @@ class View
if (! empty($config['compiler'])) {
$this->compilerClass = $config['compiler'];
}
if (! empty($config['preFile'])) {
$this->preFile = $config['preFile'];
}
} else {
// для rev. 68 и ниже
$this->cache = $config;
$this->defaultDir = $views;
$this->cacheDir = $config;
$this->defaultDir = $views;
}
$this->defaultHash = \hash('md5', $this->defaultDir);
}
/**
* Добавляет новый каталог шаблонов $pathToDir.
* Сортирует список каталогов в соответствии с приоритетом $priority. По убыванию.
*/
public function addTplDir(string $pathToDir, int $priority): View
{
$this->other[\hash('md5', $pathToDir)] = [$pathToDir, $priority];
if (\count($this->other) > 1) {
\uasort($this->other, function (array $a, array $b) {
return $b[1] <=> $a[1];
});
}
return $this;
}
/**
* Возвращает отображение страницы $p или null
*/
@ -171,13 +193,13 @@ class View
foreach ($this->other as $hash => $cur) {
if (\file_exists($tpl = "{$cur[0]}/{$name}{$this->ext}")) {
$php = "{$this->cache}/_{$st}-{$hash}.php";
$php = "{$this->cacheDir}/_{$st}-{$hash}.php";
if (
! \file_exists($php)
|| \filemtime($tpl) > \filemtime($php)
) {
$this->create($php, $tpl);
$this->create($php, $tpl, $name);
}
return $php;
@ -186,28 +208,38 @@ class View
$hash = $this->defaultHash;
$tpl = "{$this->defaultDir}/{$name}{$this->ext}";
$php = "{$this->cache}/_{$st}-{$hash}.php";
$php = "{$this->cacheDir}/_{$st}-{$hash}.php";
if (
! \file_exists($php)
|| \filemtime($tpl) > \filemtime($php)
) {
$this->create($php, $tpl);
$this->create($php, $tpl, $name);
}
return $php;
}
/**
* Удаляет файлы кэша для шаблона $name
*/
public function delete(string $name): void
{
$st = \preg_replace('%\W%', '-', $name);
\array_map('\\unlink', \glob("{$this->cacheDir}/_{$st}-*.php"));
}
/**
* Генерирует $php файл на основе шаблона $tpl
*/
protected function create(string $php, string $tpl): void
protected function create(string $php, string $tpl, string $name): void
{
if (empty($this->compilerObj)) {
$this->compilerObj = new $this->compilerClass();
$this->compilerObj = new $this->compilerClass($this->preFile);
}
$text = $this->compilerObj->create(\file_get_contents($tpl), \hash('fnv1a32', $tpl));
$text = $this->compilerObj->create($name, \file_get_contents($tpl), \hash('fnv1a32', $tpl));
if (false === \file_put_contents($php, $text, \LOCK_EX)) {
throw new RuntimeException("Failed to write {$php} file");

View file

@ -22,23 +22,33 @@ class Compiler
{
protected string $shortID;
protected int $loopsCounter = 0;
protected array $compilers = [
protected array $compilers = [
'PrePaste',
'Statements',
'Comments',
'Echos',
'Transformations',
];
protected array $preArray = [];
protected string $tplName;
public function __construct()
public function __construct(string $preFile)
{
if (
! empty($preFile)
&& \is_file($preFile)
) {
$this->preArray = include $preFile;
}
}
/**
* Генерирует php код на основе шаблона из $text
*/
public function create(string $text, string $hash): string
public function create(string $name, string $text, string $hash): string
{
$this->shortID = $hash;
$this->tplName = $name;
foreach ($this->compilers as $type) {
$text = $this->{'compile' . $type}($text);
@ -47,6 +57,26 @@ class Compiler
return $text;
}
/**
* Обрабатывает предварительную подстановку кода в шаблон
*/
protected function compilePrePaste(string $value): string
{
$pre = $this->preArray[$this->tplName] ?? null;
return \preg_replace_callback(
'%^[ \t]*+<!-- PRE (\w+) -->[ \t]*(?:\r?\n)?%m',
function($match) use ($pre) {
if (isset($pre[$match[1]])) {
return \rtrim($pre[$match[1]]) . "\n";
} else {
return '';
}
},
$value
);
}
/**
* Обрабатывает операторы начинающиеся с @
*/
@ -80,7 +110,7 @@ class Compiler
{
// {{! !}}
$value = \preg_replace_callback(
'%(@)?\{\{!\s*(.+?)\s*!\}\}(\r?\n)?%s',
'%(@)?\{\{![ \t]*+(.+?)[ \t]*!\}\}(\r?\n)?%',
function($matches) {
$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
@ -96,7 +126,7 @@ class Compiler
// {!! !!}
$value = \preg_replace_callback(
'%\{\!!\s*(.+?)\s*!!\}(\r?\n)?%s',
'%\{\!![ \t]*+(.+?)[ \t]*!!\}(\r?\n)?%',
function($matches) {
$whitespace = empty($matches[2]) ? '' : $matches[2] . $matches[2];
@ -110,7 +140,7 @@ class Compiler
// {{ }}
$value = \preg_replace_callback(
'%(@)?\{\{\s*(.+?)\s*\}\}(\r?\n)?%s',
'%(@)?\{\{(?!!)[ \t]*+(.+?)[ \t]*\}\}(\r?\n)?%',
function($matches) {
$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
@ -141,7 +171,7 @@ class Compiler
declare(strict_types=1);
use function \ForkBB\{__, num, dt, size};
use function \ForkBB\{__, num, dt, size, url};
?>
EOD;
@ -475,4 +505,20 @@ EOD;
{
return "<?php break; ?>";
}
/**
* @php
*/
protected function compilePhp(): string
{
return "<?php";
}
/**
* @endphp
*/
protected function compileEndphp(): string
{
return " ?>";
}
}

View file

@ -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 = [

View file

@ -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;

View file

@ -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'])) {

View 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;
}
}

View 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;
}
}

View file

@ -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)');
}
}

View file

@ -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,

View file

@ -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);
}
/**

View file

@ -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

View file

@ -44,7 +44,7 @@ class UpdateUsername extends Action
$isMod = true;
$forum->modAdd($user); // переименование модератора
$this->c->forums->update($forum);
$this->manager->update($forum);
}
}

View file

@ -33,11 +33,6 @@ class Groups extends Manager
return $this->c->GroupModel->setModelAttrs($attrs);
}
public function getList(): array
{
return $this->repository;
}
/**
* Загрузка списка групп
*/

View file

@ -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);

View file

@ -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;
}
}

View file

@ -37,7 +37,7 @@ class Info extends Method
'User',
[
'id' => $id,
'name' => $name,
'name' => $this->c->Func->friendly($name),
]
)
: null,

View file

@ -259,31 +259,23 @@ class Online extends Model
WHERE user_id=?i:id';
}
} else {
switch ($this->c->DB->getType()) {
case 'mysql':
$query = 'INSERT IGNORE INTO ::online (user_id, ident, logged, o_position, o_name)
VALUES (?i:id, ?s:ident, ?i:logged, ?s:pos, ?s:name)';
$query = match ($this->c->DB->getType()) {
'mysql' => 'INSERT IGNORE INTO ::online (user_id, ident, logged, o_position, o_name)
VALUES (?i:id, ?s:ident, ?i:logged, ?s:pos, ?s:name)',
break;
case 'sqlite':
case 'pgsql':
$query = 'INSERT INTO ::online (user_id, ident, logged, o_position, o_name)
VALUES (?i:id, ?s:ident, ?i:logged, ?s:pos, ?s:name)
ON CONFLICT(user_id, ident) DO NOTHING';
'sqlite', 'pgsql' => 'INSERT INTO ::online (user_id, ident, logged, o_position, o_name)
VALUES (?i:id, ?s:ident, ?i:logged, ?s:pos, ?s:name)
ON CONFLICT(user_id, ident) DO NOTHING',
break;
default:
$query = 'INSERT INTO ::online (user_id, ident, logged, o_position, o_name)
SELECT tmp.*
FROM (SELECT ?i:id AS f1, ?s:ident AS f2, ?i:logged AS f3, ?s:pos AS f4, ?s:name AS f5) AS tmp
WHERE NOT EXISTS (
SELECT 1
FROM ::online
WHERE user_id=?i:id AND ident=?s:ident
)';
break;
}
default => 'INSERT INTO ::online (user_id, ident, logged, o_position, o_name)
SELECT tmp.*
FROM (SELECT ?i:id AS f1, ?s:ident AS f2, ?i:logged AS f3, ?s:pos AS f4, ?s:name AS f5) AS tmp
WHERE NOT EXISTS (
SELECT 1
FROM ::online
WHERE user_id=?i:id AND ident=?s:ident
)',
};
}
$this->c->DB->exec($query, $vars);

View file

@ -11,7 +11,6 @@ declare(strict_types=1);
namespace ForkBB\Models\PM;
use ForkBB\Models\Method;
use ForkBB\Models\DataModel;
use ForkBB\Models\PM\Cnst;
use ForkBB\Models\PM\PPost;
use ForkBB\Models\PM\PTopic;
@ -41,7 +40,7 @@ class Delete extends Method
}
}
public function delete(DataModel ...$args): void
public function delete(PPost|PTopic|User ...$args): void
{
if (empty($args)) {
throw new InvalidArgumentException('No arguments, expected User(s), PPost(s) or PTopic(s)');
@ -77,8 +76,6 @@ class Delete extends Method
$topics[$arg->id] = $arg;
$isTopic = 1;
} else {
throw new InvalidArgumentException('Expected User(s), PPost(s) or PTopic(s)');
}
}

View file

@ -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();

View file

@ -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'],
];
}

View file

@ -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');
}
}

View file

@ -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'],

View file

@ -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([

View 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);
}
}

View file

@ -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,

View file

@ -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)) {

View file

@ -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],

View file

@ -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);

View file

@ -331,6 +331,10 @@ class Maintenance extends Admin
throw new RuntimeException('Unable to clear cache');
}
if (\function_exists('\\opcache_reset')) {
\opcache_reset();
}
return $this->c->Redirect->page('AdminMaintenance')->message('Clear cache redirect', FORK_MESS_SUCC);
}
}

View file

@ -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'],

View file

@ -25,7 +25,7 @@ class Update extends Admin
{
const PHP_MIN = '8.0.0';
const REV_MIN_FOR_UPDATE = 53;
const LATEST_REV_WITH_DB_CHANGES = 68;
const LATEST_REV_WITH_DB_CHANGES = 72;
const LOCK_NAME = 'lock_update';
const LOCK_TTL = 1800;
const CONFIG_FILE = 'main.php';
@ -360,6 +360,10 @@ class Update extends Admin
do {
if (\method_exists($this, 'stageNumber' . $stage)) {
if (\function_exists('\\set_time_limit')) {
\set_time_limit(0);
}
$start = $this->{'stageNumber' . $stage}($args);
if (null === $start) {
@ -917,4 +921,149 @@ class Update extends Admin
return null;
}
/**
* rev.69 to rev.70
*/
protected function stageNumber69(array $args): ?int
{
$coreConfig = new CoreConfig($this->configFile);
$coreConfig->add(
'shared=>%DIR_EXT%',
'\'%DIR_ROOT%/ext\'',
'%DIR_VIEWS%'
);
$coreConfig->add(
'multiple=>ExtensionModel',
'\\ForkBB\\Models\\Extension\\Extension::class',
'DBMapModel'
);
$coreConfig->add(
'multiple=>ExtensionManager',
'\\ForkBB\\Models\\Extension\\Extensions::class',
'ExtensionModel'
);
$coreConfig->add(
'shared=>extensions',
'\'@ExtensionManager:init\'',
'attachments'
);
$coreConfig->add(
'multiple=>AdminExtensions',
'\\ForkBB\\Models\\Pages\\Admin\\Extensions::class',
'AdminAntispam'
);
$coreConfig->add(
'shared=>View=>config=>preFile',
'\'%DIR_CONFIG%/ext/pre.php\''
);
$coreConfig->save();
// extensions
$schema = [
'FIELDS' => [
'ext_name' => ['VARCHAR(190)', false, ''],
'ext_status' => ['TINYINT', false, 0],
'ext_data' => ['TEXT', false],
],
'PRIMARY KEY' => ['ext_name'],
];
$this->c->DB->createTable('::extensions', $schema);
return null;
}
/**
* rev.70 to rev.71
*/
protected function stageNumber70(array $args): ?int
{
$coreConfig = new CoreConfig($this->configFile);
$coreConfig->add(
'FRIENDLY_URL',
[
'lowercase' => 'false',
'translit' => '\'\'',
'WtoHyphen' => 'false',
],
'TIME_FORMATS'
);
$coreConfig->save();
return null;
}
/**
* rev.71 to rev.72
*/
protected function stageNumber71(array $args): ?int
{
switch ($args['start'] ?? 1) {
case 3:
$f = $this->c->FRIENDLY_URL;
if (
! empty($f['lowercase'])
|| ! empty($f['translit'])
|| ! empty($f['WtoHyphen'])
) {
$names = $this->c->DB->query('SELECT id, forum_name FROM ::forums WHERE redirect_url=\'\' ORDER BY id')->fetchAll(PDO::FETCH_KEY_PAIR);
$query = 'UPDATE ::forums SET friendly_name=?s:name WHERE id=?i:id';
foreach ($names as $id => $name) {
$vars = [
':id' => $id,
':name' => \mb_substr($this->c->Func->friendly($name), 0, 80, 'UTF-8'),
];
$this->c->DB->exec($query, $vars);
}
}
return null;
case 2:
$this->c->DB->addField('::forums', 'friendly_name', 'VARCHAR(80)', false, '', null, 'forum_name');
return 3;
default:
$coreConfig = new CoreConfig($this->configFile);
$coreConfig->add(
'FRIENDLY_URL=>file',
'\'translit.default.php\'',
'WtoHyphen'
);
$coreConfig->save();
return 2;
}
}
/**
* rev.72 to rev.73
*/
protected function stageNumber72(array $args): ?int
{
$coreConfig = new CoreConfig($this->configFile);
$coreConfig->add(
'multiple=>Sitemap',
'\\ForkBB\\Models\\Pages\\Sitemap::class',
'Misc'
);
$coreConfig->save();
return null;
}
}

View file

@ -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;
}

View file

@ -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';

View file

@ -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',

View file

@ -206,7 +206,7 @@ class Auth extends Page
/**
* Проверка пользователя по базе
*/
public function vLoginCheck(Validator $v, #[SensitiveParameter] string $password ): string
public function vLoginCheck(Validator $v, #[SensitiveParameter] string $password): string
{
if (empty($v->getErrors())) {
if ($this->loginWithForm) {

View file

@ -17,6 +17,7 @@ use ForkBB\Models\Pages\PostValidatorTrait;
use ForkBB\Models\Poll\Poll;
use ForkBB\Models\Post\Post;
use ForkBB\Models\Topic\Topic;
use ForkBB\Models\User\User;
use function \ForkBB\__;
class Edit extends Page
@ -314,4 +315,199 @@ class Edit extends Page
$this->c->polls->insert($poll);
}
}
/**
* Изменение автора и даты
*/
public function change(array $args, string $method): Page
{
$post = $this->c->posts->load($args['id']);
if (
! $post instanceof Post
|| ! $post->canEdit
) {
return $this->c->Message->message('Bad request');
}
$topic = $post->parent;
$firstPost = $post->id === $topic->first_post_id;
$lastPost = $post->id === $topic->last_post_id;
$this->c->Lang->load('post');
$this->c->Lang->load('validator');
if ('POST' === $method) {
$v = $this->c->Validator->reset()
->addValidators([
'username_check' => [$this, 'vUsernameCheck'],
])->addRules([
'token' => 'token:ChangeAnD',
'username' => 'required|string|username_check',
'posted' => 'required|date',
'confirm' => 'checkbox',
'change_and' => 'required|string',
])->addAliases([
'username' => 'Username',
'posted' => 'Posted',
])->addArguments([
'token' => $args,
'username.username_check' => $post->user,
]);
if ($v->validation($_POST)) {
if ('1' !== $v->confirm) {
return $this->c->Redirect->url($post->link)->message('No confirm redirect', FORK_MESS_WARN);
}
$ids = [];
$upPost = false;
// изменить имя автора
if (
$this->newUser instanceof User
&& $this->newUser->id !== $post->user->id
) {
if (! $post->user->isGuest) {
$ids[] = $post->user->id;
}
if (! $this->newUser->isGuest) {
$ids[] = $this->newUser->id;
}
$post->poster = $this->newUser->username;
$post->poster_id = $this->newUser->id;
$upPost = true;
}
$posted = $this->c->Func->dateToTime($v->posted);
// изменит время создания
if (\abs($post->posted - $posted) >= 60) {
$post->posted = $posted;
$upPost = true;
}
if ($upPost) {
$post->edited = \time();
$post->editor = $this->user->username;
$post->editor_id = $this->user->id;
$this->c->posts->update($post);
if (
$firstPost
|| $lastPost
) {
$topic->calcStat();
$this->c->topics->update($topic);
if ($lastPost) {
$topic->parent->calcStat();
$this->c->forums->update($topic->parent);
}
}
}
if ($ids) {
$this->c->users->updateCountPosts(...$ids);
if ($firstPost) {
$this->c->users->updateCountTopics(...$ids);
}
}
return $this->c->Redirect->url($post->link)->message('Change redirect', FORK_MESS_SUCC);
}
$this->fIswev = $v->getErrors();
$data = [
'username' => $v->username ?: $post->poster,
'posted' => $v->posted ?: $this->c->Func->timeToDate($post->posted),
];
} else {
$data = [
'username' => $post->poster,
'posted' => $this->c->Func->timeToDate($post->posted),
];
}
$this->nameTpl = 'post';
$this->onlinePos = 'topic-' . $topic->id;
$this->robots = 'noindex';
$this->formTitle = $firstPost ? 'Change AnD topic' : 'Change AnD post';
$this->crumbs = $this->crumbs($this->formTitle, $topic);
$this->form = $this->formAuthorAndDate($data, $args);
return $this;
}
public function vUsernameCheck(Validator $v, string $username, $attr, User $user): string
{
if ($username !== $user->username) {
$newUser = $this->c->users->loadByName($username, true);
if ($newUser instanceof User) {
$username = $newUser->username;
$this->newUser = $newUser;
} else {
$v->addError(['User %s does not exist', $username]);
}
}
return $username;
}
/**
* Возвращает данные для построения формы изменения автора поста и времени создания
*/
protected function formAuthorAndDate(array $data, array $args): ?array
{
if (! $this->user->isAdmin) {
return null;
}
return [
'action' => $this->c->Router->link('ChangeAnD', $args),
'hidden' => [
'token' => $this->c->Csrf->create('ChangeAnD', $args),
],
'sets' => [
'author-and-date' => [
'fields' => [
'username'=> [
'type' => 'text',
'minlength' => $this->c->USERNAME['min'],
'maxlength' => $this->c->USERNAME['max'],
'caption' => 'Username',
'required' => true,
'pattern' => $this->c->USERNAME['jsPattern'],
'value' => $data['username'] ?? null,
'autofocus' => true,
],
'posted'=> [
'type' => 'datetime-local',
'caption' => 'Posted',
'required' => true,
'value' => $data['posted'] ?? null,
'step' => '1',
],
'confirm' => [
'type' => 'checkbox',
'label' => 'Confirm action',
'checked' => false,
],
],
],
],
'btns' => [
'change_and' => [
'type' => 'submit',
'value' => __('Change'),
],
],
];
}
}

View file

@ -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}",
]

View file

@ -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,

View file

@ -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' => [],

View file

@ -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[] = [

View file

@ -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',
]);

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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}"] = [

View file

@ -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}"] = [

View file

@ -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'] = [

View file

@ -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 = [];

View file

@ -501,7 +501,10 @@ class Search extends Page
case 'topics':
case 'topics_subscriptions':
case 'forums_subscriptions':
if (! isset($uid)) {
if (
! isset($uid)
|| $this->user->isBot
) {
break;
}

View 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;
}
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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)');
}
}

View file

@ -11,7 +11,6 @@ declare(strict_types=1);
namespace ForkBB\Models\Post;
use ForkBB\Models\Action;
use ForkBB\Models\DataModel;
use ForkBB\Models\Forum\Forum;
use ForkBB\Models\Post\Post;
use ForkBB\Models\Topic\Topic;
@ -25,7 +24,7 @@ class Delete extends Action
/**
* Удаляет сообщение(я)
*/
public function delete(DataModel ...$args): void
public function delete(Forum|Post|Topic|User ...$args): void
{
if (empty($args)) {
throw new InvalidArgumentException('No arguments, expected User(s), Forum(s), Topic(s) or Post(s)');
@ -85,8 +84,6 @@ class Delete extends Action
if ($arg->poster_id > 0) {
$uidsUpdate[$arg->poster_id] = $arg->poster_id;
}
} else {
throw new InvalidArgumentException('Expected User(s), Forum(s), Topic(s) or Post(s)');
}
}
@ -161,24 +158,19 @@ class Delete extends Action
$uidsUpdate = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
switch ($this->c->DB->getType()) {
case 'mysql':
$query = 'DELETE p
FROM ::posts AS p, ::topics AS t
WHERE t.forum_id IN (?ai:forums) AND p.topic_id=t.id';
$query = match ($this->c->DB->getType()) {
'mysql' => 'DELETE p
FROM ::posts AS p, ::topics AS t
WHERE t.forum_id IN (?ai:forums) AND p.topic_id=t.id',
break;
default:
$query = 'DELETE
FROM ::posts
WHERE topic_id IN (
SELECT id
FROM ::topics
WHERE forum_id IN (?ai:forums)
)';
break;
}
default => 'DELETE
FROM ::posts
WHERE topic_id IN (
SELECT id
FROM ::topics
WHERE forum_id IN (?ai:forums)
)',
};
$this->c->DB->exec($query, $vars);
}

View file

@ -11,19 +11,16 @@ declare(strict_types=1);
namespace ForkBB\Models\Post;
use ForkBB\Models\Action;
use ForkBB\Models\DataModel;
use ForkBB\Models\Topic\Topic;
use ForkBB\Models\Forum\Forum;
use InvalidArgumentException;
use RuntimeException;
use PDO;
class Feed extends Action
{
/**
* Загружает данные для feed
*/
public function Feed(DataModel $model): array
public function Feed(Forum|Topic $model): array
{
if ($model instanceof Topic) {
if (0 !== $model->moved_to) {
@ -54,16 +51,28 @@ class Feed extends Action
$vars = [
':forums' => $ids,
];
$query = 'SELECT p.id as pid, p.poster as username, p.poster_id as uid, p.message as content,
p.hide_smilies, p.posted, p.edited, t.id as tid, t.subject as topic_name, t.forum_id as fid
$query = 'SELECT p.id
FROM ::posts AS p
INNER JOIN ::topics AS t ON t.id=p.topic_id
WHERE t.forum_id IN (?ai:forums)
ORDER BY p.id DESC
LIMIT 50';
} else {
throw new InvalidArgumentException('Expected Topic or Forum');
$ids = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
if (empty($ids)) {
return [];
}
$vars = [
':ids' => $ids,
];
$query = 'SELECT p.id as pid, p.poster as username, p.poster_id as uid, p.message as content,
p.hide_smilies, p.posted, p.edited, t.id as tid, t.subject as topic_name, t.forum_id as fid
FROM ::posts AS p
INNER JOIN ::topics AS t ON t.id=p.topic_id
WHERE p.id IN (?ai:ids)
ORDER BY p.id DESC';
}
return $this->c->DB->query($query, $vars)->fetchAll();

View file

@ -194,6 +194,19 @@ class Post extends DataModel
);
}
/**
* Ссылка на страницу редактирования автора и даты
*/
protected function getlinkAnD(): string
{
return $this->c->Router->link(
'ChangeAnD',
[
'id' => $this->id,
]
);
}
/**
* Статус возможности ответа с цитированием
*/

View file

@ -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)

View file

@ -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);

View file

@ -83,14 +83,6 @@ class Providers extends Manager
return $driver;
}
/**
* Возращает список созданных провайдеров
*/
public function list(): array
{
return $this->repository;
}
/**
* Возращает список имён активных провайдеров
*/

View file

@ -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);
}

View file

@ -78,8 +78,8 @@ class ActionT extends Method
*/
// упрощенный запрос для больших форумов, дополнительная обработка ниже
$query = 'SELECT DISTINCT t.id, t.last_post
FROM forum_topics AS t
INNER JOIN forum_posts AS p ON t.id=p.topic_id
FROM ::topics AS t
INNER JOIN ::posts AS p ON t.id=p.topic_id
WHERE t.forum_id IN (?ai:forums) AND t.moved_to=0 AND p.poster_id=?i:uid';
break;

View file

@ -11,7 +11,6 @@ declare(strict_types=1);
namespace ForkBB\Models\Search;
use ForkBB\Models\Method;
use ForkBB\Models\DataModel;
use ForkBB\Models\Forum\Forum;
use ForkBB\Models\Post\Post;
use ForkBB\Models\Topic\Topic;
@ -24,7 +23,7 @@ class Delete extends Method
/**
* Удаление индекса
*/
public function delete(DataModel ...$args): void
public function delete(Forum|Post|Topic|User ...$args): void
{
if (empty($args)) {
throw new InvalidArgumentException('No arguments, expected User(s), Forum(s), Topic(s) or Post(s)');
@ -74,8 +73,6 @@ class Delete extends Method
$pids[$arg->id] = $arg->id;
$isPost = 1;
} else {
throw new InvalidArgumentException('Expected User(s), Forum(s), Topic(s) or Post(s)');
}
}
@ -87,76 +84,58 @@ class Delete extends Method
$vars = [
':users' => $uids,
];
$query = match ($this->c->DB->getType()) {
'mysql' => 'DELETE sm
FROM ::search_matches AS sm, ::posts AS p
WHERE p.poster_id IN (?ai:users) AND sm.post_id=p.id',
switch ($this->c->DB->getType()) {
case 'mysql':
$query = 'DELETE sm
FROM ::search_matches AS sm, ::posts AS p
WHERE p.poster_id IN (?ai:users) AND sm.post_id=p.id';
break;
default:
$query = 'DELETE
FROM ::search_matches
WHERE post_id IN (
SELECT p.id
FROM ::posts AS p
WHERE p.poster_id IN (?ai:users)
)';
break;
}
default => 'DELETE
FROM ::search_matches
WHERE post_id IN (
SELECT p.id
FROM ::posts AS p
WHERE p.poster_id IN (?ai:users)
)',
};
}
if ($fids) {
$vars = [
':forums' => $fids,
];
$query = match ($this->c->DB->getType()) {
'mysql' => 'DELETE sm
FROM ::search_matches AS sm, ::posts AS p, ::topics AS t
WHERE t.forum_id IN (?ai:forums) AND p.topic_id=t.id AND sm.post_id=p.id',
switch ($this->c->DB->getType()) {
case 'mysql':
$query = 'DELETE sm
FROM ::search_matches AS sm, ::posts AS p, ::topics AS t
WHERE t.forum_id IN (?ai:forums) AND p.topic_id=t.id AND sm.post_id=p.id';
break;
default:
$query = 'DELETE
FROM ::search_matches
WHERE post_id IN (
SELECT p.id
FROM ::posts AS p
INNER JOIN ::topics AS t ON t.id=p.topic_id
WHERE t.forum_id IN (?ai:forums)
)';
break;
}
default => 'DELETE
FROM ::search_matches
WHERE post_id IN (
SELECT p.id
FROM ::posts AS p
INNER JOIN ::topics AS t ON t.id=p.topic_id
WHERE t.forum_id IN (?ai:forums)
)',
};
}
if ($tids) {
$vars = [
':topics' => $tids,
];
$query = match ($this->c->DB->getType()) {
'mysql' => 'DELETE sm
FROM ::search_matches AS sm, ::posts AS p
WHERE p.topic_id IN (?ai:topics) AND sm.post_id=p.id',
switch ($this->c->DB->getType()) {
case 'mysql':
$query = 'DELETE sm
FROM ::search_matches AS sm, ::posts AS p
WHERE p.topic_id IN (?ai:topics) AND sm.post_id=p.id';
break;
default:
$query = 'DELETE
FROM ::search_matches
WHERE post_id IN (
SELECT p.id
FROM ::posts AS p
WHERE p.topic_id IN (?ai:topics)
)';
break;
}
default => 'DELETE
FROM ::search_matches
WHERE post_id IN (
SELECT p.id
FROM ::posts AS p
WHERE p.topic_id IN (?ai:topics)
)',
};
}
if ($pids) {

View file

@ -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)');
}
}

View file

@ -58,14 +58,14 @@ class Load extends Action
];
$query = $this->getSql('t.id=?i:tid', true);
$data = $this->c->DB->query($query, $vars)->fetch();
$row = $this->c->DB->query($query, $vars)->fetch();
// тема отсутствует или недоступна
if (empty($data)) {
if (empty($row)) {
return null;
}
$topic = $this->manager->create($data);
$topic = $this->manager->create($row);
$forum = $topic->parent;
if ($forum instanceof Forum) {
@ -75,8 +75,6 @@ class Load extends Action
} else {
return null;
}
}
/**
@ -117,4 +115,36 @@ class Load extends Action
return $result;
}
/**
* Загружает список тем при открытие которых идет переадресация на тему c указанным id
*/
public function loadLinks(int $id): array
{
if ($id < 1) {
throw new InvalidArgumentException('Expected a positive topic id');
}
$vars = [
':id' => $id,
];
$query = 'SELECT *
FROM ::topics
WHERE moved_to=?i:id
ORDER BY id';
$stmt = $this->c->DB->query($query, $vars);
$result = [];
while ($row = $stmt->fetch()) {
$topic = $this->manager->create($row);
if ($topic->parent instanceof Forum) {
$result[] = $topic;
}
}
return $result;
}
}

View file

@ -29,6 +29,7 @@ class Move extends Action
if ($topic->parent === $toForum) {
continue;
}
if ($redirect) {
$rTopic = $this->c->topics->create();
$rTopic->poster = $topic->poster;
@ -40,7 +41,7 @@ class Move extends Action
// $rTopic->last_post_id = $topic->last_post_id;
// $rTopic->last_poster = $topic->last_poster;
// $rTopic->last_poster_id = $topic->last_poster_id;
$rTopic->moved_to = $topic->id;
$rTopic->moved_to = $topic->moved_to ?: $topic->id;
$rTopic->forum_id = $topic->forum_id;
$this->c->topics->insert($rTopic);

View file

@ -101,7 +101,7 @@ class Topic extends DataModel
'Topic',
[
'id' => $this->moved_to ?: $this->id,
'name' => $this->name,
'name' => $this->c->Func->friendly($this->name),
]
);
}
@ -329,7 +329,7 @@ class Topic extends DataModel
'Topic',
[
'id' => $this->id,
'name' => $this->name,
'name' => $this->c->Func->friendly($this->name),
]
);
}

View file

@ -37,6 +37,7 @@ class Topics extends Manager
return $this->get($id);
} else {
$topic = $this->Load->load($id);
$this->set($id, $topic);
return $topic;
@ -57,6 +58,7 @@ class Topics extends Manager
} else {
$result[$id] = null;
$data[] = $id;
$this->set($id, null);
}
}
@ -68,6 +70,28 @@ class Topics extends Manager
foreach ($this->Load->loadByIds($data, $full) as $topic) {
if ($topic instanceof Topic) {
$result[$topic->id] = $topic;
$this->set($topic->id, $topic);
}
}
return $result;
}
/**
* Получает список тем при открытие которых идет переадресация на текущую тему
*/
public function loadLinks(int|Topic $arg): array
{
$id = \is_int($arg) ? $arg : (int) $arg->id;
$result = [];
foreach ($this->Load->loadLinks($id) as $topic) {
if ($this->isset($topic->id)) {
$result[$topic->id] = $this->get($topic->id);
} else {
$result[$topic->id] = $topic;
$this->set($topic->id, $topic);
}
}
@ -89,6 +113,7 @@ class Topics extends Manager
public function insert(Topic $topic): int
{
$id = $this->Save->insert($topic);
$this->set($id, $topic);
return $id;

View file

@ -11,12 +11,10 @@ declare(strict_types=1);
namespace ForkBB\Models\Topic;
use ForkBB\Models\Action;
use ForkBB\Models\Model;
use ForkBB\Models\Forum\Forum;
use ForkBB\Models\Search\Search;
use ForkBB\Models\Topic\Topic;
use PDO;
use InvalidArgumentException;
use RuntimeException;
class View extends Action
@ -24,14 +22,12 @@ class View extends Action
/**
* Возвращает список тем
*/
public function view(Model $arg): array
public function view(Forum|Search $arg): array
{
if ($arg instanceof Forum) {
$full = false;
} elseif ($arg instanceof Search) {
$full = true;
} else {
throw new InvalidArgumentException('Expected Forum or Search');
}
if (

View file

@ -168,7 +168,7 @@ class User extends DataModel
'User',
[
'id' => $this->id,
'name' => $this->username,
'name' => $this->c->Func->friendly($this->username),
]
);
}

View file

@ -42,8 +42,7 @@ define('FORK_GEN_FEM', 2);
define('FORK_JSON_ENCODE', \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR);
require __DIR__ . '/../vendor/autoload.php';
$loader = require __DIR__ . '/../vendor/autoload.php';
$errorHandler = new ErrorHandler();
if (\is_file(__DIR__ . '/config/main.php')) {
@ -54,6 +53,8 @@ if (\is_file(__DIR__ . '/config/main.php')) {
throw new RuntimeException('Application is not configured');
}
$c->autoloader = $loader;
$errorHandler->setContainer($c);
require __DIR__ . '/functions.php';
@ -69,7 +70,7 @@ if (
$c->BASE_URL = \str_replace('https://', 'http://', $c->BASE_URL);
}
$c->FORK_REVISION = 69;
$c->FORK_REVISION = 73;
$c->START = $forkStart;
$c->PUBLIC_URL = $c->BASE_URL . $forkPublicPrefix;

0
app/config/ext/.gitkeep Normal file
View file

View file

@ -60,6 +60,12 @@ return [
],
'DATE_FORMATS' => ['Y-m-d', 'd M Y', 'Y-m-d', 'Y-d-m', 'd-m-Y', 'm-d-Y', 'M j Y', 'jS M Y'],
'TIME_FORMATS' => ['H:i:s', 'H:i', 'H:i:s', 'H:i', 'g:i:s a', 'g:i a'],
'FRIENDLY_URL' => [
'lowercase' => true,
'translit' => true, // 'Any-Latin;Latin-ASCII;',
'WtoHyphen' => true,
'file' => 'translit.default.php',
],
'forConfig' => [
'o_default_lang' => 'en',

View file

@ -42,7 +42,7 @@ return [
],
// Разрешенные атрибуты тегов / Allowed tag attributes
'cfgAllowTagParams' => [
['a', ['class', 'title', 'href']],
['a', ['class', 'title', 'href', 'rel']],
['abbr', ['class']],
['address', ['class']],
['article', ['class']],
@ -110,7 +110,7 @@ return [
// [тег, атрибут, значение, перезапись существующего атрибута]
// [tag, attribute, value, overwrite existing attribute]
'cfgSetTagParamDefault' => [
['a', 'rel', 'ugc', true],
// ['a', 'rel', 'ugc', false],
['img', 'alt', 'image', false],
['img', 'loading', 'lazy', true],
],

View file

@ -74,6 +74,12 @@ return [
],
'DATE_FORMATS' => ['Y-m-d', 'd M Y', 'Y-m-d', 'Y-d-m', 'd-m-Y', 'm-d-Y', 'M j Y', 'jS M Y'],
'TIME_FORMATS' => ['H:i:s', 'H:i', 'H:i:s', 'H:i', 'g:i:s a', 'g:i a'],
'FRIENDLY_URL' => [
'lowercase' => true,
'translit' => true, // 'Any-Latin;Latin-ASCII;',
'WtoHyphen' => true,
'file' => 'translit.default.php',
],
'shared' => [
'%DIR_ROOT%' => \realpath(__DIR__ . '/../..'),
@ -84,6 +90,7 @@ return [
'%DIR_LANG%' => '%DIR_APP%/lang',
'%DIR_LOG%' => '%DIR_APP%/log',
'%DIR_VIEWS%' => '%DIR_APP%/templates',
'%DIR_EXT%' => '%DIR_ROOT%/ext',
'DB' => [
'class' => \ForkBB\Core\DB::class,
@ -109,6 +116,7 @@ return [
'cache' => '%DIR_CACHE%',
'defaultDir' => '%DIR_VIEWS%/_default',
'userDir' => '%DIR_VIEWS%/_user',
'preFile' => '%DIR_CONFIG%/ext/pre.php',
],
],
'Router' => [
@ -185,6 +193,7 @@ return [
],
'providerUser' => \ForkBB\Models\ProviderUser\ProviderUser::class,
'attachments' => \ForkBB\Models\Attachment\Attachments::class,
'extensions' => '@ExtensionManager:init',
'Csrf' => [
'class' => \ForkBB\Core\Csrf::class,
@ -363,6 +372,7 @@ return [
'Ban' => \ForkBB\Models\Pages\Ban::class,
'Debug' => \ForkBB\Models\Pages\Debug::class,
'Misc' => \ForkBB\Models\Pages\Misc::class,
'Sitemap' => \ForkBB\Models\Pages\Sitemap::class,
'Moderate' => \ForkBB\Models\Pages\Moderate::class,
'Report' => \ForkBB\Models\Pages\Report::class,
'Email' => \ForkBB\Models\Pages\Email::class,
@ -409,6 +419,7 @@ return [
'AdminLogs' => \ForkBB\Models\Pages\Admin\Logs::class,
'AdminUploads' => \ForkBB\Models\Pages\Admin\Uploads::class,
'AdminAntispam' => \ForkBB\Models\Pages\Admin\Antispam::class,
'AdminExtensions' => \ForkBB\Models\Pages\Admin\Extensions::class,
'AdminListModel' => \ForkBB\Models\AdminList\AdminList::class,
'BanListModel' => \ForkBB\Models\BanList\BanList::class,
@ -417,6 +428,8 @@ return [
'CensorshipModel' => \ForkBB\Models\Censorship\Censorship::class,
'ConfigModel' => \ForkBB\Models\Config\Config::class,
'DBMapModel' => \ForkBB\Models\DBMap\DBMap::class,
'ExtensionModel' => \ForkBB\Models\Extension\Extension::class,
'ExtensionManager' => \ForkBB\Models\Extension\Extensions::class,
'ForumModel' => \ForkBB\Models\Forum\Forum::class,
'ForumManager' => \ForkBB\Models\Forum\Forums::class,
'GroupModel' => \ForkBB\Models\Group\Group::class,

File diff suppressed because it is too large Load diff

View file

@ -11,8 +11,6 @@ declare(strict_types=1);
namespace ForkBB;
use ForkBB\Core\Container;
use DateTime;
use DateTimeZone;
use InvalidArgumentException;
/**
@ -105,13 +103,7 @@ function dt(int $arg, bool $dateOnly = false, string $dateFormat = null, string
}
if (null === $offset) {
if (\in_array($c->user->timezone, DateTimeZone::listIdentifiers(), true)) {
$dateTimeZone = new DateTimeZone($c->user->timezone);
$dateTime = new DateTime('now', $dateTimeZone);
$offset = $dateTime->getOffset();
} else {
$offset = 0;
}
$offset = $c->Func->offset();
}
$arg += $offset;

View file

@ -86,3 +86,6 @@ msgstr "Antispam"
msgid "Maintenance only"
msgstr "Available only in maintenance mode."
msgid "Extensions"
msgstr "Extensions"

View file

@ -0,0 +1,133 @@
#
msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Project-Id-Version: ForkBB\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en\n"
msgid "Details"
msgstr "Details:"
msgid "Name"
msgstr "Name of package"
msgid "Description"
msgstr "Description"
msgid "Release date"
msgstr "Release date"
msgid "Homepage"
msgstr "Homepage"
msgid "Licence"
msgstr "Licence"
msgid "Requirements"
msgstr "Requirements:"
msgid "Authors"
msgstr "Author(s):"
msgid "php"
msgstr "PHP version"
msgid "forkbb"
msgstr "ForkBB revision"
msgid "Not installed"
msgstr "Not installed"
msgid "Disabled"
msgstr "Disabled"
msgid "Disabled, package changed"
msgstr "Disabled, package changed"
msgid "Enabled"
msgstr "Enabled"
msgid "Enabled, package changed"
msgstr "Enabled, but package changed!"
msgid "Crash"
msgstr "Crash, package not found!"
msgid "Install_"
msgstr "Install"
msgid "Uninstall_"
msgstr "Uninstall"
msgid "Enable_"
msgstr "Enable"
msgid "Disable_"
msgstr "Disable"
msgid "Update_"
msgstr "Update"
msgid "Downdate_"
msgstr "Downdate"
msgid "Package version"
msgstr "Package version"
msgid "Extension not found"
msgstr "Extension not found."
msgid "Invalid action"
msgstr "Invalid action."
msgid "Redirect install"
msgstr "The extension is installed."
msgid "Redirect uninstall"
msgstr "The extension has been uninstalled."
msgid "Redirect update"
msgstr "The extension has been updated."
msgid "Redirect downdate"
msgstr "The extension version has been downgraded."
msgid "Redirect enable"
msgstr "The extension is enabled."
msgid "Redirect disable"
msgstr "The extension is disabled."
msgid "Invalid template type"
msgstr "Invalid template type."
msgid "PRE name not found"
msgstr "PRE name not found."
msgid "Template file '%s' not found"
msgstr "Template file '%s' not found."
msgid "An error occurred in updateCommon"
msgstr "An error occurred in updateCommon."
msgid "Empty"
msgstr "Empty"
msgid "Invalid symlink type"
msgstr "Invalid symlink type."
msgid "Bad symlink"
msgstr "Bad symlink."
msgid "Target '%s' not found"
msgstr "Target '%s' not found."
msgid "Link '%s' already exists"
msgstr "Link '%s' already exists."

View file

@ -134,3 +134,9 @@ msgstr "If YES, then new messages from users in this forum will increase their c
msgid "<span></span>"
msgstr "<span></span>"
msgid "Friendly name label"
msgstr "Name for URL"
msgid "Friendly name help"
msgstr "A string identifier on the basis of which the url to this forum will be generated. May consist of Latin letters, numbers, underscores and hyphens."

View file

@ -270,3 +270,27 @@ msgstr "First, please <a href=\"%s\">confirm</a> your email address."
msgid "Poll cannot be attached"
msgstr "Topic with a poll cannot be attached to another topic."
msgid "Link btn"
msgstr "Redirect"
msgid "Only one topic is permissible"
msgstr "Only one topic should be selected to manage links to it."
msgid "Control of redirects title"
msgstr "Control of redirects topics"
msgid "Need full topic for this operation"
msgstr "You need to choose a full-fledged topic for this operation."
msgid "Forum label"
msgstr "Forum"
msgid "Redir label"
msgstr "Set redirect topic"
msgid "Change btn"
msgstr "Change"
msgid "Redirects changed redirect"
msgstr "Redirects topics changed."

View file

@ -83,3 +83,24 @@ msgstr "%1$s (%2$s max size)"
msgid "Attachments"
msgstr "Attachments"
msgid "Change author and date"
msgstr "Change author and date"
msgid "Change"
msgstr "Change"
msgid "Posted"
msgstr "Posted"
msgid "Change AnD topic"
msgstr "Change author of topic and its date"
msgid "Change AnD post"
msgstr "Change author of post and its date"
msgid "User %s does not exist"
msgstr "User '%s' does not exist."
msgid "Change redirect"
msgstr "Changes done."

View file

@ -127,3 +127,6 @@ msgstr "%1$s (%2$s max size)"
msgid "Attachments"
msgstr "Attachments"
msgid "Change author and date"
msgstr "Change author and date"

View file

@ -218,3 +218,6 @@ msgstr "Invalid passphrase."
msgid "Javascript disabled or bot"
msgstr "Your browser most likely has javascript disabled or you are a robot :)"
msgid "The :alias contains time before start of Unix"
msgstr "The :alias field contains the time before the start of the Unix epoch."

View file

@ -86,3 +86,6 @@ msgstr "Антиспам"
msgid "Maintenance only"
msgstr "Доступно только в режиме обслуживания."
msgid "Extensions"
msgstr "Расширения"

View file

@ -0,0 +1,133 @@
#
msgid ""
msgstr ""
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"Project-Id-Version: ForkBB\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ru\n"
msgid "Details"
msgstr "Подробности:"
msgid "Name"
msgstr "Имя пакета"
msgid "Description"
msgstr "Описание"
msgid "Release date"
msgstr "Дата выпуска"
msgid "Homepage"
msgstr "Домашняя страница"
msgid "Licence"
msgstr "Лицензия"
msgid "Requirements"
msgstr "Требования:"
msgid "Authors"
msgstr "Автор(ы):"
msgid "php"
msgstr "Версия PHP"
msgid "forkbb"
msgstr "Ревизия ForkBB"
msgid "Not installed"
msgstr "Не установлено"
msgid "Disabled"
msgstr "Выключено"
msgid "Disabled, package changed"
msgstr "Выключено, пакет изменен"
msgid "Enabled"
msgstr "Включено"
msgid "Enabled, package changed"
msgstr "Включено, но пакет изменен!"
msgid "Crash"
msgstr "Сломано, пакет не найден!"
msgid "Install_"
msgstr "Установить"
msgid "Uninstall_"
msgstr "Удалить"
msgid "Enable_"
msgstr "Включить"
msgid "Disable_"
msgstr "Выключить"
msgid "Update_"
msgstr "Обновить"
msgid "Downdate_"
msgstr "Откатить"
msgid "Package version"
msgstr "Версия пакета"
msgid "Extension not found"
msgstr "Расширение не найдено."
msgid "Invalid action"
msgstr "Недопустимое действие."
msgid "Redirect install"
msgstr "Расширение установлено."
msgid "Redirect uninstall"
msgstr "Расширение деинсталлировано."
msgid "Redirect update"
msgstr "Расширение обновлено."
msgid "Redirect downdate"
msgstr "Версия расширения понижена."
msgid "Redirect enable"
msgstr "Расширение включено."
msgid "Redirect disable"
msgstr "Расширение выключено."
msgid "Invalid template type"
msgstr "Неверный тип шаблона."
msgid "PRE name not found"
msgstr "PRE-имя не найдено."
msgid "Template file '%s' not found"
msgstr "Файл шаблона '%s' не найден."
msgid "An error occurred in updateCommon"
msgstr "Возникла ошибка в updateCommon."
msgid "Empty"
msgstr "Пусто"
msgid "Invalid symlink type"
msgstr "Неверный тип символической ссылки."
msgid "Bad symlink"
msgstr "Плохая символическая ссылка."
msgid "Target '%s' not found"
msgstr "Target '%s' отсутствует."
msgid "Link '%s' already exists"
msgstr "Link '%s' уже существует."

View file

@ -134,3 +134,9 @@ msgstr "Если ДА, то новые сообщения пользовател
msgid "<span></span>"
msgstr "<span></span>"
msgid "Friendly name label"
msgstr "Имя для URL"
msgid "Friendly name help"
msgstr "Строковый идентификатор на основе которого будет сформирован адрес указывающий на данный раздел. Может состоять из латинских букв, цифр, знаков подчеркивания и дефисов."

Some files were not shown because too many files have changed in this diff Show more