Compare commits

..

370 Commits

Author SHA1 Message Date
Shish
b859e1fc60
Merge pull request #869 from thoughever/reverse_proxy
X-Real-IP support and Varnish PURGE config options
2022-05-01 10:42:54 +01:00
Shish
3f553501f9
Merge pull request #876 from friends-of-the-core/preserve-posted-date-on-rotate
Preserve posted date on rotate
2022-05-01 10:24:48 +01:00
Shish
0092572ea7
Merge pull request #875 from friends-of-the-core/pre-set-image-posted
Allow pre-setting `$image->posted` values
2022-05-01 10:24:34 +01:00
Shish
895207bf6b
Merge pull request #877 from friends-of-the-core/reduced-video-thumbnail-memory-usage
Scale thumbnails in ffmpeg to avoid excessive memory use on high-resolution videos
2022-05-01 10:19:26 +01:00
Shish
bb99133322
Merge pull request #874 from friends-of-the-core/sort-by-favorites
Allow ordering by favourites in searches
2022-05-01 10:00:18 +01:00
Jessica Stokes
891c69d94d Scale thumbnails in ffmpeg to avoid excessive memory use on UHD videos 2022-04-29 11:33:22 -07:00
Jessica Stokes
5c79e05f08 Inherit post date when rotating images 2022-04-29 11:29:04 -07:00
Jessica Stokes
6bc1ec4f81 Allow setting $image->posted values 2022-04-29 11:21:57 -07:00
Jessica Stokes
dfaf9b5a2f Add order:favorites to autocomplete 2022-04-29 11:14:12 -07:00
Jessica Stokes
4b086a8c70 Allow ordering by favorites in searches 2022-04-29 11:12:00 -07:00
Shish
6a248a0a5c
Merge pull request #871 from shish/dependabot/composer/enshrined/svg-sanitize-0.15.0
Bump enshrined/svg-sanitize from 0.14.1 to 0.15.0
2022-02-17 23:02:03 +00:00
dependabot[bot]
cefad7a786
Bump enshrined/svg-sanitize from 0.14.1 to 0.15.0
Bumps [enshrined/svg-sanitize](https://github.com/darylldoyle/svg-sanitizer) from 0.14.1 to 0.15.0.
- [Release notes](https://github.com/darylldoyle/svg-sanitizer/releases)
- [Commits](https://github.com/darylldoyle/svg-sanitizer/compare/0.14.1...0.15.0)

---
updated-dependencies:
- dependency-name: enshrined/svg-sanitize
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-15 00:44:22 +00:00
Shish
15f4e79198
Merge pull request #870 from HeCorr/docker-fix
Fix apt errors when running in Docker
2022-02-12 12:35:13 +00:00
HeCorr
eead00c601
replace debian:testing-slim image with debian:stable in Dockerfile
fixes some apt errors saying that PHP packages weren't found
2022-02-10 02:25:49 -03:00
thoughever
f15407bc75 X-Real-IP support and Varnish PURGE config options
X-Real-IP for core functionality

Global config define REVERSE_PROXY_X_HEADERS

Config host and port for varnish PURGE

config option to specify PURGE protocol

exception in curl purge now shows error code

ipv6 x-real-ip addresses are now validated properly

X-Forwarded-Proto enabled by define
2022-01-20 22:25:22 +00:00
Shish
3061a9d7d5 avoid warning when HTTP_X_FORWARDED_PROTO isn't set 2022-01-01 10:29:32 +00:00
Shish
e0f1165b6c
Merge pull request #851 from jellykells/jellykells/wiki_return_not_found
Add option to return 404 code for nonexistent wiki pages
2022-01-01 10:27:27 +00:00
jellykells
f83588fdcd always return 404 code for nonexistent wiki pages 2021-12-30 22:06:39 -06:00
jellykells
7b7596167a add support for x-forwarded-proto header 2021-12-26 00:16:07 +00:00
Shish
78349b3ae5 and fix the test 2021-12-21 23:41:04 +00:00
Shish
61068bc0d0 crude wiki history 2021-12-21 23:36:30 +00:00
Shish
a7e775de2b allow checking past revisions of wiki pages 2021-12-21 23:06:24 +00:00
=
aca5e7b9bb Fixing PHP Fixer Format 2021-12-20 10:36:14 +00:00
=
79612405e8 Fixing home page Pools link 2021-12-20 10:36:06 +00:00
flatanon
3b53ddbbc7 Fix auto_tagger issue with incorrect data type, expected int 2021-12-19 00:28:13 +00:00
flatty
9c05622d6e Fix uploader issue with SHM_FORM 2021-12-19 00:27:50 +00:00
Shish
217a36a8c5 upload microhtml 2021-12-14 19:10:21 +00:00
Shish
5f771c0138 add the config file 2021-12-14 18:33:53 +00:00
Shish
3d9e32e919 new php-cs-fixer 2021-12-14 18:32:47 +00:00
Shish
b2ceb36499 better deleting of user's uploads 2021-12-13 01:27:49 +00:00
Shish
674a821783 manual publish 2021-12-13 01:07:55 +00:00
Shish
88dcfb607b dependencies happens from workflow_run now 2021-12-13 01:06:46 +00:00
Shish
4d1ba5f9e7 try splitting test and publish 2021-12-13 01:04:47 +00:00
jellykells
1b7e505f19 add global to display_page function 2021-11-21 08:25:41 -06:00
jellykells
e114079b96 add option to return 404 code for nonexistent wiki pages 2021-11-21 08:10:03 -06:00
Shish
bed0db40d7 mark invalid url as user error 2021-11-16 14:55:37 +00:00
Shish
68a128c0ea don't log user errors into the server error log 2021-11-16 14:52:26 +00:00
Shish
3cb0a6a2c0 Merge remote-tracking branch 'holly/master' 2021-11-16 14:44:13 +00:00
Shish
0f708e7a1b derp 2021-11-16 14:43:54 +00:00
HeCorr
6b6bea6bcc add missing colon to version title on extension page 2021-11-11 21:25:04 +00:00
Shish
23d160cb33 argh 2021-11-10 19:46:12 +00:00
Shish
52fa31df3e more handling 2021-11-10 19:42:01 +00:00
Shish
58db685b29 more error handling 2021-11-10 19:33:51 +00:00
Shish
4c4b26f098 write internal errors to error log 2021-11-06 16:46:05 +00:00
Shish
6377ea19cc don't crash if recaptcha repsonse is empty 2021-11-06 16:17:38 +00:00
Shish
34f75cfb22 un-set autocomplete default to empty string rather than null 2021-11-06 16:12:28 +00:00
Shish
6de6287663 add a test to cover multiple non-existent negative tags 2021-10-06 18:06:23 +01:00
Shish
d932178670 don't crash when the user searches with no positive tags and multiple non-existent negative tags 2021-10-06 17:02:29 +00:00
Shish
2d0c942084 fix a surprise string in autocomplete 2021-10-06 17:02:29 +00:00
Shish
679360dd9c turn off timing for now 2021-10-06 17:02:29 +00:00
Shish
3f0a8399d1 clarify some nullables 2021-09-25 13:40:41 +01:00
Shish
e7808096ff format 2021-09-22 16:02:33 +01:00
Shish
3bb1566df2 Allow db->set_timeout(null) to disable DB timeouts, see #874 2021-09-22 15:42:41 +01:00
jellykells
9e52434480 add missing quote in html returned by handle_pixel theme 2021-09-06 18:26:36 +01:00
Shish
f5b3276e62 add a missing # in the danbooru original theme, fixes #834 2021-08-30 19:39:08 +01:00
MetallicAchu
d79430be1e Edit sort by Categories
Sort by the number of tags (sum) for each category descending
This will allow the most used categories to appear first and not by alphabetical order
2021-07-26 12:26:47 +01:00
Laureano Passafaro
8bd781cc8c CSS and JS files configurable by themes 2021-07-26 12:26:03 +01:00
MetallicAchu
ee0f0be535 Update main.php
Misspelled Categories as Gategories :D
2021-07-26 12:24:42 +01:00
MetallicAchu
6385f67e42 Added Tag Categories link from Tags page
For some reason it wasn't there, so you had to manually type (address)/tags/categories
For a while I had no idea it existed until I dug through the code, maybe that will expose users to that feature
2021-07-26 12:24:42 +01:00
Shish
32a308a07a more frequent health checks for faster health 2021-07-24 19:28:18 +01:00
Shish
1e0c248710 don't log in when creating a new user unless event->login is set 2021-06-11 23:47:59 +01:00
Shish
c4ae68fb47 no transcode for reporting 2021-04-25 10:50:27 +00:00
Shish
7149af7df9 rmbtc 2021-04-25 10:23:24 +00:00
Shish
f5a69e8fa9 only show some admin actions on some pages 2021-04-25 11:20:55 +01:00
Shish
faf35cc884 video references image width / height 2021-03-16 01:49:48 +00:00
Shish
89bf741ed4 bump 2021-03-15 00:59:46 +00:00
Shish
17080645d1 argh 2021-03-15 00:31:53 +00:00
Shish
a8391eb1e5 use our own php-cs-fixer 2021-03-15 00:25:30 +00:00
Shish
8bac027139 don't use php-cs-fixer 3.0 yet, our config file only works with 2.X 2021-03-15 00:04:53 +00:00
Shish
7babe9d2a6 format 2021-03-14 23:56:23 +00:00
Shish
ac981c5eab bumps 2021-03-14 23:56:06 +00:00
Shish
45cf45ed77 bump 2021-03-14 23:55:05 +00:00
Shish
77f7121e26 drop php7.3 support, make use of 7.4 features 2021-03-14 23:43:50 +00:00
Shish
c558ee3bdb format 2021-03-14 15:47:29 +00:00
Shish
6221fbb096 drop flash support 2021-03-14 15:43:35 +00:00
Shish
3e60774e4b nicer scaling 2021-03-14 15:36:33 +00:00
Shish
8a9a2dd96d theme update 2021-03-14 15:32:05 +00:00
Shish
45b1a381b8 add instructions for search by ID 2021-03-14 15:31:28 +00:00
Shish
8d478b9c39 refer to user 2021-02-26 23:56:50 +00:00
Shish
253d75ae82 format 2021-02-26 23:55:00 +00:00
Shish
e88ca1fb05 Nicer upload error messages 2021-02-26 23:54:53 +00:00
Shish
05798f9cad bump svg-sanitize 2021-02-12 21:08:26 +00:00
Shish
e65fcb9975 bumps 2021-02-12 21:06:31 +00:00
Shish
c7d214189e biography extension 2021-02-12 21:03:17 +00:00
Shish
dd94c7eda6 don't purge image on thumbnail regen 2021-02-12 20:07:48 +00:00
Shish
4c3b68b7d5 format 2021-02-12 20:07:02 +00:00
Shish
8c379c023e Log username when deleting users 2021-02-12 20:06:47 +00:00
Laureano Passafaro
f6b6c3d335 Another missing id 2021-02-09 11:03:00 +00:00
Laureano Passafaro
984b85f60c 2 missing block ids on post/view 2021-02-09 11:03:00 +00:00
Shish
02e0f925ac bumps 2021-01-31 17:56:38 +00:00
Shish
9f402b6f9d tests 2021-01-31 17:21:32 +00:00
Shish
03cebf9d68
Merge pull request #808 from LaureeGrd/mistress
Show tag info on wiki pages
2021-01-30 19:03:19 +00:00
Shish
e043f01cfb
Merge branch 'master' into mistress 2021-01-21 21:51:52 +00:00
Shish
875c40ef00
Merge pull request #814 from LaureeGrd/wiki-toggle-revisions
Option to disable wiki revisions
2021-01-20 08:33:35 +00:00
Shish
2025acd482
Merge branch 'master' into wiki-toggle-revisions 2021-01-20 08:10:19 +00:00
Shish
91971b140a
Merge pull request #815 from LaureeGrd/bbcode-doc
Add missing entries in bbcode docs
2021-01-20 08:08:53 +00:00
Shish
0e81f92e0f
Merge pull request #816 from sanmadjack/pull
Various fixes and changes
2021-01-20 08:07:17 +00:00
Matthew Barbour
bd079722c0 Added php-cs-fixer to composer dev requirements 2021-01-18 08:59:07 -06:00
Matthew Barbour
32927aea3d Formatting pass 2021-01-18 08:37:32 -06:00
Matthew Barbour
bb891f3bd0 Updated user page tests 2021-01-16 15:07:17 -06:00
Matthew Barbour
7cea8592ee Fixed issues with next/prev post functionality not working with certin URL setups, as the query was not making it back to the server, or was being lost on redirect 2021-01-16 14:54:11 -06:00
Matthew Barbour
fe7b93d6d3 Added max time remaining debug logging to cron upload 2021-01-16 14:35:59 -06:00
Matthew Barbour
6282881c4a Changed cron uploader to better handle concurrent run attmpts 2021-01-16 14:35:54 -06:00
Matthew Barbour
e7d11f2310 Converted cron_upload to be able to run per-user, using user API keys 2021-01-16 14:35:04 -06:00
Matthew Barbour
e82b9ea811 Added post peek extension 2021-01-16 14:26:19 -06:00
Matthew Barbour
1b469d9919 Added image dimension data attributes 2021-01-16 14:26:19 -06:00
Matthew Barbour
6489b388a0 Added TGA support to transcoder 2021-01-16 14:26:18 -06:00
Matthew Barbour
4f82dce662 Changed image->get_tag_array to sort the array before returning 2021-01-16 14:26:18 -06:00
Matthew Barbour
76d8416324 Changed bulk delete to display the total size of the deleted posts 2021-01-16 14:26:18 -06:00
Matthew Barbour
72268d529d Moved user options to separate page, modularized the settings page code so that it can be re-used to simplify and standardize user settings in the same manner as global settings 2021-01-16 14:26:17 -06:00
Shish
3ad6ce74d9 dedupe admin / tag_tools 2021-01-13 09:36:41 +00:00
Shish
d65bf2a322 don't set logo width 2021-01-11 01:25:15 +00:00
Laureano Passafaro
7068ee9a15 Add missing entries in bbcode docs 2020-12-28 00:12:41 -03:00
Laureano Passafaro
db13624ff3 Option to disable wiki revisions 2020-12-27 23:02:06 -03:00
Shish
89864f7d53 bumps 2020-12-25 02:42:11 +00:00
Shish
6f2febde92
Merge pull request #810 from LaureeGrd/post-tags
Option to display post tags and related tags at the same time
2020-12-22 18:24:06 +00:00
Laureano Passafaro
32cdb95d00 Option to display post tags and related tags at the same time 2020-12-21 15:40:34 -03:00
Shish
4aabb77a4f Make admin part of core, with tag_tools as a separate extension 2020-12-01 11:31:55 +00:00
LaureeGrd
dedee166a9 Remove unnecessary vars 2020-11-30 22:07:18 -03:00
LaureeGrd
a17e2eca15 Added aliases and auto-tags to tag wiki pages
Squashed commit of the following:

commit 1bc42eeb1755e82d6596014acec0361c9141999b
Author: LaureeGrd <laureegrd@gmail.com>
Date:   Fri Sep 18 01:45:04 2020 -0300

    Wiki author update

commit 9c8b923abc5d987f688f23a81b5ba93d62c68571
Author: LaureeGrd <laureegrd@gmail.com>
Date:   Fri Sep 18 00:50:49 2020 -0300

    Rename config wiki_tag_page_formatting to template

commit cf5c8d42d3c411c2413e700f1b51fd5ed6dd56cf
Author: LaureeGrd <laureegrd@gmail.com>
Date:   Fri Sep 18 00:06:38 2020 -0300

    Improved tag wiki pages formatting and configuration.

commit 53b91ff2febdb96fd9c7f4b05f9280859b199bf6
Author: LaureeGrd <laureegrd@gmail.com>
Date:   Mon Sep 14 22:32:46 2020 -0300

    Added aliases and auto-tags to tag wiki pages
2020-11-30 21:53:04 -03:00
Shish
8871591cab check formatting 2020-11-15 13:56:54 +00:00
Shish
df797745e6 format 2020-11-15 13:21:15 +00:00
Shish
3a9fd38cb0 extract autocomplete code for later api-isation 2020-11-15 12:18:23 +00:00
Shish
b03880c11d remove unused old autocomplete code 2020-11-15 12:05:34 +00:00
Shish
070d04503d note 2020-11-01 10:39:06 +00:00
Shish
6789bd5fdd booling 2020-10-29 08:08:42 +00:00
Shish
8a58ede5b3 remove some dead code 2020-10-29 01:28:46 +00:00
Shish
0961ad465f shimmie-info is redundant, just be info 2020-10-29 01:02:53 +00:00
Shish
b4c4de58a2 use get_version instead of get_int for versions 2020-10-29 00:57:58 +00:00
Shish
b9e1ce1b8d use the right link 2020-10-28 20:53:20 +00:00
Shish
3f5930b4cb simplify and add tests for upload (and replace) path 2020-10-28 20:51:34 +00:00
Shish
7cb18568e3 During uploads (including replacements), 'data' should be an array of files, fixes #735 2020-10-28 17:06:25 +00:00
Shish
ecafd4e131 Merge branch 'image2post' 2020-10-27 22:42:47 +00:00
Shish
338bbcdbcb more test passing 2020-10-27 22:19:26 +00:00
Shish
08b9729c72 more 2020-10-27 22:07:32 +00:00
Shish
81fe37de50 transactions 2020-10-27 22:03:56 +00:00
Shish
a4ff796b35 remove dead code 2020-10-27 21:51:34 +00:00
Shish
6c223d16bd use inTransaction() instead of trying to maintain our own state 2020-10-27 21:51:15 +00:00
Shish
e8561f6a04 test pages with args properly 2020-10-27 21:19:19 +00:00
Shish
8bc44f6cb5 remove redundant install step 2020-10-27 02:17:11 +00:00
Shish
c13835d2ef transaction for table creation 2020-10-27 02:08:12 +00:00
Shish
2863ff7508 trace 2020-10-27 02:04:02 +00:00
Shish
b6151ce714 ??? 2020-10-27 01:55:48 +00:00
Shish
088276ce3a cache based on .lock instead of .json 2020-10-27 01:50:56 +00:00
Shish
3ac7ab2cf3 remove old bool support 2020-10-27 01:50:39 +00:00
Shish
754f0b2f39 strpos 2020-10-27 01:41:07 +00:00
Shish
10f563ee37 boolinate locked 2020-10-27 01:05:12 +00:00
Shish
ee7a4f178c boolinate video/audio 2020-10-27 00:58:18 +00:00
Shish
ed735e17be boolinate image 2020-10-27 00:58:18 +00:00
Shish
a9f0d764ae remove redundant cast 2020-10-27 00:11:49 +00:00
Shish
0cf35db00c boolinate approval 2020-10-26 23:53:33 +00:00
Shish
607803c04f Revert "boolinate locked"
This reverts commit 533ea85da9fdeaa8c899a096d0bfdb347c452609.
2020-10-26 23:42:02 +00:00
Shish
ea29e29a06 mostly postgres doesn't need migrating 2020-10-26 23:38:52 +00:00
Shish
73022f06dd php... 2020-10-26 23:32:56 +00:00
Shish
533ea85da9 boolinate locked 2020-10-26 23:30:52 +00:00
Shish
2c8432c643 fix 2020-10-26 23:26:49 +00:00
Shish
0977efa0d9 extra bit for pools 2020-10-26 23:24:25 +00:00
Shish
aceb3e23f3 remove redundant casts 2020-10-26 23:18:14 +00:00
Shish
56f1fac4c5 Revert "different hack for old sqlite"
This reverts commit 634124e17fd036575273eb93f6917ae35e24fcf8.
2020-10-26 22:49:39 +00:00
Shish
634124e17f different hack for old sqlite 2020-10-26 22:43:47 +00:00
Shish
cc06df171a Argh, that doesn't work for OTHER versions of sqlite...
This reverts commit 55e3cb5d63f3611cf2deac714db57c41d1aded98.
2020-10-26 22:37:25 +00:00
Shish
55e3cb5d63 compatibility with out-of-date sqlite: use '(1=0)' instead of 'false' 2020-10-26 22:33:11 +00:00
Shish
e6e9d6db1c booleanise private_image 2020-10-26 21:54:43 +00:00
Shish
06cb261aa6 workaround for versions of sqlite which we don't really support 2020-10-26 21:42:06 +00:00
Shish
56f9891828 Revert "Revert "booleanise trash" - found the problem
This reverts commit e61ef97197c2115644b7c0113f6eaf04de4fe79e.
2020-10-26 21:40:26 +00:00
Shish
e61ef97197 Revert "booleanise trash" - works locally, but not with github actions??
This reverts commit 164637188e8ccfee07ea0522c919d401c6e5832a.
2020-10-26 20:55:27 +00:00
Shish
164637188e booleanise trash 2020-10-26 20:23:55 +00:00
Shish
68cdfc21ee booleanise forum 2020-10-26 20:15:34 +00:00
Shish
a378f7f73b blah 2020-10-26 20:12:58 +00:00
Shish
d3dd2f7738 bump from the start 2020-10-26 19:50:28 +00:00
Shish
6599973b01 also update test 2020-10-26 19:38:51 +00:00
Shish
7820096bd9 skip to latest relationships table 2020-10-26 19:17:11 +00:00
Shish
4ed7a1d529 booleanise pools 2020-10-26 19:12:20 +00:00
Shish
19d85e415d booleanise relationships 2020-10-26 18:55:03 +00:00
Shish
ed64cea8f6 old sqlite does not define true 2020-10-26 18:30:42 +00:00
Shish
3a8817bf57 booleanise tips 2020-10-26 18:22:47 +00:00
Shish
0c8c31b6c9 booleanise blotter 2020-10-26 18:10:34 +00:00
Shish
4a5863b750 argh args 2020-10-26 17:33:40 +00:00
Shish
631cf29424 argh 2020-10-26 17:29:05 +00:00
Shish
08a4a6d41f pm also used char for postgres 2020-10-26 17:28:21 +00:00
Shish
d18ac39e29 composer update 2020-10-26 17:21:37 +00:00
Shish
f91bdfac02 booleanise private_messages 2020-10-26 17:13:41 +00:00
Shish
4d6dc7e98b standardise_boolean function to ease conversion 2020-10-26 17:03:42 +00:00
Shish
7b3555eaa7 fff 2020-10-26 16:56:56 +00:00
Shish
89f5d5524f pg fixes 2020-10-26 16:54:08 +00:00
Shish
006c53f499 conert wiki to use native booleans 2020-10-26 16:52:48 +00:00
Shish
345c64b821 set REQUEST_URI in get-page 2020-10-26 16:26:25 +00:00
Matthew Barbour
ba982e4451 Image to Post - cron upload 2020-10-26 10:53:28 -05:00
Matthew Barbour
b4169821ed Image to Post - transcode video 2020-10-26 10:53:05 -05:00
Matthew Barbour
c18c7347bf Image to Post - danbooru theme 2020-10-26 10:53:05 -05:00
Matthew Barbour
f869eaefcd Image to Post - danbooru2 theme 2020-10-26 10:53:05 -05:00
Matthew Barbour
992d97d278 Image to Post - lite theme 2020-10-26 10:53:04 -05:00
Matthew Barbour
de084d2c55 Image to Post - material theme 2020-10-26 10:53:04 -05:00
Matthew Barbour
15c9cf453b Image to Post - rule34 theme 2020-10-26 10:53:04 -05:00
Matthew Barbour
cef07afcbd Image to Post - bulk add csv 2020-10-26 10:53:04 -05:00
Matthew Barbour
ee30c6d06e Image to Post - artists 2020-10-26 10:53:04 -05:00
Matthew Barbour
d6a2b3840c Image to Post - tag history 2020-10-26 10:53:04 -05:00
Matthew Barbour
2aa4634ef1 Image to Post - tag categories 2020-10-26 10:53:04 -05:00
Matthew Barbour
4e2faa97f9 Image to Post - source history 2020-10-26 10:53:04 -05:00
Matthew Barbour
8d2fe22358 Image to Post - rule34 2020-10-26 10:53:04 -05:00
Matthew Barbour
8910de48c1 Image to Post - relationships 2020-10-26 10:53:04 -05:00
Matthew Barbour
1dcb694f3d Image to Post - not a tag 2020-10-26 10:53:04 -05:00
Matthew Barbour
13582d842f Image to Post - eokm 2020-10-26 10:53:04 -05:00
Matthew Barbour
08d28a2513 Image to Post - bbcode 2020-10-26 10:53:04 -05:00
Matthew Barbour
497df530dd Image to Post - autotagger 2020-10-26 10:53:04 -05:00
Matthew Barbour
cc8f32a65e Image to Post - bulk import export 2020-10-26 10:53:04 -05:00
Matthew Barbour
fb4a1391df Image to Post - pools 2020-10-26 10:53:04 -05:00
Matthew Barbour
3ac5d05a25 Image to Post - notes 2020-10-26 10:53:04 -05:00
Matthew Barbour
a1fc842af6 Image to Post - qr code 2020-10-26 10:53:04 -05:00
Matthew Barbour
fa2a982303 Image to Post - regen thumb 2020-10-26 10:53:04 -05:00
Matthew Barbour
93259db601 Image to Post - report 2020-10-26 10:53:04 -05:00
Matthew Barbour
13ad9d3b6d Image to Post - resize 2020-10-26 10:53:04 -05:00
Matthew Barbour
3fe9774158 Image to Post - rotate 2020-10-26 10:53:04 -05:00
Matthew Barbour
badfaa6400 Image to Post - rss 2020-10-26 10:53:03 -05:00
Matthew Barbour
1520e6cfe3 Image to Post - tag list 2020-10-26 10:53:03 -05:00
Matthew Barbour
df4f2821b2 Image to Post - tips 2020-10-26 10:53:03 -05:00
Matthew Barbour
eecb737501 Image to Post - transcode 2020-10-26 10:53:03 -05:00
Matthew Barbour
e6e4e4c962 Image to Post - trash 2020-10-26 10:53:03 -05:00
Matthew Barbour
e4ab2e5d39 Image to Post - alias editor 2020-10-26 10:53:03 -05:00
Matthew Barbour
3aa1926f72 Image to Post - upload 2020-10-26 10:53:03 -05:00
Matthew Barbour
886d7cfd99 Image to Post - tag edit 2020-10-26 10:53:03 -05:00
Matthew Barbour
21f48456a2 Image to Post - res limit 2020-10-26 10:53:03 -05:00
Matthew Barbour
61250bd3a3 Image to Post - report 2020-10-26 10:53:03 -05:00
Matthew Barbour
456176ac11 Image to Post - random list 2020-10-26 10:53:03 -05:00
Matthew Barbour
f497094482 Image to Post - rating 2020-10-26 10:53:03 -05:00
Matthew Barbour
5371669e54 Image to Post - private 2020-10-26 10:53:03 -05:00
Matthew Barbour
9d7b0db3d3 Image to Post - pm triggers 2020-10-26 10:53:03 -05:00
Matthew Barbour
0db32f2ccc Image to Post - random 2020-10-26 10:53:03 -05:00
Matthew Barbour
bfe95a09e7 Image to Post - numeric score 2020-10-26 10:53:03 -05:00
Matthew Barbour
64649133e2 Image to Post - link 2020-10-26 10:53:03 -05:00
Matthew Barbour
708acd461c Image to Post - hash bans 2020-10-26 10:53:03 -05:00
Matthew Barbour
83f8b61c4e Image to Post - approval 2020-10-26 10:53:03 -05:00
Matthew Barbour
d7c16176d3 Image to Post - mime 2020-10-26 10:53:03 -05:00
Matthew Barbour
dadef22d90 Image to Post - User 2020-10-26 10:53:03 -05:00
Matthew Barbour
a8c39085cb Image to Post - view counter 2020-10-26 10:53:02 -05:00
Matthew Barbour
4f48be8e15 Image to Post - Home 2020-10-26 10:53:02 -05:00
Matthew Barbour
439c3a8320 Image to Post - Index 2020-10-26 10:53:02 -05:00
Matthew Barbour
2b6e28683f Image to Post - Comments 2020-10-26 10:53:02 -05:00
Matthew Barbour
773be88994 Image to Post - bulk actions 2020-10-26 10:53:02 -05:00
Matthew Barbour
77ea8617d7 Image to Post - Admin extension 2020-10-26 10:53:02 -05:00
Matthew Barbour
840e661a42 Image to Post - featured 2020-10-26 10:53:02 -05:00
Matthew Barbour
82e88969d6 Image to Post - favorites 2020-10-26 10:53:02 -05:00
Matthew Barbour
27b2988bd8 Image to Post - Image 2020-10-26 10:53:02 -05:00
Matthew Barbour
52bce9ffd0 Image to Post - media 2020-10-26 10:53:02 -05:00
Matthew Barbour
7e0349164a Image to Post - log_db 2020-10-26 10:53:02 -05:00
Matthew Barbour
769fbecbb0 Image to Post - View 2020-10-26 10:53:02 -05:00
Matthew Barbour
e7b109a7f2 Image to Post - Some core files 2020-10-26 10:53:02 -05:00
Matthew Barbour
dbb90c7a99 Updated copyright notice to media-neutral language 2020-10-26 10:53:01 -05:00
Shish
360a46e09b
Merge pull request #785 from sanmadjack/pull
Various fixes and changes
2020-10-26 15:30:45 +00:00
Matthew Barbour
df93e2e5d5 Added support for transcoding from the PPM image format 2020-10-26 08:58:47 -05:00
Matthew Barbour
cb81e46f9e Changing images to posts 2020-10-26 08:58:36 -05:00
Matthew Barbour
8d34fc360c Added length search to handle #777 2020-10-26 08:58:17 -05:00
Matthew Barbour
c05fb2106c Changed video background color to black 2020-10-26 07:28:15 -05:00
Matthew Barbour
91ac271a2b Corrected video aspect ratio issue
Handles #781
2020-10-26 07:28:10 -05:00
Matthew Barbour
45d38d8833 Added more specific upload error message that includes detected mimetype 2020-10-26 07:27:16 -05:00
Shish
ac2652e729 Revert "test removing scoresql_value_prepare now that bools are handled properly upstream"
This reverts commit 3a0f172a8c4b27126a0828261eea6d4954886804.
2020-10-26 01:33:46 +00:00
Shish
3a0f172a8c test removing scoresql_value_prepare now that bools are handled properly upstream 2020-10-26 00:33:01 +00:00
Shish
8ad783ff4c be more snake_case 2020-10-26 00:27:06 +00:00
Shish
5816aa3084 execute consistently 2020-10-25 21:34:52 +00:00
Shish
7cb36da4c6 line up comments 2020-10-25 21:25:38 +00:00
Shish
0dd7f62b54 allow use of polyfills in installer 2020-10-25 19:40:24 +00:00
Shish
19a6b39c70 make use of str_starts_with / str_ends_with / str_contains 2020-10-25 19:31:58 +00:00
Shish
c783ff0e8d polyfills for php8's str_starts_with and str_ends_with 2020-10-25 19:15:13 +00:00
Shish
4ac9ab2ad6 round up number of comment pages instead of rounding down, fixes #755 2020-10-25 18:06:36 +00:00
Shish
91d04bad5d z-index 2020-10-25 18:03:47 +00:00
Shish
723f316512 avoid repeatedly fetching the same user when viewing PM list 2020-10-25 17:05:36 +00:00
Shish
a617ed4331 newlines in tooltips 2020-10-25 13:30:00 +00:00
Shish
c8bd5e8d49 clarity for not-emptiness 2020-10-25 13:09:51 +00:00
Shish
0dca09c230 make order querylets a first class citizen instead of a hack 2020-10-25 12:55:36 +00:00
Shish
19d5cfe8b9 make search term parse ID a standard thing 2020-10-25 12:55:36 +00:00
Shish
f88445e639 measure client-side page-speed stats 2020-10-25 10:48:46 +00:00
Shish
03dda8144c pull microcrud update to fix r__size issue 2020-10-25 10:44:30 +00:00
Shish
d19f9cb287 Rephrase ratio search so that it works across databases, fixes #759 2020-10-25 00:57:21 +01:00
Shish
6d074cfef8 defer notes js loading, fixes #737 2020-10-24 23:46:32 +01:00
Shish
0e1f6d0ef0 add tests for parent/child search, to try and trigger #743 2020-10-24 23:32:08 +01:00
Shish
7f37607d9d all the parallel 2020-10-24 23:16:44 +01:00
Shish
ec8734cf45 You are using the deprecated option "--no-suggest". It has no effect and will break in Composer 3. 2020-10-24 23:16:16 +01:00
Shish
44636c4de3 only publish master pushes 2020-10-24 23:00:10 +01:00
Shish
548bc7aa64 cache vendor 2020-10-24 22:55:04 +01:00
Shish
b45bc1d61c cleanup image view counter 2020-10-24 22:33:29 +01:00
MetallicAchu
b8c6736327 Added a list that sorts by total views
Most of the code is stolen from numeric_score and ported in order to be usable here
2020-10-24 22:16:31 +01:00
Shish
c11f0dafd0 format 2020-10-24 22:16:18 +01:00
Shish
583cf6751a test php8 build 2020-10-24 21:56:33 +01:00
Shish
9ec8c2f807 update 2020-10-24 20:44:05 +01:00
Shish
574ec0b4c0 more semver 2020-10-24 20:02:55 +01:00
Shish
e208f9d297 phpunit 9 2020-10-24 19:27:48 +01:00
Shish
58a0f060ce avoid numeric columns 2020-10-24 19:18:40 +01:00
Shish
69112fdee3 phpunit9 2020-10-24 18:55:07 +01:00
Shish
4bf2cb8ee7
Merge pull request #779 from shish/php7.4
Have docker use modern php
2020-10-24 15:03:22 +01:00
Shish
ad88679e61 php7.4 in docker 2020-10-24 14:22:46 +01:00
Shish
78463709cd gd is also required 2020-10-24 14:05:54 +01:00
Shish
7308514e53 Run tests with php 7.4 2020-10-24 14:05:54 +01:00
Shish
8ff70134ae nits 2020-10-24 13:46:57 +01:00
Shish
9b878d98d6 replace xml_tag with HTMLElement 2020-10-24 13:46:57 +01:00
Shish
c29fe0583f increase database timeout for admin ops 2020-10-15 23:50:09 +00:00
Shish
a51cab58aa make sure extension is not-null for transloads 2020-10-14 16:07:12 +00:00
Shish
ce26a87aa6 bump 2020-10-14 15:59:49 +00:00
Shish
3c10d1cfee
Merge pull request #757 from sanmadjack/pull
Adjusted mime type update statement to include NULLs. Changed get_mim…
2020-10-13 01:57:24 +01:00
Shish
b8c7714ac9 bump ffsphp dependency 2020-10-09 16:45:12 +01:00
Shish
0899f552f8 composer update 2020-10-09 13:51:44 +01:00
Shish
7b3ab71b1e standard post linking - avoid the word "Image" 2020-10-09 13:51:24 +01:00
Shish
d00569431b arrow format to link images in logs 2020-10-09 13:47:48 +01:00
Matthew Barbour
bf2a34c106 Adjusted mime type update statement to include NULLs. Changed get_mime() so that it can return a null in the event of the mime type not being properly set. 2020-10-08 17:41:24 -05:00
Shish
547c6d98da
Merge pull request #756 from sanmadjack/pull
Pull
2020-10-08 23:33:09 +01:00
Matthew Barbour
45511cbcda Fixed issue with detecting animated GIFs 2020-10-08 17:24:32 -05:00
Matthew Barbour
49a3277cca Fixed imagemagick thumbnailing issue with metadata-rotated images 2020-10-08 17:24:29 -05:00
Matthew Barbour
59bb8a31a2 Relaxed filename search to allow for non alpha-numeric filenames 2020-10-08 17:24:25 -05:00
Matthew Barbour
6bc8b791d2 Added option to go to next image when an image is deleted 2020-10-08 17:24:21 -05:00
Matthew Barbour
79a2fe23b3 Added support for specifying conflicting extensions 2020-10-08 17:24:14 -05:00
Matthew Barbour
0fc0b8e723 Added get_pairs_iterable to database object 2020-10-08 17:24:09 -05:00
Shish
81af2e99f8 avoid redundant updates in db upgrade 2020-10-03 23:06:09 +00:00
Shish
33d5693c5e use notify 2020-10-03 12:54:38 +00:00
Shish
1ea9ceff85 notify after config updates 2020-10-03 12:54:38 +00:00
Shish
e696357c06 database notification API 2020-10-03 12:54:38 +00:00
Shish
2ac695c135 bump some dependencies 2020-09-20 17:53:26 +01:00
Shish
3e017818dc more explicit versions 2020-09-20 12:40:51 +01:00
Shish
9be859a01e Link to forum from navigation, not user links, see #753 2020-09-19 00:20:09 +01:00
Shish
1159ba2fa1 format 2020-09-19 00:18:51 +01:00
Shish
6284f3bcb8 Add a link to the forum, see #753 2020-09-19 00:18:45 +01:00
Shish
cae24210cc s/dorum/forum/ - fixes #753 - thanks @psychosoundwave :D 2020-09-18 23:20:42 +01:00
Shish
a93c66515b
Merge pull request #749 from sanmadjack/video_transcode
Video transcoding
2020-09-16 13:46:18 +01:00
Shish
4131bcbd64
Merge pull request #748 from sanmadjack/image_info
Added image info option to admin, changed the info template parser to…
2020-09-16 11:21:31 +01:00
Shish
111c4e3fb5
Merge pull request #747 from sanmadjack/ffmpeg_thumb_temp_file
Added handling to ensure that video thumbnails don't leave temp files…
2020-09-16 11:20:51 +01:00
Shish
5d86314fc2
Merge pull request #746 from sanmadjack/resize_types
Added "Fit Blur Tall, Fill Wide" resize type
2020-09-16 11:20:28 +01:00
Shish
d320efea3d
Merge pull request #745 from sanmadjack/image_count_speed_hax
Added SPEED_HAX criteria to image count shortcuts.
2020-09-16 11:19:35 +01:00
Shish
16652399a2
Merge pull request #744 from sanmadjack/lower_filename_search
Added lower to filename search
2020-09-16 11:19:13 +01:00
Matthew Barbour
ecd860ec6d Video transcoding extension, currently just supports changing the video container 2020-08-28 09:52:01 -05:00
Matthew Barbour
65aca09203 Added understanding of video codecs, primarily to allow us to determine whether a file is a webm or not, but also to support my forthcoming video transcoding extension 2020-08-28 09:51:55 -05:00
Matthew Barbour
4c5ef85906 Added image info option to admin, changed the info template parser to not use the url-escaped option 2020-08-28 09:50:36 -05:00
Matthew Barbour
2ddfbe69a6 Added handling to ensure that video thumbnails don't leave temp files behind 2020-08-28 09:50:14 -05:00
Matthew Barbour
2dedeb06e0 Added "Fit Blur Tall, Fill Wide" resize type 2020-08-28 09:49:24 -05:00
Matthew Barbour
ab008e351d Added SPEED_HAX criteria to image count shortcuts. 2020-08-28 09:48:10 -05:00
Matthew Barbour
b8b33eecfe Added lower to filename search 2020-08-28 09:45:51 -05:00
Shish
988bc831b2 no headers 2020-08-26 19:43:11 +00:00
Shish
d5993c02f3 eokm filter 2020-08-23 15:43:16 +01:00
Shish
cb6c9ba987 ffs, php 2020-08-01 21:01:47 +00:00
Shish
49e4c3f9c7 info as part of view ext 2020-07-31 15:50:57 +01:00
Shish
804fc038bf extra info 2020-07-31 15:37:33 +01:00
Shish
446e7e0841 add not-a-tag tests 2020-07-31 15:27:38 +01:00
Shish
b1f37f1ccb another escape 2020-07-31 14:55:30 +01:00
Shish
aeb9829c9a if an admin tries to use a banned tag (eg during a mass-revert), ignore it 2020-07-29 14:03:28 +01:00
Shish
8c713af952 remove custom DB for untags now that we use microcrud 2020-07-23 11:17:23 +01:00
Shish
e0519bb125 r34 theme-specific stuff to theme/script.js 2020-07-23 11:13:33 +01:00
Shish
e6ec2340af fixes for tabular settings 2020-07-23 11:13:33 +01:00
Shish
cbe1f792f9 format 2020-07-07 16:07:23 +00:00
Shish
de26b7f373 don't die if database is missing a MIME type 2020-07-07 16:07:19 +00:00
Shish
153aa481da re-add support for $ext in link template 2020-07-07 16:02:29 +00:00
Shish
065f9da2d6 theme update 2020-07-07 09:41:31 +00:00
Shish
0362d07cd5 less jquery 2020-07-06 17:51:11 +01:00
Shish
05a1b63061 php 7.4 is stable now 2020-07-06 17:49:52 +01:00
Shish
926cdc31a8 nicer error for unsupported version 2020-07-06 17:46:20 +01:00
Shish
3297e71636 Add image/vnd.microsoft.icon because OSX uses that instead of image/x-icon 2020-06-25 14:54:05 +01:00
Matthew Barbour
35626d3989 Added options and onImageDownloading event handlers to allow resizing and converting images via get arguments 2020-06-25 00:43:13 +01:00
Matthew Barbour
a3a3aba040 Added ImageDownloadingEvent that allows extensions to stop a user from viewing an image or perform other operations on the image before the image is downloaded to the user 2020-06-25 00:43:11 +01:00
Matthew Barbour
688fb8b04b Added ability to upload multiple files through a single upload input 2020-06-25 00:42:43 +01:00
Matthew Barbour
7f68ef1cfd Added option to transcode extension and thumbnailing to choose the color to use to fill in the background when converting an image with an alpha channel to a format without 2020-06-25 00:42:43 +01:00
Matthew Barbour
984c9702ec The great MIMEing 2020-06-25 00:42:34 +01:00
Matthew Barbour
8dd5ad16f3 Bulk import/export adjustment to prevent zip uploader overlap so the test won't bomb 2020-06-24 23:55:54 +01:00
Shish
d14c2ce8ea test fixes 2020-06-24 17:32:06 +01:00
Matthew Barbour
95ef714447 Changed metadata truncation to reflect new filename column length 2020-06-24 17:21:57 +01:00
Shish
b893da927f format 2020-06-24 17:02:24 +01:00
Matthew Barbour
bd9808081e Added false is_content flags to several blocks so that the 404 extension can work properly 2020-06-24 17:01:09 +01:00
Matthew Barbour
9e1aabe17f Added handling for empty archives 2020-06-24 16:59:35 +01:00
Matthew Barbour
7a009541ce Added more explicit failure handling to list_files 2020-06-24 16:59:21 +01:00
Matthew Barbour
e18fe295b4 Added default media engine to for thumbnails
Fixes #729
2020-06-24 16:58:33 +01:00
Matthew Barbour
e61bbbe03d Added object-fit style to video handler poster to allow better scaling when using exact-size-only thumbs 2020-06-24 16:58:20 +01:00
Matthew Barbour
846d978a52 Added mute option to video handler 2020-06-24 16:58:05 +01:00
Matthew Barbour
9b5d963aa3 Added ImageDownloadingEvent that allows extensions to stop a user from viewing an image or perform other operations on the image before the image is downloaded to the user 2020-06-24 16:55:58 +01:00
Shish
40b80bca93 add fileinfo to composer.json 2020-06-24 16:50:00 +01:00
Matthew Barbour
fc3c6625bd Changed references to /tmp to sys_get_temp_dir 2020-06-24 16:47:22 +01:00
503 changed files with 13496 additions and 6429 deletions

24
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Publish
on:
workflow_run:
workflows: Tests
branches: master
types: completed
workflow_dispatch:
jobs:
publish:
name: Publish
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@master
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
with:
name: shish2k/shimmie2
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
cache: ${{ github.event_name != 'schedule' }}
buildoptions: "--build-arg RUN_TESTS=false"

View File

@ -1,4 +1,4 @@
name: Test & Publish
name: Tests
on:
push:
@ -7,13 +7,40 @@ on:
- cron: '0 2 * * 0' # Weekly on Sundays at 02:00
jobs:
format:
name: Format
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set Up Cache
uses: actions/cache@v2
with:
path: |
vendor
key: php-cs-fixer-${{ hashFiles('composer.lock') }}
- name: Validate composer.json and composer.lock
run: composer validate
- name: Install PHP dependencies
run: composer update && composer install --prefer-dist --no-progress
- name: Set up PHP
uses: shivammathur/setup-php@master
with:
php-version: 7.4
- name: Check format
run: ./vendor/bin/php-cs-fixer fix --dry-run
test:
name: PHP ${{ matrix.php }} / DB ${{ matrix.database }}
strategy:
max-parallel: 3
fail-fast: false
matrix:
php: ['7.3']
php: ['7.4', '8.0']
database: ['pgsql', 'mysql', 'sqlite']
runs-on: ubuntu-latest
@ -21,6 +48,13 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Set Up Cache
uses: actions/cache@v2
with:
path: |
vendor
key: vendor-${{ matrix.php }}-${{ hashFiles('composer.lock') }}
- name: Set up PHP
uses: shivammathur/setup-php@master
with:
@ -56,10 +90,7 @@ jobs:
run: composer validate
- name: Install PHP dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Install shimmie
run: php index.php
run: composer update && composer install --prefer-dist --no-progress
- name: Run test suite
run: |
@ -75,21 +106,7 @@ jobs:
vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover
- name: Upload coverage
if: matrix.php == '7.4'
run: |
wget https://scrutinizer-ci.com/ocular.phar
php ocular.phar code-coverage:upload --format=php-clover data/coverage.clover
publish:
name: Publish
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push'
steps:
- uses: actions/checkout@master
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
with:
name: shish2k/shimmie2
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
cache: ${{ github.event_name != 'schedule' }}
buildoptions: "--build-arg RUN_TESTS=false"

19
.php-cs-fixer.dist.php Normal file
View File

@ -0,0 +1,19 @@
<?php
$finder = PhpCsFixer\Finder::create()
->exclude('ext/amazon_s3/lib')
->exclude('vendor')
->exclude('data')
->in(__DIR__)
;
$config = new PhpCsFixer\Config();
return $config->setRules([
'@PSR12' => true,
//'strict_param' => true,
'array_syntax' => ['syntax' => 'short'],
])
->setFinder($finder)
;
?>

View File

@ -1,19 +0,0 @@
<?php
$finder = PhpCsFixer\Finder::create()
->exclude('ext/amazon_s3/lib')
->exclude('vendor')
->exclude('data')
->in(__DIR__)
;
return PhpCsFixer\Config::create()
->setRules([
'@PSR2' => true,
//'strict_param' => true,
'array_syntax' => ['syntax' => 'short'],
])
->setFinder($finder)
;
?>

View File

@ -1,7 +1,7 @@
# "Build" shimmie (composer install - done in its own stage so that we don't
# need to include all the composer fluff in the final image)
FROM debian:stable-slim AS app
RUN apt update && apt install -y composer php7.3-gd php7.3-dom php7.3-sqlite3 php-xdebug imagemagick
FROM debian:stable AS app
RUN apt update && apt install -y composer php7.4-gd php7.4-dom php7.4-sqlite3 php-xdebug imagemagick
COPY composer.json composer.lock /app/
WORKDIR /app
RUN composer install --no-dev
@ -10,8 +10,8 @@ COPY . /app/
# Tests in their own image. Really we should inherit from app and then
# `composer install` phpunit on top of that; but for some reason
# `composer install --no-dev && composer install` doesn't install dev
FROM debian:stable-slim AS tests
RUN apt update && apt install -y composer php7.3-gd php7.3-dom php7.3-sqlite3 php-xdebug imagemagick
FROM debian:stable AS tests
RUN apt update && apt install -y composer php7.4-gd php7.4-dom php7.4-sqlite3 php-xdebug imagemagick
COPY composer.json composer.lock /app/
WORKDIR /app
RUN composer install
@ -25,7 +25,7 @@ RUN [ $RUN_TESTS = false ] || (\
echo '=== Cleaning ===' && rm -rf data)
# Build su-exec so that our final image can be nicer
FROM debian:stable-slim AS suexec
FROM debian:stable AS suexec
RUN apt-get update && apt-get install -y --no-install-recommends gcc libc-dev curl
RUN curl -k -o /usr/local/bin/su-exec.c https://raw.githubusercontent.com/ncopa/su-exec/master/su-exec.c; \
gcc -Wall /usr/local/bin/su-exec.c -o/usr/local/bin/su-exec; \
@ -33,13 +33,13 @@ RUN curl -k -o /usr/local/bin/su-exec.c https://raw.githubusercontent.com/ncopa
chmod 0755 /usr/local/bin/su-exec;
# Actually run shimmie
FROM debian:stable-slim
FROM debian:stable
EXPOSE 8000
HEALTHCHECK --interval=5m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1
HEALTHCHECK --interval=1m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1
ENV UID=1000 \
GID=1000
RUN apt update && apt install -y curl \
php7.3-cli php7.3-gd php7.3-pgsql php7.3-mysql php7.3-sqlite3 php7.3-zip php7.3-dom php7.3-mbstring \
php7.4-cli php7.4-gd php7.4-pgsql php7.4-mysql php7.4-sqlite3 php7.4-zip php7.4-dom php7.4-mbstring \
imagemagick zip unzip && \
rm -rf /var/lib/apt/lists/*
COPY --from=app /app /app

View File

@ -5,7 +5,17 @@
"license" : "GPL-2.0-or-later",
"minimum-stability" : "dev",
"config": {
"platform": {
"php": "7.4.0"
}
},
"repositories" : [
{
"type": "composer",
"url": "https://asset-packagist.org"
},
{
"type" : "package",
"package" : {
@ -21,30 +31,30 @@
],
"require" : {
"php" : "^7.3",
"php" : "^7.4 | ^8.0",
"ext-pdo": "*",
"ext-json": "*",
"ext-fileinfo": "*",
"flexihash/flexihash" : "^2.0.0",
"ifixit/php-akismet" : "1.*",
"google/recaptcha" : "~1.1",
"dapphp/securimage" : "3.6.*",
"shish/eventtracer-php" : "^2.0.0",
"shish/ffsphp" : "^1.0.0",
"shish/microcrud" : "^2.0.0",
"shish/microhtml" : "^2.0.0",
"enshrined/svg-sanitize" : "0.13.*",
"flexihash/flexihash" : "^2.0",
"ifixit/php-akismet" : "^1.0",
"google/recaptcha" : "^1.1",
"dapphp/securimage" : "^3.6",
"shish/eventtracer-php" : "^2.0",
"shish/ffsphp" : "^1.0",
"shish/microcrud" : "^2.0",
"shish/microhtml" : "^2.0",
"enshrined/svg-sanitize" : "^0.15",
"bower-asset/jquery" : "1.12.*",
"bower-asset/jquery-timeago" : "1.5.*",
"bower-asset/mediaelement" : "2.21.*",
"bower-asset/js-cookie" : "2.1.*"
"bower-asset/jquery" : "^1.12",
"bower-asset/jquery-timeago" : "^1.5",
"bower-asset/js-cookie" : "^2.1"
},
"require-dev" : {
},
"phpunit/phpunit" : "^9.0",
"friendsofphp/php-cs-fixer" : "^3.4"
},
"suggest": {
"ext-memcache": "memcache caching",
"ext-memcached": "memcached caching",
@ -58,10 +68,5 @@
"ext-zlib": "anti-spam",
"ext-xml": "some extensions",
"ext-gd": "GD-based thumbnailing"
},"replace": {
"bower-asset/jquery": ">=1.11.0",
"bower-asset/inputmask": ">=3.2.0",
"bower-asset/punycode": ">=1.3.0",
"bower-asset/yii2-pjax": ">=2.0.0"
}
}
}

4312
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,15 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
require_once "core/event.php";
abstract class PageMode
{
const REDIRECT = 'redirect';
const DATA = 'data';
const PAGE = 'page';
const FILE = 'file';
const MANUAL = 'manual';
public const REDIRECT = 'redirect';
public const DATA = 'data';
public const PAGE = 'page';
public const FILE = 'file';
public const MANUAL = 'manual';
}
/**
@ -20,10 +22,8 @@ abstract class PageMode
*/
class BasePage
{
/** @var string */
public $mode = PageMode::PAGE;
/** @var string */
private $type = "text/html; charset=utf-8";
public string $mode = PageMode::PAGE;
private string $mime;
/**
* Set what this page should do; "page", "data", or "redirect".
@ -36,13 +36,14 @@ class BasePage
/**
* Set the page's MIME type.
*/
public function set_type(string $type): void
public function set_mime(string $mime): void
{
$this->type = $type;
$this->mime = $mime;
}
public function __construct()
{
$this->mime = MimeType::add_parameters(MimeType::HTML, MimeType::CHARSET_UTF8);
if (@$_GET["flash"]) {
$this->flash[] = $_GET['flash'];
unset($_GET["flash"]);
@ -51,19 +52,11 @@ class BasePage
// ==============================================
/** @var string; public only for unit test */
public $data = "";
/** @var string */
private $file = null;
/** @var bool */
private $file_delete = false;
/** @var string */
private $filename = null;
private $disposition = null;
public string $data = ""; // public only for unit test
private ?string $file = null;
private bool $file_delete = false;
private ?string $filename = null;
private ?string $disposition = null;
/**
* Set the raw data to be sent.
@ -90,8 +83,7 @@ class BasePage
// ==============================================
/** @var string */
public $redirect = "";
public string $redirect = "";
/**
* Set the URL to redirect to (remember to use make_link() if linking
@ -104,32 +96,25 @@ class BasePage
// ==============================================
/** @var int */
public $code = 200;
/** @var string */
public $title = "";
/** @var string */
public $heading = "";
/** @var string */
public $subheading = "";
public int $code = 200;
public string $title = "";
public string $heading = "";
public string $subheading = "";
/** @var string[] */
public $html_headers = [];
public array $html_headers = [];
/** @var string[] */
public $http_headers = [];
public array $http_headers = [];
/** @var string[][] */
public $cookies = [];
public array $cookies = [];
/** @var Block[] */
public $blocks = [];
public array $blocks = [];
/** @var string[] */
public $flash = [];
public array $flash = [];
/**
* Set the HTTP status code
@ -243,7 +228,7 @@ class BasePage
{
if (!headers_sent()) {
header("HTTP/1.0 {$this->code} Shimmie");
header("Content-type: " . $this->type);
header("Content-type: " . $this->mime);
header("X-Powered-By: Shimmie-" . VERSION);
foreach ($this->http_headers as $head) {
@ -297,7 +282,7 @@ class BasePage
if (isset($_SERVER['HTTP_RANGE'])) {
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (strpos($range, ',') !== false) {
if (str_contains($range, ',')) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
break;
@ -334,7 +319,7 @@ class BasePage
break;
case PageMode::REDIRECT:
if ($this->flash) {
$this->redirect .= (strpos($this->redirect, "?") === false) ? "?" : "&";
$this->redirect .= str_contains($this->redirect, "?") ? "&" : "?";
$this->redirect .= "flash=" . url_escape(implode("\n", $this->flash));
}
header('Location: ' . $this->redirect);
@ -354,8 +339,6 @@ class BasePage
* Why do this? Two reasons:
* 1. Reduces the number of files the user's browser needs to download.
* 2. Allows these cached files to be compressed/minified by the admin.
*
* TODO: This should really be configurable somehow...
*/
public function add_auto_html_headers(): void
{
@ -380,7 +363,7 @@ class BasePage
$css_latest = $config_latest;
$css_files = array_merge(
zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/style.css"),
zglob("themes/$theme_name/style.css")
zglob("themes/$theme_name/{" . implode(",", $this->get_theme_stylesheets()) . "}")
);
foreach ($css_files as $css) {
$css_latest = max($css_latest, filemtime($css));
@ -404,12 +387,13 @@ class BasePage
$js_latest = $config_latest;
$js_files = array_merge(
[
"vendor/bower-asset/jquery/dist/jquery.min.js",
"vendor/bower-asset/jquery-timeago/jquery.timeago.js",
"vendor/bower-asset/js-cookie/src/js.cookie.js",
"ext/static_files/modernizr-3.3.1.custom.js",
],
zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/script.js"),
zglob("themes/$theme_name/script.js")
zglob("themes/$theme_name/{" . implode(",", $this->get_theme_scripts()) . "}")
);
foreach ($js_files as $js) {
$js_latest = max($js_latest, filemtime($js));
@ -426,7 +410,25 @@ class BasePage
$this->add_html_header("<script defer src='$data_href/$js_cache_file' type='text/javascript'></script>", 44);
}
protected function get_nav_links()
/**
* @return array A list of stylesheets relative to the theme root.
*/
protected function get_theme_stylesheets(): array
{
return ["style.css"];
}
/**
* @return array A list of script files relative to the theme root.
*/
protected function get_theme_scripts(): array
{
return ["script.js"];
}
protected function get_nav_links(): array
{
$pnbe = send_event(new PageNavBuildingEvent());
@ -558,7 +560,7 @@ EOD;
$contact = empty($contact_link) ? "" : "<br><a href='$contact_link'>Contact</a>";
return "
Images &copy; their respective owners,
Media &copy; their respective owners,
<a href=\"https://code.shishnet.org/shimmie2/\">Shimmie</a> &copy;
<a href=\"https://www.shishnet.org/\">Shish</a> &amp;
<a href=\"https://github.com/shish/shimmie2/graphs/contributors\">The Team</a>
@ -572,7 +574,7 @@ EOD;
class PageNavBuildingEvent extends Event
{
public $links = [];
public array $links = [];
public function add_nav_link(string $name, Link $link, string $desc, ?bool $active = null, int $order = 50)
{
@ -582,9 +584,9 @@ class PageNavBuildingEvent extends Event
class PageSubNavBuildingEvent extends Event
{
public $parent;
public string $parent;
public $links = [];
public array $links = [];
public function __construct(string $parent)
{
@ -600,11 +602,11 @@ class PageSubNavBuildingEvent extends Event
class NavLink
{
public $name;
public $link;
public $description;
public $order;
public $active = false;
public string $name;
public Link $link;
public string $description;
public int $order;
public bool $active = false;
public function __construct(String $name, Link $link, String $description, ?bool $active = null, int $order = 50)
{
@ -661,7 +663,7 @@ class NavLink
}
}
function sort_nav_links(NavLink $a, NavLink $b)
function sort_nav_links(NavLink $a, NavLink $b): int
{
return $a->order - $b->order;
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/**
* Class BaseThemelet
@ -7,7 +9,6 @@
*/
class BaseThemelet
{
/**
* Generic error message display
*/
@ -53,8 +54,9 @@ class BaseThemelet
$h_tip = html_escape($image->get_tooltip());
$h_tags = html_escape(strtolower($image->get_tag_list()));
$extArr = array_flip([EXTENSION_FLASH, EXTENSION_SVG, EXTENSION_MP3]); //List of thumbless filetypes
if (!isset($extArr[$image->ext])) {
// TODO: Set up a function for fetching what kind of files are currently thumbnailable
$mimeArr = array_flip([MimeType::MP3]); //List of thumbless filetypes
if (!isset($mimeArr[$image->get_mime()])) {
$tsize = get_thumbnail_size($image->width, $image->height);
} else {
//Use max thumbnail size if using thumbless filetype
@ -71,7 +73,7 @@ class BaseThemelet
}
}
return "<a href='$h_view_link' class='thumb shm-thumb shm-thumb-link {$custom_classes}' data-tags='$h_tags' data-post-id='$i_id'>".
return "<a href='$h_view_link' class='thumb shm-thumb shm-thumb-link {$custom_classes}' data-tags='$h_tags' data-height='$image->height' data-width='$image->width' data-mime='{$image->get_mime()}' data-post-id='$i_id'>".
"<img id='thumb_$i_id' title='$h_tip' alt='$h_tip' height='{$tsize[1]}' width='{$tsize[0]}' src='$h_thumb_link'>".
"</a>\n";
}
@ -123,7 +125,7 @@ class BaseThemelet
$at_end = ($current_page >= $total_pages);
$first_html = $at_start ? "First" : $this->gen_page_link($base_url, $query, 1, "First");
$prev_html = $at_start ? "Prev" : $this->gen_page_link($base_url, $query, $prev, "Prev");
$prev_html = $at_start ? "Prev" : $this->gen_page_link($base_url, $query, $prev, "Prev");
$random_html = "-";
if ($show_random) {
@ -131,8 +133,8 @@ class BaseThemelet
$random_html = $this->gen_page_link($base_url, $query, $rand, "Random");
}
$next_html = $at_end ? "Next" : $this->gen_page_link($base_url, $query, $next, "Next");
$last_html = $at_end ? "Last" : $this->gen_page_link($base_url, $query, $total_pages, "Last");
$next_html = $at_end ? "Next" : $this->gen_page_link($base_url, $query, $next, "Next");
$last_html = $at_end ? "Last" : $this->gen_page_link($base_url, $query, $total_pages, "Last");
$start = $current_page-5 > 1 ? $current_page-5 : 1;
$end = $start+10 < $total_pages ? $start+10 : $total_pages;

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/**
* Class Block
@ -9,49 +11,37 @@ class Block
{
/**
* The block's title.
*
* @var string
*/
public $header;
public ?string $header;
/**
* The content of the block.
*
* @var string
*/
public $body;
public ?string $body;
/**
* Where the block should be placed. The default theme supports
* "main" and "left", other themes can add their own areas.
*
* @var string
*/
public $section;
public string $section;
/**
* How far down the section the block should appear, higher
* numbers appear lower. The scale is 0-100 by convention,
* though any number will work.
*
* @var int
*/
public $position;
public int $position;
/**
* A unique ID for the block.
*
* @var string
*/
public $id;
public string $id;
/**
* Should this block count as content for the sake of
* the 404 handler
*
* @var boolean
*/
public $is_content = true;
public bool $is_content = true;
public function __construct(string $header=null, string $body=null, string $section="main", int $position=50, string $id=null)
{
@ -63,7 +53,9 @@ class Block
if (is_null($id)) {
$id = (empty($header) ? md5($body ?? '') : $header) . $section;
}
$this->id = preg_replace('/[^\w-]/', '', str_replace(' ', '_', $id));
$str_id = preg_replace('/[^\w-]/', '', str_replace(' ', '_', $id));
assert(is_string($str_id));
$this->id = $str_id;
}
/**

View File

@ -1,9 +1,11 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
interface CacheEngine
{
public function get(string $key);
public function set(string $key, $val, int $time=0);
public function delete(string $key);
public function set(string $key, $val, int $time=0): void;
public function delete(string $key): void;
}
class NoCache implements CacheEngine
@ -12,23 +14,22 @@ class NoCache implements CacheEngine
{
return false;
}
public function set(string $key, $val, int $time=0)
public function set(string $key, $val, int $time=0): void
{
}
public function delete(string $key)
public function delete(string $key): void
{
}
}
class MemcachedCache implements CacheEngine
{
/** @var ?Memcached */
public $memcache=null;
public ?Memcached $memcache=null;
public function __construct(string $args)
{
$hp = explode(":", $args);
$this->memcache = new Memcached;
$this->memcache = new Memcached();
#$this->memcache->setOption(Memcached::OPT_COMPRESSION, False);
#$this->memcache->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP);
#$this->memcache->setOption(Memcached::OPT_PREFIX_KEY, phpversion());
@ -52,7 +53,7 @@ class MemcachedCache implements CacheEngine
}
}
public function set(string $key, $val, int $time=0)
public function set(string $key, $val, int $time=0): void
{
$key = urlencode($key);
@ -63,7 +64,7 @@ class MemcachedCache implements CacheEngine
}
}
public function delete(string $key)
public function delete(string $key): void
{
$key = urlencode($key);
@ -87,12 +88,12 @@ class APCCache implements CacheEngine
return apc_fetch($key);
}
public function set(string $key, $val, int $time=0)
public function set(string $key, $val, int $time=0): void
{
apc_store($key, $val, $time);
}
public function delete(string $key)
public function delete(string $key): void
{
apc_delete($key);
}
@ -100,7 +101,7 @@ class APCCache implements CacheEngine
class RedisCache implements CacheEngine
{
private $redis=null;
private Redis $redis;
public function __construct(string $args)
{
@ -116,7 +117,7 @@ class RedisCache implements CacheEngine
return $this->redis->get($key);
}
public function set(string $key, $val, int $time=0)
public function set(string $key, $val, int $time=0): void
{
if ($time > 0) {
$this->redis->setEx($key, $time, $val);
@ -125,7 +126,7 @@ class RedisCache implements CacheEngine
}
}
public function delete(string $key)
public function delete(string $key): void
{
$this->redis->del($key);
}
@ -134,9 +135,9 @@ class RedisCache implements CacheEngine
class Cache
{
public $engine;
public $hits=0;
public $misses=0;
public $time=0;
public int $hits=0;
public int $misses=0;
public int $time=0;
public function __construct(?string $dsn)
{

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* CAPTCHA abstraction *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@ -9,7 +11,7 @@ function captcha_get_html(): string
{
global $config, $user;
if (DEBUG && ip_in_range($_SERVER['REMOTE_ADDR'], "127.0.0.0/8")) {
if (DEBUG && ip_in_range(get_real_ip(), "127.0.0.0/8")) {
return "";
}
@ -32,7 +34,7 @@ function captcha_check(): bool
{
global $config, $user;
if (DEBUG && ip_in_range($_SERVER['REMOTE_ADDR'], "127.0.0.0/8")) {
if (DEBUG && ip_in_range(get_real_ip(), "127.0.0.0/8")) {
return true;
}
@ -40,7 +42,7 @@ function captcha_check(): bool
$r_privatekey = $config->get_string('api_recaptcha_privkey');
if (!empty($r_privatekey)) {
$recaptcha = new ReCaptcha($r_privatekey);
$resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']);
$resp = $recaptcha->verify($_POST['g-recaptcha-response'] ?? "", get_real_ip());
if (!$resp->isSuccess()) {
log_info("core", "Captcha failed (ReCaptcha): " . implode("", $resp->getErrorCodes()));

67
core/command_builder.php Normal file
View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
// Provides mechanisms for cleanly executing command-line applications
// Was created to try to centralize a solution for whatever caused this:
// quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
class CommandBuilder
{
private string $executable;
private array $args = [];
public array $output;
public function __construct(String $executable)
{
if (empty($executable)) {
throw new InvalidArgumentException("executable cannot be empty");
}
$this->executable = $executable;
}
public function add_flag(string $value): void
{
$this->args[] = $value;
}
public function add_escaped_arg(string $value): void
{
$this->args[] = escapeshellarg($value);
}
public function generate(): string
{
$command = escapeshellarg($this->executable);
if (!empty($this->args)) {
$command .= " ";
$command .= join(" ", $this->args);
}
return escapeshellcmd($command)." 2>&1";
}
public function combineOutput(string $empty_output = ""): string
{
if (empty($this->output)) {
return $empty_output;
} else {
return implode("\r\n", $this->output);
}
}
public function execute(bool $fail_on_non_zero_return = false): int
{
$cmd = $this->generate();
exec($cmd, $this->output, $ret);
$output = $this->combineOutput("nothing");
log_debug('command_builder', "Command `$cmd` returned $ret and outputted $output");
if ($fail_on_non_zero_return&&(int)$ret!==(int)0) {
throw new SCoreException("Command `$cmd` failed, returning $ret and outputting $output");
}
return $ret;
}
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/**
* Interface Config
@ -130,7 +132,7 @@ interface Config
*/
abstract class BaseConfig implements Config
{
public $values = [];
public array $values = [];
public function set_int(string $name, ?int $value): void
{
@ -256,12 +258,10 @@ abstract class BaseConfig implements Config
*/
class DatabaseConfig extends BaseConfig
{
/** @var Database */
private $database = null;
private $table_name;
private $sub_column;
private $sub_value;
private Database $database;
private string $table_name;
private ?string $sub_column;
private ?string $sub_value;
public function __construct(
Database $database,
@ -323,10 +323,10 @@ class DatabaseConfig extends BaseConfig
$params[] = ":sub_value";
}
$this->database->Execute($query, $args);
$this->database->execute($query, $args);
$args["value"] =$this->values[$name];
$this->database->Execute(
$this->database->execute(
"INSERT INTO {$this->table_name} (".join(",", $cols).") VALUES (".join(",", $params).")",
$args
);
@ -334,5 +334,6 @@ class DatabaseConfig extends BaseConfig
// rather than deleting and having some other request(s) do a thundering
// herd of race-conditioned updates, just save the updated version once here
$cache->set("config", $this->values);
$this->database->notify("config");
}
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
use FFSPHP\PDO;
abstract class DatabaseDriver
@ -13,38 +15,23 @@ abstract class DatabaseDriver
*/
class Database
{
/** @var string */
private $dsn;
private string $dsn;
/**
* The PDO database connection object, for anyone who wants direct access.
* @var null|PDO
*/
private $db = null;
/**
* @var float
*/
public $dbtime = 0.0;
private ?PDO $db = null;
public float $dbtime = 0.0;
/**
* Meta info about the database engine.
* @var DBEngine|null
*/
private $engine = null;
/**
* A boolean flag to track if we already have an active transaction.
* (ie: True if beginTransaction() already called)
*
* @var bool
*/
public $transaction = false;
private ?DBEngine $engine = null;
/**
* How many queries this DB object has run
*/
public $query_count = 0;
public int $query_count = 0;
public function __construct(string $dsn)
{
@ -53,13 +40,9 @@ class Database
private function connect_db(): void
{
$this->db = new PDO($this->dsn, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$this->db = new PDO($this->dsn);
$this->connect_engine();
$this->engine->init($this->db);
$this->begin_transaction();
}
@ -87,21 +70,19 @@ class Database
public function begin_transaction(): void
{
if ($this->transaction === false) {
if ($this->is_transaction_open() === false) {
$this->db->beginTransaction();
$this->transaction = true;
}
}
public function is_transaction_open(): bool
{
return !is_null($this->db) && $this->transaction === true;
return !is_null($this->db) && $this->db->inTransaction();
}
public function commit(): bool
{
if ($this->is_transaction_open()) {
$this->transaction = false;
return $this->db->commit();
} else {
throw new SCoreException("Unable to call commit() as there is no transaction currently open.");
@ -111,7 +92,6 @@ class Database
public function rollback(): bool
{
if ($this->is_transaction_open()) {
$this->transaction = false;
return $this->db->rollback();
} else {
throw new SCoreException("Unable to call rollback() as there is no transaction currently open.");
@ -126,19 +106,6 @@ class Database
return $this->engine->scoreql_to_sql($input);
}
public function scoresql_value_prepare($input)
{
if (is_null($this->engine)) {
$this->connect_engine();
}
if ($input===true) {
return $this->engine->BOOL_Y;
} elseif ($input===false) {
return $this->engine->BOOL_N;
}
return $input;
}
public function get_driver_name(): string
{
if (is_null($this->engine)) {
@ -164,11 +131,16 @@ class Database
$this->dbtime += $dur;
}
public function set_timeout(int $time): void
public function set_timeout(?int $time): void
{
$this->engine->set_timeout($this->db, $time);
}
public function notify(string $channel, ?string $data=null): void
{
$this->engine->notify($this->db, $channel, $data);
}
public function execute(string $query, array $args = []): PDOStatement
{
try {
@ -253,6 +225,20 @@ class Database
return $res;
}
/**
* Execute an SQL query and return the the first column => the second column as an iterable object.
*/
public function get_pairs_iterable(string $query, array $args = []): Generator
{
$_start = microtime(true);
$stmt = $this->execute($query, $args);
$this->count_time("get_pairs_iterable", $_start, $query, $args);
foreach ($stmt as $row) {
yield $row[0] => $row[1];
}
}
/**
* Execute an SQL query and return a single value, or null.
*/
@ -336,4 +322,29 @@ class Database
{
return $this->db;
}
public function standardise_boolean(string $table, string $column, bool $include_postgres=false): void
{
$d = $this->get_driver_name();
if ($d == DatabaseDriver::MYSQL) {
# In mysql, ENUM('Y', 'N') is secretly INTEGER where Y=1 and N=2.
# BOOLEAN is secretly TINYINT where true=1 and false=0.
# So we can cast directly from ENUM to BOOLEAN which gives us a
# column of values 'true' and 'invalid but who cares lol', which
# we can then UPDATE to be 'true' and 'false'.
$this->execute("ALTER TABLE $table MODIFY COLUMN $column BOOLEAN;");
$this->execute("UPDATE $table SET $column=0 WHERE $column=2;");
}
if ($d == DatabaseDriver::SQLITE) {
# SQLite doesn't care about column types at all, everything is
# text, so we can in-place replace a char with a bool
$this->execute("UPDATE $table SET $column = ($column IN ('Y', 1))");
}
if ($d == DatabaseDriver::PGSQL && $include_postgres) {
$this->execute("ALTER TABLE $table ADD COLUMN ${column}_b BOOLEAN DEFAULT FALSE NOT NULL");
$this->execute("UPDATE $table SET ${column}_b = ($column = 'Y')");
$this->execute("ALTER TABLE $table DROP COLUMN $column");
$this->execute("ALTER TABLE $table RENAME COLUMN ${column}_b TO $column");
}
}
}

View File

@ -1,28 +1,23 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
abstract class SCORE
{
const AIPK = "SCORE_AIPK";
const INET = "SCORE_INET";
const BOOL_Y = "SCORE_BOOL_Y";
const BOOL_N = "SCORE_BOOL_N";
const BOOL = "SCORE_BOOL";
public const AIPK = "SCORE_AIPK";
public const INET = "SCORE_INET";
}
abstract class DBEngine
{
/** @var null|string */
public $name = null;
public $BOOL_Y = null;
public $BOOL_N = null;
public ?string $name = null;
public function init(PDO $db)
{
}
public function scoreql_to_sql(string $scoreql): string
public function scoreql_to_sql(string $data): string
{
return $scoreql;
return $data;
}
public function create_table_sql(string $name, string $data): string
@ -30,18 +25,16 @@ abstract class DBEngine
return 'CREATE TABLE '.$name.' ('.$data.')';
}
abstract public function set_timeout(PDO $db, int $time);
abstract public function set_timeout(PDO $db, ?int $time);
abstract public function get_version(PDO $db): string;
abstract public function notify(PDO $db, string $channel, ?string $data=null): void;
}
class MySQL extends DBEngine
{
/** @var string */
public $name = DatabaseDriver::MYSQL;
public $BOOL_Y = 'Y';
public $BOOL_N = 'N';
public ?string $name = DatabaseDriver::MYSQL;
public function init(PDO $db)
{
@ -52,9 +45,6 @@ class MySQL extends DBEngine
{
$data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY auto_increment", $data);
$data = str_replace(SCORE::INET, "VARCHAR(45)", $data);
$data = str_replace(SCORE::BOOL_Y, "'$this->BOOL_Y'", $data);
$data = str_replace(SCORE::BOOL_N, "'$this->BOOL_N'", $data);
$data = str_replace(SCORE::BOOL, "ENUM('Y', 'N')", $data);
return $data;
}
@ -65,12 +55,16 @@ class MySQL extends DBEngine
return 'CREATE TABLE '.$name.' ('.$data.') '.$ctes;
}
public function set_timeout(PDO $db, int $time): void
public function set_timeout(PDO $db, ?int $time): void
{
// These only apply to read-only queries, which appears to be the best we can to mysql-wise
// $db->exec("SET SESSION MAX_EXECUTION_TIME=".$time.";");
}
public function notify(PDO $db, string $channel, ?string $data=null): void
{
}
public function get_version(PDO $db): string
{
return $db->query('select version()')->fetch()[0];
@ -79,11 +73,7 @@ class MySQL extends DBEngine
class PostgreSQL extends DBEngine
{
/** @var string */
public $name = DatabaseDriver::PGSQL;
public $BOOL_Y = "true";
public $BOOL_N = "false";
public ?string $name = DatabaseDriver::PGSQL;
public function init(PDO $db)
{
@ -101,9 +91,6 @@ class PostgreSQL extends DBEngine
{
$data = str_replace(SCORE::AIPK, "INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY", $data);
$data = str_replace(SCORE::INET, "INET", $data);
$data = str_replace(SCORE::BOOL_Y, "true", $data);
$data = str_replace(SCORE::BOOL_N, "false", $data);
$data = str_replace(SCORE::BOOL, "BOOL", $data);
return $data;
}
@ -113,11 +100,23 @@ class PostgreSQL extends DBEngine
return "CREATE TABLE $name ($data)";
}
public function set_timeout(PDO $db, int $time): void
public function set_timeout(PDO $db, ?int $time): void
{
if (is_null($time)) {
$time = 0;
}
$db->exec("SET statement_timeout TO ".$time.";");
}
public function notify(PDO $db, string $channel, ?string $data=null): void
{
if ($data) {
$db->exec("NOTIFY $channel, '$data';");
} else {
$db->exec("NOTIFY $channel;");
}
}
public function get_version(PDO $db): string
{
return $db->query('select version()')->fetch()[0];
@ -125,19 +124,19 @@ class PostgreSQL extends DBEngine
}
// shimmie functions for export to sqlite
function _unix_timestamp($date)
function _unix_timestamp($date): int
{
return strtotime($date);
}
function _now()
function _now(): string
{
return date("Y-m-d H:i:s");
}
function _floor($a)
function _floor($a): float
{
return floor($a);
}
function _log($a, $b=null)
function _log($a, $b=null): float
{
if (is_null($b)) {
return log($a);
@ -145,39 +144,34 @@ function _log($a, $b=null)
return log($a, $b);
}
}
function _isnull($a)
function _isnull($a): bool
{
return is_null($a);
}
function _md5($a)
function _md5($a): string
{
return md5($a);
}
function _concat($a, $b)
function _concat($a, $b): string
{
return $a . $b;
}
function _lower($a)
function _lower($a): string
{
return strtolower($a);
}
function _rand()
function _rand(): int
{
return rand();
}
function _ln($n)
function _ln($n): float
{
return log($n);
}
class SQLite extends DBEngine
{
/** @var string */
public $name = DatabaseDriver::SQLITE;
public $BOOL_Y = 'Y';
public $BOOL_N = 'N';
public ?string $name = DatabaseDriver::SQLITE;
public function init(PDO $db)
{
@ -199,9 +193,6 @@ class SQLite extends DBEngine
{
$data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY", $data);
$data = str_replace(SCORE::INET, "VARCHAR(45)", $data);
$data = str_replace(SCORE::BOOL_Y, "'$this->BOOL_Y'", $data);
$data = str_replace(SCORE::BOOL_N, "'$this->BOOL_N'", $data);
$data = str_replace(SCORE::BOOL, "CHAR(1)", $data);
return $data;
}
@ -224,11 +215,15 @@ class SQLite extends DBEngine
return "CREATE TABLE $name ($cols_redone); $extras";
}
public function set_timeout(PDO $db, int $time): void
public function set_timeout(PDO $db, ?int $time): void
{
// There doesn't seem to be such a thing for SQLite, so it does nothing
}
public function notify(PDO $db, string $channel, ?string $data=null): void
{
}
public function get_version(PDO $db): string
{
return $db->query('select sqlite_version()')->fetch()[0];

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/**
* Generic parent class for all events.
*
@ -6,13 +8,13 @@
*/
abstract class Event
{
public $stop_processing = false;
public bool $stop_processing = false;
public function __construct()
{
}
public function __toString()
public function __toString(): string
{
return var_export($this, true);
}
@ -42,19 +44,11 @@ class InitExtEvent extends Event
class PageRequestEvent extends Event
{
/**
* @var array
* @var string[]
*/
public $args;
/**
* @var int
*/
public $arg_count;
/**
* @var int
*/
public $part_count;
public int $arg_count;
public int $part_count;
public function __construct(string $path)
{
@ -109,7 +103,7 @@ class PageRequestEvent extends Event
return $this->args[$offset];
} else {
$nm1 = $this->arg_count - 1;
throw new SCoreException("Requested an invalid page argument {$offset} / {$nm1}");
throw new UserErrorException("Requested an invalid page argument {$offset} / {$nm1}");
}
}
@ -179,15 +173,12 @@ class PageRequestEvent extends Event
*/
class CommandEvent extends Event
{
/**
* @var string
*/
public $cmd = "help";
public string $cmd = "help";
/**
* @var array
* @var string[]
*/
public $args = [];
public array $args = [];
/**
* #param string[] $args
@ -256,24 +247,18 @@ class TextFormattingEvent extends Event
{
/**
* For reference
*
* @var string
*/
public $original;
public string $original;
/**
* with formatting applied
*
* @var string
*/
public $formatted;
public string $formatted;
/**
* with formatting removed
*
* @var string
*/
public $stripped;
public string $stripped;
public function __construct(string $text)
{
@ -296,38 +281,30 @@ class LogEvent extends Event
{
/**
* a category, normally the extension name
*
* @var string
*/
public $section;
public string $section;
/**
* See python...
*
* @var int
*/
public $priority = 0;
public int $priority = 0;
/**
* Free text to be logged
*
* @var string
*/
public $message;
public string $message;
/**
* The time that the event was created
*
* @var int
*/
public $time;
public int $time;
/**
* Extra data to be held separate
*
* @var array
* @var string[]
*/
public $args;
public array $args;
public function __construct(string $section, int $priority, string $message)
{

View File

@ -1,17 +1,15 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/**
* Class SCoreException
*
* A base exception to be caught by the upper levels.
*/
class SCoreException extends RuntimeException
{
/** @var string|null */
public $query;
/** @var string */
public $error;
public ?string $query;
public string $error;
public int $http_code = 500;
public function __construct(string $msg, ?string $query=null)
{
@ -23,61 +21,71 @@ class SCoreException extends RuntimeException
class InstallerException extends RuntimeException
{
/** @var string */
public $title;
public string $title;
public string $body;
public int $exit_code;
/** @var string */
public $body;
/** @var int */
public $code;
public function __construct(string $title, string $body, int $code)
public function __construct(string $title, string $body, int $exit_code)
{
parent::__construct($body);
$this->title = $title;
$this->body = $body;
$this->code = $code;
$this->exit_code = $exit_code;
}
}
/**
* Class PermissionDeniedException
*
* A fairly common, generic exception.
*/
class PermissionDeniedException extends SCoreException
class UserErrorException extends SCoreException
{
public int $http_code = 400;
}
class ServerErrorException extends SCoreException
{
public int $http_code = 500;
}
/**
* Class ImageDoesNotExist
*
* This exception is used when an Image cannot be found by ID.
*
* Example: Image::by_id(-1) returns null
* A fairly common, generic exception.
*/
class ImageDoesNotExist extends SCoreException
class PermissionDeniedException extends UserErrorException
{
public int $http_code = 403;
}
/**
* This exception is used when an Image cannot be found by ID.
*/
class ImageDoesNotExist extends UserErrorException
{
public int $http_code = 404;
}
/**
* This exception is used when a User cannot be found by some criteria.
*/
class UserDoesNotExist extends UserErrorException
{
public int $http_code = 404;
}
/*
* For validate_input()
*/
class InvalidInput extends SCoreException
class InvalidInput extends UserErrorException
{
public int $http_code = 402;
}
/*
* This is used by the image resizing code when there is not enough memory to perform a resize.
*/
class InsufficientMemoryException extends SCoreException
class InsufficientMemoryException extends ServerErrorException
{
}
/*
* This is used by the image resizing code when there is an error while resizing
*/
class ImageResizeException extends SCoreException
class ImageResizeException extends ServerErrorException
{
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/**
* Class Extension
*
@ -13,16 +15,11 @@
*/
abstract class Extension
{
/** @var string */
public $key;
public string $key;
protected ?Themelet $theme;
public ?ExtensionInfo $info;
/** @var Themelet */
protected $theme;
/** @var ExtensionInfo */
public $info;
private static $enabled_extensions = [];
private static array $enabled_extensions = [];
public function __construct($class = null)
{
@ -61,7 +58,7 @@ abstract class Extension
return 50;
}
public static function determine_enabled_extensions()
public static function determine_enabled_extensions(): void
{
self::$enabled_extensions = [];
foreach (array_merge(
@ -122,34 +119,31 @@ abstract class ExtensionInfo
public const LICENSE_MIT = "MIT";
public const LICENSE_WTFPL = "WTFPL";
public const VISIBLE_DEFAULT = "default";
public const VISIBLE_ADMIN = "admin";
public const VISIBLE_HIDDEN = "hidden";
private const VALID_VISIBILITY = [self::VISIBLE_ADMIN, self::VISIBLE_HIDDEN];
private const VALID_VISIBILITY = [self::VISIBLE_DEFAULT, self::VISIBLE_ADMIN, self::VISIBLE_HIDDEN];
public $key;
public string $key;
public $core = false;
public bool $core = false;
public bool $beta = false;
public $beta = false;
public string $name;
public string $license;
public string $description;
public array $authors = [];
public array $dependencies = [];
public array $conflicts = [];
public string $visibility = self::VISIBLE_DEFAULT;
public ?string $link = null;
public ?string $version = null;
public ?string $documentation = null;
public $name;
public $authors = [];
public $link;
public $license;
public $version;
public $dependencies = [];
public $visibility;
public $description;
public $documentation;
/** @var array which DBs this ext supports (blank for 'all') */
public $db_support = [];
/** @var bool */
private $supported = null;
/** @var string */
private $support_info = null;
/** @var string[] which DBs this ext supports (blank for 'all') */
public array $db_support = [];
private ?bool $supported = null;
private ?string $support_info = null;
public function is_supported(): bool
{
@ -167,9 +161,9 @@ abstract class ExtensionInfo
return $this->support_info;
}
private static $all_info_by_key = [];
private static $all_info_by_class = [];
private static $core_extensions = [];
private static array $all_info_by_key = [];
private static array $all_info_by_class = [];
private static array $core_extensions = [];
protected function __construct()
{
@ -193,6 +187,13 @@ abstract class ExtensionInfo
if (!empty($this->db_support) && !in_array($database->get_driver_name(), $this->db_support)) {
$this->support_info .= "Database not supported. ";
}
if (!empty($this->conflicts)) {
$intersects = array_intersect($this->conflicts, Extension::get_enabled_extensions());
if (!empty($intersects)) {
$this->support_info .= "Conflicts with other extension(s): " . join(", ", $intersects);
}
}
// Additional checks here as needed
$this->supported = empty($this->support_info);
@ -235,7 +236,7 @@ abstract class ExtensionInfo
public static function load_all_extension_info()
{
foreach (getSubclassesOf("ExtensionInfo") as $class) {
foreach (get_subclasses_of("ExtensionInfo") as $class) {
$extension_info = new $class();
if (array_key_exists($extension_info->key, self::$all_info_by_key)) {
throw new ScoreException("Extension Info $class with key $extension_info->key has already been loaded");
@ -275,7 +276,7 @@ abstract class FormatterExtension extends Extension
*/
abstract class DataHandlerExtension extends Extension
{
protected $SUPPORTED_MIME = [];
protected array $SUPPORTED_MIME = [];
protected function move_upload_to_archive(DataUploadEvent $event)
{
@ -291,11 +292,11 @@ abstract class DataHandlerExtension extends Extension
public function onDataUpload(DataUploadEvent $event)
{
$supported_ext = $this->supported_ext($event->type);
$supported_mime = $this->supported_mime($event->mime);
$check_contents = $this->check_contents($event->tmpname);
if ($supported_ext && $check_contents) {
if ($supported_mime && $check_contents) {
$this->move_upload_to_archive($event);
send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
send_event(new ThumbnailGenerationEvent($event->hash, $event->mime));
/* Check if we are replacing an image */
if (!is_null($event->replace_id)) {
@ -305,20 +306,20 @@ abstract class DataHandlerExtension extends Extension
$existing = Image::by_id($event->replace_id);
if (is_null($existing)) {
throw new UploadException("Image to replace does not exist!");
throw new UploadException("Post to replace does not exist!");
}
if ($existing->hash === $event->metadata['hash']) {
throw new UploadException("The uploaded image is the same as the one to replace.");
throw new UploadException("The uploaded post is the same as the one to replace.");
}
// even more hax..
$event->metadata['tags'] = $existing->get_tag_list();
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->metadata['hash']), $event->metadata);
if (is_null($image)) {
throw new UploadException("Data handler failed to create image object from data");
throw new UploadException("Data handler failed to create post object from data");
}
if (empty($image->ext)) {
throw new UploadException("Unable to determine extension for ". $event->tmpname);
if (empty($image->get_mime())) {
throw new UploadException("Unable to determine MIME for ". $event->tmpname);
}
try {
send_event(new MediaCheckPropertiesEvent($image));
@ -327,14 +328,16 @@ abstract class DataHandlerExtension extends Extension
}
send_event(new ImageReplaceEvent($event->replace_id, $image));
$event->image_id = $event->replace_id;
$_id = $event->replace_id;
assert(!is_null($_id));
$event->image_id = $_id;
} else {
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
if (is_null($image)) {
throw new UploadException("Data handler failed to create image object from data");
throw new UploadException("Data handler failed to create post object from data");
}
if (empty($image->ext)) {
throw new UploadException("Unable to determine extension for ". $event->tmpname);
if (empty($image->get_mime())) {
throw new UploadException("Unable to determine MIME for ". $event->tmpname);
}
try {
send_event(new MediaCheckPropertiesEvent($image));
@ -358,7 +361,7 @@ abstract class DataHandlerExtension extends Extension
send_event(new LockSetEvent($image, !empty($locked)));
}
}
} elseif ($supported_ext && !$check_contents) {
} elseif ($supported_mime && !$check_contents) {
// We DO support this extension - but the file looks corrupt
throw new UploadException("Invalid or corrupted file");
}
@ -367,15 +370,15 @@ abstract class DataHandlerExtension extends Extension
public function onThumbnailGeneration(ThumbnailGenerationEvent $event)
{
$result = false;
if ($this->supported_ext($event->type)) {
if ($this->supported_mime($event->mime)) {
if ($event->force) {
$result = $this->create_thumb($event->hash, $event->type);
$result = $this->create_thumb($event->hash, $event->mime);
} else {
$outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash);
if (file_exists($outname)) {
return;
}
$result = $this->create_thumb($event->hash, $event->type);
$result = $this->create_thumb($event->hash, $event->mime);
}
}
if ($result) {
@ -386,7 +389,7 @@ abstract class DataHandlerExtension extends Extension
public function onDisplayingImage(DisplayingImageEvent $event)
{
global $page;
if ($this->supported_ext($event->image->ext)) {
if ($this->supported_mime($event->image->get_mime())) {
/** @noinspection PhpPossiblePolymorphicInvocationInspection */
$this->theme->display_image($page, $event->image);
}
@ -394,25 +397,23 @@ abstract class DataHandlerExtension extends Extension
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
if ($this->supported_ext($event->ext)) {
if ($this->supported_mime($event->mime)) {
$this->media_check_properties($event);
}
}
protected function create_image_from_data(string $filename, array $metadata): Image
{
global $config;
$image = new Image();
$image->filesize = $metadata['size'];
$image->hash = $metadata['hash'];
$image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename'];
if ($config->get_bool("upload_use_mime")) {
$image->ext = get_extension_for_file($filename);
}
if (empty($image->ext)) {
$image->ext = (($pos = strpos($metadata['extension'], '?')) !== false) ? substr($metadata['extension'], 0, $pos) : $metadata['extension'];
if (array_key_exists("extension", $metadata)) {
$image->set_mime(MimeType::get_for_file($filename, $metadata["extension"]));
} else {
$image->set_mime(MimeType::get_for_file($filename));
}
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
@ -423,22 +424,35 @@ abstract class DataHandlerExtension extends Extension
abstract protected function media_check_properties(MediaCheckPropertiesEvent $event): void;
abstract protected function check_contents(string $tmpname): bool;
abstract protected function create_thumb(string $hash, string $type): bool;
abstract protected function create_thumb(string $hash, string $mime): bool;
protected function supported_ext(string $ext): bool
protected function supported_mime(string $mime): bool
{
return in_array(get_mime_for_extension($ext), $this->SUPPORTED_MIME);
return MimeType::matches_array($mime, $this->SUPPORTED_MIME);
}
public static function get_all_supported_mimes(): array
{
$arr = [];
foreach (get_subclasses_of("DataHandlerExtension") as $handler) {
$handler = (new $handler());
$arr = array_merge($arr, $handler->SUPPORTED_MIME);
}
// Not sure how to handle this otherwise, don't want to set up a whole other event for this one class
if (class_exists("TranscodeImage")) {
$arr = array_merge($arr, TranscodeImage::get_enabled_mimes());
}
$arr = array_unique($arr);
return $arr;
}
public static function get_all_supported_exts(): array
{
$arr = [];
foreach (getSubclassesOf("DataHandlerExtension") as $handler) {
$handler = (new $handler());
foreach ($handler->SUPPORTED_MIME as $mime) {
$arr = array_merge($arr, get_all_extension_for_mime($mime));
}
foreach (self::get_all_supported_mimes() as $mime) {
$arr = array_merge($arr, FileExtension::get_all_for_mime($mime));
}
$arr = array_unique($arr);
return $arr;

View File

@ -1,448 +0,0 @@
<?php
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* MIME types and extension information and resolvers *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
const EXTENSION_ANI = 'ani';
const EXTENSION_ASC = 'asc';
const EXTENSION_ASF = 'asf';
const EXTENSION_AVI = 'avi';
const EXTENSION_BMP = 'bmp';
const EXTENSION_BZIP = 'bz';
const EXTENSION_BZIP2 = 'bz2';
const EXTENSION_CBR = 'cbr';
const EXTENSION_CBZ = 'cbz';
const EXTENSION_CBT = 'cbt';
const EXTENSION_CBA = 'cbA';
const EXTENSION_CB7 = 'cb7';
const EXTENSION_CSS = 'css';
const EXTENSION_CSV = 'csv';
const EXTENSION_CUR = 'cur';
const EXTENSION_FLASH = 'swf';
const EXTENSION_FLASH_VIDEO = 'flv';
const EXTENSION_GIF = 'gif';
const EXTENSION_GZIP = 'gz';
const EXTENSION_HTML = 'html';
const EXTENSION_HTM = 'htm';
const EXTENSION_ICO = 'ico';
const EXTENSION_JFIF = 'jfif';
const EXTENSION_JFI = 'jfi';
const EXTENSION_JPEG = 'jpeg';
const EXTENSION_JPG = 'jpg';
const EXTENSION_JS = 'js';
const EXTENSION_JSON = 'json';
const EXTENSION_MKV = 'mkv';
const EXTENSION_MP3 = 'mp3';
const EXTENSION_MP4 = 'mp4';
const EXTENSION_M4V = 'm4v';
const EXTENSION_M4A = 'm4a';
const EXTENSION_MPEG = 'mpeg';
const EXTENSION_MPG = 'mpg';
const EXTENSION_OGG = 'ogg';
const EXTENSION_OGG_VIDEO = 'ogv';
const EXTENSION_OGG_AUDIO = 'oga';
const EXTENSION_PDF = 'pdf';
const EXTENSION_PHP = 'php';
const EXTENSION_PHP5 = 'php5';
const EXTENSION_PNG = 'png';
const EXTENSION_PSD = 'psd';
const EXTENSION_MOV = 'mov';
const EXTENSION_RSS = 'rss';
const EXTENSION_SVG = 'svg';
const EXTENSION_TAR = 'tar';
const EXTENSION_TEXT = 'txt';
const EXTENSION_TIFF = 'tiff';
const EXTENSION_TIF = 'tif';
const EXTENSION_WAV = 'wav';
const EXTENSION_WEBM = 'webm';
const EXTENSION_WEBP = 'webp';
const EXTENSION_WMA = 'wma';
const EXTENSION_WMV = 'wmv';
const EXTENSION_XML = 'xml';
const EXTENSION_XSL = 'xsl';
const EXTENSION_ZIP = 'zip';
// Couldn't find a mimetype for ani, so made one up based on it being a riff container
const MIME_TYPE_ANI = 'application/riff+ani';
const MIME_TYPE_ASF = 'video/x-ms-asf';
const MIME_TYPE_AVI = 'video/x-msvideo';
// Went with mime types from http://fileformats.archiveteam.org/wiki/Comic_Book_Archive
const MIME_TYPE_COMIC_ZIP = 'application/vnd.comicbook+zip';
const MIME_TYPE_COMIC_RAR = 'application/vnd.comicbook-rar';
const MIME_TYPE_BMP = 'image/x-ms-bmp';
const MIME_TYPE_BZIP = 'application/x-bzip';
const MIME_TYPE_BZIP2 = 'application/x-bzip2';
const MIME_TYPE_CSS = 'text/css';
const MIME_TYPE_CSV = 'text/csv';
const MIME_TYPE_FLASH = 'application/x-shockwave-flash';
const MIME_TYPE_FLASH_VIDEO = 'video/x-flv';
const MIME_TYPE_GIF = 'image/gif';
const MIME_TYPE_GZIP = 'application/x-gzip';
const MIME_TYPE_HTML = 'text/html';
const MIME_TYPE_ICO = 'image/x-icon';
const MIME_TYPE_JPEG = 'image/jpeg';
const MIME_TYPE_JS = 'text/javascript';
const MIME_TYPE_JSON = 'application/json';
const MIME_TYPE_MKV = 'video/x-matroska';
const MIME_TYPE_MP3 = 'audio/mpeg';
const MIME_TYPE_MP4_AUDIO = 'audio/mp4';
const MIME_TYPE_MP4_VIDEO = 'video/mp4';
const MIME_TYPE_MPEG = 'video/mpeg';
const MIME_TYPE_OCTET_STREAM = 'application/octet-stream';
const MIME_TYPE_OGG = 'application/ogg';
const MIME_TYPE_OGG_VIDEO = 'video/ogg';
const MIME_TYPE_OGG_AUDIO = 'audio/ogg';
const MIME_TYPE_PDF = 'application/pdf';
const MIME_TYPE_PHP = 'text/x-php';
const MIME_TYPE_PNG = 'image/png';
const MIME_TYPE_PSD = 'image/vnd.adobe.photoshop';
const MIME_TYPE_QUICKTIME = 'video/quicktime';
const MIME_TYPE_RSS = 'application/rss+xml';
const MIME_TYPE_SVG = 'image/svg+xml';
const MIME_TYPE_TAR = 'application/x-tar';
const MIME_TYPE_TEXT = 'text/plain';
const MIME_TYPE_TIFF = 'image/tiff';
const MIME_TYPE_WAV = 'audio/x-wav';
const MIME_TYPE_WEBM = 'video/webm';
const MIME_TYPE_WEBP = 'image/webp';
const MIME_TYPE_WIN_BITMAP = 'image/x-win-bitmap';
const MIME_TYPE_XML = 'text/xml';
const MIME_TYPE_XML_APPLICATION = 'application/xml';
const MIME_TYPE_XSL = 'application/xsl+xml';
const MIME_TYPE_ZIP = 'application/zip';
const MIME_TYPE_MAP_NAME = 'name';
const MIME_TYPE_MAP_EXT = 'ext';
const MIME_TYPE_MAP_MIME = 'mime';
// Mime type map. Each entry in the MIME_TYPE_ARRAY represents a kind of file, identified by the "correct" mimetype as the key.
// The value for each entry is a map of twokeys, ext and mime.
// ext's value is an array of all of the extensions that the file type can use, with the "correct" one being first.
// mime's value is an array of all mime types that the file type is known to use, with the current "correct" one being first.
const MIME_TYPE_MAP = [
MIME_TYPE_ANI => [
MIME_TYPE_MAP_NAME => "ANI Cursor",
MIME_TYPE_MAP_EXT => [EXTENSION_ANI],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ANI],
],
MIME_TYPE_AVI => [
MIME_TYPE_MAP_NAME => "AVI",
MIME_TYPE_MAP_EXT => [EXTENSION_AVI],
MIME_TYPE_MAP_MIME => [MIME_TYPE_AVI,'video/avi','video/msvideo'],
],
MIME_TYPE_ASF => [
MIME_TYPE_MAP_NAME => "ASF/WMV",
MIME_TYPE_MAP_EXT => [EXTENSION_ASF,EXTENSION_WMA,EXTENSION_WMV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ASF,'audio/x-ms-wma','video/x-ms-wmv'],
],
MIME_TYPE_BMP => [
MIME_TYPE_MAP_NAME => "BMP",
MIME_TYPE_MAP_EXT => [EXTENSION_BMP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_BMP],
],
MIME_TYPE_BZIP => [
MIME_TYPE_MAP_NAME => "BZIP",
MIME_TYPE_MAP_EXT => [EXTENSION_BZIP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_BZIP],
],
MIME_TYPE_BZIP2 => [
MIME_TYPE_MAP_NAME => "BZIP2",
MIME_TYPE_MAP_EXT => [EXTENSION_BZIP2],
MIME_TYPE_MAP_MIME => [MIME_TYPE_BZIP2],
],
MIME_TYPE_COMIC_ZIP => [
MIME_TYPE_MAP_NAME => "CBZ",
MIME_TYPE_MAP_EXT => [EXTENSION_CBZ],
MIME_TYPE_MAP_MIME => [MIME_TYPE_COMIC_ZIP],
],
MIME_TYPE_CSS => [
MIME_TYPE_MAP_NAME => "Cascading Style Sheet",
MIME_TYPE_MAP_EXT => [EXTENSION_CSS],
MIME_TYPE_MAP_MIME => [MIME_TYPE_CSS],
],
MIME_TYPE_CSV => [
MIME_TYPE_MAP_NAME => "CSV",
MIME_TYPE_MAP_EXT => [EXTENSION_CSV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_CSV],
],
MIME_TYPE_FLASH => [
MIME_TYPE_MAP_NAME => "Flash",
MIME_TYPE_MAP_EXT => [EXTENSION_FLASH],
MIME_TYPE_MAP_MIME => [MIME_TYPE_FLASH],
],
MIME_TYPE_FLASH_VIDEO => [
MIME_TYPE_MAP_NAME => "Flash Video",
MIME_TYPE_MAP_EXT => [EXTENSION_FLASH_VIDEO],
MIME_TYPE_MAP_MIME => [MIME_TYPE_FLASH_VIDEO,'video/flv'],
],
MIME_TYPE_GIF => [
MIME_TYPE_MAP_NAME => "GIF",
MIME_TYPE_MAP_EXT => [EXTENSION_GIF],
MIME_TYPE_MAP_MIME => [MIME_TYPE_GIF],
],
MIME_TYPE_GZIP => [
MIME_TYPE_MAP_NAME => "GZIP",
MIME_TYPE_MAP_EXT => [EXTENSION_GZIP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TAR],
],
MIME_TYPE_HTML => [
MIME_TYPE_MAP_NAME => "HTML",
MIME_TYPE_MAP_EXT => [EXTENSION_HTM, EXTENSION_HTML],
MIME_TYPE_MAP_MIME => [MIME_TYPE_HTML],
],
MIME_TYPE_ICO => [
MIME_TYPE_MAP_NAME => "Icon",
MIME_TYPE_MAP_EXT => [EXTENSION_ICO, EXTENSION_CUR],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ICO, MIME_TYPE_WIN_BITMAP],
],
MIME_TYPE_JPEG => [
MIME_TYPE_MAP_NAME => "JPEG",
MIME_TYPE_MAP_EXT => [EXTENSION_JPG, EXTENSION_JPEG, EXTENSION_JFIF, EXTENSION_JFI],
MIME_TYPE_MAP_MIME => [MIME_TYPE_JPEG],
],
MIME_TYPE_JS => [
MIME_TYPE_MAP_NAME => "JavaScript",
MIME_TYPE_MAP_EXT => [EXTENSION_JS],
MIME_TYPE_MAP_MIME => [MIME_TYPE_JS],
],
MIME_TYPE_JSON => [
MIME_TYPE_MAP_NAME => "JSON",
MIME_TYPE_MAP_EXT => [EXTENSION_JSON],
MIME_TYPE_MAP_MIME => [MIME_TYPE_JSON],
],
MIME_TYPE_MKV => [
MIME_TYPE_MAP_NAME => "Matroska",
MIME_TYPE_MAP_EXT => [EXTENSION_MKV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MKV],
],
MIME_TYPE_MP3 => [
MIME_TYPE_MAP_NAME => "MP3",
MIME_TYPE_MAP_EXT => [EXTENSION_MP3],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP3],
],
MIME_TYPE_MP4_AUDIO => [
MIME_TYPE_MAP_NAME => "MP4 Audio",
MIME_TYPE_MAP_EXT => [EXTENSION_M4A],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP4_AUDIO,"audio/m4a"],
],
MIME_TYPE_MP4_VIDEO => [
MIME_TYPE_MAP_NAME => "MP4 Video",
MIME_TYPE_MAP_EXT => [EXTENSION_MP4,EXTENSION_M4V],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP4_VIDEO,'video/x-m4v'],
],
MIME_TYPE_MPEG => [
MIME_TYPE_MAP_NAME => "MPEG",
MIME_TYPE_MAP_EXT => [EXTENSION_MPG,EXTENSION_MPEG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MPEG],
],
MIME_TYPE_PDF => [
MIME_TYPE_MAP_NAME => "PDF",
MIME_TYPE_MAP_EXT => [EXTENSION_PDF],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PDF],
],
MIME_TYPE_PHP => [
MIME_TYPE_MAP_NAME => "PHP",
MIME_TYPE_MAP_EXT => [EXTENSION_PHP,EXTENSION_PHP5],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PHP],
],
MIME_TYPE_PNG => [
MIME_TYPE_MAP_NAME => "PNG",
MIME_TYPE_MAP_EXT => [EXTENSION_PNG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PNG],
],
MIME_TYPE_PSD => [
MIME_TYPE_MAP_NAME => "PSD",
MIME_TYPE_MAP_EXT => [EXTENSION_PSD],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PSD],
],
MIME_TYPE_OGG_AUDIO => [
MIME_TYPE_MAP_NAME => "Ogg Vorbis",
MIME_TYPE_MAP_EXT => [EXTENSION_OGG_AUDIO,EXTENSION_OGG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_OGG_AUDIO,MIME_TYPE_OGG],
],
MIME_TYPE_OGG_VIDEO => [
MIME_TYPE_MAP_NAME => "Ogg Theora",
MIME_TYPE_MAP_EXT => [EXTENSION_OGG_VIDEO],
MIME_TYPE_MAP_MIME => [MIME_TYPE_OGG_VIDEO],
],
MIME_TYPE_QUICKTIME => [
MIME_TYPE_MAP_NAME => "Quicktime",
MIME_TYPE_MAP_EXT => [EXTENSION_MOV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_QUICKTIME],
],
MIME_TYPE_RSS => [
MIME_TYPE_MAP_NAME => "RSS",
MIME_TYPE_MAP_EXT => [EXTENSION_RSS],
MIME_TYPE_MAP_MIME => [MIME_TYPE_RSS],
],
MIME_TYPE_SVG => [
MIME_TYPE_MAP_NAME => "SVG",
MIME_TYPE_MAP_EXT => [EXTENSION_SVG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_SVG],
],
MIME_TYPE_TAR => [
MIME_TYPE_MAP_NAME => "TAR",
MIME_TYPE_MAP_EXT => [EXTENSION_TAR],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TAR],
],
MIME_TYPE_TEXT => [
MIME_TYPE_MAP_NAME => "Text",
MIME_TYPE_MAP_EXT => [EXTENSION_TEXT, EXTENSION_ASC],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TEXT],
],
MIME_TYPE_TIFF => [
MIME_TYPE_MAP_NAME => "TIFF",
MIME_TYPE_MAP_EXT => [EXTENSION_TIF,EXTENSION_TIFF],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TIFF],
],
MIME_TYPE_WAV => [
MIME_TYPE_MAP_NAME => "Wave",
MIME_TYPE_MAP_EXT => [EXTENSION_WAV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_WAV],
],
MIME_TYPE_WEBM => [
MIME_TYPE_MAP_NAME => "WebM",
MIME_TYPE_MAP_EXT => [EXTENSION_WEBM],
MIME_TYPE_MAP_MIME => [MIME_TYPE_WEBM],
],
MIME_TYPE_WEBP => [
MIME_TYPE_MAP_NAME => "WebP",
MIME_TYPE_MAP_EXT => [EXTENSION_WEBP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_WEBP],
],
MIME_TYPE_XML => [
MIME_TYPE_MAP_NAME => "XML",
MIME_TYPE_MAP_EXT => [EXTENSION_XML],
MIME_TYPE_MAP_MIME => [MIME_TYPE_XML,MIME_TYPE_XML_APPLICATION],
],
MIME_TYPE_XSL => [
MIME_TYPE_MAP_NAME => "XSL",
MIME_TYPE_MAP_EXT => [EXTENSION_XSL],
MIME_TYPE_MAP_MIME => [MIME_TYPE_XSL],
],
MIME_TYPE_ZIP => [
MIME_TYPE_MAP_NAME => "ZIP",
MIME_TYPE_MAP_EXT => [EXTENSION_ZIP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ZIP],
],
];
/**
* Returns the mimetype that matches the provided extension.
*/
function get_mime_for_extension(string $ext): ?string
{
$ext = strtolower($ext);
foreach (MIME_TYPE_MAP as $key=>$value) {
if (in_array($ext, $value[MIME_TYPE_MAP_EXT])) {
return $key;
}
}
return null;
}
/**
* Returns the mimetype for the specified file, trying file inspection methods before falling back on extension-based detection.
* @param String $file
* @param String $ext The files extension, for if the current filename somehow lacks the extension
* @return String The extension that was found.
*/
function get_mime(string $file, string $ext=""): string
{
if (!file_exists($file)) {
throw new SCoreException("File not found: ".$file);
}
$type = false;
if (extension_loaded('fileinfo')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
try {
$type = finfo_file($finfo, $file);
} finally {
finfo_close($finfo);
}
} elseif (function_exists('mime_content_type')) {
// If anyone is still using mime_content_type()
$type = trim(mime_content_type($file));
}
if ($type===false || empty($type)) {
// Checking by extension is our last resort
if ($ext==null||strlen($ext) == 0) {
$ext = pathinfo($file, PATHINFO_EXTENSION);
}
$type = get_mime_for_extension($ext);
}
if ($type !== false && strlen($type) > 0) {
return $type;
}
return MIME_TYPE_OCTET_STREAM;
}
/**
* Returns the file extension associated with the specified mimetype.
*/
function get_extension(?string $mime_type): ?string
{
if (empty($mime_type)) {
return null;
}
if ($mime_type==MIME_TYPE_OCTET_STREAM) {
return null;
}
foreach (MIME_TYPE_MAP as $key=>$value) {
if (in_array($mime_type, $value[MIME_TYPE_MAP_MIME])) {
return $value[MIME_TYPE_MAP_EXT][0];
}
}
return null;
}
/**
* Returns all of the file extensions associated with the specified mimetype.
*/
function get_all_extension_for_mime(?string $mime_type): array
{
$output = [];
if (empty($mime_type)) {
return $output;
}
foreach (MIME_TYPE_MAP as $key=>$value) {
if (in_array($mime_type, $value[MIME_TYPE_MAP_MIME])) {
$output = array_merge($output, $value[MIME_TYPE_MAP_EXT]);
}
}
return $output;
}
/**
* Gets an the extension defined in MIME_TYPE_MAP for a file.
*
* @param String $file_path
* @return String The extension that was found, or null if one can not be found.
*/
function get_extension_for_file(String $file_path): ?String
{
$mime = get_mime($file_path);
if (!empty($mime)) {
if ($mime==MIME_TYPE_OCTET_STREAM) {
return null;
} else {
$ext = get_extension($mime);
}
if (!empty($ext)) {
return $ext;
}
}
return null;
}

View File

@ -1,17 +1,15 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/**
* An image is being added to the database.
*/
class ImageAdditionEvent extends Event
{
/** @var User */
public $user;
/** @var Image */
public $image;
public $merged = false;
public User $user;
public Image $image;
public bool $merged = false;
/**
* Inserts a new image into the database with its associated
@ -34,11 +32,8 @@ class ImageAdditionException extends SCoreException
*/
class ImageDeletionEvent extends Event
{
/** @var Image */
public $image;
/** @var bool */
public $force = false;
public Image $image;
public bool $force = false;
/**
* Deletes an image.
@ -59,10 +54,8 @@ class ImageDeletionEvent extends Event
*/
class ImageReplaceEvent extends Event
{
/** @var int */
public $id;
/** @var Image */
public $image;
public int $id;
public Image $image;
/**
* Replaces an image.
@ -88,24 +81,19 @@ class ImageReplaceException extends SCoreException
*/
class ThumbnailGenerationEvent extends Event
{
/** @var string */
public $hash;
/** @var string */
public $type;
/** @var bool */
public $force;
/** @var bool */
public $generated;
public string $hash;
public string $mime;
public bool $force;
public bool $generated;
/**
* Request a thumbnail be made for an image object
*/
public function __construct(string $hash, string $type, bool $force=false)
public function __construct(string $hash, string $mime, bool $force=false)
{
parent::__construct();
$this->hash = $hash;
$this->type = $type;
$this->mime = $mime;
$this->force = $force;
$this->generated = false;
}
@ -121,14 +109,10 @@ class ThumbnailGenerationEvent extends Event
*/
class ParseLinkTemplateEvent extends Event
{
/** @var string */
public $link;
/** @var string */
public $text;
/** @var string */
public $original;
/** @var Image */
public $image;
public string $link;
public string $text;
public string $original;
public Image $image;
public function __construct(string $link, Image $image)
{

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/**
* Class Image
*
@ -13,64 +15,31 @@ class Image
public const IMAGE_DIR = "images";
public const THUMBNAIL_DIR = "thumbs";
public static $order_sql = null; // this feels ugly
public ?int $id = null;
public int $height = 0;
public int $width = 0;
public string $hash;
public int $filesize;
public string $filename;
private string $ext;
private string $mime;
/** @var null|int */
public $id = null;
/** @var ?string[] */
public ?array $tag_array;
public int $owner_id;
public string $owner_ip;
public ?string $posted = null;
public ?string $source;
public bool $locked = false;
public ?bool $lossless = null;
public ?bool $video = null;
public ?string $video_codec = null;
public ?bool $image = null;
public ?bool $audio = null;
public ?int $length = null;
/** @var int */
public $height;
/** @var int */
public $width;
/** @var string */
public $hash;
/** @var int */
public $filesize;
/** @var string */
public $filename;
/** @var string */
public $ext;
/** @var string[]|null */
public $tag_array;
/** @var int */
public $owner_id;
/** @var string */
public $owner_ip;
/** @var string */
public $posted;
/** @var string */
public $source;
/** @var boolean */
public $locked = false;
/** @var boolean */
public $lossless = null;
/** @var boolean */
public $video = null;
/** @var boolean */
public $image = null;
/** @var boolean */
public $audio = null;
/** @var int */
public $length = null;
public static $bool_props = ["locked", "lossless", "video", "audio"];
public static $int_props = ["id", "owner_id", "height", "width", "filesize", "length"];
public static array $bool_props = ["locked", "lossless", "video", "audio", "image"];
public static array $int_props = ["id", "owner_id", "height", "width", "filesize", "length"];
/**
* One will very rarely construct an image directly, more common
@ -80,6 +49,10 @@ class Image
{
if (!is_null($row)) {
foreach ($row as $name => $value) {
if (is_numeric($name)) {
continue;
}
// some databases use table.name rather than name
$name = str_replace("images.", "", $name);
@ -137,7 +110,7 @@ class Image
private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags=[]): iterable
{
global $database, $user, $config;
global $database, $user;
if ($start < 0) {
$start = 0;
@ -148,17 +121,12 @@ class Image
if (SPEED_HAX) {
if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > 3) {
throw new SCoreException("Anonymous users may only search for up to 3 tags at a time");
throw new PermissionDeniedException("Anonymous users may only search for up to 3 tags at a time");
}
}
$order = (Image::$order_sql ?: "images.".$config->get_string(IndexConfig::ORDER));
$querylet = Image::build_search_querylet($tags, $order, $limit, $start);
$result = $database->get_all_iterable($querylet->sql, $querylet->variables);
Image::$order_sql = null;
return $result;
$querylet = Image::build_search_querylet($tags, $limit, $start);
return $database->get_all_iterable($querylet->sql, $querylet->variables);
}
/**
@ -213,27 +181,6 @@ class Image
);
}
/**
* Counts consecutive days of image uploads
*/
public static function count_upload_streak(): int
{
$now = date_create();
$last_date = $now;
foreach (self::find_images_iterable() as $img) {
$next_date = date_create($img->posted);
if (date_diff($next_date, $last_date)->days > 0) {
break;
}
$last_date = $next_date;
}
if ($last_date === $now) {
return 0;
}
$diff_d = ($now->getTimestamp() - $last_date->getTimestamp()) / 86400;
return (int)ceil($diff_d);
}
/**
* Count the number of image results for a given search
*
@ -244,11 +191,11 @@ class Image
global $cache, $database;
$tag_count = count($tags);
if ($tag_count === 0) {
if (SPEED_HAX && $tag_count === 0) {
// total number of images in the DB
$total = self::count_total_images();
} elseif ($tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) {
if (!startsWith($tags[0], "-")) {
} elseif (SPEED_HAX && $tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) {
if (!str_starts_with($tags[0], "-")) {
// one tag - we can look that up directly
$total = self::count_tag($tags[0]);
} else {
@ -294,14 +241,19 @@ class Image
{
$tag_conditions = [];
$img_conditions = [];
$stpen = 0; // search term parse event number
$order = null;
/*
* Turn a bunch of strings into a bunch of TagCondition
* and ImgCondition objects
*/
$stpe = send_event(new SearchTermParseEvent(null, $terms));
if ($stpe->is_querylet_set()) {
foreach ($stpe->get_querylets() as $querylet) {
/** @var $stpe SearchTermParseEvent */
$stpe = send_event(new SearchTermParseEvent($stpen++, null, $terms));
if ($stpe->order) {
$order = $stpe->order;
} elseif (!empty($stpe->querylets)) {
foreach ($stpe->querylets as $querylet) {
$img_conditions[] = new ImgCondition($querylet, true);
}
}
@ -316,9 +268,12 @@ class Image
continue;
}
$stpe = send_event(new SearchTermParseEvent($term, $terms));
if ($stpe->is_querylet_set()) {
foreach ($stpe->get_querylets() as $querylet) {
/** @var $stpe SearchTermParseEvent */
$stpe = send_event(new SearchTermParseEvent($stpen++, $term, $terms));
if ($stpe->order) {
$order = $stpe->order;
} elseif (!empty($stpe->querylets)) {
foreach ($stpe->querylets as $querylet) {
$img_conditions[] = new ImgCondition($querylet, $positive);
}
} else {
@ -328,7 +283,7 @@ class Image
}
}
}
return [$tag_conditions, $img_conditions];
return [$tag_conditions, $img_conditions, $order];
}
/*
@ -365,8 +320,9 @@ class Image
');
} else {
$tags[] = 'id'. $gtlt . $this->id;
$tags[] = 'order:id_'. strtolower($dir);
$querylet = Image::build_search_querylet($tags);
$querylet->append_sql(' ORDER BY images.id '.$dir.' LIMIT 1');
$querylet->append_sql(' LIMIT 1');
$row = $database->get_row($querylet->sql, $querylet->variables);
}
@ -403,7 +359,7 @@ class Image
SET owner_id=:owner_id
WHERE id=:id
", ["owner_id"=>$owner->id, "id"=>$this->id]);
log_info("core_image", "Owner for Image #{$this->id} set to {$owner->name}");
log_info("core_image", "Owner for Post #{$this->id} set to {$owner->name}");
}
}
@ -412,27 +368,32 @@ class Image
global $database, $user;
$cut_name = substr($this->filename, 0, 255);
if (is_null($this->posted) || $this->posted == "") {
$this->posted = date('c', time());
}
if (is_null($this->id)) {
$database->execute(
"INSERT INTO images(
owner_id, owner_ip,
filename, filesize,
hash, ext,
hash, mime, ext,
width, height,
posted, source
)
VALUES (
:owner_id, :owner_ip,
:filename, :filesize,
:hash, :ext,
:hash, :mime, :ext,
0, 0,
now(), :source
:posted, :source
)",
[
"owner_id" => $user->id, "owner_ip" => $_SERVER['REMOTE_ADDR'],
"owner_id" => $user->id, "owner_ip" => get_real_ip(),
"filename" => $cut_name, "filesize" => $this->filesize,
"hash" => $this->hash, "ext" => strtolower($this->ext),
"source" => $this->source
"hash" => $this->hash, "mime" => strtolower($this->mime),
"ext" => strtolower($this->ext),
"posted" => $this->posted, "source" => $this->source
]
);
$this->id = $database->get_last_insert_id('images_id_seq');
@ -440,13 +401,16 @@ class Image
$database->execute(
"UPDATE images SET ".
"filename = :filename, filesize = :filesize, hash = :hash, ".
"ext = :ext, width = 0, height = 0, source = :source ".
"mime = :mime, ext = :ext, width = 0, height = 0, ".
"posted = :posted, source = :source ".
"WHERE id = :id",
[
"filename" => $cut_name,
"filesize" => $this->filesize,
"hash" => $this->hash,
"mime" => strtolower($this->mime),
"ext" => strtolower($this->ext),
"posted" => $this->posted,
"source" => $this->source,
"id" => $this->id,
]
@ -456,17 +420,18 @@ class Image
$database->execute(
"UPDATE images SET ".
"lossless = :lossless, ".
"video = :video, audio = :audio,image = :image, ".
"video = :video, video_codec = :video_codec, audio = :audio,image = :image, ".
"height = :height, width = :width, ".
"length = :length WHERE id = :id",
[
"id" => $this->id,
"width" => $this->width ?? 0,
"height" => $this->height ?? 0,
"lossless" => $database->scoresql_value_prepare($this->lossless),
"video" => $database->scoresql_value_prepare($this->video),
"image" => $database->scoresql_value_prepare($this->image),
"audio" => $database->scoresql_value_prepare($this->audio),
"lossless" => $this->lossless,
"video" => $this->video,
"video_codec" => $this->video_codec,
"image" => $this->image,
"audio" => $this->audio,
"length" => $this->length
]
);
@ -488,6 +453,7 @@ class Image
WHERE image_id=:id
ORDER BY tag
", ["id"=>$this->id]);
sort($this->tag_array);
}
return $this->tag_array;
}
@ -524,7 +490,8 @@ class Image
public function get_thumb_link(): string
{
global $config;
$ext = $config->get_string(ImageConfig::THUMB_TYPE);
$mime = $config->get_string(ImageConfig::THUMB_MIME);
$ext = FileExtension::get_for_mime($mime);
return $this->get_link(ImageConfig::TLINK, '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext);
}
@ -538,7 +505,7 @@ class Image
$image_link = $config->get_string($template);
if (!empty($image_link)) {
if (!(strpos($image_link, "://") > 0) && !startsWith($image_link, "/")) {
if (!str_contains($image_link, "://") && !str_starts_with($image_link, "/")) {
$image_link = make_link($image_link);
}
$chosen = $image_link;
@ -562,6 +529,19 @@ class Image
return $plte->text;
}
/**
* Get the info for this image, formatted according to the
* configured template.
*/
public function get_info(): string
{
global $config;
$plte = new ParseLinkTemplateEvent($config->get_string(ImageConfig::INFO), $this);
send_event($plte);
return $plte->text;
}
/**
* Figure out where the full size image is on disk.
*/
@ -587,21 +567,40 @@ class Image
}
/**
* Get the image's mime type.
*/
public function get_mime_type(): string
{
return get_mime($this->get_image_filename(), $this->get_ext());
}
/**
* Get the image's filename extension
* Get the image's extension.
*/
public function get_ext(): string
{
return $this->ext;
}
/**
* Get the image's mime type.
*/
public function get_mime(): ?string
{
if ($this->mime===MimeType::WEBP&&$this->lossless) {
return MimeType::WEBP_LOSSLESS;
}
$m = $this->mime;
if (is_null($m)) {
$m = MimeMap::get_for_extension($this->ext)[0];
}
return $m;
}
/**
* Set the image's mime type.
*/
public function set_mime($mime): void
{
$this->mime = $mime;
$ext = FileExtension::get_for_mime($this->get_mime());
assert($ext != null);
$this->ext = $ext;
}
/**
* Get the image's source URL
*/
@ -622,7 +621,7 @@ class Image
}
if ($new_source != $old_source) {
$database->execute("UPDATE images SET source=:source WHERE id=:id", ["source"=>$new_source, "id"=>$this->id]);
log_info("core_image", "Source for Image #{$this->id} set to: $new_source (was $old_source)");
log_info("core_image", "Source for Post #{$this->id} set to: $new_source (was $old_source)");
}
}
@ -634,16 +633,12 @@ class Image
return $this->locked;
}
public function set_locked(bool $tf): void
public function set_locked(bool $locked): void
{
global $database;
$ln = $tf ? "Y" : "N";
$sln = $database->scoreql_to_sql('SCORE_BOOL_'.$ln);
$sln = str_replace("'", "", $sln);
$sln = str_replace('"', "", $sln);
if (bool_escape($sln) !== $this->locked) {
$database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn"=>$sln, "id"=>$this->id]);
log_info("core_image", "Setting Image #{$this->id} lock to: $ln");
if ($locked !== $this->locked) {
$database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn"=>$locked, "id"=>$this->id]);
log_info("core_image", "Setting Post #{$this->id} lock to: $locked");
}
}
@ -698,7 +693,7 @@ class Image
$page->flash("Can't set a tag longer than 255 characters");
continue;
}
if (startsWith($tag, "-")) {
if (str_starts_with($tag, "-")) {
$page->flash("Can't set a tag which starts with a minus");
continue;
}
@ -760,7 +755,7 @@ class Image
);
}
log_info("core_image", "Tags for Image #{$this->id} set to: ".Tag::implode($tags));
log_info("core_image", "Tags for Post #{$this->id} set to: ".Tag::implode($tags));
$cache->delete("image-{$this->id}-tags");
}
}
@ -773,7 +768,7 @@ class Image
global $database;
$this->delete_tags_from_image();
$database->execute("DELETE FROM images WHERE id=:id", ["id"=>$this->id]);
log_info("core_image", 'Deleted Image #'.$this->id.' ('.$this->hash.')');
log_info("core_image", 'Deleted Post #'.$this->id.' ('.$this->hash.')');
unlink($this->get_image_filename());
unlink($this->get_thumb_filename());
@ -785,7 +780,7 @@ class Image
*/
public function remove_image_only(): void
{
log_info("core_image", 'Removed Image File ('.$this->hash.')');
log_info("core_image", 'Removed Post File ('.$this->hash.')');
@unlink($this->get_image_filename());
@unlink($this->get_thumb_filename());
}
@ -811,12 +806,14 @@ class Image
* #param string[] $terms
*/
private static function build_search_querylet(
array $tags,
?string $order=null,
array $terms,
?int $limit=null,
?int $offset=null
): Querylet {
list($tag_conditions, $img_conditions) = self::terms_to_conditions($tags);
global $config;
list($tag_conditions, $img_conditions, $order) = self::terms_to_conditions($terms);
$order = ($order ?: "images.".$config->get_string(IndexConfig::ORDER));
$positive_tag_count = 0;
$negative_tag_count = 0;
@ -894,12 +891,14 @@ class Image
$positive_tag_id_array = [];
$positive_wildcard_id_array = [];
$negative_tag_id_array = [];
$all_nonexistent_negatives = true;
foreach ($tag_conditions as $tq) {
$tag_ids = self::tag_or_wildcard_to_ids($tq->tag);
$tag_count = count($tag_ids);
if ($tq->positive) {
$all_nonexistent_negatives = false;
if ($tag_count== 0) {
# one of the positive tags had zero results, therefor there
# can be no results; "where 1=0" should shortcut things
@ -913,14 +912,20 @@ class Image
$positive_wildcard_id_array[] = $tag_ids;
}
} else {
// Unlike positive criteria, negative criteria are all handled in an OR fashion,
// so we can just compile them all into a single sub-query.
$negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids);
if ($tag_count > 0) {
$all_nonexistent_negatives = false;
// Unlike positive criteria, negative criteria are all handled in an OR fashion,
// so we can just compile them all into a single sub-query.
$negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids);
}
}
}
assert($positive_tag_id_array || $positive_wildcard_id_array || $negative_tag_id_array, @$_GET['q']);
if (!empty($positive_tag_id_array) || !empty($positive_wildcard_id_array)) {
assert($positive_tag_id_array || $positive_wildcard_id_array || $negative_tag_id_array || $all_nonexistent_negatives, @$_GET['q']);
if ($all_nonexistent_negatives) {
$query = new Querylet("SELECT images.* FROM images WHERE 1=1");
} elseif (!empty($positive_tag_id_array) || !empty($positive_wildcard_id_array)) {
$inner_joins = [];
if (!empty($positive_tag_id_array)) {
foreach ($positive_tag_id_array as $tag) {

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Misc functions *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@ -33,11 +35,6 @@ function add_dir(string $base): array
/**
* Sends a DataUploadEvent for a file.
*
* @param string $tmpname
* @param string $filename
* @param string $tags
* @throws UploadException
*/
function add_image(string $tmpname, string $filename, string $tags): int
{
@ -75,7 +72,12 @@ function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_sca
$fit = $config->get_string(ImageConfig::THUMB_FIT);
if (in_array($fit, [Media::RESIZE_TYPE_FILL, Media::RESIZE_TYPE_STRETCH, Media::RESIZE_TYPE_FIT_BLUR])) {
if (in_array($fit, [
Media::RESIZE_TYPE_FILL,
Media::RESIZE_TYPE_STRETCH,
Media::RESIZE_TYPE_FIT_BLUR,
Media::RESIZE_TYPE_FIT_BLUR_PORTRAIT
])) {
return [$config->get_int(ImageConfig::THUMB_WIDTH), $config->get_int(ImageConfig::THUMB_HEIGHT)];
}
@ -110,7 +112,7 @@ function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_sca
}
}
function get_scaled_by_aspect_ratio(int $original_width, int $original_height, int $max_width, int $max_height) : array
function get_scaled_by_aspect_ratio(int $original_width, int $original_height, int $max_width, int $max_height): array
{
$xscale = ($max_width/ $original_width);
$yscale = ($max_height/ $original_height);
@ -136,7 +138,7 @@ function get_thumbnail_max_size_scaled(): array
}
function create_image_thumb(string $hash, string $type, string $engine = null)
function create_image_thumb(string $hash, string $mime, string $engine = null)
{
global $config;
@ -147,7 +149,7 @@ function create_image_thumb(string $hash, string $type, string $engine = null)
$inname,
$outname,
$tsize,
$type,
$mime,
$engine,
$config->get_string(ImageConfig::THUMB_FIT)
);
@ -155,7 +157,7 @@ function create_image_thumb(string $hash, string $type, string $engine = null)
function create_scaled_image(string $inname, string $outname, array $tsize, string $type, ?string $engine = null, ?string $resize_type = null)
function create_scaled_image(string $inname, string $outname, array $tsize, string $mime, ?string $engine = null, ?string $resize_type = null)
{
global $config;
if (empty($engine)) {
@ -165,22 +167,44 @@ function create_scaled_image(string $inname, string $outname, array $tsize, stri
$resize_type = $config->get_string(ImageConfig::THUMB_FIT);
}
$output_format = $config->get_string(ImageConfig::THUMB_TYPE);
if ($output_format==EXTENSION_WEBP) {
$output_format = Media::WEBP_LOSSY;
}
$output_mime = $config->get_string(ImageConfig::THUMB_MIME);
send_event(new MediaResizeEvent(
$engine,
$inname,
$type,
$mime,
$outname,
$tsize[0],
$tsize[1],
$resize_type,
$output_format,
$output_mime,
$config->get_string(ImageConfig::THUMB_ALPHA_COLOR),
$config->get_int(ImageConfig::THUMB_QUALITY),
true,
true
));
}
function redirect_to_next_image(Image $image): void
{
global $page;
if (isset($_GET['search'])) {
$search_terms = Tag::explode(Tag::decaret($_GET['search']));
$query = "search=" . url_escape($_GET['search']);
} else {
$search_terms = [];
$query = null;
}
$target_image = $image->get_next($search_terms);
if ($target_image == null) {
$redirect_target = referer_or(make_link("post/list"), ['post/view']);
} else {
$redirect_target = make_link("post/view/{$target_image->id}", null, $query);
}
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($redirect_target);
}

View File

@ -1,10 +1,10 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class Querylet
{
/** @var string */
public $sql;
/** @var array */
public $variables;
public string $sql;
public array $variables;
public function __construct(string $sql, array $variables=[])
{
@ -31,10 +31,8 @@ class Querylet
class TagCondition
{
/** @var string */
public $tag;
/** @var bool */
public $positive;
public string $tag;
public bool $positive;
public function __construct(string $tag, bool $positive)
{
@ -45,10 +43,8 @@ class TagCondition
class ImgCondition
{
/** @var Querylet */
public $qlet;
/** @var bool */
public $positive;
public Querylet $qlet;
public bool $positive;
public function __construct(Querylet $qlet, bool $positive)
{

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/**
* Class Tag
*

View File

@ -49,7 +49,6 @@ function get_dsn()
{
if (getenv("INSTALL_DSN")) {
$dsn = getenv("INSTALL_DSN");
;
} elseif (@$_POST["database_type"] == DatabaseDriver::SQLITE) {
/** @noinspection PhpUnhandledExceptionInspection */
$id = bin2hex(random_bytes(5));
@ -69,7 +68,7 @@ function do_install($dsn)
create_tables(new Database($dsn));
write_config($dsn);
} catch (InstallerException $e) {
die_nicely($e->title, $e->body, $e->code);
die_nicely($e->title, $e->body, $e->exit_code);
}
}
@ -110,8 +109,8 @@ function ask_questions()
";
}
$db_m = in_array(DatabaseDriver::MYSQL, $drivers) ? '<option value="'. DatabaseDriver::MYSQL .'">MySQL</option>' : "";
$db_p = in_array(DatabaseDriver::PGSQL, $drivers) ? '<option value="'. DatabaseDriver::PGSQL .'">PostgreSQL</option>' : "";
$db_m = in_array(DatabaseDriver::MYSQL, $drivers) ? '<option value="'. DatabaseDriver::MYSQL .'">MySQL</option>' : "";
$db_p = in_array(DatabaseDriver::PGSQL, $drivers) ? '<option value="'. DatabaseDriver::PGSQL .'">PostgreSQL</option>' : "";
$db_s = in_array(DatabaseDriver::SQLITE, $drivers) ? '<option value="'. DatabaseDriver::SQLITE .'">SQLite</option>' : "";
$warn_msg = $warnings ? "<h3>Warnings</h3>".implode("\n<p>", $warnings) : "";
@ -256,7 +255,7 @@ function create_tables(Database $db)
width INTEGER NOT NULL,
height INTEGER NOT NULL,
posted TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
locked SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N,
locked BOOLEAN NOT NULL DEFAULT FALSE,
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT
");
$db->execute("CREATE INDEX images_owner_id_idx ON images(owner_id)", []);
@ -282,13 +281,19 @@ function create_tables(Database $db)
$db->execute("CREATE INDEX images_tags_tag_id_idx ON image_tags(tag_id)", []);
$db->execute("INSERT INTO config(name, value) VALUES('db_version', 11)");
$db->commit();
// mysql auto-commits when creating a table, so the transaction
// is closed; other databases need to commit
if ($db->is_transaction_open()) {
$db->commit();
}
} catch (PDOException $e) {
throw new InstallerException(
"PDO Error:",
"<p>An error occurred while trying to create the database tables necessary for Shimmie.</p>
<p>Please check and ensure that the database configuration options are all correct.</p>
<p>{$e->getMessage()}</p>",
<p>{$e->getMessage()}</p>
",
3
);
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Logging convenience *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
// action_object_attribute
// action = create / view / edit / delete
@ -7,6 +9,9 @@ abstract class Permissions
{
public const CHANGE_SETTING = "change_setting"; # modify web-level settings, eg the config table
public const OVERRIDE_CONFIG = "override_config"; # modify sys-level settings, eg shimmie.conf.php
public const CHANGE_USER_SETTING = "change_user_setting"; # modify own user-level settings
public const CHANGE_OTHER_USER_SETTING = "change_other_user_setting"; # modify own user-level settings
public const BIG_SEARCH = "big_search"; # search for more than 3 tags at once (speed mode only)
public const MANAGE_EXTENSION_LIST = "manage_extension_list";
@ -100,6 +105,7 @@ abstract class Permissions
public const SET_PRIVATE_IMAGE = "set_private_image";
public const SET_OTHERS_PRIVATE_IMAGES = "set_others_private_images";
public const CRON_RUN = "cron_run";
public const BULK_IMPORT = "bulk_import";
public const BULK_EXPORT = "bulk_export";
public const BULK_DOWNLOAD = "bulk_download";

View File

@ -1,11 +1,10 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Things which should be in the core API *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
require_once "filetypes.php";
/**
* Return the unique elements of an array, case insensitively
*/
@ -127,10 +126,16 @@ function list_files(string $base, string $_sub_dir=""): array
$files = [];
$dir = opendir("$base/$_sub_dir");
while ($f = readdir($dir)) {
$files[] = $f;
if ($dir===false) {
throw new SCoreException("Unable to open directory $base/$_sub_dir");
}
try {
while ($f = readdir($dir)) {
$files[] = $f;
}
} finally {
closedir($dir);
}
closedir($dir);
sort($files);
foreach ($files as $filename) {
@ -187,8 +192,8 @@ function stream_file(string $file, int $start, int $end): void
}
}
if (!function_exists('http_parse_headers')) { #http://www.php.net/manual/en/function.http-parse-headers.php#112917
# http://www.php.net/manual/en/function.http-parse-headers.php#112917
if (!function_exists('http_parse_headers')) {
/**
* #return string[]
*/
@ -219,7 +224,7 @@ if (!function_exists('http_parse_headers')) { #http://www.php.net/manual/en/func
* HTTP Headers can sometimes be lowercase which will cause issues.
* In cases like these, we need to make sure to check for them if the camelcase version does not exist.
*/
function findHeader(array $headers, string $name): ?string
function find_header(array $headers, string $name): ?string
{
if (!is_array($headers)) {
return null;
@ -243,21 +248,22 @@ function findHeader(array $headers, string $name): ?string
if (!function_exists('mb_strlen')) {
// TODO: we should warn the admin that they are missing multibyte support
function mb_strlen($str, $encoding)
/** @noinspection PhpUnusedParameterInspection */
function mb_strlen($str, $encoding): int
{
return strlen($str);
}
function mb_internal_encoding($encoding)
function mb_internal_encoding($encoding): void
{
}
function mb_strtolower($str)
function mb_strtolower($str): string
{
return strtolower($str);
}
}
/** @noinspection PhpUnhandledExceptionInspection */
function getSubclassesOf(string $parent)
function get_subclasses_of(string $parent): array
{
$result = [];
foreach (get_declared_classes() as $class) {
@ -324,13 +330,13 @@ function get_base_href(): string
/**
* The opposite of the standard library's parse_url
*/
function unparse_url($parsed_url)
function unparse_url(array $parsed_url): string
{
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? "$pass@" : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$query = !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
@ -338,17 +344,26 @@ function unparse_url($parsed_url)
return "$scheme$user$pass$host$port$path$query$fragment";
}
function startsWith(string $haystack, string $needle): bool
{
$length = strlen($needle);
return (substr($haystack, 0, $length) === $needle);
# finally in the core library starting from php8
if (!function_exists('str_starts_with')) {
function str_starts_with(string $haystack, string $needle): bool
{
return strncmp($haystack, $needle, strlen($needle)) === 0;
}
}
function endsWith(string $haystack, string $needle): bool
{
$length = strlen($needle);
$start = $length * -1; //negative
return (substr($haystack, $start) === $needle);
if (!function_exists('str_ends_with')) {
function str_ends_with(string $haystack, string $needle): bool
{
return $needle === '' || $needle === substr($haystack, - strlen($needle));
}
}
if (!function_exists('str_contains')) {
function str_contains(string $haystack, string $needle): bool
{
return '' === $needle || false !== strpos($haystack, $needle);
}
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
@ -475,25 +490,6 @@ function clamp(?int $val, ?int $min=null, ?int $max=null): int
return $val;
}
function xml_tag(string $name, array $attrs=[], array $children=[]): string
{
$xml = "<$name ";
foreach ($attrs as $k => $v) {
$xv = str_replace('&#039;', '&apos;', htmlspecialchars((string)$v, ENT_QUOTES));
$xml .= "$k=\"$xv\" ";
}
if (count($children) > 0) {
$xml .= ">\n";
foreach ($children as $child) {
$xml .= xml_tag($child);
}
$xml .= "</$name>\n";
} else {
$xml .= "/>\n";
}
return $xml;
}
/**
* Original PHP code by Chirp Internet: www.chirp.com.au
* Please acknowledge use of this code by including this header.
@ -532,7 +528,6 @@ function parse_shorthand_int(string $limit): int
/** @noinspection PhpMissingBreakStatementInspection */
// no break
case 'm': $value *= 1024; // fall through
/** @noinspection PhpMissingBreakStatementInspection */
// no break
case 'k': $value *= 1024; break;
default: $value = -1;
@ -563,17 +558,41 @@ function to_shorthand_int(int $int): string
return (string)$int;
}
}
const TIME_UNITS = ["s"=>60,"m"=>60,"h"=>24,"d"=>365,"y"=>PHP_INT_MAX];
function format_milliseconds(int $input): string
abstract class TIME_UNITS
{
public const MILLISECONDS = "ms";
public const SECONDS = "s";
public const MINUTES = "m";
public const HOURS = "h";
public const DAYS = "d";
public const YEARS = "y";
public const CONVERSION = [
self::MILLISECONDS=>1000,
self::SECONDS=>60,
self::MINUTES=>60,
self::HOURS=>24,
self::DAYS=>365,
self::YEARS=>PHP_INT_MAX
];
}
function format_milliseconds(int $input, string $min_unit = TIME_UNITS::SECONDS): string
{
$output = "";
$remainder = floor($input / 1000);
$remainder = $input;
foreach (TIME_UNITS as $unit=>$conversion) {
$found = false;
foreach (TIME_UNITS::CONVERSION as $unit=>$conversion) {
$count = $remainder % $conversion;
$remainder = floor($remainder / $conversion);
if ($found||$unit==$min_unit) {
$found = true;
} else {
continue;
}
if ($count==0&&$remainder<1) {
break;
}
@ -582,6 +601,32 @@ function format_milliseconds(int $input): string
return trim($output);
}
function parse_to_milliseconds(string $input): int
{
$output = 0;
$current_multiplier = 1;
if (preg_match('/^([0-9]+)$/i', $input, $match)) {
// If just a number, then we treat it as milliseconds
$length = $match[0];
if (is_numeric($length)) {
$length = floatval($length);
$output += $length;
}
} else {
foreach (TIME_UNITS::CONVERSION as $unit=>$conversion) {
if (preg_match('/([0-9]+)'.$unit.'/i', $input, $match)) {
$length = $match[1];
if (is_numeric($length)) {
$length = floatval($length);
$output += $length * $current_multiplier;
}
}
$current_multiplier *= $conversion;
}
}
return intval($output);
}
/**
* Turn a date into a time, a date, an "X minutes ago...", etc
@ -757,7 +802,7 @@ function iterator_map_to_array(callable $callback, iterator $iter): array
return iterator_to_array(iterator_map($callback, $iter));
}
function stringer($s)
function stringer($s): string
{
if (is_array($s)) {
if (isset($s[0])) {

View File

@ -1,42 +1,11 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/*
* A small number of PHP-sanity things (eg don't silently ignore errors) to
* be included right at the very start of index.php and tests/bootstrap.php
*/
$min_php = "7.3";
if (version_compare(phpversion(), $min_php, ">=") === false) {
print "
Shimmie does not support versions of PHP lower than $min_php
(PHP reports that it is version ".phpversion().").
If your web host is running an older version, they are dangerously out of
date and you should plan on moving elsewhere.
";
exit;
}
# ini_set('zend.assertions', '1'); // generate assertions
ini_set('assert.exception', '1'); // throw exceptions when failed
set_error_handler(function ($errNo, $errStr) {
// Should we turn ALL notices into errors? PHP allows a lot of
// terrible things to happen by default...
if (strpos($errStr, 'Use of undefined constant ') === 0) {
throw new Exception("PHP Error#$errNo: $errStr");
} else {
return false;
}
});
ob_start();
if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') {
if (isset($_SERVER['REMOTE_ADDR'])) {
die("CLI with remote addr? Confused, not taking the risk.");
}
$_SERVER['REMOTE_ADDR'] = "0.0.0.0";
$_SERVER['HTTP_HOST'] = "<cli command>";
}
function die_nicely($title, $body, $code=0)
{
print("<!DOCTYPE html>
@ -61,3 +30,33 @@ function die_nicely($title, $body, $code=0)
}
exit($code);
}
$min_php = "7.3";
if (version_compare(phpversion(), $min_php, ">=") === false) {
die_nicely("Not Supported", "
Shimmie does not support versions of PHP lower than $min_php
(PHP reports that it is version ".phpversion().").
", 1);
}
# ini_set('zend.assertions', '1'); // generate assertions
ini_set('assert.exception', '1'); // throw exceptions when failed
set_error_handler(function ($errNo, $errStr) {
// Should we turn ALL notices into errors? PHP allows a lot of
// terrible things to happen by default...
if (str_starts_with($errStr, 'Use of undefined constant ')) {
throw new Exception("PHP Error#$errNo: $errStr");
} else {
return false;
}
});
ob_start();
if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') {
if (isset($_SERVER['REMOTE_ADDR'])) {
die("CLI with remote addr? Confused, not taking the risk.");
}
$_SERVER['REMOTE_ADDR'] = "0.0.0.0";
$_SERVER['HTTP_HOST'] = "cli-command";
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Event API *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@ -35,7 +37,7 @@ function _set_event_listeners(): void
global $_shm_event_listeners;
$_shm_event_listeners = [];
foreach (getSubclassesOf("Extension") as $class) {
foreach (get_subclasses_of("Extension") as $class) {
/** @var Extension $extension */
$extension = new $class();
@ -61,7 +63,7 @@ function _dump_event_listeners(array $event_listeners, string $path): void
{
$p = "<"."?php\n";
foreach (getSubclassesOf("Extension") as $class) {
foreach (get_subclasses_of("Extension") as $class) {
$p .= "\$$class = new $class(); ";
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/**
* For any values that aren't defined in data/config/*.php,
* Shimmie will set the values to their defaults
@ -19,16 +21,17 @@ function _d(string $name, $value): void
}
}
$_g = file_exists(".git") ? '+' : '';
_d("DATABASE_DSN", null); // string PDO database connection details
_d("DATABASE_TIMEOUT", 10000);// int Time to wait for each statement to complete
_d("CACHE_DSN", null); // string cache connection details
_d("DEBUG", false); // boolean print various debugging details
_d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes
_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
_d("VERSION", "2.8.4$_g"); // string shimmie version
_d("TIMEZONE", null); // string timezone
_d("EXTRA_EXTS", ""); // string optional extra extensions
_d("BASE_HREF", null); // string force a specific base URL (default is auto-detect)
_d("TRACE_FILE", null); // string file to log performance data into
_d("TRACE_THRESHOLD", 0.0); // float log pages which take more time than this many seconds
_d("DATABASE_DSN", null); // string PDO database connection details
_d("DATABASE_TIMEOUT", 10000); // int Time to wait for each statement to complete
_d("CACHE_DSN", null); // string cache connection details
_d("DEBUG", false); // boolean print various debugging details
_d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes
_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
_d("VERSION", "2.9.1$_g"); // string shimmie version
_d("TIMEZONE", null); // string timezone
_d("EXTRA_EXTS", ""); // string optional extra extensions
_d("BASE_HREF", null); // string force a specific base URL (default is auto-detect)
_d("TRACE_FILE", null); // string file to log performance data into
_d("TRACE_THRESHOLD", 0.0); // float log pages which take more time than this many seconds
_d("REVERSE_PROXY_X_HEADERS", false); // boolean get request IPs from "X-Real-IP" and protocol from "X-Forwarded-Proto" HTTP headers

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
@ -9,13 +11,13 @@ class PolyfillsTest extends TestCase
public function test_html_escape()
{
$this->assertEquals(
html_escape("Foo & <waffles>"),
"Foo &amp; &lt;waffles&gt;"
"Foo &amp; &lt;main&gt;",
html_escape("Foo & <main>")
);
$this->assertEquals(
html_unescape("Foo &amp; &lt;waffles&gt;"),
"Foo & <waffles>"
"Foo & <main>",
html_unescape("Foo &amp; &lt;main&gt;")
);
$x = "Foo &amp; &lt;waffles&gt;";
@ -24,17 +26,17 @@ class PolyfillsTest extends TestCase
public function test_int_escape()
{
$this->assertEquals(int_escape(""), 0);
$this->assertEquals(int_escape("1"), 1);
$this->assertEquals(int_escape("-1"), -1);
$this->assertEquals(int_escape("-1.5"), -1);
$this->assertEquals(int_escape(null), 0);
$this->assertEquals(0, int_escape(""));
$this->assertEquals(1, int_escape("1"));
$this->assertEquals(-1, int_escape("-1"));
$this->assertEquals(-1, int_escape("-1.5"));
$this->assertEquals(0, int_escape(null));
}
public function test_url_escape()
{
$this->assertEquals(url_escape("^\o/^"), "%5E%5Co%2F%5E");
$this->assertEquals(url_escape(null), "");
$this->assertEquals("%5E%5Co%2F%5E", url_escape("^\o/^"));
$this->assertEquals("", url_escape(null));
}
public function test_bool_escape()
@ -69,41 +71,33 @@ class PolyfillsTest extends TestCase
public function test_clamp()
{
$this->assertEquals(clamp(0, 5, 10), 5);
$this->assertEquals(clamp(5, 5, 10), 5);
$this->assertEquals(clamp(7, 5, 10), 7);
$this->assertEquals(clamp(10, 5, 10), 10);
$this->assertEquals(clamp(15, 5, 10), 10);
}
public function test_xml_tag()
{
$this->assertEquals(
"<test foo=\"bar\" >\n<cake />\n</test>\n",
xml_tag("test", ["foo"=>"bar"], ["cake"])
);
$this->assertEquals(5, clamp(0, 5, 10));
$this->assertEquals(5, clamp(5, 5, 10));
$this->assertEquals(7, clamp(7, 5, 10));
$this->assertEquals(10, clamp(10, 5, 10));
$this->assertEquals(10, clamp(15, 5, 10));
}
public function test_truncate()
{
$this->assertEquals(truncate("test words", 10), "test words");
$this->assertEquals(truncate("test...", 9), "test...");
$this->assertEquals(truncate("test...", 6), "test...");
$this->assertEquals(truncate("te...", 2), "te...");
$this->assertEquals("test words", truncate("test words", 10));
$this->assertEquals("test...", truncate("test...", 9));
$this->assertEquals("test...", truncate("test...", 6));
$this->assertEquals("te...", truncate("te...", 2));
}
public function test_to_shorthand_int()
{
$this->assertEquals(to_shorthand_int(1231231231), "1.1GB");
$this->assertEquals(to_shorthand_int(2), "2");
$this->assertEquals("1.1GB", to_shorthand_int(1231231231));
$this->assertEquals("2", to_shorthand_int(2));
}
public function test_parse_shorthand_int()
{
$this->assertEquals(parse_shorthand_int("foo"), -1);
$this->assertEquals(parse_shorthand_int("32M"), 33554432);
$this->assertEquals(parse_shorthand_int("43.4KB"), 44441);
$this->assertEquals(parse_shorthand_int("1231231231"), 1231231231);
$this->assertEquals(-1, parse_shorthand_int("foo"));
$this->assertEquals(33554432, parse_shorthand_int("32M"));
$this->assertEquals(44441, parse_shorthand_int("43.4KB"));
$this->assertEquals(1231231231, parse_shorthand_int("1231231231"));
}
public function test_format_milliseconds()
@ -113,6 +107,13 @@ class PolyfillsTest extends TestCase
$this->assertEquals("1y 213d 16h 53m 20s", format_milliseconds(50000000000));
}
public function test_parse_to_milliseconds()
{
$this->assertEquals(10, parse_to_milliseconds("10"));
$this->assertEquals(5000, parse_to_milliseconds("5s"));
$this->assertEquals(50000000000, parse_to_milliseconds("1y 213d 16h 53m 20s"));
}
public function test_autodate()
{
$this->assertEquals(

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
@ -43,13 +45,13 @@ class UrlsTest extends TestCase
{
// relative to shimmie install
$this->assertEquals(
"http://<cli command>/test/foo",
"http://cli-command/test/foo",
make_http("foo")
);
// relative to web server
$this->assertEquals(
"http://<cli command>/foo",
"http://cli-command/foo",
make_http("/foo")
);

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;

View File

@ -1,9 +1,11 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class Link
{
public $page;
public $query;
public ?string $page;
public ?string $query;
public function __construct(?string $page=null, ?string $query=null)
{
@ -79,7 +81,7 @@ function modify_url(string $url, array $changes): string
*/
function make_http(string $link): string
{
if (strpos($link, "://") > 0) {
if (str_contains($link, "://")) {
return $link;
}
@ -105,7 +107,7 @@ function referer_or(string $dest, ?array $blacklist=null): string
}
if ($blacklist) {
foreach ($blacklist as $b) {
if (strstr($_SERVER['HTTP_REFERER'], $b)) {
if (str_contains($_SERVER['HTTP_REFERER'], $b)) {
return $dest;
}
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
function _new_user(array $row): User
{
@ -15,22 +17,12 @@ function _new_user(array $row): User
*/
class User
{
/** @var int */
public $id;
/** @var string */
public $name;
/** @var string */
public $email;
public $join_date;
/** @var string */
public $passhash;
/** @var UserClass */
public $class;
public int $id;
public string $name;
public ?string $email;
public string $join_date;
public ?string $passhash;
public UserClass $class;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Initialisation *
@ -107,7 +99,7 @@ class User
{
$u = User::by_name($name);
if (is_null($u)) {
throw new ScoreException("Can't find any user named $name");
throw new UserDoesNotExist("Can't find any user named $name");
} else {
return $u->id;
}
@ -118,7 +110,7 @@ class User
$my_user = User::by_name($name);
// If user tried to log in as "foo bar" and failed, try "foo_bar"
if (!$my_user && strpos($name, " ") !== false) {
if (!$my_user && str_contains($name, " ")) {
$my_user = User::by_name(str_replace(" ", "_", $name));
}
@ -163,7 +155,7 @@ class User
public function set_class(string $class): void
{
global $database;
$database->Execute("UPDATE users SET class=:class WHERE id=:id", ["class"=>$class, "id"=>$this->id]);
$database->execute("UPDATE users SET class=:class WHERE id=:id", ["class"=>$class, "id"=>$this->id]);
log_info("core-user", 'Set class for '.$this->name.' to '.$class);
}
@ -175,7 +167,7 @@ class User
}
$old_name = $this->name;
$this->name = $name;
$database->Execute("UPDATE users SET name=:name WHERE id=:id", ["name"=>$this->name, "id"=>$this->id]);
$database->execute("UPDATE users SET name=:name WHERE id=:id", ["name"=>$this->name, "id"=>$this->id]);
log_info("core-user", "Changed username for {$old_name} to {$this->name}");
}
@ -185,7 +177,7 @@ class User
$hash = password_hash($password, PASSWORD_BCRYPT);
if (is_string($hash)) {
$this->passhash = $hash;
$database->Execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash"=>$this->passhash, "id"=>$this->id]);
$database->execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash"=>$this->passhash, "id"=>$this->id]);
log_info("core-user", 'Set password for '.$this->name);
} else {
throw new SCoreException("Failed to hash password");
@ -195,7 +187,7 @@ class User
public function set_email(string $address): void
{
global $database;
$database->Execute("UPDATE users SET email=:email WHERE id=:id", ["email"=>$address, "id"=>$this->id]);
$database->execute("UPDATE users SET email=:email WHERE id=:id", ["email"=>$address, "id"=>$this->id]);
log_info("core-user", 'Set email for '.$this->name);
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/**
* @global UserClass[] $_shm_user_classes
*/
@ -10,21 +12,9 @@ $_shm_user_classes = [];
*/
class UserClass
{
/**
* @var ?string
*/
public $name = null;
/**
* @var ?UserClass
*/
public $parent = null;
/**
* @var array
*/
public $abilities = [];
public ?string $name = null;
public ?UserClass $parent = null;
public array $abilities = [];
public function __construct(string $name, string $parent = null, array $abilities = [])
{
@ -100,6 +90,7 @@ new UserClass("user", "base", [
Permissions::READ_PM => true,
Permissions::SET_PRIVATE_IMAGE => true,
Permissions::BULK_DOWNLOAD => true,
Permissions::CHANGE_USER_SETTING => true
]);
new UserClass("hellbanned", "user", [
@ -108,6 +99,8 @@ new UserClass("hellbanned", "user", [
new UserClass("admin", "base", [
Permissions::CHANGE_SETTING => true,
Permissions::CHANGE_USER_SETTING => true,
Permissions::CHANGE_OTHER_USER_SETTING => true,
Permissions::OVERRIDE_CONFIG => true,
Permissions::BIG_SEARCH => true,
@ -200,6 +193,8 @@ new UserClass("admin", "base", [
Permissions::APPROVE_IMAGE => true,
Permissions::APPROVE_COMMENT => true,
Permissions::CRON_RUN =>true,
Permissions::BULK_IMPORT =>true,
Permissions::BULK_EXPORT =>true,
Permissions::BULK_DOWNLOAD => true,

View File

@ -1,4 +1,7 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
use MicroHTML\HTMLElement;
use function MicroHTML\emptyHTML;
use function MicroHTML\rawHTML;
use function MicroHTML\FORM;
@ -20,13 +23,6 @@ use function MicroHTML\TD;
const DATA_DIR = "data";
function mtimefile(string $file): string
{
$data_href = get_base_href();
$mtime = filemtime($file);
return "$data_href/$file?$mtime";
}
function get_theme(): string
{
global $config;
@ -46,18 +42,18 @@ function contact_link(): ?string
}
if (
startsWith($text, "http:") ||
startsWith($text, "https:") ||
startsWith($text, "mailto:")
str_starts_with($text, "http:") ||
str_starts_with($text, "https:") ||
str_starts_with($text, "mailto:")
) {
return $text;
}
if (strpos($text, "@")) {
if (str_contains($text, "@")) {
return "mailto:$text";
}
if (strpos($text, "/")) {
if (str_contains($text, "/")) {
return "http://$text";
}
@ -69,6 +65,10 @@ function contact_link(): ?string
*/
function is_https_enabled(): bool
{
// check forwarded protocol
if (REVERSE_PROXY_X_HEADERS && !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
$_SERVER['HTTPS']='on';
}
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
}
@ -160,6 +160,29 @@ function check_im_version(): int
return (empty($convert_check) ? 0 : 1);
}
/**
* Get request IP
*/
function get_remote_addr() {
return $_SERVER['REMOTE_ADDR'];
}
/**
* Get real IP if behind a reverse proxy
*/
function get_real_ip() {
$ip = get_remote_addr();
if (REVERSE_PROXY_X_HEADERS && isset($_SERVER['HTTP_X_REAL_IP'])) {
$ip = $_SERVER['HTTP_X_REAL_IP'];
if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$ip = "0.0.0.0";
}
}
return $ip;
}
/**
* Get the currently active IP, masked to make it not change when the last
* octet or two change, for use in session cookies and such
@ -167,7 +190,7 @@ function check_im_version(): int
function get_session_ip(Config $config): string
{
$mask = $config->get_string("session_hash_mask", "255.255.0.0");
$addr = $_SERVER['REMOTE_ADDR'];
$addr = get_real_ip();
$addr = inet_ntop(inet_pton($addr) & inet_pton($mask));
return $addr;
}
@ -259,11 +282,11 @@ function load_balance_url(string $tmpl, string $hash, int $n=0): string
return $tmpl;
}
function transload(string $url, string $mfile): ?array
function fetch_url(string $url, string $mfile): ?array
{
global $config;
if ($config->get_string("transload_engine") === "curl" && function_exists("curl_init")) {
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "curl" && function_exists("curl_init")) {
$ch = curl_init($url);
$fp = fopen($mfile, "w");
@ -276,8 +299,7 @@ function transload(string $url, string $mfile): ?array
$response = curl_exec($ch);
if ($response === false) {
log_warning("core-util", "Failed to transload $url");
throw new SCoreException("Failed to fetch $url");
return null;
}
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
@ -291,7 +313,7 @@ function transload(string $url, string $mfile): ?array
return $headers;
}
if ($config->get_string("transload_engine") === "wget") {
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "wget") {
$s_url = escapeshellarg($url);
$s_mfile = escapeshellarg($mfile);
system("wget --no-check-certificate $s_url --output-document=$s_mfile");
@ -299,14 +321,14 @@ function transload(string $url, string $mfile): ?array
return file_exists($mfile) ? ["ok"=>"true"] : null;
}
if ($config->get_string("transload_engine") === "fopen") {
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "fopen") {
$fp_in = @fopen($url, "r");
$fp_out = fopen($mfile, "w");
if (!$fp_in || !$fp_out) {
return null;
}
$length = 0;
while (!feof($fp_in) && $length <= $config->get_int('upload_size')) {
while (!feof($fp_in) && $length <= $config->get_int(UploadConfig::SIZE)) {
$data = fread($fp_in, 8192);
$length += strlen($data);
fwrite($fp_out, $data);
@ -348,7 +370,7 @@ function path_to_tags(string $path): string
// which is for inheriting to tags on the subfolder
$category_to_inherit = $tag;
} else {
if ($category!=""&&strpos($tag, ":") === false) {
if ($category!="" && !str_contains($tag, ":")) {
// This indicates that category inheritance is active,
// and we've encountered a tag that does not specify a category.
// So we attach the inherited category to the tag.
@ -367,7 +389,7 @@ function path_to_tags(string $path): string
}
function join_url(string $base, string ...$paths)
function join_url(string $base, string ...$paths): string
{
$output = $base;
foreach ($paths as $path) {
@ -418,7 +440,7 @@ function remove_empty_dirs(string $dir): bool
}
}
if ($result===true) {
$result = $result && rmdir($dir);
$result = rmdir($dir);
}
return $result;
}
@ -592,7 +614,6 @@ function _get_themelet_files(string $_theme): array
/**
* Used to display fatal errors to the web user.
* @noinspection PhpPossiblePolymorphicInvocationInspection
*/
function _fatal_error(Exception $e): void
{
@ -600,6 +621,7 @@ function _fatal_error(Exception $e): void
$message = $e->getMessage();
$phpver = phpversion();
$query = is_subclass_of($e, "SCoreException") ? $e->query : null;
$code = is_subclass_of($e, "SCoreException") ? $e->http_code : 500;
//$hash = exec("git rev-parse HEAD");
//$h_hash = $hash ? "<p><b>Hash:</b> $hash" : "";
@ -624,7 +646,10 @@ function _fatal_error(Exception $e): void
print("Version: $version (on $phpver)\n");
} else {
$q = $query ? "" : "<p><b>Query:</b> " . html_escape($query);
header("HTTP/1.0 500 Internal Error");
if ($code >= 500) {
error_log("Shimmie Error: $message (Query: $query)\n{$e->getTraceAsString()}");
}
header("HTTP/1.0 $code Error");
echo '
<!doctype html>
<html lang="en">
@ -663,7 +688,7 @@ function _get_user(): User
function _get_query(): string
{
return (@$_POST["q"]?:@$_GET["q"])?:"/";
return (@$_POST["q"] ?: @$_GET["q"]) ?: "/";
}
@ -711,7 +736,7 @@ function make_form(string $target, string $method="POST", bool $multipart=false,
return '<form action="'.$target.'" method="'.$method.'" '.$extra.'>'.$extra_inputs;
}
function SHM_FORM(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit="")
function SHM_FORM(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit=""): HTMLElement
{
global $user;
@ -736,19 +761,19 @@ function SHM_FORM(string $target, string $method="POST", bool $multipart=false,
);
}
function SHM_SIMPLE_FORM($target, ...$children)
function SHM_SIMPLE_FORM($target, ...$children): HTMLElement
{
$form = SHM_FORM($target);
$form->appendChild(emptyHTML(...$children));
return $form;
}
function SHM_SUBMIT(string $text)
function SHM_SUBMIT(string $text): HTMLElement
{
return INPUT(["type"=>"submit", "value"=>$text]);
}
function SHM_COMMAND_EXAMPLE(string $ex, string $desc)
function SHM_COMMAND_EXAMPLE(string $ex, string $desc): HTMLElement
{
return DIV(
["class"=>"command_example"],
@ -757,7 +782,7 @@ function SHM_COMMAND_EXAMPLE(string $ex, string $desc)
);
}
function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot)
function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot): HTMLElement
{
if (is_string($foot)) {
$foot = TFOOT(TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>$foot]))));
@ -777,16 +802,16 @@ function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot)
}
const BYTE_DENOMINATIONS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
function human_filesize(int $bytes, $decimals = 2)
function human_filesize(int $bytes, $decimals = 2): string
{
$factor = floor((strlen(strval($bytes)) - 1) / 3);
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @BYTE_DENOMINATIONS[$factor];
}
/*
/**
* Generates a unique key for the website to prevent unauthorized access.
*/
function generate_key(int $length = 20)
function generate_key(int $length = 20): string
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
@ -797,3 +822,4 @@ function generate_key(int $length = 20)
return $randomString;
}

View File

@ -1,23 +1,17 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class AdminPageInfo extends ExtensionInfo
{
public const KEY = "admin";
public $key = self::KEY;
public $name = "Admin Controls";
public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2;
public $description = "Various things to make admins' lives easier";
public $documentation =
"Various moderate-level tools for admins; for advanced, obscure, and possibly dangerous tools see the shimmie2-utils script set
<p>Lowercase all tags:
<br>Set all tags to lowercase for consistency
<p>Recount tag use:
<br>If the counts of images per tag get messed up somehow, this will reset them, and remove any unused tags
<p>Database dump:
<br>Download the contents of the database in plain text format, useful for backups.
<p>Image dump:
<br>Download all the images as a .zip file (Requires ZipArchive)";
public string $key = self::KEY;
public string $name = "Admin Controls";
public string $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2;
public string $description = "Provides a base for various small admin functions";
public bool $core = true;
public string $visibility = self::VISIBLE_HIDDEN;
}

View File

@ -1,4 +1,5 @@
<?php /** @noinspection PhpUnusedPrivateMethodInspection */
<?php
declare(strict_types=1);
/**
@ -6,8 +7,7 @@ declare(strict_types=1);
*/
class AdminBuildingEvent extends Event
{
/** @var Page */
public $page;
public Page $page;
public function __construct(Page $page)
{
@ -18,10 +18,8 @@ class AdminBuildingEvent extends Event
class AdminActionEvent extends Event
{
/** @var string */
public $action;
/** @var bool */
public $redirect = true;
public string $action;
public bool $redirect = true;
public function __construct(string $action)
{
@ -33,11 +31,11 @@ class AdminActionEvent extends Event
class AdminPage extends Extension
{
/** @var AdminPageTheme */
protected $theme;
protected ?Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{
global $page, $user;
global $database, $page, $user;
if ($event->page_matches("admin")) {
if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) {
@ -52,6 +50,7 @@ class AdminPage extends Extension
if ($user->check_auth_token()) {
log_info("admin", "Util: $action");
set_time_limit(0);
$database->set_timeout(null);
send_event($aae);
}
@ -80,8 +79,10 @@ class AdminPage extends Extension
}
if ($event->cmd == "get-page") {
global $page;
$_SERVER['REQUEST_URI'] = $event->args[0];
if (isset($event->args[1])) {
parse_str($event->args[1], $_GET);
$_SERVER['REQUEST_URI'] .= "?" . $event->args[1];
}
send_event(new PageRequestEvent($event->args[0]));
$page->display();
@ -103,7 +104,7 @@ class AdminPage extends Extension
$uid = $event->args[0];
$image = Image::by_id_or_hash($uid);
if ($image) {
send_event(new ThumbnailGenerationEvent($image->hash, $image->ext, true));
send_event(new ThumbnailGenerationEvent($image->hash, $image->get_mime(), true));
} else {
print("No post with ID '$uid'\n");
}
@ -129,7 +130,6 @@ class AdminPage extends Extension
public function onAdminBuilding(AdminBuildingEvent $event)
{
$this->theme->display_page();
$this->theme->display_form();
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
@ -149,46 +149,4 @@ class AdminPage extends Extension
$event->add_link("Board Admin", make_link("admin"));
}
}
public function onAdminAction(AdminActionEvent $event)
{
$action = $event->action;
if (method_exists($this, $action)) {
$event->redirect = $this->$action();
}
}
private function set_tag_case()
{
global $database;
$database->execute(
"UPDATE tags SET tag=:tag1 WHERE LOWER(tag) = LOWER(:tag2)",
["tag1" => $_POST['tag'], "tag2" => $_POST['tag']]
);
log_info("admin", "Fixed the case of {$_POST['tag']}", "Fixed case");
return true;
}
private function lowercase_all_tags()
{
global $database;
$database->execute("UPDATE tags SET tag=lower(tag)");
log_warning("admin", "Set all tags to lowercase", "Set all tags to lowercase");
return true;
}
private function recount_tag_use()
{
global $database;
$database->Execute("
UPDATE tags
SET count = COALESCE(
(SELECT COUNT(image_id) FROM image_tags WHERE tag_id=tags.id GROUP BY tag_id),
0
)
");
$database->Execute("DELETE FROM tags WHERE count=0");
log_warning("admin", "Re-counted tags", "Re-counted tags");
return true;
}
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class AdminPageTest extends ShimmiePHPUnitTestCase
{
public function testAuth()
@ -19,59 +21,6 @@ class AdminPageTest extends ShimmiePHPUnitTestCase
$this->assertEquals("Admin Tools", $page->title);
}
public function testLowercaseAndSetCase()
{
// Create a problem
$ts = time(); // we need a tag that hasn't been used before
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "TeStCase$ts");
// Validate problem
$page = $this->get_page("post/view/$image_id_1");
$this->assertEquals("Image $image_id_1: TeStCase$ts", $page->title);
// Fix
send_event(new AdminActionEvent('lowercase_all_tags'));
// Validate fix
$this->get_page("post/view/$image_id_1");
$this->assert_title("Image $image_id_1: testcase$ts");
// Change
$_POST["tag"] = "TestCase$ts";
send_event(new AdminActionEvent('set_tag_case'));
// Validate change
$this->get_page("post/view/$image_id_1");
$this->assert_title("Image $image_id_1: TestCase$ts");
}
# FIXME: make sure the admin tools actually work
public function testRecount()
{
global $database;
// Create a problem
$ts = time(); // we need a tag that hasn't been used before
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
$database->execute(
"INSERT INTO tags(tag, count) VALUES(:tag, :count)",
["tag"=>"tes$ts", "count"=>42]
);
// Fix
send_event(new AdminActionEvent('recount_tag_use'));
// Validate fix
$this->assertEquals(
0,
$database->get_one(
"SELECT count FROM tags WHERE tag = :tag",
["tag"=>"tes$ts"]
)
);
}
public function testCommands()
{
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));

View File

@ -1,5 +1,6 @@
<?php declare(strict_types=1);
use function MicroHTML\INPUT;
<?php
declare(strict_types=1);
class AdminPageTheme extends Themelet
{
@ -14,41 +15,4 @@ class AdminPageTheme extends Themelet
$page->set_heading("Admin Tools");
$page->add_block(new NavBlock());
}
protected function button(string $name, string $action, bool $protected=false): string
{
$c_protected = $protected ? " protected" : "";
$html = make_form(make_link("admin/$action"), "POST", false, "admin$c_protected");
if ($protected) {
$html .= "<input type='submit' id='$action' value='$name' disabled='disabled'>";
$html .= "<input type='checkbox' onclick='$(\"#$action\").attr(\"disabled\", !$(this).is(\":checked\"))'>";
} else {
$html .= "<input type='submit' id='$action' value='$name'>";
}
$html .= "</form>\n";
return $html;
}
/*
* Show a form which links to admin_utils with POST[action] set to one of:
* 'lowercase all tags'
* 'recount tag use'
* etc
*/
public function display_form()
{
global $page;
$html = "";
$html .= $this->button("All tags to lowercase", "lowercase_all_tags", true);
$html .= $this->button("Recount tag use", "recount_tag_use", false);
$page->add_block(new Block("Misc Admin Tools", $html));
$html = (string)SHM_SIMPLE_FORM(
"admin/set_tag_case",
INPUT(["type"=>'text', "name"=>'tag', "placeholder"=>'Enter tag with correct case', "class"=>'autocomplete_tags', "autocomplete"=>'off']),
SHM_SUBMIT('Set Tag Case'),
);
$page->add_block(new Block("Set Tag Case", $html));
}
}

View File

@ -1,15 +1,17 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class AliasEditorInfo extends ExtensionInfo
{
public const KEY = "alias_editor";
public $key = self::KEY;
public $name = "Alias Editor";
public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2;
public $description = "Edit the alias list";
public $documentation = 'The list is visible at <a href="$site/alias/list">/alias/list</a>; only site admins can edit it, other people can view and download it';
public $core = true;
public string $key = self::KEY;
public string $name = "Alias Editor";
public string $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2;
public string $description = "Edit the alias list";
public ?string $documentation = 'The list is visible at <a href="$site/alias/list">/alias/list</a>; only site admins can edit it, other people can view and download it';
public bool $core = true;
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
use MicroCRUD\ActionColumn;
use MicroCRUD\TextColumn;
@ -26,10 +28,8 @@ class AliasTable extends Table
class AddAliasEvent extends Event
{
/** @var string */
public $oldtag;
/** @var string */
public $newtag;
public string $oldtag;
public string $newtag;
public function __construct(string $oldtag, string $newtag)
{
@ -41,7 +41,7 @@ class AddAliasEvent extends Event
class DeleteAliasEvent extends Event
{
public $oldtag;
public string $oldtag;
public function __construct(string $oldtag)
{
@ -57,7 +57,7 @@ class AddAliasException extends SCoreException
class AliasEditor extends Extension
{
/** @var AliasEditorTheme */
protected $theme;
protected ?Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{
@ -96,7 +96,7 @@ class AliasEditor extends Extension
$this->theme->display_aliases($t->table($t->query()), $t->paginator());
} elseif ($event->get_arg(0) == "export") {
$page->set_mode(PageMode::DATA);
$page->set_type(MIME_TYPE_CSV);
$page->set_mime(MimeType::CSV);
$page->set_filename("aliases.csv");
$page->set_data($this->get_alias_csv($database));
} elseif ($event->get_arg(0) == "import") {

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class AliasEditorTest extends ShimmiePHPUnitTestCase
{
public function testAliasList()
@ -36,7 +38,7 @@ class AliasEditorTest extends ShimmiePHPUnitTestCase
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
$this->get_page("post/view/$image_id"); # check that the tag has been replaced
$this->assert_title("Image $image_id: test2");
$this->assert_title("Post $image_id: test2");
$this->get_page("post/list/test1/1"); # searching for an alias should find the master tag
$this->assert_response(302);
$this->get_page("post/list/test2/1"); # check that searching for the main tag still works
@ -67,13 +69,13 @@ class AliasEditorTest extends ShimmiePHPUnitTestCase
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "onetag");
$this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases
$this->assert_title("multi tag");
$this->assert_no_text("No Images Found");
$this->assert_no_text("No Posts Found");
$this->get_page("post/list/multi/1");
$this->assert_title("multi");
$this->assert_no_text("No Images Found");
$this->assert_no_text("No Posts Found");
$this->get_page("post/list/multi tag/1");
$this->assert_title("multi tag");
$this->assert_no_text("No Images Found");
$this->assert_no_text("No Posts Found");
$this->delete_image($image_id_1);
$this->delete_image($image_id_2);

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class AliasEditorTheme extends Themelet
{

View File

@ -1,12 +1,14 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class ApprovalInfo extends ExtensionInfo
{
public const KEY = "approval";
public $key = self::KEY;
public $name = "Approval";
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public $license = self::LICENSE_WTFPL;
public $description = "Adds an approval step to the upload/import process.";
public string $key = self::KEY;
public string $name = "Approval";
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public string $license = self::LICENSE_WTFPL;
public string $description = "Adds an approval step to the upload/import process.";
}

View File

@ -1,16 +1,18 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
abstract class ApprovalConfig
{
const VERSION = "ext_approval_version";
const IMAGES = "approve_images";
const COMMENTS = "approve_comments";
public const VERSION = "ext_approval_version";
public const IMAGES = "approve_images";
public const COMMENTS = "approve_comments";
}
class Approval extends Extension
{
/** @var ApprovalTheme */
protected $theme;
protected ?Themelet $theme;
public function onInitExt(InitExtEvent $event)
{
@ -33,7 +35,7 @@ class Approval extends Extension
$image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
}
if (empty($image_id)) {
throw new SCoreException("Can not approve image: No valid Image ID given.");
throw new SCoreException("Can not approve post: No valid Post ID given.");
}
self::approve_image($image_id);
@ -48,7 +50,7 @@ class Approval extends Extension
$image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
}
if (empty($image_id)) {
throw new SCoreException("Can not disapprove image: No valid Image ID given.");
throw new SCoreException("Can not disapprove image: No valid Post ID given.");
}
self::disapprove_image($image_id);
@ -77,14 +79,14 @@ class Approval extends Extension
$approval_action = $_POST["approval_action"];
switch ($approval_action) {
case "approve_all":
$database->set_timeout(300000); // These updates can take a little bit
$database->set_timeout(null); // These updates can take a little bit
$database->execute(
"UPDATE images SET approved = :true, approved_by_id = :approved_by_id WHERE approved = :false",
["approved_by_id"=>$user->id, "true"=>true, "false"=>false]
);
break;
case "disapprove_all":
$database->set_timeout(300000); // These updates can take a little bit
$database->set_timeout(null); // These updates can take a little bit
$database->execute(
"UPDATE images SET approved = :false, approved_by_id = NULL WHERE approved = :true",
["true"=>true, "false"=>false]
@ -99,9 +101,9 @@ class Approval extends Extension
public function onDisplayingImage(DisplayingImageEvent $event)
{
global $user, $page, $config;
global $page;
if ($config->get_bool(ApprovalConfig::IMAGES) && $event->image->approved===false && !$user->can(Permissions::APPROVE_IMAGE)) {
if (!$this->check_permissions(($event->image))) {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/list"));
}
@ -118,16 +120,16 @@ class Approval extends Extension
}
const SEARCH_REGEXP = "/^approved:(yes|no)/";
public const SEARCH_REGEXP = "/^approved:(yes|no)/";
public function onSearchTermParse(SearchTermParseEvent $event)
{
global $user, $database, $config;
global $user, $config;
if ($config->get_bool(ApprovalConfig::IMAGES)) {
$matches = [];
if (is_null($event->term) && $this->no_approval_query($event->context)) {
$event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
$event->add_querylet(new Querylet("approved = :true", ["true"=>true]));
}
if (is_null($event->term)) {
@ -135,9 +137,9 @@ class Approval extends Extension
}
if (preg_match(self::SEARCH_REGEXP, strtolower($event->term), $matches)) {
if ($user->can(Permissions::APPROVE_IMAGE) && $matches[1] == "no") {
$event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_N ")));
$event->add_querylet(new Querylet("approved != :true", ["true"=>true]));
} else {
$event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
$event->add_querylet(new Querylet("approved = :true", ["true"=>true]));
}
}
}
@ -187,6 +189,26 @@ class Approval extends Extension
);
}
private function check_permissions(Image $image): bool
{
global $user, $config;
if ($config->get_bool(ApprovalConfig::IMAGES) && $image->approved===false && !$user->can(Permissions::APPROVE_IMAGE)) {
return false;
}
return true;
}
public function onImageDownloading(ImageDownloadingEvent $event)
{
/**
* Deny images upon insufficient permissions.
**/
if (!$this->check_permissions($event->image)) {
throw new SCoreException("Access denied");
}
}
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
{
global $user, $config;
@ -241,15 +263,15 @@ class Approval extends Extension
global $database;
if ($this->get_version(ApprovalConfig::VERSION) < 1) {
$database->execute($database->scoreql_to_sql(
"ALTER TABLE images ADD COLUMN approved SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N"
));
$database->execute(
"ALTER TABLE images ADD COLUMN approved_by_id INTEGER NULL"
);
$database->execute("ALTER TABLE images ADD COLUMN approved BOOLEAN NOT NULL DEFAULT FALSE");
$database->execute("ALTER TABLE images ADD COLUMN approved_by_id INTEGER NULL");
$database->execute("CREATE INDEX images_approved_idx ON images(approved)");
$this->set_version(ApprovalConfig::VERSION, 1);
$this->set_version(ApprovalConfig::VERSION, 2);
}
if ($this->get_version(ApprovalConfig::VERSION) < 2) {
$database->standardise_boolean("images", "approved");
$this->set_version(ApprovalConfig::VERSION, 2);
}
}
}

View File

@ -1,11 +1,13 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
use function MicroHTML\BR;
use function MicroHTML\BUTTON;
use function MicroHTML\INPUT;
class ApprovalTheme extends Themelet
{
public function get_image_admin_html(Image $image)
public function get_image_admin_html(Image $image): string
{
if ($image->approved===true) {
$html = SHM_SIMPLE_FORM(
@ -24,26 +26,24 @@ class ApprovalTheme extends Themelet
return (string)$html;
}
public function get_help_html()
public function get_help_html(): string
{
return '<p>Search for images that are approved/not approved.</p>
return '<p>Search for posts that are approved/not approved.</p>
<div class="command_example">
<pre>approved:yes</pre>
<p>Returns images that have been approved.</p>
<p>Returns posts that have been approved.</p>
</div>
<div class="command_example">
<pre>approved:no</pre>
<p>Returns images that have not been approved.</p>
<p>Returns posts that have not been approved.</p>
</div>
';
}
public function display_admin_block(SetupBuildingEvent $event)
{
$sb = new SetupBlock("Approval");
$sb->add_bool_option(ApprovalConfig::IMAGES, "Images: ");
$event->panel->add_block($sb);
$sb = $event->panel->create_new_block("Approval");
$sb->add_bool_option(ApprovalConfig::IMAGES, "Posts: ");
}
public function display_admin_form()
@ -52,9 +52,9 @@ class ApprovalTheme extends Themelet
$html = (string)SHM_SIMPLE_FORM(
"admin/approval",
BUTTON(["name"=>'approval_action', "value"=>'approve_all'], "Approve All Images"),
BUTTON(["name"=>'approval_action', "value"=>'approve_all'], "Approve All Posts"),
BR(),
BUTTON(["name"=>'approval_action', "value"=>'disapprove_all'], "Disapprove All Images"),
BUTTON(["name"=>'approval_action', "value"=>'disapprove_all'], "Disapprove All Posts"),
);
$page->add_block(new Block("Approval", $html));
}

View File

@ -1,14 +1,16 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class ArtistsInfo extends ExtensionInfo
{
public const KEY = "artists";
public $key = self::KEY;
public $name = "Artists System";
public $url = self::SHIMMIE_URL;
public $authors = ["Sein Kraft"=>"mail@seinkraft.info","Alpha"=>"alpha@furries.com.ar"];
public $license = self::LICENSE_GPLV2;
public $description = "Simple artists extension";
public $beta = true;
public string $key = self::KEY;
public string $name = "Artists System";
public string $url = self::SHIMMIE_URL;
public array $authors = ["Sein Kraft"=>"mail@seinkraft.info","Alpha"=>"alpha@furries.com.ar"];
public string $license = self::LICENSE_GPLV2;
public string $description = "Simple artists extension";
public bool $beta = true;
}

View File

@ -1,13 +1,12 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class AuthorSetEvent extends Event
{
/** @var Image */
public $image;
/** @var User */
public $user;
/** @var string */
public $author;
public Image $image;
public User $user;
public string $author;
public function __construct(Image $image, User $user, string $author)
{
@ -21,7 +20,7 @@ class AuthorSetEvent extends Event
class Artists extends Extension
{
/** @var ArtistsTheme */
protected $theme;
protected ?Themelet $theme;
public function onImageInfoSet(ImageInfoSetEvent $event)
{
@ -553,7 +552,7 @@ class Artists extends Extension
$urlsAsString = $inputs["urls"];
$urlsIDsAsString = $inputs["urlsIDs"];
if (strpos($name, " ")) {
if (str_contains($name, " ")) {
return;
}
@ -683,7 +682,7 @@ class Artists extends Extension
);
}
private function add_artist()
private function add_artist(): int
{
global $user;
$inputs = validate_input([
@ -695,7 +694,7 @@ class Artists extends Extension
]);
$name = $inputs["name"];
if (strpos($name, " ")) {
if (str_contains($name, " ")) {
return -1;
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class ArtistsTest extends ShimmiePHPUnitTestCase
{
public function testSearch()

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class ArtistsTheme extends Themelet
{
public function get_author_editor_html(string $author): string
@ -408,7 +410,7 @@ class ArtistsTheme extends Themelet
'</span>';
}
$page->add_block(new Block("Artist Images", $artist_images, "main", 20));
$page->add_block(new Block("Artist Posts", $artist_images, "main", 20));
}
private function render_aliases(array $aliases, bool $userIsLogged, bool $userIsAdmin): string
@ -546,12 +548,12 @@ class ArtistsTheme extends Themelet
return $html;
}
public function get_help_html()
public function get_help_html(): string
{
return '<p>Search for images with a particular artist.</p>
return '<p>Search for posts with a particular artist.</p>
<div class="command_example">
<pre>artist=leonardo</pre>
<p>Returns images with the artist "leonardo".</p>
<p>Returns posts with the artist "leonardo".</p>
</div>
';
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
abstract class AutoTaggerConfig
{

View File

@ -1,12 +1,14 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class AutoTaggerInfo extends ExtensionInfo
{
public const KEY = "auto_tagger";
public $key = self::KEY;
public $name = "Auto-Tagger";
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public $license = self::LICENSE_WTFPL;
public $description = "Provides several automatic tagging functions";
public string $key = self::KEY;
public string $name = "Auto-Tagger";
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public string $license = self::LICENSE_WTFPL;
public string $description = "Provides several automatic tagging functions";
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
require_once 'config.php';
@ -28,10 +30,8 @@ class AutoTaggerTable extends Table
class AddAutoTagEvent extends Event
{
/** @var string */
public $tag;
/** @var string */
public $additional_tags;
public string $tag;
public string $additional_tags;
public function __construct(string $tag, string $additional_tags)
{
@ -43,7 +43,7 @@ class AddAutoTagEvent extends Event
class DeleteAutoTagEvent extends Event
{
public $tag;
public string $tag;
public function __construct(string $tag)
{
@ -63,7 +63,7 @@ class AddAutoTagException extends SCoreException
class AutoTagger extends Extension
{
/** @var AutoTaggerTheme */
protected $theme;
protected ?Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{
@ -102,7 +102,7 @@ class AutoTagger extends Extension
$this->theme->display_auto_tagtable($t->table($t->query()), $t->paginator());
} elseif ($event->get_arg(0) == "export") {
$page->set_mode(PageMode::DATA);
$page->set_type(MIME_TYPE_CSV);
$page->set_mime(MimeType::CSV);
$page->set_filename("auto_tag.csv");
$page->set_data($this->get_auto_tag_csv($database));
} elseif ($event->get_arg(0) == "import") {
@ -269,6 +269,7 @@ class AutoTagger extends Extension
if (!empty($tag_id)) {
$image_ids = $database->get_col_iterable("SELECT image_id FROM image_tags WHERE tag_id = :tag_id", ["tag_id"=>$tag_id]);
foreach ($image_ids as $image_id) {
$image_id = (int) $image_id;
$image = Image::by_id($image_id);
$event = new TagSetEvent($image, $image->get_tag_array());
send_event($event);
@ -314,14 +315,10 @@ class AutoTagger extends Extension
$tags_mixed = array_merge($tags_mixed, $new_tags);
}
$results = array_intersect_key(
return array_intersect_key(
$tags_mixed,
array_unique(array_map('strtolower', $tags_mixed))
);
return $results;
}
/**

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class AutoTaggerTest extends ShimmiePHPUnitTestCase
{
public function testAutoTaggerList()
@ -37,14 +39,14 @@ class AutoTaggerTest extends ShimmiePHPUnitTestCase
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
$this->get_page("post/view/$image_id"); # check that the tag has been replaced
$this->assert_title("Image $image_id: test1 test2");
$this->assert_title("Post $image_id: test1 test2");
$this->delete_image($image_id);
send_event(new AddAutoTagEvent("test2", "test3"));
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
$this->get_page("post/view/$image_id"); # check that the tag has been replaced
$this->assert_title("Image $image_id: test1 test2 test3");
$this->assert_title("Post $image_id: test1 test2 test3");
$this->delete_image($image_id);
send_event(new DeleteAutoTagEvent("test1"));

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class AutoTaggerTheme extends Themelet
{

View File

@ -1,11 +1,13 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class AutoCompleteInfo extends ExtensionInfo
{
public const KEY = "autocomplete";
public $key = self::KEY;
public $name = "Autocomplete";
public $authors = ["Daku"=>"admin@codeanimu.net"];
public $description = "Adds autocomplete to search & tagging.";
public string $key = self::KEY;
public string $name = "Autocomplete";
public array $authors = ["Daku"=>"admin@codeanimu.net"];
public string $description = "Adds autocomplete to search & tagging.";
}

View File

@ -1,9 +1,11 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class AutoComplete extends Extension
{
/** @var AutoCompleteTheme */
protected $theme;
protected ?Themelet $theme;
public function get_priority(): int
{
@ -12,58 +14,68 @@ class AutoComplete extends Extension
public function onPageRequest(PageRequestEvent $event)
{
global $cache, $page, $database;
global $page;
if ($event->page_matches("api/internal/autocomplete")) {
if (!isset($_GET["s"])) {
return;
}
$limit = (int)($_GET["limit"] ?? 0);
$s = $_GET["s"] ?? "";
$res = $this->complete($s, $limit);
$page->set_mode(PageMode::DATA);
$page->set_type(MIME_TYPE_JSON);
$s = strtolower($_GET["s"]);
if (
$s == '' ||
$s[0] == '_' ||
$s[0] == '%' ||
strlen($s) > 32
) {
$page->set_data("{}");
return;
}
//$limit = 0;
$cache_key = "autocomplete-$s";
$limitSQL = "";
$s = str_replace('_', '\_', $s);
$s = str_replace('%', '\%', $s);
$SQLarr = ["search"=>"$s%"]; #, "cat_search"=>"%:$s%"];
if (isset($_GET["limit"]) && $_GET["limit"] !== 0) {
$limitSQL = "LIMIT :limit";
$SQLarr['limit'] = $_GET["limit"];
$cache_key .= "-" . $_GET["limit"];
}
$res = $cache->get($cache_key);
if (!$res) {
$res = $database->get_pairs(
"
SELECT tag, count
FROM tags
WHERE LOWER(tag) LIKE LOWER(:search)
-- OR LOWER(tag) LIKE LOWER(:cat_search)
AND count > 0
ORDER BY count DESC
$limitSQL",
$SQLarr
);
$cache->set($cache_key, $res, 600);
}
$page->set_mime(MimeType::JSON);
$page->set_data(json_encode($res));
}
$this->theme->build_autocomplete($page);
}
private function complete(string $search, int $limit): array
{
global $cache, $database;
if (!$search) {
return [];
}
$search = strtolower($search);
if (
$search == '' ||
$search[0] == '_' ||
$search[0] == '%' ||
strlen($search) > 32
) {
return [];
}
$cache_key = "autocomplete-$search";
$limitSQL = "";
$search = str_replace('_', '\_', $search);
$search = str_replace('%', '\%', $search);
$SQLarr = ["search"=>"$search%"]; #, "cat_search"=>"%:$search%"];
if ($limit !== 0) {
$limitSQL = "LIMIT :limit";
$SQLarr['limit'] = $limit;
$cache_key .= "-" . $limit;
}
$res = $cache->get($cache_key);
if (!$res) {
$res = $database->get_pairs(
"
SELECT tag, count
FROM tags
WHERE LOWER(tag) LIKE LOWER(:search)
-- OR LOWER(tag) LIKE LOWER(:cat_search)
AND count > 0
ORDER BY count DESC
$limitSQL
",
$SQLarr
);
$cache->set($cache_key, $res, 600);
}
return $res;
}
}

View File

@ -1,5 +1,5 @@
document.addEventListener('DOMContentLoaded', () => {
var metatags = ['order:id', 'order:width', 'order:height', 'order:filesize', 'order:filename'];
var metatags = ['order:id', 'order:width', 'order:height', 'order:filesize', 'order:filename', 'order:favorites'];
$('[name="search"]').tagit({
singleFieldDelimiter: ' ',
@ -63,12 +63,12 @@ document.addEventListener('DOMContentLoaded', () => {
var keyCode = e.keyCode || e.which;
//Stop tags containing space.
if(keyCode == 32) {
if(keyCode === 32) {
e.preventDefault();
$('.autocomplete_tags').tagit('createTag', $(this).val());
$(this).autocomplete('close');
} else if (keyCode == 9) {
} else if (keyCode === 9) {
e.preventDefault();
var tag = $('.tagit-autocomplete[style*=\"display: block\"] > li:focus, .tagit-autocomplete[style*=\"display: block\"] > li:first').first();

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class AutoCompleteTest extends ShimmiePHPUnitTestCase

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class AutoCompleteTheme extends Themelet
{

View File

@ -1,16 +1,18 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BanWordsInfo extends ExtensionInfo
{
public const KEY = "ban_words";
public $key = self::KEY;
public $name = "Comment Word Ban";
public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2;
public $description = "For stopping spam and other comment abuse";
public $documentation =
public string $key = self::KEY;
public string $name = "Comment Word Ban";
public string $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2;
public string $description = "For stopping spam and other comment abuse";
public ?string $documentation =
"Allows an administrator to ban certain words
from comments. This can be a very simple but effective way
of stopping spam; just add \"viagra\", \"porn\", etc to the

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BanWords extends Extension
{
@ -55,7 +57,7 @@ xanax
public function onSetupBuilding(SetupBuildingEvent $event)
{
$sb = new SetupBlock("Banned Phrases");
$sb = $event->panel->create_new_block("Banned Phrases");
$sb->add_label("One per line, lines that start with slashes are treated as regex<br/>");
$sb->add_longtext_option("banned_words");
$failed = [];
@ -69,7 +71,6 @@ xanax
if ($failed) {
$sb->add_label("Failed regexes: ".join(", ", $failed));
}
$event->panel->add_block($sb);
}
/**
@ -87,7 +88,7 @@ xanax
}
} else {
// other words are literal
if (strpos($comment, $word) !== false) {
if (str_contains($comment, $word)) {
throw $ex;
}
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BanWordsTest extends ShimmiePHPUnitTestCase
{
public function check_blocked($image_id, $words)
@ -8,7 +10,7 @@ class BanWordsTest extends ShimmiePHPUnitTestCase
send_event(new CommentPostingEvent($image_id, $user, $words));
$this->fail("Exception not thrown");
} catch (CommentPostingException $e) {
$this->assertEquals($e->getMessage(), "Comment contains banned terms");
$this->assertEquals("Comment contains banned terms", $e->getMessage());
}
}

View File

@ -1,32 +1,57 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BBCodeInfo extends ExtensionInfo
{
public const KEY = "bbcode";
public $key = self::KEY;
public $name = "BBCode";
public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2;
public $core = true;
public $description = "Turns BBCode into HTML";
public $documentation =
" Supported tags:
public string $key = self::KEY;
public string $name = "BBCode";
public string $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2;
public bool $core = true;
public string $description = "Turns BBCode into HTML";
public ?string $documentation =
" Basic formatting tags:
<ul>
<li>[img]url[/img]
<li>[url]<a href=\"{self::SHIMMIE_URL}\">https://code.shishnet.org/</a>[/url]
<li>[email]<a href=\"mailto:{self::SHISH_EMAIL}\">webmaster@shishnet.org</a>[/email]
<li>[b]<b>bold</b>[/b]
<li>[i]<i>italic</i>[/i]
<li>[u]<u>underline</u>[/u]
<li>[s]<s>strikethrough</s>[/s]
<li>[sup]<sup>superscript</sup>[/sup]
<li>[sub]<sub>subscript</sub>[/sub]
<li>[h1]Heading 1[/h1]
<li>[h2]Heading 2[/h2]
<li>[h3]Heading 3[/h3]
<li>[h4]Heading 4[/h4]
<li>[align=left|center|right]Aligned Text[/align]
</ul>
<br>
Link tags:
<ul>
<li>[img]url[/img]
<li>[url]<a href=\"{self::SHIMMIE_URL}\">https://code.shishnet.org/</a>[/url]
<li>[url=<a href=\"{self::SHIMMIE_URL}\">https://code.shishnet.org/</a>]some text[/url]
<li>[url]site://ext_doc/bbcode[/url]
<li>[url=site://ext_doc/bbcode]Link to BBCode docs[/url]
<li>[email]<a href=\"mailto:{self::SHISH_EMAIL}\">webmaster@shishnet.org</a>[/email]
<li>[[wiki article]]
<li>[[wiki article|with some text]]
<li>[quote]text[/quote]
<li>[quote=Username]text[/quote]
<li>&gt;&gt;123 (link to image #123)
<li>&gt;&gt;123 (link to post #123)
<li>[anchor=target]Scroll to #bb-target[/anchor]
</ul>
<br>
More format Tags:
<ul>
<li>[list]Unordered list[/list]
<li>[ul]Unordered list[/ul]
<li>[ol]Ordered list[/ol]
<li>[li]List Item[/li]
<li>[code]<pre>print(\"Hello World!\");</pre>[/code]
<li>[spoiler]<span style=\"background-color:#000; color:#000;\">Voldemort is bad</span>[/spoiler]
<li>[quote]<blockquote><small>To be or not to be...</small></blockquote>[/quote]
<li>[quote=Shakespeare]<blockquote><em>Shakespeare said:</em><br><small>... That is the question</small></blockquote>[/quote]
</ul>";
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BBCode extends FormatterExtension

View File

@ -1,63 +1,65 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BBCodeTest extends ShimmiePHPUnitTestCase
{
public function testBasics()
{
$this->assertEquals(
$this->filter("[b]bold[/b][i]italic[/i]"),
"<b>bold</b><i>italic</i>"
"<b>bold</b><i>italic</i>",
$this->filter("[b]bold[/b][i]italic[/i]")
);
}
public function testStacking()
{
$this->assertEquals(
$this->filter("[b]B[/b][i]I[/i][b]B[/b]"),
"<b>B</b><i>I</i><b>B</b>"
"<b>B</b><i>I</i><b>B</b>",
$this->filter("[b]B[/b][i]I[/i][b]B[/b]")
);
$this->assertEquals(
$this->filter("[b]bold[i]bolditalic[/i]bold[/b]"),
"<b>bold<i>bolditalic</i>bold</b>"
"<b>bold<i>bolditalic</i>bold</b>",
$this->filter("[b]bold[i]bolditalic[/i]bold[/b]")
);
}
public function testFailure()
{
$this->assertEquals(
$this->filter("[b]bold[i]italic"),
"[b]bold[i]italic"
"[b]bold[i]italic",
$this->filter("[b]bold[i]italic")
);
}
public function testCode()
{
$this->assertEquals(
$this->filter("[code][b]bold[/b][/code]"),
"<pre>[b]bold[/b]</pre>"
"<pre>[b]bold[/b]</pre>",
$this->filter("[code][b]bold[/b][/code]")
);
}
public function testNestedList()
{
$this->assertEquals(
$this->filter("[list][*]a[list][*]a[*]b[/list][*]b[/list]"),
"<ul><li>a<ul><li>a<li>b</ul><li>b</ul>"
"<ul><li>a<ul><li>a<li>b</ul><li>b</ul>",
$this->filter("[list][*]a[list][*]a[*]b[/list][*]b[/list]")
);
$this->assertEquals(
$this->filter("[ul][*]a[ol][*]a[*]b[/ol][*]b[/ul]"),
"<ul><li>a<ol><li>a<li>b</ol><li>b</ul>"
"<ul><li>a<ol><li>a<li>b</ol><li>b</ul>",
$this->filter("[ul][*]a[ol][*]a[*]b[/ol][*]b[/ul]")
);
}
public function testSpoiler()
{
$this->assertEquals(
$this->filter("[spoiler]ShishNet[/spoiler]"),
"<span style=\"background-color:#000; color:#000;\">ShishNet</span>"
"<span style=\"background-color:#000; color:#000;\">ShishNet</span>",
$this->filter("[spoiler]ShishNet[/spoiler]")
);
$this->assertEquals(
$this->strip("[spoiler]ShishNet[/spoiler]"),
"FuvfuArg"
"FuvfuArg",
$this->strip("[spoiler]ShishNet[/spoiler]")
);
#$this->assertEquals(
# $this->filter("[spoiler]ShishNet"),
@ -67,42 +69,42 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
public function testURL()
{
$this->assertEquals(
$this->filter("[url]https://shishnet.org[/url]"),
"<a href=\"https://shishnet.org\">https://shishnet.org</a>"
"<a href=\"https://shishnet.org\">https://shishnet.org</a>",
$this->filter("[url]https://shishnet.org[/url]")
);
$this->assertEquals(
$this->filter("[url=https://shishnet.org]ShishNet[/url]"),
"<a href=\"https://shishnet.org\">ShishNet</a>"
"<a href=\"https://shishnet.org\">ShishNet</a>",
$this->filter("[url=https://shishnet.org]ShishNet[/url]")
);
$this->assertEquals(
$this->filter("[url=javascript:alert(\"owned\")]click to fail[/url]"),
"[url=javascript:alert(\"owned\")]click to fail[/url]"
"[url=javascript:alert(\"owned\")]click to fail[/url]",
$this->filter("[url=javascript:alert(\"owned\")]click to fail[/url]")
);
}
public function testEmailURL()
{
$this->assertEquals(
$this->filter("[email]spam@shishnet.org[/email]"),
"<a href=\"mailto:spam@shishnet.org\">spam@shishnet.org</a>"
"<a href=\"mailto:spam@shishnet.org\">spam@shishnet.org</a>",
$this->filter("[email]spam@shishnet.org[/email]")
);
}
public function testAnchor()
{
$this->assertEquals(
$this->filter("[anchor=rules]Rules[/anchor]"),
'<span class="anchor">Rules <a class="alink" href="#bb-rules" name="bb-rules" title="link to this anchor"> ¶ </a></span>'
'<span class="anchor">Rules <a class="alink" href="#bb-rules" name="bb-rules" title="link to this anchor"> ¶ </a></span>',
$this->filter("[anchor=rules]Rules[/anchor]")
);
}
private function filter($in)
private function filter($in): string
{
$bb = new BBCode();
return $bb->format($in);
}
private function strip($in)
private function strip($in): string
{
$bb = new BBCode();
return $bb->strip($in);

15
ext/biography/info.php Normal file
View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
class BiographyInfo extends ExtensionInfo
{
public const KEY = "biography";
public string $key = self::KEY;
public string $name = "User Bios";
public string $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2;
public string $description = "Allow users to write a bit about themselves";
}

36
ext/biography/main.php Normal file
View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
class Biography extends Extension
{
/** @var BiographyTheme */
protected ?Themelet $theme;
public function onUserPageBuilding(UserPageBuildingEvent $event)
{
global $page, $user;
$duser = $event->display_user;
$duser_config = UserConfig::get_for_user($event->display_user->id);
$bio = $duser_config->get_string("biography", "");
if ($user->id == $duser->id) {
$this->theme->display_composer($page, $bio);
} else {
$this->theme->display_biography($page, $bio);
}
}
public function onPageRequest(PageRequestEvent $event)
{
global $page, $user, $user_config;
if ($event->page_matches("biography")) {
if ($user->check_auth_token()) {
$user_config->set_string("biography", $_POST['biography']);
$page->flash("Bio Updated");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(referer_or(make_link()));
}
}
}
}

20
ext/biography/test.php Normal file
View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
class BiographyTest extends ShimmiePHPUnitTestCase
{
public function testBio()
{
$this->log_in_as_user();
$this->post_page("biography", ["biography"=>"My bio goes here"]);
$this->get_page("user/" . self::$user_name);
$this->assert_text("My bio goes here");
$this->log_in_as_admin();
$this->get_page("user/" . self::$user_name);
$this->assert_text("My bio goes here");
$this->get_page("user/" . self::$admin_name);
$this->assert_no_text("My bio goes here");
}
}

27
ext/biography/theme.php Normal file
View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
use function MicroHTML\TEXTAREA;
class BiographyTheme extends Themelet
{
public function display_biography(Page $page, string $bio)
{
$page->add_block(new Block("About Me", format_text($bio), "main", 30, "about-me"));
}
public function display_composer(Page $page, string $bio)
{
global $user;
$post_url = make_link("biography");
$auth = $user->get_auth_html();
$html = SHM_SIMPLE_FORM(
$post_url,
TEXTAREA(["style"=>"width: 100%", "rows"=>"6", "name"=>"biography"], $bio),
SHM_SUBMIT("Save")
);
$page->add_block(new Block("About Me", (string)$html, "main", 30));
}
}

View File

@ -1,13 +1,15 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BlocksInfo extends ExtensionInfo
{
public const KEY = "blocks";
public $key = self::KEY;
public $name = "Generic Blocks";
public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2;
public $description = "Add HTML to some space (News, Ads, etc)";
public string $key = self::KEY;
public string $name = "Generic Blocks";
public string $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2;
public string $description = "Add HTML to some space (News, Ads, etc)";
}

View File

@ -1,9 +1,11 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class Blocks extends Extension
{
/** @var BlocksTheme */
protected $theme;
protected ?Themelet $theme;
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BlocksTest extends ShimmiePHPUnitTestCase
{
public function testBlocks()

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
use function MicroHTML\TABLE;
use function MicroHTML\TR;
use function MicroHTML\TH;

View File

@ -1,15 +1,17 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BlotterInfo extends ExtensionInfo
{
public const KEY = "blotter";
public $key = self::KEY;
public $name = "Blotter";
public $url = "http://seemslegit.com/";
public $authors = ["Zach Hall"=>"zach@sosguy.net"];
public $license = self::LICENSE_GPLV2;
public $description = "Displays brief updates about whatever you want on every page.
public string $key = self::KEY;
public string $name = "Blotter";
public string $url = "http://seemslegit.com/";
public array $authors = ["Zach Hall"=>"zach@sosguy.net"];
public string $license = self::LICENSE_GPLV2;
public string $description = "Displays brief updates about whatever you want on every page.
Colors and positioning can be configured to match your site's design.
Development TODO at https://github.com/zshall/shimmie2/issues";

View File

@ -1,9 +1,11 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class Blotter extends Extension
{
/** @var BlotterTheme */
protected $theme;
protected ?Themelet $theme;
public function onInitExt(InitExtEvent $event)
{
@ -15,41 +17,35 @@ class Blotter extends Extension
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{
global $config;
$version = $config->get_int("blotter_version", 0);
/**
* If this version is less than "1", it's time to install.
*
* REMINDER: If I change the database tables, I must change up version by 1.
*/
if ($version < 1) {
/**
* Installer
*/
global $database, $config;
global $database;
if ($this->get_version("blotter_version") < 1) {
$database->create_table("blotter", "
id SCORE_AIPK,
entry_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
entry_text TEXT NOT NULL,
important SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N
");
id SCORE_AIPK,
entry_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
entry_text TEXT NOT NULL,
important BOOLEAN NOT NULL DEFAULT FALSE
");
// Insert sample data:
$database->execute(
"INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)",
["text"=>"Installed the blotter extension!", "important"=>"Y"]
["text"=>"Installed the blotter extension!", "important"=>true]
);
log_info("blotter", "Installed tables for blotter extension.");
$config->set_int("blotter_version", 1);
$this->set_version("blotter_version", 2);
}
if ($this->get_version("blotter_version") < 2) {
$database->standardise_boolean("blotter", "important");
$this->set_version("blotter_version", 2);
}
}
public function onSetupBuilding(SetupBuildingEvent $event)
{
$sb = new SetupBlock("Blotter");
$sb = $event->panel->create_new_block("Blotter");
$sb->add_int_option("blotter_recent", "<br />Number of recent entries to display: ");
$sb->add_text_option("blotter_color", "<br />Color of important updates: (ABCDEF format) ");
$sb->add_choice_option("blotter_position", ["Top of page" => "subheading", "In navigation bar" => "left"], "<br>Position: ");
$event->panel->add_block($sb);
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
@ -98,11 +94,7 @@ class Blotter extends Extension
if ($entry_text == "") {
die("No entry message!");
}
if (isset($_POST['important'])) {
$important = 'Y';
} else {
$important = 'N';
}
$important = isset($_POST['important']);
// Now insert into db:
$database->execute(
"INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)",
@ -124,7 +116,7 @@ class Blotter extends Extension
if (!isset($id)) {
die("No ID!");
}
$database->Execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]);
$database->execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]);
log_info("blotter", "Removed Entry #$id");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("blotter/editor"));

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BlotterTest extends ShimmiePHPUnitTestCase
{
public function testDenial()

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BlotterTheme extends Themelet
{
public function display_editor($entries)
@ -20,7 +22,7 @@ class BlotterTheme extends Themelet
$page->add_block(new Block("Blotter Entries", $html, "main", 10));
}
public function display_blotter($entries)
public function display_blotter(array $entries): void
{
global $page, $config;
$html = $this->get_html_for_blotter($entries);
@ -28,7 +30,7 @@ class BlotterTheme extends Themelet
$page->add_block(new Block(null, $html, $position, 20));
}
private function get_html_for_blotter_editor($entries)
private function get_html_for_blotter_editor(array $entries): string
{
global $user;
@ -99,7 +101,7 @@ class BlotterTheme extends Themelet
return $html;
}
private function get_html_for_blotter_page($entries)
private function get_html_for_blotter_page(array $entries): string
{
/**
* This one displays a list of all blotter entries.
@ -130,7 +132,7 @@ class BlotterTheme extends Themelet
return $html;
}
private function get_html_for_blotter($entries)
private function get_html_for_blotter(array $entries): string
{
global $config;
$i_color = $config->get_string("blotter_color", "#FF0000");

View File

@ -1,17 +1,19 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BrowserSearchInfo extends ExtensionInfo
{
public const KEY = "browser_search";
public $key = self::KEY;
public $name = "Browser Search";
public $url = "http://atravelinggeek.com/";
public $authors = ["ATravelingGeek"=>"atg@atravelinggeek.com"];
public $license = self::LICENSE_GPLV2;
public $version = "0.1c, October 26, 2007";
public $description = "Allows the user to add a browser 'plugin' to search the site with real-time suggestions";
public $documentation =
public string $key = self::KEY;
public string $name = "Browser Search";
public string $url = "http://atravelinggeek.com/";
public array $authors = ["ATravelingGeek"=>"atg@atravelinggeek.com"];
public string $license = self::LICENSE_GPLV2;
public ?string $version = "0.1c, October 26, 2007";
public string $description = "Allows the user to add a browser 'plugin' to search the site with real-time suggestions";
public ?string $documentation =
"Once installed, users with an opensearch compatible browser should see their search box light up with whatever \"click here to add a search engine\" notification they have
Some code (and lots of help) by Artanis (Erik Youngren <artanis.00@gmail.com>) from the 'tagger' extension - Used with permission";

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BrowserSearch extends Extension
{
@ -42,7 +44,7 @@ class BrowserSearch extends Extension
// And now to send it to the browser
$page->set_mode(PageMode::DATA);
$page->set_type(MIME_TYPE_XML);
$page->set_mime(MimeType::XML);
$page->set_data($xml);
} elseif ($event->page_matches("browser_search")) {
$suggestions = $config->get_string("search_suggestions_results_order");
@ -78,8 +80,7 @@ class BrowserSearch extends Extension
$sort_by['Tag Count'] = 't';
$sort_by['Disabled'] = 'n';
$sb = new SetupBlock("Browser Search");
$sb = $event->panel->create_new_block("Browser Search");
$sb->add_choice_option("search_suggestions_results_order", $sort_by, "Sort the suggestions by:");
$event->panel->add_block($sb);
}
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BrowserSearchTest extends ShimmiePHPUnitTestCase
{
public function testBasic()

View File

@ -1,13 +1,15 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BulkActionsInfo extends ExtensionInfo
{
public const KEY = "bulk_actions";
public $key = self::KEY;
public $name = "Bulk Actions";
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public $license = self::LICENSE_WTFPL;
public $description = "Provides query and selection-based bulk action support";
public $documentation = "Provides bulk action section in list view. Allows performing actions against a set of images based on query or manual selection. Based on Mass Tagger by Christian Walde <walde.christian@googlemail.com>, contributions by Shish and Agasa.";
public string $key = self::KEY;
public string $name = "Bulk Actions";
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public string $license = self::LICENSE_WTFPL;
public string $description = "Provides query and selection-based bulk action support";
public ?string $documentation = "Provides bulk action section in list view. Allows performing actions against a set of posts based on query or manual selection. Based on Mass Tagger by Christian Walde <walde.christian@googlemail.com>, contributions by Shish and Agasa.";
}

View File

@ -1,14 +1,14 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BulkActionException extends SCoreException
{
}
class BulkActionBlockBuildingEvent extends Event
{
/** @var array */
public $actions = [];
public $search_terms = [];
public array $actions = [];
public array $search_terms = [];
public function add_action(String $action, string $button_text, string $access_key = null, String $confirmation_message = "", String $block = "", int $position = 40)
{
@ -38,12 +38,9 @@ class BulkActionBlockBuildingEvent extends Event
class BulkActionEvent extends Event
{
/** @var string */
public $action;
/** @var array */
public $items;
/** @var bool */
public $redirect = true;
public string $action;
public Generator $items;
public bool $redirect = true;
public function __construct(String $action, Generator $items)
{
@ -56,7 +53,7 @@ class BulkActionEvent extends Event
class BulkActions extends Extension
{
/** @var BulkActionsTheme */
protected $theme;
protected ?Themelet $theme;
public function onPostListBuilding(PostListBuildingEvent $event)
{
@ -127,8 +124,8 @@ class BulkActions extends Extension
switch ($event->action) {
case "bulk_delete":
if ($user->can(Permissions::DELETE_IMAGE)) {
$i = $this->delete_items($event->items);
$page->flash("Deleted $i items");
$i = $this->delete_posts($event->items);
$page->flash("Deleted $i[0] items, totaling ".human_filesize($i[1]));
}
break;
case "bulk_tag":
@ -193,14 +190,14 @@ class BulkActions extends Extension
if (is_iterable($items)) {
send_event($bae);
}
if ($bae->redirect) {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(referer_or(make_link()));
}
} catch (BulkActionException $e) {
log_error(BulkActionsInfo::KEY, $e->getMessage(), $e->getMessage());
}
if ($bae->redirect) {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(referer_or(make_link()));
}
}
}
@ -227,25 +224,27 @@ class BulkActions extends Extension
return $a["position"] - $b["position"];
}
private function delete_items(iterable $items): int
private function delete_posts(iterable $posts): array
{
global $page;
$total = 0;
foreach ($items as $image) {
$size = 0;
foreach ($posts as $post) {
try {
if (class_exists("ImageBan") && isset($_POST['bulk_ban_reason'])) {
$reason = $_POST['bulk_ban_reason'];
if ($reason) {
send_event(new AddImageHashBanEvent($image->hash, $reason));
send_event(new AddImageHashBanEvent($post->hash, $reason));
}
}
send_event(new ImageDeletionEvent($image));
send_event(new ImageDeletionEvent($post));
$total++;
$size += $post->filesize;
} catch (Exception $e) {
$page->flash("Error while removing {$image->id}: " . $e->getMessage());
$page->flash("Error while removing {$post->id}: " . $e->getMessage());
}
}
return $total;
return [$total, $size];
}
private function tag_items(iterable $items, string $tags, bool $replace): int
@ -255,7 +254,7 @@ class BulkActions extends Extension
$pos_tag_array = [];
$neg_tag_array = [];
foreach ($tags as $new_tag) {
if (strpos($new_tag, '-') === 0) {
if (str_starts_with($new_tag, '-')) {
$neg_tag_array[] = substr($new_tag, 1);
} else {
$pos_tag_array[] = $new_tag;

View File

@ -8,13 +8,13 @@ function validate_selections(form, confirmationMessage) {
var queryOnly = false;
if(bulk_selector_active) {
var data = get_selected_items();
if(data.length==0) {
if(data.length===0) {
return false;
}
} else {
var query = $(form).find('input[name="bulk_query"]').val();
if (query == null || query == "") {
if (query == null || query === "") {
return false;
} else {
queryOnly = true;
@ -22,7 +22,7 @@ function validate_selections(form, confirmationMessage) {
}
if(confirmationMessage!=null&&confirmationMessage!="") {
if(confirmationMessage!=null&&confirmationMessage!=="") {
return confirm(confirmationMessage);
} else if(queryOnly) {
var action = $(form).find('input[name="submit_button"]').val();
@ -59,7 +59,7 @@ function deactivate_bulk_selector() {
function get_selected_items() {
var data = $('#bulk_selected_ids').val();
if(data==""||data==null) {
if(data===""||data==null) {
data = [];
} else {
data = JSON.parse(data);
@ -145,7 +145,7 @@ function select_range(start, end) {
function ( index, block ) {
block = $(block);
var id = block.data("post-id");
if(id==start)
if(id===start)
selecting = true;
if(selecting) {
@ -153,7 +153,7 @@ function select_range(start, end) {
data.push(id);
}
if(id==end) {
if(id===end) {
selecting = false;
}
}

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BulkActionsTheme extends Themelet
{
@ -8,7 +10,7 @@ class BulkActionsTheme extends Themelet
<input id='bulk_selector_activate' type='button' onclick='activate_bulk_selector();' value='Activate (M)anual Select' accesskey='m'/>
<div id='bulk_selector_controls' style='display: none;'>
<input id='bulk_selector_deactivate' type='button' onclick='deactivate_bulk_selector();' value='Deactivate (M)anual Select' accesskey='m'/>
Click on images to mark them.
Click on posts to mark them.
<br />
<table><tr><td>
<input id='bulk_selector_select_all' type='button'
@ -45,7 +47,7 @@ class BulkActionsTheme extends Themelet
$page->add_block($block);
}
public function render_ban_reason_input()
public function render_ban_reason_input(): string
{
if (class_exists("ImageBan")) {
return "<input type='text' name='bulk_ban_reason' placeholder='Ban reason (leave blank to not ban)' />";
@ -54,13 +56,13 @@ class BulkActionsTheme extends Themelet
}
}
public function render_tag_input()
public function render_tag_input(): string
{
return "<label><input type='checkbox' style='width:13px;' name='bulk_tags_replace' value='true'/>Replace tags</label>" .
"<input type='text' name='bulk_tags' required='required' placeholder='Enter tags here' />";
}
public function render_source_input()
public function render_source_input(): string
{
return "<input type='text' name='bulk_source' required='required' placeholder='Enter source here' />";
}

View File

@ -1,16 +1,18 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BulkAddInfo extends ExtensionInfo
{
public const KEY = "bulk_add";
public $key = self::KEY;
public $name = "Bulk Add";
public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2;
public $description = "Bulk add server-side images";
public $documentation =
public string $key = self::KEY;
public string $name = "Bulk Add";
public string $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2;
public string $description = "Bulk add server-side images";
public ?string $documentation =
"Upload the images into a new directory via ftp or similar, go to
shimmie's admin page and put that directory in the bulk add box.
If there are subdirectories, they get used as tags (eg if you

View File

@ -1,9 +1,11 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BulkAddEvent extends Event
{
public $dir;
public $results;
public string $dir;
public array $results;
public function __construct(string $dir)
{
@ -16,7 +18,7 @@ class BulkAddEvent extends Event
class BulkAdd extends Extension
{
/** @var BulkAddTheme */
protected $theme;
protected ?Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{

View File

@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BulkAddTest extends ShimmiePHPUnitTestCase
{
@ -6,7 +8,7 @@ class BulkAddTest extends ShimmiePHPUnitTestCase
{
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
$bae = send_event(new BulkAddEvent('asdf'));
$this->assertContains(
$this->assertContainsEquals(
"Error, asdf is not a readable directory",
$bae->results,
implode("\n", $bae->results)

View File

@ -1,8 +1,10 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BulkAddTheme extends Themelet
{
private $messages = [];
private array $messages = [];
/*
* Show a standard page for results to be put into

View File

@ -1,24 +1,26 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BulkAddCSVInfo extends ExtensionInfo
{
public const KEY = "bulk_add_csv";
public $key = self::KEY;
public $name = "Bulk Add CSV";
public $url = self::SHIMMIE_URL;
public $authors = ["velocity37"=>"velocity37@gmail.com"];
public $license = self::LICENSE_GPLV2;
public $description = "Bulk add server-side images with metadata from CSV file";
public $documentation =
public string $key = self::KEY;
public string $name = "Bulk Add CSV";
public string $url = self::SHIMMIE_URL;
public array $authors = ["velocity37"=>"velocity37@gmail.com"];
public string $license = self::LICENSE_GPLV2;
public string $description = "Bulk add server-side posts with metadata from CSV file";
public ?string $documentation =
"Modification of \"Bulk Add\" by Shish.<br><br>
Adds images from a CSV with the five following values: <br>
Adds posts from a CSV with the five following values: <br>
\"/path/to/image.jpg\",\"spaced tags\",\"source\",\"rating s/q/e\",\"/path/thumbnail.jpg\" <br>
<b>e.g.</b> \"/tmp/cat.png\",\"shish oekaki\",\"shimmie.shishnet.org\",\"s\",\"tmp/custom.jpg\" <br><br>
Any value but the first may be omitted, but there must be five values per line.<br>
<b>e.g.</b> \"/why/not/try/bulk_add.jpg\",\"\",\"\",\"\",\"\"<br><br>
Image thumbnails will be displayed at the AR of the full image. Thumbnails that are
Post thumbnails will be displayed at the AR of the full post. Thumbnails that are
normally static (e.g. SWF) will be displayed at the board's max thumbnail size<br><br>
Useful for importing tagged images without having to do database manipulation.<br>
<p><b>Note:</b> requires \"Admin Controls\" and optionally \"Image Ratings\" to be enabled<br><br>";
Useful for importing tagged posts without having to do database manipulation.<br>
<p><b>Note:</b> requires \"Admin Controls\" and optionally \"Post Ratings\" to be enabled<br><br>";
}

View File

@ -1,9 +1,11 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BulkAddCSV extends Extension
{
/** @var BulkAddCSVTheme */
protected $theme;
protected ?Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{

View File

@ -1,16 +1,18 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
class BulkAddCSVTheme extends Themelet
{
private $messages = [];
private array $messages = [];
/*
* Show a standard page for results to be put into
*/
public function display_upload_results(Page $page)
{
$page->set_title("Adding images from csv");
$page->set_heading("Adding images from csv");
$page->set_title("Adding posts from csv");
$page->set_heading("Adding posts from csv");
$page->add_block(new NavBlock());
foreach ($this->messages as $block) {
$page->add_block($block);
@ -26,8 +28,8 @@ class BulkAddCSVTheme extends Themelet
{
global $page;
$html = "
Add images from a csv. Images will be tagged and have their
source and rating set (if \"Image Ratings\" is enabled)
Add posts from a csv. Posts will be tagged and have their
source and rating set (if \"Post Ratings\" is enabled)
<br>Specify the absolute or relative path to a local .csv file. Check <a href=\"" . make_link("ext_doc/bulk_add_csv") . "\">here</a> for the expected format.
<p>".make_form(make_link("bulk_add_csv"))."

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