Compare commits

..

199 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
Visman
fd760cb9ff Up rev 2023-09-20 21:03:19 +07:00
Visman
73047a8155 Make userDir parameter optional 2023-09-20 21:01:08 +07:00
Visman
1a13f93722 Update config files 2023-09-20 20:56:26 +07:00
Visman
4d096d5786 Create a directory for user templates 2023-09-20 20:53:30 +07:00
Visman
a2309c16d0 Move templates to new default directory 2023-09-20 20:52:59 +07:00
Visman
a77b4e4ecb typo fix 2023-09-20 20:51:21 +07:00
Visman
c7980386c3 Fix delete user with posts 2023-09-20 16:40:04 +07:00
Visman
6f84ba979b Add for #21 2023-09-19 22:16:55 +07:00
Visman
4bb906c300 Minor edits 2023-09-19 20:54:25 +07:00
Visman
9f46e4e302 Add sorting for UPDATE queries 2023-09-19 20:53:58 +07:00
Visman
e18280fbb0 Add sorting for delete in Search\Index 2023-09-19 20:36:33 +07:00
Visman
a2410a371a Simplify Search\Delete 2023-09-19 20:35:16 +07:00
Visman
8ade529ff8 Add spaceship 2023-09-19 18:54:28 +07:00
Visman
e45170cd0e Add sorting for multi-line DELETE queries 2023-09-19 18:38:45 +07:00
Visman
bc4fec2e34 Add another message for the administrator if MAINTENANCE_OFF = true #21 2023-09-18 18:24:14 +07:00
Visman
16f22acf61 Fix Deadlock 2
Set the same order for deleted records from the ::online table.
https://forkbb.ru/topic/81/SQLSTATE%5B40001%5D%3A%20Serialization%20failure%3A%201213%20Deadlock%20found%20when%20tryin
2023-09-17 16:29:20 +07:00
Visman
62e39a8cf5 Update Log 2023-09-16 19:11:47 +07:00
Visman
128692b4c7 Update Simple Cache 2023-09-16 18:51:06 +07:00
Visman
59dbdd47b1 Delete Dirk 2023-09-16 18:20:26 +07:00
Visman
9b1008e22b copyright 2023-09-16 18:00:58 +07:00
Visman
a0521b376c Move Dirk to the core of the engine
To further expand the functionality of the template engine.
2023-09-16 18:00:45 +07:00
Visman
70620c155f Add more database information for MySQL 2023-09-10 22:52:26 +07:00
Visman
257658ca5f Remove the time limit for recalculating the number of messages and topics 2023-09-10 11:18:29 +07:00
Visman
48bcd118ad Fix transaction abort in Search\truncateIndex() 2023-09-08 20:48:16 +07:00
Visman
faab11a2fb Rebuilding the search index is available only in maintenance mode 2023-09-08 20:07:15 +07:00
Visman
6f97f2c78d Recalculation of the number of user messages/topics (in the admin panel - in users) is available only in maintenance mode 2023-09-08 19:48:15 +07:00
Visman
267f126a04 Update admin.css 2023-09-08 19:26:02 +07:00
Visman
9ccee7c135 Typo fix 2023-09-07 15:54:18 +07:00
Visman
461c8c3e60 Border for quote bbcode 2023-09-03 23:15:46 +07:00
Visman
62bf8a0779 Fix Deadlock
https://forkbb.ru/topic/81/SQLSTATE%5B40001%5D%3A%20Serialization%20failure%3A%201213%20Deadlock%20found%20when%20tryin
2023-09-01 18:01:50 +07:00
Visman
a0e241d548 Add parameter filter for size and color bbcodes 2023-08-30 14:55:01 +07:00
Visman
1c6f7f271b Update style.css 2023-08-30 14:53:42 +07:00
Visman
c3a4ceb1c3 Update style.css 2023-08-30 08:29:43 +07:00
Visman
09da9ff6a6 Update admin.css 2023-08-26 18:05:07 +07:00
Visman
3229cd44f5 Update install 2023-08-23 17:45:47 +07:00
Visman
3710ac1dc4 Fix translate 2023-08-22 23:11:07 +07:00
Visman
1f34e1eefc Optimize (attempt to) recalculate user posts 2023-08-22 23:00:47 +07:00
Visman
8b13230ef4 Update bootstrap.php 2023-08-22 18:18:23 +07:00
Visman
676ace3ce7 Update readme.md 2023-08-20 20:09:37 +07:00
Visman
2c4711f425 Update style 2023-08-18 18:26:04 +07:00
Visman
9ce0e18f1d Update style 2023-08-18 15:43:41 +07:00
Visman
33e007b9ce Update readme.md 2023-08-17 17:56:48 +07:00
Visman
81031a50f0 Up rev 2023-08-17 17:55:14 +07:00
Visman
1e29f0a0ba For previous commit 2023-08-16 16:22:40 +07:00
Visman
dacb8a3191 Add topics to recalculate posts 2023-08-16 16:19:07 +07:00
Visman
3cf8b82a3d Change style to dark 2023-08-15 22:47:22 +07:00
Visman
cb20565b96 Update style 2023-08-14 00:11:05 +07:00
Visman
4e4465e81e Update style.css 2023-08-14 00:01:40 +07:00
Visman
4eb77bbc1f Replace index (with composite) 2023-08-13 22:20:17 +07:00
Visman
62fd33bec2 Simplified query "Topics with your posts", part of the logic moved to php, for large forums 2023-08-13 21:41:00 +07:00
Visman
cc8231c09a Simplify query 2023-08-13 20:07:52 +07:00
Visman
8d70b9cae2 Simplify the "User Topics" query 2023-08-13 19:03:32 +07:00
Visman
6e89a3023e Update index 2023-08-13 18:29:35 +07:00
Visman
d18ec05c37 Replace index (with composite) 2023-08-13 18:08:03 +07:00
Visman
5be5ce1271 Optimize Forum\CalcStat 2023-08-13 18:05:53 +07:00
Visman
268ca03d2a Delete extra GROUP BY 2023-08-12 22:18:38 +07:00
Visman
40f9b55dfc Fix DELETE queries for MariaDB and MySQL(?)
MariaDB refuses to use index in queries like DELETE FROM WHERE IN (SELECT ...).
2023-08-12 21:36:08 +07:00
Visman
a828436496 Add editor_id_idx index for ::posts table 2023-08-11 22:39:27 +07:00
Visman
8b829e571a Add Open Graph 2023-08-11 18:49:54 +07:00
Visman
053bfe9b76 For og:image 2023-08-10 22:39:00 +07:00
Visman
421a19f1d5 Add apple-touch-icon.png 2023-08-10 17:48:23 +07:00
Visman
d0af7e913a Fix for aced07f 2023-08-10 00:54:01 +07:00
Visman
046c1dbe92 Move the formation of canonical and robots to templates 2023-08-09 23:28:41 +07:00
Visman
e726eb1024 Update INSERT queries 2023-08-09 20:46:19 +07:00
Visman
ab43a46433 Fix Profile\View page 2023-08-09 18:06:14 +07:00
Visman
aced07fa48 Allow robots to see the userlist (but not index it)
Otherwise, robots have problems accessing user profile pages.
2023-08-09 17:57:50 +07:00
Visman
4a32811d33 Add an individual description for each profile 2023-08-09 17:54:17 +07:00
Visman
1057701d0c Add a description meta tag, while common to all pages 2023-08-08 23:08:04 +07:00
Visman
fcee58adea Set order in canonical addresses 2023-08-08 21:52:44 +07:00
Visman
80a4ffff6e Fix for 8b1c9de 2023-08-08 21:38:38 +07:00
Visman
a313b142be Remove H3 from the main page and forum pages
The search engines are behaving very strangely :(
2023-08-07 19:59:22 +07:00
246 changed files with 6077 additions and 2544 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>
@ -14,6 +17,7 @@ AddDefaultCharset UTF-8
<IfModule !litespeed>
RewriteRule ^favicon\.ico$ public/favicon.ico [L]
RewriteRule ^apple-touch-icon\.png$ public/apple-touch-icon.png [L]
RewriteRule ^robots\.txt$ public/robots.txt [L]
RewriteRule !^public/ index.php [L]
@ -28,6 +32,7 @@ AddDefaultCharset UTF-8
</IfModule>
<IfModule litespeed>
RewriteRule ^favicon\.ico$ public/favicon.ico
RewriteRule ^apple-touch-icon\.png$ public/apple-touch-icon.png
RewriteRule ^robots\.txt$ public/robots.txt
RewriteRule !^public/ index.php [L]

27
.gitignore vendored
View file

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

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;
@ -47,7 +48,7 @@ class FileCache implements CacheInterface
/**
* Получает данные из кэша по ключу
*/
public function get($key, $default = null)
public function get(string $key, mixed $default = null): mixed
{
$file = $this->path($key);
@ -71,7 +72,7 @@ class FileCache implements CacheInterface
/**
* Устанавливает данные в кэш по ключу
*/
public function set($key, $value, $ttl = null)
public function set(string $key, mixed $value, null|int|DateInterval $ttl = null): bool
{
$file = $this->path($key);
@ -96,7 +97,7 @@ class FileCache implements CacheInterface
/**
* Удаляет данные по ключу
*/
public function delete($key)
public function delete(string $key): bool
{
$file = $this->path($key);
@ -115,10 +116,11 @@ class FileCache implements CacheInterface
/**
* Очищает папку кэша от php файлов (рекурсивно)
*/
public function clear()
public function clear(): bool
{
$dir = new RecursiveDirectoryIterator($this->cacheDir, RecursiveDirectoryIterator::SKIP_DOTS);
$iterator = new RecursiveIteratorIterator($dir);
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->cacheDir, FilesystemIterator::SKIP_DOTS)
);
$files = new RegexIterator($iterator, '%\.(?:php|tmp)$%i', RegexIterator::MATCH);
$result = true;
@ -132,7 +134,7 @@ class FileCache implements CacheInterface
/**
* Получает данные по списку ключей
*/
public function getMultiple($keys, $default = null)
public function getMultiple(iterable $keys, mixed $default = null): iterable
{
$this->validateIterable($keys);
@ -147,7 +149,7 @@ class FileCache implements CacheInterface
/**
* Устанавливает данные в кэш по списку ключ => значение
*/
public function setMultiple($values, $ttl = null)
public function setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool
{
$this->validateIterable($keys);
@ -162,7 +164,7 @@ class FileCache implements CacheInterface
/**
* Удаляет данные по списку ключей
*/
public function deleteMultiple($keys)
public function deleteMultiple(iterable $keys): bool
{
$this->validateIterable($keys);
@ -177,7 +179,7 @@ class FileCache implements CacheInterface
/**
* Проверяет кеш на наличие ключа
*/
public function has($key)
public function has(string $key): bool
{
$file = $this->path($key);
@ -224,8 +226,6 @@ class FileCache implements CacheInterface
{
if (\function_exists('\\opcache_invalidate')) {
\opcache_invalidate($file, true);
} elseif (\function_exists('\\apc_delete_file')) {
\apc_delete_file($file);
}
}

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

@ -1,10 +1,15 @@
<?php
/**
* This file is part of the ForkBB <https://github.com/forkbb>.
*
* @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
* @license The MIT License (MIT)
*/
/**
* based on Container https://github.com/artoodetoo/container
* by artoodetoo
* based on Container <https://github.com/artoodetoo/container>
*
* @copyright (c) 2016 artoodetoo <https://github.com/artoodetoo>
* @license The MIT License (MIT)
*/
declare(strict_types=1);

View file

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

View file

@ -503,11 +503,19 @@ class Mysql
$tmp[] = "{$key}({$val})";
}
$other = [];
$stmt = $this->db->query("SHOW VARIABLES LIKE 'character\\_set\\_%'");
$other = [];
$queries = [
"SHOW VARIABLES LIKE 'character\\_set\\_%'",
"SHOW VARIABLES LIKE '%max\\_conn%'",
"SHOW STATUS LIKE '%\\_conn%'",
];
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
$other[$row[0]] = $row[1];
foreach ($queries as $query) {
$stmt = $this->db->query($query);
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
$other[$row[0]] = $row[1];
}
}
return [

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

@ -127,6 +127,16 @@ class Image extends File
return $result;
}
public function width(): int
{
return $this->imgDriver->width($this->image);
}
public function height(): int
{
return $this->imgDriver->height($this->image);
}
public function __destruct()
{
$this->imgDriver->destroy($this->image);

View file

@ -49,6 +49,16 @@ class DefaultDriver
return $image;
}
public function width(mixed $image): int
{
return 0;
}
public function height(mixed $image): int
{
return 0;
}
public function destroy(mixed $image): void
{
}

View file

@ -162,4 +162,14 @@ class GDDriver extends DefaultDriver
return $result;
}
public function width(mixed $image): int
{
return \imagesx($image);
}
public function height(mixed $image): int
{
return \imagesy($image);
}
}

View file

@ -123,4 +123,14 @@ class ImagickDriver extends DefaultDriver
throw new FileException($e->getMessage());
}
}
public function width(mixed $imagick): int
{
return $imagick->getImageWidth();
}
public function height(mixed $imagick): int
{
return $imagick->getImageHeight();
}
}

View file

@ -17,6 +17,7 @@ use Psr\Log\InvalidArgumentException;
use DateTimeZone;
use DateTime;
use RuntimeException;
use Stringable;
use Throwable;
class Log implements LoggerInterface
@ -44,16 +45,8 @@ class Log implements LoggerInterface
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return void
*
* @throws \Psr\Log\InvalidArgumentException
*/
public function log($level, $message, array $context = [])
public function log($level, string|Stringable $message, array $context = []): void
{
if (
\is_object($message)
@ -233,13 +226,8 @@ class Log implements LoggerInterface
/**
* System is unusable.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function emergency($message, array $context = [])
public function emergency(string|Stringable $message, array $context = []): void
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
@ -249,13 +237,8 @@ class Log implements LoggerInterface
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function alert($message, array $context = [])
public function alert(string|Stringable $message, array $context = []): void
{
$this->log(LogLevel::ALERT, $message, $context);
}
@ -264,13 +247,8 @@ class Log implements LoggerInterface
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function critical($message, array $context = [])
public function critical(string|Stringable $message, array $context = []): void
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
@ -278,13 +256,8 @@ class Log implements LoggerInterface
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function error($message, array $context = [])
public function error(string|Stringable $message, array $context = []): void
{
$this->log(LogLevel::ERROR, $message, $context);
}
@ -294,26 +267,16 @@ class Log implements LoggerInterface
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function warning($message, array $context = [])
public function warning(string|Stringable $message, array $context = []): void
{
$this->log(LogLevel::WARNING, $message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function notice($message, array $context = [])
public function notice(string|Stringable $message, array $context = []): void
{
$this->log(LogLevel::NOTICE, $message, $context);
}
@ -322,26 +285,16 @@ class Log implements LoggerInterface
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function info($message, array $context = [])
public function info(string|Stringable $message, array $context = []): void
{
$this->log(LogLevel::INFO, $message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function debug($message, array $context = [])
public function debug(string|Stringable $message, array $context = []): void
{
$this->log(LogLevel::DEBUG, $message, $context);
}

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

@ -5,65 +5,90 @@
* @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
* @license The MIT License (MIT)
*/
/**
* based on Dirk <https://github.com/artoodetoo/dirk>
*
* @copyright (c) 2015 artoodetoo <i.am@artoodetoo.org, https://github.com/artoodetoo>
* @license The MIT License (MIT)
*/
declare(strict_types=1);
namespace ForkBB\Core;
use R2\Templating\Dirk;
use ForkBB\Core\View\Compiler;
use ForkBB\Models\Page;
use RuntimeException;
class View extends Dirk
class View
{
public function __construct (string $cache, string $views)
{
$config = [
'views' => $views,
'cache' => $cache,
'ext' => '.forkbb.php',
'echo' => '\\htmlspecialchars((string) %s, \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE, \'UTF-8\')',
'separator' => '/',
];
$this->compilers[] = 'Transformations';
protected string $ext = '.forkbb.php';
protected string $preFile = '';
parent::__construct($config);
protected ?Compiler $compilerObj;
protected string $compilerClass = Compiler::class;
protected string $cacheDir;
protected string $defaultDir;
protected string $defaultHash;
protected array $other = [];
protected array $composers = [];
protected array $blocks = [];
protected array $blockStack = [];
protected array $templates = [];
public function __construct(string|array $config, mixed $views)
{
if (\is_array($config)) {
$this->cacheDir = $config['cache'];
$this->defaultDir = $config['defaultDir'];
if (! empty($config['userDir'])) {
$this->addTplDir($config['userDir'], 10);
}
if (! empty($config['composers'])) {
foreach ($config['composers'] as $name => $composer) {
$this->composer($name, $composer);
}
}
if (! empty($config['compiler'])) {
$this->compilerClass = $config['compiler'];
}
if (! empty($config['preFile'])) {
$this->preFile = $config['preFile'];
}
} else {
// для rev. 68 и ниже
$this->cacheDir = $config;
$this->defaultDir = $views;
}
$this->defaultHash = \hash('md5', $this->defaultDir);
}
/**
* Трансформация скомпилированного шаблона
* Добавляет новый каталог шаблонов $pathToDir.
* Сортирует список каталогов в соответствии с приоритетом $priority. По убыванию.
*/
protected function compileTransformations(string $value): string
public function addTplDir(string $pathToDir, int $priority): View
{
if (\str_starts_with($value, '<?xml ')) {
$value = \str_replace(' \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE,', ' \\ENT_XML1,', $value);
$this->other[\hash('md5', $pathToDir)] = [$pathToDir, $priority];
if (\count($this->other) > 1) {
\uasort($this->other, function (array $a, array $b) {
return $b[1] <=> $a[1];
});
}
$perfix = <<<'EOD'
<?php
declare(strict_types=1);
use function \ForkBB\{__, num, dt, size};
?>
EOD;
if (false === \strpos($value, '<!-- inline -->')) {
return $perfix . $value;
}
return $perfix . \preg_replace_callback(
'%<!-- inline -->([^<]*(?:<(?!!-- endinline -->)[^<]*)*+)(?:<!-- endinline -->)?%',
function ($matches) {
return \preg_replace('%\h*\R\s*%', '', $matches[1]);
},
$value
);
return $this;
}
/**
* Return result of templating
* Возвращает отображение страницы $p или null
*/
public function rendering(Page $p): ?string
{
@ -76,8 +101,10 @@ EOD;
$p->prepare();
$this->templates[] = $p->nameTpl;
while ($_name = \array_shift($this->templates)) {
$this->beginBlock('content');
foreach ($this->composers as $_cname => $_cdata) {
if (\preg_match($_cname, $_name)) {
foreach ($_cdata as $_citem) {
@ -85,7 +112,9 @@ EOD;
}
}
}
require($this->prepare($_name));
require $this->prepare($_name);
$this->endBlock(true);
}
@ -94,29 +123,6 @@ EOD;
return $this->block('content');
}
/**
* Compile echos
*/
protected function compileEchos(string $value): string
{
$value = \preg_replace_callback(
'%(@)?\{\{!\s*(.+?)\s*!\}\}(\r?\n)?%s',
function($matches) {
$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
return $matches[1]
? \substr($matches[0], 1)
: '<?= \\htmlspecialchars((string) '
. $this->compileEchoDefaults($matches[2])
. ', \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE, \'UTF-8\', false) ?>'
. $whitespace;
},
$value
);
return parent::compileEchos($value);
}
/**
* Отправляет HTTP заголовки
*/
@ -128,4 +134,166 @@ EOD;
}
}
}
/**
* Возвращает отображение шаблона $name
*/
public function fetch(string $name, array $data = []): string
{
$this->templates[] = $name;
if (! empty($data)) {
\extract($data);
}
while ($_name = \array_shift($this->templates)) {
$this->beginBlock('content');
foreach ($this->composers as $_cname => $_cdata) {
if (\preg_match($_cname, $_name)) {
foreach ($_cdata as $_citem) {
\extract((\is_callable($_citem) ? $_citem($this) : $_citem) ?: []);
}
}
}
require $this->prepare($_name);
$this->endBlock(true);
}
return $this->block('content');
}
/**
* Add view composer
* @param mixed $name template name or array of names
* @param mixed $composer data in the same meaning as for fetch() call, or callable returning such data
*/
public function composer(string|array $name, mixed $composer): void
{
if (\is_array($name)) {
foreach ($name as $n) {
$this->composer($n, $composer);
}
} else {
$p = '~^'
. \str_replace('\*', '[^' . $this->separator . ']+', \preg_quote($name, $this->separator . '~'))
. '$~';
$this->composers[$p][] = $composer;
}
}
/**
* Подготавливает файл для подключения
*/
protected function prepare(string $name): string
{
$st = \preg_replace('%\W%', '-', $name);
foreach ($this->other as $hash => $cur) {
if (\file_exists($tpl = "{$cur[0]}/{$name}{$this->ext}")) {
$php = "{$this->cacheDir}/_{$st}-{$hash}.php";
if (
! \file_exists($php)
|| \filemtime($tpl) > \filemtime($php)
) {
$this->create($php, $tpl, $name);
}
return $php;
}
}
$hash = $this->defaultHash;
$tpl = "{$this->defaultDir}/{$name}{$this->ext}";
$php = "{$this->cacheDir}/_{$st}-{$hash}.php";
if (
! \file_exists($php)
|| \filemtime($tpl) > \filemtime($php)
) {
$this->create($php, $tpl, $name);
}
return $php;
}
/**
* Удаляет файлы кэша для шаблона $name
*/
public function delete(string $name): void
{
$st = \preg_replace('%\W%', '-', $name);
\array_map('\\unlink', \glob("{$this->cacheDir}/_{$st}-*.php"));
}
/**
* Генерирует $php файл на основе шаблона $tpl
*/
protected function create(string $php, string $tpl, string $name): void
{
if (empty($this->compilerObj)) {
$this->compilerObj = new $this->compilerClass($this->preFile);
}
$text = $this->compilerObj->create($name, \file_get_contents($tpl), \hash('fnv1a32', $tpl));
if (false === \file_put_contents($php, $text, \LOCK_EX)) {
throw new RuntimeException("Failed to write {$php} file");
}
if (\function_exists('\\opcache_invalidate')) {
\opcache_invalidate($php, true);
}
}
/**
* Задает родительский шаблон
*/
protected function extend(string $name): void
{
$this->templates[] = $name;
}
/**
* Возвращает содержимое блока или $default
*/
protected function block(string $name, string $default = ''): string
{
return \array_key_exists($name, $this->blocks)
? $this->blocks[$name]
: $default;
}
/**
* Задает начало блока
*/
protected function beginBlock(string $name): void
{
$this->blockStack[] = $name;
\ob_start();
}
/**
* Задает конец блока
*/
protected function endBlock(bool $overwrite = false): string
{
$name = \array_pop($this->blockStack);
if (
$overwrite
|| ! \array_key_exists($name, $this->blocks)
) {
$this->blocks[$name] = \ob_get_clean();
} else {
$this->blocks[$name] .= \ob_get_clean();
}
return $name;
}
}

View file

@ -1,79 +1,84 @@
<?php
/**
* This file is part of the ForkBB <https://github.com/forkbb>.
*
* @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
* @license The MIT License (MIT)
*/
/**
* based on Dirk <https://github.com/artoodetoo/dirk>
*
* @copyright (c) 2015 artoodetoo <i.am@artoodetoo.org, https://github.com/artoodetoo>
* @license The MIT License (MIT)
*/
declare(strict_types=1);
namespace R2\Templating;
namespace ForkBB\Core\View;
use R2\Templating\PhpEngine;
use RuntimeException;
class Dirk extends PhpEngine
class Compiler
{
protected $cache;
protected $echoFormat;
public function __construct(array $config = [])
{
$config = \array_replace_recursive(
[
'ext' => '.blade.php',
'cache' => '.',
'echo' => '\\htmlspecialchars((string) %s, \\ENT_QUOTES, \'UTF-8\')',
],
$config
);
$this->cache = $config['cache'] ?? '.';
$this->echoFormat = $config['echo'] ?? '%s';
parent::__construct($config);
}
protected $compilers = [
protected string $shortID;
protected int $loopsCounter = 0;
protected array $compilers = [
'PrePaste',
'Statements',
'Comments',
'Echos',
'Transformations',
];
protected array $preArray = [];
protected string $tplName;
protected $shortID = '';
protected $shortArr = [];
/**
* Prepare file to include
* @param string $name
* @return string
*/
protected function prepare(string $name): string
public function __construct(string $preFile)
{
$name = \str_replace('.', '/', $name);
$tpl = $this->views . '/' . $name . $this->ext;
$sha1 = \sha1($name);
$php = $this->cache . '/' . $sha1 . '.php';
if (
! \file_exists($php)
|| \filemtime($tpl) > \filemtime($php)
! empty($preFile)
&& \is_file($preFile)
) {
$this->shortArr[] = $this->shortID;
$this->shortID = \substr($sha1, 0, 4);
$text = \file_get_contents($tpl);
foreach ($this->compilers as $type) {
$text = $this->{'compile' . $type}($text);
}
\file_put_contents($php, $text);
$this->shortID = \array_pop($this->shortArr);
$this->preArray = include $preFile;
}
return $php;
}
/**
* Compile Statements that start with "@"
*
* @param string $value
* @return string
* Генерирует php код на основе шаблона из $text
*/
public function create(string $name, string $text, string $hash): string
{
$this->shortID = $hash;
$this->tplName = $name;
foreach ($this->compilers as $type) {
$text = $this->{'compile' . $type}($text);
}
return $text;
}
/**
* Обрабатывает предварительную подстановку кода в шаблон
*/
protected function compilePrePaste(string $value): string
{
$pre = $this->preArray[$this->tplName] ?? null;
return \preg_replace_callback(
'%^[ \t]*+<!-- PRE (\w+) -->[ \t]*(?:\r?\n)?%m',
function($match) use ($pre) {
if (isset($pre[$match[1]])) {
return \rtrim($pre[$match[1]]) . "\n";
} else {
return '';
}
},
$value
);
}
/**
* Обрабатывает операторы начинающиеся с @
*/
protected function compileStatements(string $value): string
{
@ -91,10 +96,7 @@ class Dirk extends PhpEngine
}
/**
* Compile comments
*
* @param string $value
* @return string
* Обрабатывает комментарии
*/
protected function compileComments(string $value): string
{
@ -102,30 +104,29 @@ class Dirk extends PhpEngine
}
/**
* Compile echos
*
* @param string $value
* @return string
* Обрабатывает вывод информации
*/
protected function compileEchos(string $value): string
{
// compile escaped echoes
// {{! !}}
$value = \preg_replace_callback(
'%\{\{\{\s*(.+?)\s*\}\}\}(\r?\n)?%s',
'%(@)?\{\{![ \t]*+(.+?)[ \t]*!\}\}(\r?\n)?%',
function($matches) {
$whitespace = empty($matches[2]) ? '' : $matches[2] . $matches[2];
$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
return '<?= \\htmlspecialchars('
. $this->compileEchoDefaults($matches[1])
. ', \\ENT_QUOTES, \'UTF-8\') ?>'
. $whitespace;
return $matches[1]
? \substr($matches[0], 1)
: '<?= \\htmlspecialchars((string) '
. $this->compileEchoDefaults($matches[2])
. ', \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE, \'UTF-8\', false) ?>'
. $whitespace;
},
$value
);
// compile not escaped echoes
// {!! !!}
$value = \preg_replace_callback(
'%\{\!!\s*(.+?)\s*!!\}(\r?\n)?%s',
'%\{\!![ \t]*+(.+?)[ \t]*!!\}(\r?\n)?%',
function($matches) {
$whitespace = empty($matches[2]) ? '' : $matches[2] . $matches[2];
@ -137,17 +138,18 @@ class Dirk extends PhpEngine
$value
);
// compile regular echoes
// {{ }}
$value = \preg_replace_callback(
'%(@)?\{\{\s*(.+?)\s*\}\}(\r?\n)?%s',
'%(@)?\{\{(?!!)[ \t]*+(.+?)[ \t]*\}\}(\r?\n)?%',
function($matches) {
$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
return $matches[1]
? \substr($matches[0], 1)
: '<?= '
. \sprintf($this->echoFormat, $this->compileEchoDefaults($matches[2]))
. ' ?>' . $whitespace;
: '<?= \\htmlspecialchars((string) '
. $this->compileEchoDefaults($matches[2])
. ', \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE, \'UTF-8\') ?>'
. $whitespace;
},
$value
);
@ -156,10 +158,39 @@ class Dirk extends PhpEngine
}
/**
* Compile the default values for the echo statement.
*
* @param string $value
* @return string
* Трансформирует скомпилированный шаблон
*/
protected function compileTransformations(string $value): string
{
if (\str_starts_with($value, '<?xml ')) {
$value = \str_replace(' \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE,', ' \\ENT_XML1,', $value);
}
$perfix = <<<'EOD'
<?php
declare(strict_types=1);
use function \ForkBB\{__, num, dt, size, url};
?>
EOD;
if (false === \strpos($value, '<!-- inline -->')) {
return $perfix . $value;
}
return $perfix . \preg_replace_callback(
'%<!-- inline -->([^<]*(?:<(?!!-- endinline -->)[^<]*)*+)(?:<!-- endinline -->)?%',
function ($matches) {
return \preg_replace('%\h*\R\s*%', '', $matches[1]);
},
$value
);
}
/**
* Обрабатывает значение по умолчанию для вывода информации
*/
public function compileEchoDefaults(string $value): string
{
@ -167,10 +198,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the if statements
*
* @param string $expression
* @return string
* @if()
*/
protected function compileIf(string $expression): string
{
@ -186,10 +214,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the else-if statements
*
* @param string $expression
* @return string
* @elseif()
*/
protected function compileElseif(string $expression): string
{
@ -197,9 +222,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the else statements
*
* @return string
* @else
*/
protected function compileElse(): string
{
@ -207,9 +230,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the end-if statements
*
* @return string
* @endif
*/
protected function compileEndif(): string
{
@ -217,10 +238,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the isset statements
*
* @param string $expression
* @return string
* @isset()
*/
protected function compileIsset(string $expression): string
{
@ -228,9 +246,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the end-isset statements
*
* @return string
* @endisset
*/
protected function compileEndisset(): string
{
@ -238,9 +254,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the end-empty statements
*
* @return string
* @endempty
*/
protected function compileEndempty(): string
{
@ -248,10 +262,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the unless statements
*
* @param string $expression
* @return string
* @unless()
*/
protected function compileUnless(string $expression): string
{
@ -259,9 +270,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the end unless statements
*
* @return string
* @endunless
*/
protected function compileEndunless(): string
{
@ -269,10 +278,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the for statements
*
* @param string $expression
* @return string
* @for()
*/
protected function compileFor(string $expression): string
{
@ -280,22 +286,15 @@ class Dirk extends PhpEngine
}
/**
* Compile the end-for statements
*
* @return string
* @endfor
*/
protected function compileEndfor(): string
{
return "<?php endfor; ?>";
}
protected $loopsCounter = 0;
/**
* Compile the foreach statements
*
* @param string $expression
* @return string
* @foreach()
*/
protected function compileForeach(string $expression): string
{
@ -307,9 +306,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the end-for-each statements
*
* @return string
* @endforeach
*/
protected function compileEndforeach(): string
{
@ -318,16 +315,16 @@ class Dirk extends PhpEngine
return "<?php endforeach; ?>";
}
/**
* @iteration
*/
protected function compileIteration(): string
{
return "((int) \$__iter{$this->shortID}_{$this->loopsCounter})";
}
/**
* Compile the forelse statements
*
* @param string $expression
* @return string
* @forelse()
*/
protected function compileForelse(string $expression): string
{
@ -339,11 +336,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the end-forelse statements
* Compile the empty statements
*
* @param string $expression
* @return string
* @empty / @empty()
*/
protected function compileEmpty(string $expression): string
{
@ -362,9 +355,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the end-forelse statements
*
* @return string
* @endforelse
*/
protected function compileEndforelse(): string
{
@ -372,10 +363,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the while statements
*
* @param string $expression
* @return string
* @while()
*/
protected function compileWhile(string $expression): string
{
@ -383,9 +371,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the end-while statements
*
* @return string
* @endwhile
*/
protected function compileEndwhile(): string
{
@ -393,10 +379,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the extends statements
*
* @param string $expression
* @return string
* @extends()
*/
protected function compileExtends(string $expression): string
{
@ -411,10 +394,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the include statements
*
* @param string $expression
* @return string
* @include()
*/
protected function compileInclude(string $expression): string
{
@ -429,10 +409,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the yield statements
*
* @param string $expression
* @return string
* @yield()
*/
protected function compileYield(string $expression): string
{
@ -440,10 +417,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the section statements
*
* @param string $expression
* @return string
* @section()
*/
protected function compileSection(string $expression): string
{
@ -451,9 +425,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the end-section statements
*
* @return string
* @endsection
*/
protected function compileEndsection(): string
{
@ -461,9 +433,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the show statements
*
* @return string
* @show()
*/
protected function compileShow(): string
{
@ -471,9 +441,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the append statements
*
* @return string
* @append
*/
protected function compileAppend(): string
{
@ -481,9 +449,7 @@ class Dirk extends PhpEngine
}
/**
* Compile the stop statements
*
* @return string
* @stop
*/
protected function compileStop(): string
{
@ -491,20 +457,24 @@ class Dirk extends PhpEngine
}
/**
* Compile the overwrite statements
*
* @return string
* @overwrite
*/
protected function compileOverwrite(): string
{
return "<?php \$this->endBlock(true); ?>";
}
/**
* @switch()
*/
protected function compileSwitch(string $expression): string
{
return "<?php switch {$expression}: ?>";
}
/**
* @case()
*/
protected function compileCase(string $expression): string
{
$expression = \substr($expression, 1, -1);
@ -512,18 +482,43 @@ class Dirk extends PhpEngine
return "<?php case {$expression}: ?>";
}
/**
* @default
*/
protected function compileDefault(): string
{
return "<?php default: ?>";
}
/**
* @endswitch
*/
protected function compileEndswitch(): string
{
return "<?php endswitch; ?>";
}
/**
* @break
*/
protected function compileBreak(): string
{
return "<?php break; ?>";
}
/**
* @php
*/
protected function compilePhp(): string
{
return "<?php";
}
/**
* @endphp
*/
protected function compileEndphp(): string
{
return " ?>";
}
}

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

@ -22,6 +22,10 @@ class Delete extends Method
public function delete(int ...$ids): BanList
{
if (! empty($ids)) {
if (\count($ids) > 1) {
\sort($ids, \SORT_NUMERIC);
}
$vars = [
':ids' => $ids,
];

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

@ -29,6 +29,7 @@ class Save extends Method
if (! isset($list[$id]['search_for'], $list[$id]['replace_with'])) {
continue;
}
if ('' === \trim($list[$id]['search_for'])) {
if ($id > 0) {
$forDel[] = $id;
@ -60,9 +61,14 @@ class Save extends Method
$this->c->DB->exec($query, $vars);
}
}
if ($forDel) {
if (\count($forDel) > 1) {
\sort($forDel, \SORT_NUMERIC);
}
$vars = [
':del' => $forDel
':del' => $forDel,
];
$query = 'DELETE
FROM ::censoring

View file

@ -79,14 +79,13 @@ class Save extends Method
$this->c->DB->exec($query, $vars);
$query = 'INSERT INTO ::config (conf_name, conf_value)
SELECT ?s:name, ?s:value
FROM ::groups
SELECT tmp.*
FROM (SELECT ?s:name AS f1, ?s:value AS f2) AS tmp
WHERE NOT EXISTS (
SELECT 1
FROM ::config
WHERE conf_name=?s:name
)
LIMIT 1';
)';
break;
}

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

@ -30,18 +30,15 @@ class CalcStat extends Method
];
$query = 'SELECT COUNT(t.id)
FROM ::topics AS t
WHERE t.forum_id=?i:fid AND t.moved_to!=0';
WHERE t.forum_id=?i:fid';
$moved = (int) $this->c->DB->query($query, $vars)->fetchColumn();
$this->model->num_topics = (int) $this->c->DB->query($query, $vars)->fetchColumn();
$query = 'SELECT COUNT(t.id) as num_topics, SUM(t.num_replies) as num_replies
$query = 'SELECT SUM(t.num_replies + 1)
FROM ::topics AS t
WHERE t.forum_id=?i:fid AND t.moved_to=0';
$result = $this->c->DB->query($query, $vars)->fetch();
$this->model->num_topics = $result['num_topics'] + $moved;
$this->model->num_posts = $result['num_topics'] + $result['num_replies'];
$this->model->num_posts = (int) $this->c->DB->query($query, $vars)->fetchColumn();
$query = 'SELECT t.last_post, t.last_post_id, t.last_poster, t.last_poster_id, t.subject as last_topic
FROM ::topics AS t

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)');
}
}
@ -87,6 +84,10 @@ class Delete extends Action
}
if ($forums) {
if (\count($forums) > 1) {
\ksort($forums, \SORT_NUMERIC);
}
$this->c->subscriptions->unsubscribe(...$forums);
foreach ($forums as $forum) {

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

@ -125,9 +125,9 @@ class LoadTree extends Action
$query = 'SELECT t.forum_id, t.last_post
FROM ::topics AS t
LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND mot.tid=t.id)
WHERE t.forum_id IN(?ai:forums)
AND t.last_post>?i:max
WHERE t.forum_id IN (?ai:forums)
AND t.moved_to=0
AND t.last_post>?i:max
AND (mot.mt_last_visit IS NULL OR t.last_post>mot.mt_last_visit)';
$stmt = $this->c->DB->query($query, $vars);

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
@ -89,6 +89,7 @@ class Refresh extends Action
$sub[] = $id;
$all = \array_merge($this->createList($list, $id), $all);
}
if (0 === $parent) {
if (empty($sub)) {
return [];
@ -99,6 +100,11 @@ class Refresh extends Action
}
$all = \array_merge($sub, $all);
if (\count($all) > 1) {
\sort($all, \SORT_NUMERIC);
}
$list[$parent]['subforums'] = $sub ?: null;
$list[$parent]['descendants'] = $all ?: null;

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

@ -98,7 +98,8 @@ class Online extends Model
$users = [];
$guests = [];
$bots = [];
$needClean = false;
$upUsers = [];
$delGuests = [];
if ($detail) {
$query = 'SELECT o.user_id, o.ident, o.logged, o.o_position, o.o_name
@ -116,16 +117,10 @@ class Online extends Model
// посетитель уже не онлайн (или почти не онлайн)
if ($cur['logged'] < $tOnline) {
if ($cur['logged'] < $tVisit) {
$needClean = true;
if ($cur['user_id'] > 0) {
$this->c->users->updateLastVisit(
$this->c->users->create([
'id' => $cur['user_id'],
'group_id' => FORK_GROUP_MEMBER,
'logged' => $cur['logged'],
])
);
$upUsers[$cur['user_id']] = $cur['logged'];
} else {
$delGuests[] = $cur['ident'];
}
}
@ -161,12 +156,41 @@ class Online extends Model
}
// удаление просроченных посетителей
if ($needClean) {
if ($upUsers) {
if (\count($upUsers) > 1) {
\ksort($upUsers, \SORT_NUMERIC);
}
foreach ($upUsers as $id => $logged) {
$this->c->users->updateLastVisit(
$this->c->users->create([
'id' => $id,
'group_id' => FORK_GROUP_MEMBER,
'logged' => $logged,
])
);
}
$vars = [
':visit' => $tVisit,
':ids' => \array_keys($upUsers),
];
$query = 'DELETE FROM ::online
WHERE logged<?i:visit';
WHERE user_id IN (?ai:ids)';
$this->c->DB->exec($query, $vars);
}
// удаление просроченных гостей
if ($delGuests) {
if (\count($delGuests) > 1) {
\sort($delGuests, \SORT_STRING);
}
$vars = [
':idents' => $delGuests,
];
$query = 'DELETE FROM ::online
WHERE user_id=0 AND ident IN (?as:idents)';
$this->c->DB->exec($query, $vars);
}
@ -235,31 +259,23 @@ class Online extends Model
WHERE user_id=?i:id';
}
} else {
switch ($this->c->DB->getType()) {
case 'mysql':
$query = 'INSERT IGNORE INTO ::online (user_id, ident, logged, o_position, o_name)
VALUES (?i:id, ?s:ident, ?i:logged, ?s:pos, ?s:name)';
$query = match ($this->c->DB->getType()) {
'mysql' => 'INSERT IGNORE INTO ::online (user_id, ident, logged, o_position, o_name)
VALUES (?i:id, ?s:ident, ?i:logged, ?s:pos, ?s:name)',
break;
case 'sqlite':
case 'pgsql':
$query = 'INSERT INTO ::online (user_id, ident, logged, o_position, o_name)
VALUES (?i:id, ?s:ident, ?i:logged, ?s:pos, ?s:name)
ON CONFLICT(user_id, ident) DO NOTHING';
'sqlite', 'pgsql' => 'INSERT INTO ::online (user_id, ident, logged, o_position, o_name)
VALUES (?i:id, ?s:ident, ?i:logged, ?s:pos, ?s:name)
ON CONFLICT(user_id, ident) DO NOTHING',
break;
default:
$query = 'INSERT INTO ::online (user_id, ident, logged, o_position, o_name)
SELECT tmp.*
FROM (SELECT ?i:id AS f1, ?s:ident AS f2, ?i:logged AS f3, ?s:pos AS f4, ?s:name AS f5) AS tmp
WHERE NOT EXISTS (
SELECT 1
FROM ::online
WHERE user_id=?i:id AND ident=?s:ident
)';
break;
}
default => 'INSERT INTO ::online (user_id, ident, logged, o_position, o_name)
SELECT tmp.*
FROM (SELECT ?i:id AS f1, ?s:ident AS f2, ?i:logged AS f3, ?s:pos AS f4, ?s:name AS f5) AS tmp
WHERE NOT EXISTS (
SELECT 1
FROM ::online
WHERE user_id=?i:id AND ident=?s:ident
)',
};
}
$this->c->DB->exec($query, $vars);

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)');
@ -62,7 +61,7 @@ class Delete extends Method
}
$users[$arg->id] = $arg;
$isUser = 1;
$isUser = 1;
} elseif ($arg instanceof PPost) {
if (! $arg->parent instanceof PTopic) {
throw new RuntimeException('Bad ppost');
@ -77,8 +76,6 @@ class Delete extends Method
$topics[$arg->id] = $arg;
$isTopic = 1;
} else {
throw new InvalidArgumentException('Expected User(s), PPost(s) or PTopic(s)');
}
}
@ -87,6 +84,10 @@ class Delete extends Method
}
if ($topics) {
if (\count($topics) > 1) {
\ksort($topics, \SORT_NUMERIC);
}
$ids = [];
foreach ($topics as $topic) {
@ -103,6 +104,10 @@ class Delete extends Method
}
if ($posts) {
if (\count($posts) > 1) {
\ksort($posts, \SORT_NUMERIC);
}
$calcTopics = [];
foreach ($posts as $post) {
@ -123,6 +128,10 @@ class Delete extends Method
$this->c->DB->exec($query, $vars);
if (\count($calcTopics) > 1) {
\ksort($calcTopics, \SORT_NUMERIC);
}
foreach ($calcTopics as $topic) {
$this->model->update(Cnst::PTOPIC, $topic->calcStat());
}
@ -184,6 +193,10 @@ class Delete extends Method
$uids[$row['target_id']] = $row['target_id'];
}
if (\count($ids) > 1) {
\sort($ids, \SORT_NUMERIC);
}
$this->deletePTopics($ids);
foreach ($this->c->users->loadByIds($uids) as $user) {
@ -193,6 +206,10 @@ class Delete extends Method
}
}
if (\count($calcUsers) > 1) {
\ksort($calcUsers, \SORT_NUMERIC);
}
foreach ($calcUsers as $user) {
$this->model->recalculate($user);
}

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

@ -69,6 +69,14 @@ abstract class Page extends Model
$this->fDescription = $container->config->o_board_desc;
$this->fRootLink = $container->Router->link('Index');
$this->mDescription = $this->c->config->s_meta_desc;
if (! empty($this->c->config->a_og_image['file'])) {
$this->mOgImage = $this->c->PUBLIC_URL . '/img/og/' . $this->c->config->a_og_image['file'];
$this->mOgImageX = $this->c->config->a_og_image['width'] ?? null;
$this->mOgImageY = $this->c->config->a_og_image['height'] ?? null;
}
if (1 === $container->config->b_announcement) {
$this->fAnnounce = $container->config->o_announcement_message;
}
@ -304,7 +312,11 @@ abstract class Page extends Model
1 === $this->c->config->b_maintenance
&& $this->user->isAdmin
) {
$this->fIswev = [FORK_MESS_WARN, ['Maintenance mode enabled', $this->c->Router->link('AdminMaintenance')]];
if ($this->c->MAINTENANCE_OFF) {
$this->fIswev = [FORK_MESS_ERR, ['Maintenance mode enabled off', $this->c->Router->link('AdminMaintenance')]];
} else {
$this->fIswev = [FORK_MESS_WARN, ['Maintenance mode enabled', $this->c->Router->link('AdminMaintenance')]];
}
}
if (
@ -354,20 +366,6 @@ abstract class Page extends Model
*/
protected function getpageHeaders(): array
{
if ($this->canonical) {
$this->pageHeader('canonical', 'link', 0, [
'rel' => 'canonical',
'href' => $this->canonical,
]);
}
if ($this->robots) {
$this->pageHeader('robots', 'meta', 11000, [
'name' => 'robots',
'content' => $this->robots,
]);
}
if (1 === $this->user->g_search) {
$this->pageHeader('opensearch', 'link', 0, [
'rel' => 'search',
@ -378,11 +376,7 @@ abstract class Page extends Model
}
\uasort($this->pageHeaders, function (array $a, array $b) {
if ($a['weight'] === $b['weight']) {
return 0;
} else {
return $a['weight'] > $b['weight'] ? -1 : 1;
}
return $b['weight'] <=> $a['weight'];
});
return $this->pageHeaders;

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],
@ -931,8 +947,9 @@ class Install extends Admin
],
'PRIMARY KEY' => ['id'],
'INDEXES' => [
'topic_id_idx' => ['topic_id'],
'multi_idx' => ['poster_id', 'topic_id', 'posted'],
'topic_id_idx' => ['topic_id'],
'multi_idx' => ['poster_id', 'topic_id', 'posted'],
'editor_id_idx' => ['editor_id'],
],
'ENGINE' => $this->DBEngine,
];
@ -1051,10 +1068,10 @@ class Install extends Admin
],
'PRIMARY KEY' => ['id'],
'INDEXES' => [
'forum_id_idx' => ['forum_id'],
'moved_to_idx' => ['moved_to'],
'multi_2_idx' => ['forum_id', 'sticky', 'last_post'],
'last_post_idx' => ['last_post'],
'first_post_id_idx' => ['first_post_id'],
'multi_1_idx' => ['moved_to', 'forum_id', 'num_replies', 'last_post'],
],
'ENGINE' => $this->DBEngine,
];
@ -1581,6 +1598,8 @@ class Install extends Admin
'i_search_ttl' => 900,
'b_ant_hidden_ch' => 1,
'b_ant_use_js' => 0,
's_meta_desc' => '',
'a_og_image' => \json_encode([], FORK_JSON_ENCODE),
];
foreach ($forkConfig as $name => $value) {

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

@ -106,7 +106,7 @@ class Maintenance extends Admin
*/
protected function formRebuild(): array
{
return [
$form = [
'action' => $this->c->Router->link('AdminMaintenanceRebuild'),
'hidden' => [
'token' => $this->c->Csrf->create('AdminMaintenanceRebuild'),
@ -162,6 +162,24 @@ class Maintenance extends Admin
],
];
if (
1 !== $this->c->config->b_maintenance
|| $this->c->MAINTENANCE_OFF
) {
$form['sets']['maintenance-only'] = [
'inform' => [
[
'html' => '- - -',
],
[
'message' => 'Maintenance only',
],
],
];
$form['btns']['rebuild']['disabled'] = true;
}
return $form;
}
/**
@ -217,6 +235,13 @@ class Maintenance extends Admin
*/
public function rebuild(array $args, string $method): Page
{
if (
1 !== $this->c->config->b_maintenance
|| $this->c->MAINTENANCE_OFF
) {
return $this->c->Message->message('Maintenance only');
}
$this->c->Lang->load('validator');
$this->c->Lang->load('admin_maintenance');
@ -306,6 +331,10 @@ class Maintenance extends Admin
throw new RuntimeException('Unable to clear cache');
}
if (\function_exists('\\opcache_reset')) {
\opcache_reset();
}
return $this->c->Redirect->page('AdminMaintenance')->message('Clear cache redirect', FORK_MESS_SUCC);
}
}

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace ForkBB\Models\Pages\Admin;
use ForkBB\Core\Image;
use ForkBB\Core\Validator;
use ForkBB\Models\Page;
use ForkBB\Models\Pages\Admin;
@ -43,6 +44,7 @@ class Options extends Admin
'token' => 'token:AdminOptions',
'o_board_title' => 'required|string:trim|max:255',
'o_board_desc' => 'exist|string:trim,empty|max:65000 bytes|html',
's_meta_desc' => 'exist|string:trim,empty|max:255',
'o_default_timezone' => [
'required',
'string:trim',
@ -113,6 +115,8 @@ class Options extends Admin
'b_poll_guest' => 'required|integer|in:0,1',
'b_pm' => 'required|integer|in:0,1',
'b_oauth_allow' => 'required|integer|in:0,1',
'upload_og_image' => 'image',
'delete_og_image' => 'checkbox',
])->addAliases([
])->addArguments([
])->addMessages([
@ -121,20 +125,56 @@ class Options extends Admin
'o_webmaster_email' => 'Invalid webmaster e-mail message',
]);
$valid = $v->validation($_POST);
$valid = $v->validation($_FILES + $_POST);
$data = $v->getData();
if (empty($data['changeSmtpPassword'])) {
unset($data['o_smtp_pass']);
}
unset($data['changeSmtpPassword'], $data['token']);
unset($data['changeSmtpPassword'], $data['token'], $data['upload_og_image'], $data['delete_og_image']);
foreach ($data as $attr => $value) {
$config->$attr = $value;
}
if ($valid) {
if (
$v->delete_og_image
|| $v->upload_og_image instanceof Image
) {
$folder = $this->c->DIR_PUBLIC . '/img/og/';
$this->deleteOgImage($folder);
$config->a_og_image = [];
}
if ($v->upload_og_image instanceof Image) {
$path = $folder . $this->c->Secury->randomPass(8) . '.webp';
$result = $v->upload_og_image
->rename(true)
->rewrite(false)
->setQuality($this->c->config->i_avatars_quality ?? 75)
->toFile($path);
if (true === $result) {
$config->a_og_image = [
'file' => $v->upload_og_image->name() . '.' . $v->upload_og_image->ext(),
'width' => $v->upload_og_image->width(),
'height' => $v->upload_og_image->height(),
];
} else {
$config->a_og_image = [];
$this->c->Log->warning('og:image Failed image processing', [
'user' => $this->user->fLog(),
'error' => $v->upload_og_image->error(),
]);
}
}
$config->save();
return $this->c->Redirect->page('AdminOptions')->message('Options updated redirect', FORK_MESS_SUCC);
@ -152,6 +192,20 @@ class Options extends Admin
return $this;
}
/**
* Удаляет текущую картинку Open Graph
*/
protected function deleteOgImage(string $folder): void
{
if (! empty($this->c->config->a_og_image['file'])) {
$path = $folder . $this->c->config->a_og_image['file'];
if (\is_file($path)) {
\unlink($path);
}
}
}
/**
* Дополнительная проверка времени online
*/
@ -201,12 +255,13 @@ class Options extends Admin
protected function formEdit(Config $config): array
{
$form = [
'action' => $this->c->Router->link('AdminOptions'),
'hidden' => [
'action' => $this->c->Router->link('AdminOptions'),
'hidden' => [
'token' => $this->c->Csrf->create('AdminOptions'),
],
'sets' => [],
'btns' => [
'enctype' => 'multipart/form-data',
'sets' => [],
'btns' => [
'save' => [
'type' => 'submit',
'value' => __('Save changes'),
@ -218,6 +273,10 @@ class Options extends Admin
$langs = $this->c->Func->getNameLangs();
$styles = $this->c->Func->getStyles();
if (isset($config->a_og_image['file'])) {
$this->ogImageUrl = $this->c->PUBLIC_URL . '/img/og/' . $config->a_og_image['file'];
}
$form['sets']['essentials'] = [
'legend' => 'Essentials subhead',
'fields' => [
@ -236,6 +295,13 @@ class Options extends Admin
'caption' => 'Board desc label',
'help' => 'Board desc help',
],
's_meta_desc' => [
'type' => 'text',
'maxlength' => '255',
'value' => $config->s_meta_desc,
'caption' => 'Meta desc label',
'help' => 'Meta desc help',
],
'o_default_timezone' => [
'type' => 'select',
'options' => $this->createTimeZoneOptions(),
@ -257,6 +323,23 @@ class Options extends Admin
'caption' => 'Default style label',
'help' => 'Default style help',
],
'a_og_image' => [
'type' => empty($this->ogImageUrl) ? 'str' : 'yield',
'caption' => 'Og image label',
'value' => empty($this->ogImageUrl) ? __('Not uploaded') : 'og:image',
'help' => empty($this->ogImageUrl) ? null : ['Og image help', $config->a_og_image['width'], $config->a_og_image['height']],
],
'delete_og_image' => [
'type' => 'checkbox',
'label' => 'Delete og image',
'checked' => false,
],
'upload_og_image' => [
'type' => 'file',
'caption' => 'New og image label',
'help' => 'New og image help',
'accept' => 'image/*',
],
],
];

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 = 67;
const LATEST_REV_WITH_DB_CHANGES = 72;
const LOCK_NAME = 'lock_update';
const LOCK_TTL = 1800;
const CONFIG_FILE = 'main.php';
@ -360,6 +360,10 @@ class Update extends Admin
do {
if (\method_exists($this, 'stageNumber' . $stage)) {
if (\function_exists('\\set_time_limit')) {
\set_time_limit(0);
}
$start = $this->{'stageNumber' . $stage}($args);
if (null === $start) {
@ -871,4 +875,195 @@ class Update extends Admin
return null;
}
/**
* rev.67 to rev.68
*/
protected function stageNumber67(array $args): ?int
{
$config = $this->c->config;
$config->s_meta_desc ??= '';
$config->a_og_image ??= [];
$config->save();
$this->c->DB->addIndex('::posts', 'editor_id_idx', ['editor_id']);
$this->c->DB->dropIndex('::topics', 'forum_id_idx');
$this->c->DB->dropIndex('::topics', 'moved_to_idx');
$this->c->DB->addIndex('::topics', 'multi_1_idx', ['moved_to', 'forum_id', 'num_replies', 'last_post']);
$this->c->DB->addIndex('::topics', 'multi_2_idx', ['forum_id', 'sticky', 'last_post']);
return null;
}
/**
* rev.68 to rev.69
*/
protected function stageNumber68(array $args): ?int
{
$coreConfig = new CoreConfig($this->configFile);
$coreConfig->add(
'shared=>View',
[
'class' => '\\ForkBB\\Core\\View::class',
'config' => [
'cache' => '\'%DIR_CACHE%\'',
'defaultDir' => '\'%DIR_VIEWS%/_default\'',
'userDir' => '\'%DIR_VIEWS%/_user\'',
],
]
);
$coreConfig->save();
return null;
}
/**
* rev.69 to rev.70
*/
protected function stageNumber69(array $args): ?int
{
$coreConfig = new CoreConfig($this->configFile);
$coreConfig->add(
'shared=>%DIR_EXT%',
'\'%DIR_ROOT%/ext\'',
'%DIR_VIEWS%'
);
$coreConfig->add(
'multiple=>ExtensionModel',
'\\ForkBB\\Models\\Extension\\Extension::class',
'DBMapModel'
);
$coreConfig->add(
'multiple=>ExtensionManager',
'\\ForkBB\\Models\\Extension\\Extensions::class',
'ExtensionModel'
);
$coreConfig->add(
'shared=>extensions',
'\'@ExtensionManager:init\'',
'attachments'
);
$coreConfig->add(
'multiple=>AdminExtensions',
'\\ForkBB\\Models\\Pages\\Admin\\Extensions::class',
'AdminAntispam'
);
$coreConfig->add(
'shared=>View=>config=>preFile',
'\'%DIR_CONFIG%/ext/pre.php\''
);
$coreConfig->save();
// extensions
$schema = [
'FIELDS' => [
'ext_name' => ['VARCHAR(190)', false, ''],
'ext_status' => ['TINYINT', false, 0],
'ext_data' => ['TEXT', false],
],
'PRIMARY KEY' => ['ext_name'],
];
$this->c->DB->createTable('::extensions', $schema);
return null;
}
/**
* rev.70 to rev.71
*/
protected function stageNumber70(array $args): ?int
{
$coreConfig = new CoreConfig($this->configFile);
$coreConfig->add(
'FRIENDLY_URL',
[
'lowercase' => 'false',
'translit' => '\'\'',
'WtoHyphen' => 'false',
],
'TIME_FORMATS'
);
$coreConfig->save();
return null;
}
/**
* rev.71 to rev.72
*/
protected function stageNumber71(array $args): ?int
{
switch ($args['start'] ?? 1) {
case 3:
$f = $this->c->FRIENDLY_URL;
if (
! empty($f['lowercase'])
|| ! empty($f['translit'])
|| ! empty($f['WtoHyphen'])
) {
$names = $this->c->DB->query('SELECT id, forum_name FROM ::forums WHERE redirect_url=\'\' ORDER BY id')->fetchAll(PDO::FETCH_KEY_PAIR);
$query = 'UPDATE ::forums SET friendly_name=?s:name WHERE id=?i:id';
foreach ($names as $id => $name) {
$vars = [
':id' => $id,
':name' => \mb_substr($this->c->Func->friendly($name), 0, 80, 'UTF-8'),
];
$this->c->DB->exec($query, $vars);
}
}
return null;
case 2:
$this->c->DB->addField('::forums', 'friendly_name', 'VARCHAR(80)', false, '', null, 'forum_name');
return 3;
default:
$coreConfig = new CoreConfig($this->configFile);
$coreConfig->add(
'FRIENDLY_URL=>file',
'\'translit.default.php\'',
'WtoHyphen'
);
$coreConfig->save();
return 2;
}
}
/**
* rev.72 to rev.73
*/
protected function stageNumber72(array $args): ?int
{
$coreConfig = new CoreConfig($this->configFile);
$coreConfig->add(
'multiple=>Sitemap',
'\\ForkBB\\Models\\Pages\\Sitemap::class',
'Misc'
);
$coreConfig->save();
return null;
}
}

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',
@ -416,6 +416,13 @@ class View extends Users
*/
public function recalculate(array $args, string $method): Page
{
if (
1 !== $this->c->config->b_maintenance
|| $this->c->MAINTENANCE_OFF
) {
return $this->c->Message->message('Maintenance only');
}
$v = $this->c->Validator->reset()
->addValidators([
])->addRules([
@ -435,7 +442,12 @@ class View extends Users
);
}
if (\function_exists('\\set_time_limit')) {
\set_time_limit(0);
}
$this->c->users->updateCountPosts();
$this->c->users->updateCountTopics();
return $this->c->Redirect->page('AdminUsers')->message('Updated the number of users posts redirect', FORK_MESS_SUCC);
}
@ -470,6 +482,20 @@ class View extends Users
],
];
if (
1 !== $this->c->config->b_maintenance
|| $this->c->MAINTENANCE_OFF
) {
$form['sets']['maintenance-only'] = [
'inform' => [
[
'message' => 'Maintenance only',
],
],
];
$form['btns']['recalculate']['disabled'] = true;
}
return $form;
}
}

View file

@ -120,6 +120,7 @@ class Auth extends Page
$this->nameTpl = 'login';
$this->onlinePos = 'login';
$this->onlineDetail = null;
$this->canonical = $this->c->Router->link('Login');
$this->robots = 'noindex';
$this->titles = 'Login';
$this->regLink = 1 === $this->c->config->b_regs_allow ? $this->c->Router->link('Register') : null;
@ -205,7 +206,7 @@ class Auth extends Page
/**
* Проверка пользователя по базе
*/
public function vLoginCheck(Validator $v, #[SensitiveParameter] string $password ): string
public function vLoginCheck(Validator $v, #[SensitiveParameter] string $password): string
{
if (empty($v->getErrors())) {
if ($this->loginWithForm) {
@ -363,6 +364,7 @@ class Auth extends Page
$this->nameTpl = 'passphrase_reset';
$this->onlinePos = 'passphrase_reset';
$this->onlineDetail = null;
$this->canonical = $this->c->Router->link('Forget');
$this->robots = 'noindex';
$this->titles = 'Passphrase reset';
$this->form = $this->formForget($v->email ?? $email);

View file

@ -72,7 +72,7 @@ class Delete extends Page
$this->nameTpl = 'post';
$this->onlinePos = 'topic-' . $topic->id;
$this->canonical = $post->linkDelete;
// $this->canonical = $post->linkDelete;
$this->robots = 'noindex';
$this->formTitle = $deleteTopic ? 'Delete topic' : 'Delete post';
$this->crumbs = $this->crumbs($this->formTitle, $topic);

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

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

@ -45,6 +45,8 @@ abstract class Profile extends Page
$this->nameTpl = 'profile';
$this->onlinePos = 'profile-' . $this->curUser->id; // ????
$this->mDescription = __(['mDescription for %s', $this->curUser->username]);
return true;
}

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'] = [
@ -391,11 +396,13 @@ class View extends Profile
];
}
$form['sets']['private'] = [
'class' => ['data'],
'legend' => 'Private information',
'fields' => $fields,
];
if (! empty($fields)) {
$form['sets']['private'] = [
'class' => ['data'],
'legend' => 'Private information',
'fields' => $fields,
];
}
return $form;
}

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

@ -47,6 +47,7 @@ class Rules extends Page
$this->nameTpl = 'rules';
$this->onlinePos = 'rules';
$this->onlineDetail = null;
$this->canonical = $this->c->Router->link('Register');
$this->robots = 'noindex';
$this->crumbs = $this->crumbs(
'Forum rules',

View file

@ -143,7 +143,7 @@ class Search extends Page
$this->nameTpl = 'search';
$this->onlinePos = 'search';
$this->onlineDetail = null;
$this->canonical = $this->c->Router->link('Search');
$this->canonical = $this->c->Router->link($advanced ? 'SearchAdvanced' : 'Search');
$this->robots = 'noindex';
$this->form = $advanced ? $this->formSearchAdvanced($v) : $this->formSearch($v);
$this->crumbs = $this->crumbs();
@ -334,7 +334,6 @@ class Search extends Page
if (! $search->prepare($query)) {
$v->addError([$search->queryError, $search->queryText]);
} else {
if ($this->c->search->execute($v, $this->listOfIndexes, $flood)) {
$flood = false;
@ -384,7 +383,9 @@ class Search extends Page
if (! empty(\array_diff($forums, $this->listOfIndexes))) {
$v->addError('The :alias contains an invalid value');
}
\sort($forums, SORT_NUMERIC);
$forums = \implode('.', $forums);
}
@ -500,7 +501,10 @@ class Search extends Page
case 'topics':
case 'topics_subscriptions':
case 'forums_subscriptions':
if (! isset($uid)) {
if (
! isset($uid)
|| $this->user->isBot
) {
break;
}

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)');
}
}
@ -55,6 +52,10 @@ class Delete extends Action
throw new InvalidArgumentException('Expected only Poll(s) or Topic(s)');
}
if (\count($tids) > 1) {
\sort($tids, \SORT_NUMERIC);
}
$vars = [
':tids' => $tids,
];

View file

@ -43,9 +43,9 @@ class Save extends Action
SET qna_text=?s:qna
WHERE tid=?i:tid AND question_id=?i:qid AND field_id=?i:fid';
$queryD1 = 'DELETE FROM ::poll
WHERE tid=?i:tid AND question_id IN(?ai:qids)';
WHERE tid=?i:tid AND question_id IN (?ai:qids)';
$queryD2 = 'DELETE FROM ::poll
WHERE tid=?i:tid AND question_id=?i:qid AND field_id IN(?ai:fid)';
WHERE tid=?i:tid AND question_id=?i:qid AND field_id IN (?ai:fid)';
$modified = false;
$oldQuestion = $old->question;

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)');
}
}
@ -136,17 +133,17 @@ class Delete extends Action
$parents = $this->c->topics->loadByIds($tids, false);
$query = 'UPDATE ::posts
SET editor_id=0
WHERE editor_id IN (?ai:users)';
$this->c->DB->exec($query, $vars);
$query = 'DELETE
FROM ::posts
WHERE poster_id IN (?ai:users)';
$this->c->DB->exec($query, $vars);
$query = 'UPDATE ::posts
SET editor_id=0
WHERE editor_id IN (?ai:users)';
$this->c->DB->exec($query, $vars);
}
if ($forums) {
@ -161,13 +158,19 @@ class Delete extends Action
$uidsUpdate = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
$query = 'DELETE
FROM ::posts
WHERE topic_id IN (
SELECT id
FROM ::topics
WHERE forum_id IN (?ai:forums)
)';
$query = match ($this->c->DB->getType()) {
'mysql' => 'DELETE p
FROM ::posts AS p, ::topics AS t
WHERE t.forum_id IN (?ai:forums) AND p.topic_id=t.id',
default => 'DELETE
FROM ::posts
WHERE topic_id IN (
SELECT id
FROM ::topics
WHERE forum_id IN (?ai:forums)
)',
};
$this->c->DB->exec($query, $vars);
}
@ -191,6 +194,10 @@ class Delete extends Action
}
if ($pids) {
if (\count($pids) > 1) {
\sort($pids, \SORT_NUMERIC);
}
$vars = [
':posts' => $pids,
];
@ -202,15 +209,24 @@ class Delete extends Action
}
if ($parents) {
if (\count($parents) > 1) {
\ksort($parents, \SORT_NUMERIC);
}
$topics = $parents;
$parents = [];
foreach ($topics as $topic) {
$parents[$topic->parent->id] = $topic->parent;
$this->c->topics->update($topic->calcStat());
}
if (! $forums) {
if (\count($parents) > 1) {
\ksort($parents, \SORT_NUMERIC);
}
foreach ($parents as $forum) {
$this->c->forums->update($forum->calcStat());
}

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
FROM ::posts AS p
INNER JOIN ::topics AS t ON t.id=p.topic_id
WHERE t.forum_id IN (?ai:forums)
ORDER BY p.id DESC
LIMIT 50';
$ids = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
if (empty($ids)) {
return [];
}
$vars = [
':ids' => $ids,
];
$query = 'SELECT p.id as pid, p.poster as username, p.poster_id as uid, p.message as content,
p.hide_smilies, p.posted, p.edited, t.id as tid, t.subject as topic_name, t.forum_id as fid
FROM ::posts AS p
INNER JOIN ::topics AS t ON t.id=p.topic_id
WHERE t.forum_id IN(?ai:forums)
ORDER BY p.id DESC
LIMIT 50';
} else {
throw new InvalidArgumentException('Expected Topic or Forum');
WHERE p.id IN (?ai:ids)
ORDER BY p.id DESC';
}
return $this->c->DB->query($query, $vars)->fetchAll();

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

@ -68,19 +68,25 @@ class ActionT extends Method
break;
case 'topics_with_your_posts':
/*
$query = 'SELECT t.id
FROM ::topics AS t
INNER JOIN ::posts AS p ON t.id=p.topic_id
WHERE t.forum_id IN (?ai:forums) AND t.moved_to=0 AND p.poster_id=?i:uid
GROUP BY t.id
ORDER BY t.last_post DESC';
*/
// упрощенный запрос для больших форумов, дополнительная обработка ниже
$query = 'SELECT DISTINCT t.id, t.last_post
FROM ::topics AS t
INNER JOIN ::posts AS p ON t.id=p.topic_id
WHERE t.forum_id IN (?ai:forums) AND t.moved_to=0 AND p.poster_id=?i:uid';
break;
case 'topics':
$query = 'SELECT t.id
FROM ::topics AS t
INNER JOIN ::posts AS p ON t.first_post_id=p.id
WHERE t.forum_id IN (?ai:forums) AND t.moved_to=0 AND p.poster_id=?i:uid
WHERE t.forum_id IN (?ai:forums) AND t.moved_to=0 AND t.poster_id=?i:uid
ORDER BY t.first_post_id DESC'; // t.last_post
break;
@ -90,8 +96,8 @@ class ActionT extends Method
LEFT JOIN ::mark_of_topic AS mot ON (mot.uid=?i:uid AND mot.tid=t.id)
LEFT JOIN ::mark_of_forum AS mof ON (mof.uid=?i:uid AND mof.fid=t.forum_id)
WHERE t.forum_id IN (?ai:forums)
AND t.last_post>?i:max
AND t.moved_to=0
AND t.last_post>?i:max
AND (mot.mt_last_visit IS NULL OR t.last_post>mot.mt_last_visit)
AND (mof.mf_mark_all_read IS NULL OR t.last_post>mof.mf_mark_all_read)
ORDER BY t.last_post DESC';
@ -126,7 +132,15 @@ class ActionT extends Method
':max' => \max((int) $this->c->user->last_visit, (int) $this->c->user->u_mark_all_read),
];
$list = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
if ('topics_with_your_posts' === $action) {
$list = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_KEY_PAIR);
\arsort($list, \SORT_NUMERIC);
$list = \array_keys($list);
} else {
$list = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
}
}
$this->model->numPages = (int) \ceil(($this->model->count($list) ?: 1) / $this->c->user->disp_topics);

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,16 +23,16 @@ class Delete extends Method
/**
* Удаление индекса
*/
public function delete(DataModel ...$args): void
public function delete(Forum|Post|Topic|User ...$args): void
{
if (empty($args)) {
throw new InvalidArgumentException('No arguments, expected User(s), Forum(s), Topic(s) or Post(s)');
}
$uids = [];
$forums = [];
$topics = [];
$posts = [];
$fids = [];
$tids = [];
$pids = [];
$isUser = 0;
$isForum = 0;
$isTopic = 0;
@ -55,15 +54,15 @@ class Delete extends Method
throw new RuntimeException('Forum unavailable');
}
$forums[$arg->id] = $arg;
$isForum = 1;
$fids[$arg->id] = $arg->id;
$isForum = 1;
} elseif ($arg instanceof Topic) {
if (! $arg->parent instanceof Forum) {
throw new RuntimeException('Parent unavailable');
}
$topics[$arg->id] = $arg;
$isTopic = 1;
$tids[$arg->id] = $arg->id;
$isTopic = 1;
} elseif ($arg instanceof Post) {
if (
! $arg->parent instanceof Topic
@ -72,10 +71,8 @@ class Delete extends Method
throw new RuntimeException('Parents unavailable');
}
$posts[$arg->id] = $arg;
$isPost = 1;
} else {
throw new InvalidArgumentException('Expected User(s), Forum(s), Topic(s) or Post(s)');
$pids[$arg->id] = $arg->id;
$isPost = 1;
}
}
@ -87,45 +84,67 @@ class Delete extends Method
$vars = [
':users' => $uids,
];
$query = 'DELETE
FROM ::search_matches
WHERE post_id IN (
SELECT p.id
FROM ::posts AS p
WHERE p.poster_id IN (?ai:users)
)';
$query = match ($this->c->DB->getType()) {
'mysql' => 'DELETE sm
FROM ::search_matches AS sm, ::posts AS p
WHERE p.poster_id IN (?ai:users) AND sm.post_id=p.id',
default => 'DELETE
FROM ::search_matches
WHERE post_id IN (
SELECT p.id
FROM ::posts AS p
WHERE p.poster_id IN (?ai:users)
)',
};
}
if ($forums) {
if ($fids) {
$vars = [
':forums' => \array_keys($forums),
':forums' => $fids,
];
$query = 'DELETE
FROM ::search_matches
WHERE post_id IN (
SELECT p.id
FROM ::posts AS p
INNER JOIN ::topics AS t ON t.id=p.topic_id
WHERE t.forum_id IN (?ai:forums)
)';
$query = match ($this->c->DB->getType()) {
'mysql' => 'DELETE sm
FROM ::search_matches AS sm, ::posts AS p, ::topics AS t
WHERE t.forum_id IN (?ai:forums) AND p.topic_id=t.id AND sm.post_id=p.id',
default => 'DELETE
FROM ::search_matches
WHERE post_id IN (
SELECT p.id
FROM ::posts AS p
INNER JOIN ::topics AS t ON t.id=p.topic_id
WHERE t.forum_id IN (?ai:forums)
)',
};
}
if ($topics) {
if ($tids) {
$vars = [
':topics' => \array_keys($topics),
':topics' => $tids,
];
$query = 'DELETE
FROM ::search_matches
WHERE post_id IN (
SELECT p.id
FROM ::posts AS p
WHERE p.topic_id IN (?ai:topics)
)';
$query = match ($this->c->DB->getType()) {
'mysql' => 'DELETE sm
FROM ::search_matches AS sm, ::posts AS p
WHERE p.topic_id IN (?ai:topics) AND sm.post_id=p.id',
default => 'DELETE
FROM ::search_matches
WHERE post_id IN (
SELECT p.id
FROM ::posts AS p
WHERE p.topic_id IN (?ai:topics)
)',
};
}
if ($posts) {
if ($pids) {
if (\count($pids) > 1) {
\sort($pids, \SORT_NUMERIC);
}
$vars = [
':posts' => \array_keys($posts),
':posts' => $pids,
];
$query = 'DELETE
FROM ::search_matches

View file

@ -80,7 +80,7 @@ class Index extends Method
];
$query = 'SELECT sw.word
FROM ::search_words AS sw
WHERE sw.word IN(?as:words)';
WHERE sw.word IN (?as:words)';
$oldWords = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
$newWords = \array_diff($allWords, $oldWords);
@ -106,6 +106,10 @@ class Index extends Method
continue;
}
if (\count($list) > 1) {
\sort($list, \SORT_NUMERIC);
}
$vars = [
':pid' => $post->id,
':subj' => 's' === $key ? 1 : 0,
@ -113,7 +117,7 @@ class Index extends Method
];
$query = 'DELETE
FROM ::search_matches
WHERE word_id IN(?ai:ids) AND post_id=?i:pid AND subject_match=?i:subj';
WHERE word_id IN (?ai:ids) AND post_id=?i:pid AND subject_match=?i:subj';
$this->c->DB->exec($query, $vars);
}
@ -132,7 +136,7 @@ class Index extends Method
$query = 'INSERT INTO ::search_matches (post_id, word_id, subject_match)
SELECT ?i:pid, id, ?i:subj
FROM ::search_words
WHERE word IN(?as:words)';
WHERE word IN (?as:words)';
$this->c->DB->exec($query, $vars);
}

View file

@ -19,8 +19,14 @@ class TruncateIndex extends Method
*/
public function truncateIndex(): void
{
if ($this->c->DB->inTransaction()) {
$this->c->DB->commit();
}
$this->c->DB->truncateTable('::search_cache');
$this->c->DB->truncateTable('::search_matches');
$this->c->DB->truncateTable('::search_words');
$this->c->DB->beginTransaction();
}
}

View file

@ -89,14 +89,13 @@ class Subscription extends Model
if (! empty($this->forums)) {
$query = 'INSERT INTO ::forum_subscriptions (user_id, forum_id)
SELECT ?i:uid, ?i:id
FROM ::groups
SELECT tmp.*
FROM (SELECT ?i:uid AS f1, ?i:id AS f2) AS tmp
WHERE NOT EXISTS (
SELECT 1
FROM ::forum_subscriptions
WHERE user_id=?i:uid AND forum_id=?i:id
)
LIMIT 1';
)';
foreach ($this->forums as $id) {
$vars[':id'] = $id;
@ -107,14 +106,13 @@ class Subscription extends Model
if (! empty($this->topics)) {
$query = 'INSERT INTO ::topic_subscriptions (user_id, topic_id)
SELECT ?i:uid, ?i:id
FROM ::groups
SELECT tmp.*
FROM (SELECT ?i:uid AS f1, ?i:id AS f2) AS tmp
WHERE NOT EXISTS (
SELECT 1
FROM ::topic_subscriptions
WHERE user_id=?i:uid AND topic_id=?i:id
)
LIMIT 1';
)';
foreach ($this->topics as $id) {
$vars[':id'] = $id;
@ -143,20 +141,23 @@ class Subscription extends Model
$where[':uid'] = 'user_id=?i:uid';
$vars[':uid'] = \reset($this->users);
} else {
$where[':uid'] = 'user_id IN(?ai:uid)';
$where[':uid'] = 'user_id IN (?ai:uid)';
$vars[':uid'] = $this->users;
}
}
$all = empty($this->forums) && empty($this->topics);
if ($all || ! empty($this->forums)) {
if (
$all
|| ! empty($this->forums)
) {
if (! empty($this->forums)) {
if (1 === \count($this->forums)) {
$where[':id'] = 'forum_id=?i:id';
$vars[':id'] = \reset($this->forums);
} else {
$where[':id'] = 'forum_id IN(?ai:id)';
$where[':id'] = 'forum_id IN (?ai:id)';
$vars[':id'] = $this->forums;
}
}
@ -170,13 +171,16 @@ class Subscription extends Model
unset($where[':id'], $vars[':id']);
if ($all || ! empty($this->topics)) {
if (
$all
|| ! empty($this->topics)
) {
if (! empty($this->topics)) {
if (1 === \count($this->topics)) {
$where[':id'] = 'topic_id=?i:id';
$vars[':id'] = \reset($this->topics);
} else {
$where[':id'] = 'topic_id IN(?ai:id)';
$where[':id'] = 'topic_id IN (?ai:id)';
$vars[':id'] = $this->topics;
}
}

View file

@ -28,6 +28,10 @@ class Access extends Action
}
if (! empty($ids)) {
if (\count($ids) > 1) {
\sort($ids, \SORT_NUMERIC);
}
$vars = [
':ids' => $ids,
':closed' => $open ? 0 : 1,

View file

@ -53,7 +53,7 @@ class CalcStat extends Method
];
$query = 'SELECT p.id, p.poster, p.poster_id, p.posted, p.edited
FROM ::posts AS p
WHERE p.id IN(?ai:ids)';
WHERE p.id IN (?ai:ids)';
$result = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_UNIQUE);

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)');
}
}
@ -106,12 +103,15 @@ class Delete extends Action
FROM ::topics AS t
WHERE t.poster_id IN (?ai:users)';
$tids = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
$topics = $this->manager->loadByIds($tids, false);
$tids = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
foreach ($topics as $topic) {
$parents[$topic->parent->id] = $topic->parent;
}
# $topics = $this->manager->loadByIds($tids, false);
#
# foreach ($topics as $topic) {
# $parents[$topic->parent->id] = $topic->parent;
# }
$this->delete(...($this->manager->loadByIds($tids, false)));
}
$this->c->posts->delete(...$args);
@ -149,6 +149,10 @@ class Delete extends Action
throw new RuntimeException('Bad topic');
}
if (\count($topics) > 1) {
\ksort($topics, \SORT_NUMERIC);
}
$this->c->subscriptions->unsubscribe(...$topics);
$this->c->polls->delete(...$topics);
@ -175,6 +179,10 @@ class Delete extends Action
}
if ($parents) {
if (\count($parents) > 1) {
\ksort($parents, \SORT_NUMERIC);
}
foreach ($parents as $forum) {
$this->c->forums->update($forum->calcStat());
}

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