Compare commits

...

128 Commits

Author SHA1 Message Date
6631e2cb6f Add Rin to homepage counter 2023-10-23 23:10:45 -07:00
James Shiffer
7040b1b8e5 Fembooru changes 2022-05-14 16:44:50 -07:00
Shish
c1068f1b2b bump 2020-06-24 16:14:49 +01:00
Shish
bb5614c5ef strip auth info from debug info dump 2020-06-24 15:09:53 +01:00
Shish
81417a5031 make info command match help text 2020-06-24 15:00:44 +01:00
Shish
2197b15012 Add core/sanitize_php.php
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
2020-06-24 14:54:46 +01:00
Shish
eecd35d175 turn 'Use of undefined constant' into an error 2020-06-24 13:00:46 +01:00
Shish
1c216e8d51 formatting 2020-06-24 12:44:35 +01:00
Shish
04987ea70e 'resize' support for static thumbnailer 2020-06-23 15:17:54 +01:00
Matthew Barbour
844ec8b53e Removed stump user config section 2020-06-23 15:15:55 +01:00
Matthew Barbour
72de50aa71 Added bulk download extension 2020-06-23 15:15:43 +01:00
Matthew Barbour
ac63992efa Added BulkActionException to allow clean error feedback from bulk actions 2020-06-23 15:15:10 +01:00
Matthew Barbour
7c32b1f7a8 Fixed issue where enabling bulk selection, then disabling it, resulted in subsequent bulk actions not applying to any items 2020-06-23 15:14:58 +01:00
Matthew Barbour
919a3039c4 Added user API key system 2020-06-23 15:14:04 +01:00
Matthew Barbour
ea34d9b756 Changes and bugfixes for bulk import extension 2020-06-23 15:12:26 +01:00
Matthew Barbour
30f62c2ff8 Fixed transaction issue between cron uploader and bulk import 2020-06-23 15:11:35 +01:00
Matthew Barbour
9b9f1d0341 Cleaned up some warnings in bulk import/export extension
Added transactions to bulk import
Renamed beginTransaction to begin_transaction for naming consistency
Updated cron uploader to handle bulk import transactions
2020-06-23 15:07:00 +01:00
Matthew Barbour
835c3b68a1 Added null return option 2020-06-23 15:06:26 +01:00
Matthew Barbour
b937ad6255 Added thumbnail scaling options
Changed ffmpeg thumbnailer to instead output a full-size png which is forwarded to the image thumbnailer, to allow it to take advantage of all available scaling options
2020-06-23 15:05:55 +01:00
Matthew Barbour
8e976fb812 Added "any" search option for private images 2020-06-23 15:03:44 +01:00
Matthew Barbour
dd08b936e3 Added skipped count to bulk import 2020-06-23 15:03:32 +01:00
Matthew Barbour
1fdd5bf575 New private image extension 2020-06-23 15:03:17 +01:00
Matthew Barbour
6d16c52367 New bulk import/export extension 2020-06-23 15:01:22 +01:00
Matthew Barbour
587735a866 Added terabyte support to the shorthand functions 2020-06-23 14:58:41 +01:00
Shish
a2fe0725f5 extra credit for Tag EditCloud 2020-06-22 18:14:07 +01:00
Shish
73660b376e
Merge pull request #732 from LaureeGrd/master
Tag EditCloud: Added category sorting and grouped tags.
2020-06-22 18:13:32 +01:00
Matthew Barbour
d243867b18 Removed errant colon 2020-06-22 18:07:45 +01:00
Matthew Barbour
fac2067069 Corrected issue with post title edit field width 2020-06-22 18:07:02 +01:00
Matthew Barbour
2f313b704a Making document ready events consistent 2020-06-22 18:06:35 +01:00
Matthew Barbour
8fe7038e73 Added option to BulkActionEvent to prevent redirect 2020-06-22 18:06:05 +01:00
Matthew Barbour
c171e98591 Changed int input field to type number 2020-06-22 18:03:40 +01:00
Matthew Barbour
edc8e5aa43 Added bulk import/export to gd suggestion 2020-06-22 18:03:13 +01:00
Matthew Barbour
f6923af8ab Added source logging to cron upload output when all sources are enabled 2020-06-22 18:03:03 +01:00
Matthew Barbour
18cd74f57d Changed a bunch of core extensions to be hidden since there's nothing to interact with 2020-06-22 18:02:47 +01:00
Matthew Barbour
4d69e7ce34 Added early 404 kill if requested image is not found 2020-06-22 18:02:21 +01:00
Matthew Barbour
04cde74226 Performance improvement for counting image category tags 2020-06-22 18:02:01 +01:00
Matthew Barbour
b2405166b3 Added git information to system info for telling what git commit a submission is for 2020-06-22 18:01:02 +01:00
Matthew Barbour
4d0b90921d Added missing AutoTaggerException 2020-06-22 18:00:23 +01:00
Shish
05d4a3a592
Merge pull request #734 from shish/pg-action
Fix postgres testing
2020-06-22 17:27:18 +01:00
Shish
b3fb923cd1 Fix postgres testing
Looks like github updated their default container, so now it contains
postgres and postgres-client by default - we just need to manually
launch the installed-but-disabled daemon
2020-06-22 16:19:11 +01:00
Shish
ad1e52bf05 DATABASE_TIMEOUT isn't set in the installer 2020-06-22 16:08:04 +01:00
LaureeGrd
549ec593bb Tag EditCloud: Added category sorting and grouped tags.
This change implements a simple category-based alphabetical sorting system that puts all tags containing ':' in front of general tabs. It also groups them together for easier styling into columns, grids, or even opening the door for drop-down categories in the edit menu. A much needed feature for me since I have hundreds of tags and I manage them all by my own.
2020-06-14 05:32:53 -03:00
Matthew Barbour
ed8a9fca52 Removed function stub 2020-06-03 20:02:55 +01:00
Matthew Barbour
ec290d8676 Added additional optional video formats to the video handler 2020-06-03 20:01:52 +01:00
Matthew Barbour
5446f29141 improved filetype error handling 2020-06-03 20:01:08 +01:00
Matthew Barbour
86f7a06ed0 New options for cron uploader:
Logging level
Including all logs in output
Stop on error instead of continuing
2020-06-03 20:00:54 +01:00
Matthew Barbour
e1aefb78ab Fix for cron uploader issue with root paths ending in a slash 2020-06-03 19:59:29 +01:00
Matthew Barbour
12c331cbd2 Removed extra tag set from cron uploader, no longer needed now that it runs as a user 2020-06-03 19:58:57 +01:00
Matthew Barbour
8b407e3df3 Changed video html element to use the image objects height and width, if available 2020-06-03 19:58:23 +01:00
Matthew Barbour
bccb206369 formatting pass 2020-06-03 19:58:13 +01:00
Matthew Barbour
2bb5f349f9 Added file type names
Formatted file
2020-06-03 19:57:47 +01:00
Matthew Barbour
007e07e507 Various changes to cron uploader:
Removed count limit, the cron job now checks the max PH execution time and auto-stops itself at 80% of that value.

Now skips os-specific image cache files like thumbs.db and the __macosx folder.

Changed failed folder re-deployment to allow re-deploying to populated queue, making it easier to re-process lots of failed batches all at once.

Changed page to output as a stream, allowing a long-running process to provide output as it runs rather than just at the very end.

Changed import loop to use the yield convention, allowing faster consumption of found files and lower memory use overall.
2020-06-03 19:57:27 +01:00
Matthew Barbour
63b2601e67 Mime type handling overhaul
Changed mime type map to deal with the reality that certain file types have multiple extensions and/or multiple mime types, as well as constants supporting all of the data. Created new functions using the updated mime type map to resolve mime types and extensions. Updated various items around the project that determine mime/extension to take advantage of the new functions.
2020-06-03 19:47:40 +01:00
Matthew Barbour
16c58e266b Added manual page mode to allow extensions to have direct control of the output 2020-06-03 19:40:43 +01:00
Matthew Barbour
6145ecc6f8 Updated post title extension to resolve set_title being removed from display image event
Resolves #724
2020-05-29 22:59:37 +01:00
Matthew Barbour
830915adf2 Fixed lite theme not showing image titles 2020-05-29 22:59:30 +01:00
Shish
06bd4589da option for admins to create new users 2020-05-19 19:33:51 +01:00
Shish
1e76fb239e s/fullrandom/dailyshuffle/, and run formatter 2020-05-13 13:03:49 +01:00
Shish
f7c6b662cd
Merge pull request #720 from MetallicAchu/master
Added a new search criteria "fullrandom"
2020-05-13 13:02:11 +01:00
MetallicAchu
00060c34c2
Merge pull request #1 from MetallicAchu/MetallicAchu-patch-1
Update main.php
2020-05-07 08:12:32 +03:00
MetallicAchu
10d46395d7
Update main.php
Added order[=|:]fullrandom so user doesn't need to choose a new seed every time.
Thus the list will change on a daily basis without user interaction, giving a more dynamic feel to the website
2020-05-07 08:10:15 +03:00
Shish
72645af9a4 refactor a bunch of weirdness in image replacement 2020-04-25 21:38:11 +01:00
Shish
2cae6cd273 format 2020-04-25 21:36:28 +01:00
Shish
0b2e36303d allow bypassing auth tokens in unit tests 2020-04-25 21:35:14 +01:00
Shish
b0cb46abca test a couple extra branches 2020-04-24 14:10:45 +01:00
Shish
78710166a1 add a extra escape, fixes #718 2020-04-20 09:53:44 +01:00
Shish
c146a9f53d don't double-escape 2020-04-12 12:45:19 +01:00
Shish
f6112d26a2 unify single and global history pages 2020-04-12 12:43:12 +01:00
Shish
b04b5af190
Merge pull request #717 from DanielOaks/add-logout-button
Add logout button to themes that use subnav bar
2020-04-06 16:54:48 +01:00
Daniel Oaks
02d42a01b4 Add logout button to themes that use subnav bar 2020-04-07 01:36:10 +10:00
Shish
0039aafe94 avoid excess ampersands 2020-04-02 22:38:50 +01:00
Shish
1d389f0156 make tests/router.php more like .htaccess 2020-04-02 22:31:57 +01:00
Shish
69cb67fe24 stagger thumbnail cache 2020-03-28 16:11:05 +00:00
Shish
5ea26a80cc nicetest without http vs https pain 2020-03-28 15:48:27 +00:00
Shish
126c629a1a test 2020-03-28 14:39:03 +00:00
Shish
ab4b745310 test 2020-03-28 14:33:48 +00:00
Shish
f47e35e4e5 make make_link more sane 2020-03-28 14:11:14 +00:00
Shish
fd359fb08c remove broken tagger extension 2020-03-28 00:56:54 +00:00
Shish
866b77ab19 set max-width / max-height for random and featured image blocks 2020-03-28 00:23:29 +00:00
Shish
b60e8ac5b4 make modify_url work better 2020-03-27 23:35:07 +00:00
Shish
70acc6015b drop support for ie6 2020-03-27 20:57:15 +00:00
Shish
a3a129df5f more niceurlness 2020-03-27 20:53:21 +00:00
Shish
02675609b4 more referer dedupe 2020-03-27 20:24:26 +00:00
Shish
85662575c5 stop warning for lack of referer / user-agent - not having those is now pretty normal 2020-03-27 19:42:46 +00:00
Shish
c16e3fd939 dedupe some referer handling 2020-03-27 19:41:34 +00:00
Shish
5ea7cc5b36 SCRIPT_NAME instead of PHP_SELF to find self for niceurl test 2020-03-27 19:03:46 +00:00
Shish
efde5e1edf bump 2020-03-27 14:56:59 +00:00
Shish
7b9c9dc208 Make SHM_FORM generate the correct targets 2020-03-27 14:46:55 +00:00
Shish
bc3de6a52a a bunch more testing and fixes for Pools 2020-03-27 14:41:24 +00:00
Shish
d85f4d3799 automatic version 2020-03-27 12:24:47 +00:00
Shish
880a702b42 dedupe page_number parsing 2020-03-27 00:23:29 +00:00
Shish
b5f0bc7621 a bunch of pools cleanup 2020-03-27 00:15:15 +00:00
Shish
599043baa5 whitespace 2020-03-27 00:06:55 +00:00
Shish
36a2125e90 basic tests for page nav 2020-03-26 21:28:36 +00:00
Shish
1f50f14672 basic tests for help pages 2020-03-26 21:28:23 +00:00
Shish
b0c5043892 test InitExt / DatabaseUpgrade 2020-03-26 19:06:30 +00:00
Shish
861ee946a9 typo 2020-03-26 19:03:34 +00:00
Shish
237f8148f3 a bunch of pools tests and fixes 2020-03-26 18:52:31 +00:00
Shish
17f3b44212 mention system_info in bug report form 2020-03-26 17:23:23 +00:00
Shish
511a82f2ba include database version in sys info 2020-03-26 16:57:08 +00:00
Shish
2d0b107adb convert pool IDs to ints 2020-03-26 16:50:16 +00:00
Shish
ecbf4f52a0 make sysinfo part of core, and use YAML for easier parsing 2020-03-26 16:46:09 +00:00
Shish
06e5b02874 streamline bug template 2020-03-26 15:20:46 +00:00
Shish
f819993685 remove nit 2020-03-26 15:16:30 +00:00
Shish
1b10d8583e missed a reference 2020-03-26 15:01:26 +00:00
Shish
e362f3bad2 stop depending on unmaintained tablesorter plugin 2020-03-26 14:57:38 +00:00
Shish
5f5b858175 allow images and thumbs to be cached when served in docker 2020-03-25 19:57:50 +00:00
Shish
168cf99188 shrink docker image from ~550MB to ~300MB 2020-03-25 19:43:28 +00:00
Shish
134fd7d919 xdebug only for unit-test image 2020-03-25 19:37:38 +00:00
Shish
ea637132bd named layers 2020-03-25 19:36:41 +00:00
Shish
566c92b780 make sure version is in core/sys_config 2020-03-25 15:31:28 +00:00
Shish
4e4deed889 bump 2020-03-25 15:27:50 +00:00
Shish
d4e05d947a bump 2020-03-25 15:20:55 +00:00
Shish
25248c089d inherit signup page from default on lite theme 2020-03-25 15:19:28 +00:00
Shish
0094d5c1a8 attempt 2 at auto-release 2020-03-25 13:53:21 +00:00
Shish
f0db4f9a02 first attempt at automated release 2020-03-25 13:40:54 +00:00
Shish
03806d0420 https a bunch of things 2020-03-25 11:47:00 +00:00
Shish
c3b67f346b Actually, let's have the docs in the wiki 2020-03-25 11:26:33 +00:00
Shish
edee8e7427 fix for EXIF with strict types 2020-03-25 10:50:32 +00:00
Shish
c794e457b1 + 2020-03-23 20:01:37 +00:00
Shish
1753cbd72b bump for cleaner install 2020-03-23 20:00:53 +00:00
Shish
8b1b4d257e stick a blank index.php in data/ just in case somebody left directory-indexing on 2020-03-23 19:56:05 +00:00
Shish
ea0e83abc9 clearer missing-vendor screen 2020-03-23 19:50:28 +00:00
Shish
bf4280461f upgrade docs 2020-03-23 19:01:37 +00:00
Shish
d0b00a72ce
Update INSTALL.md 2020-03-23 18:51:17 +00:00
Shish
d14819387e composer is a pain on windows 2020-03-23 18:50:45 +00:00
Shish
040bffa4f6 even the installer requires composer now, so check for that first 2020-03-23 18:47:18 +00:00
Shish
3f26013b28 the 'binary' releases are hugely out of date 2020-03-23 18:38:08 +00:00
185 changed files with 3602 additions and 4124 deletions

View File

@ -4,27 +4,25 @@ about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**Server Software**
(You can get all these stats from `http://<your site>/system_info`)
**To Reproduce**
Steps to reproduce the behavior:
- Shimmie version:
- Database: [mysql, postgres, ...]
- Web server: [apache, nginx, ...]
**Client Software (please complete the following information)**
- Device: [e.g. iphone, windows desktop]
- Browser: [e.g. chrome, safari]
**What steps trigger this bug**
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
**What did you expect to happen?**
A clear and concise description of what you expected to happen.
**Screenshots**
**What actually happened?**
If applicable, add screenshots to help explain your problem.
**Server Software, if you're the server admin (please complete the following information):**
- Shimmie version
- Database [mysql, postgres, ...]
- Web server [apache, nginx, ...]
**Client Software (please complete the following information):**
- Device [e.g. iphone, windows desktop]
- Browser [e.g. chrome, safari]

62
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: Create Release
on:
push:
tags:
- 'v*'
jobs:
build:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Get version from tag
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/}
- name: Test version in sys_config
run: grep ${{ steps.get_version.outputs.VERSION }} core/sys_config.php
- name: Build
run: |
composer install --no-dev
cd ..
tar cvzf shimmie2-${{ steps.get_version.outputs.VERSION }}.tgz shimmie2
zip -r shimmie2-${{ steps.get_version.outputs.VERSION }}.zip shimmie2
- name: Create Release
id: create_release
uses: actions/create-release@latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Shimmie ${{ steps.get_version.outputs.VERSION }}
body: Automated release from tags
draft: false
prerelease: false
- name: Upload Zip
id: upload-release-asset-zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ../shimmie2-${{ steps.get_version.outputs.VERSION }}.zip
asset_name: shimmie2-${{ steps.get_version.outputs.VERSION }}.zip
asset_content_type: application/zip
- name: Upload Tar
id: upload-release-asset-tar
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ../shimmie2-${{ steps.get_version.outputs.VERSION }}.tgz
asset_name: shimmie2-${{ steps.get_version.outputs.VERSION }}.tgz
asset_content_type: application/gzip

View File

@ -32,7 +32,7 @@ jobs:
run: |
mkdir -p data/config
if [[ "${{ matrix.database }}" == "pgsql" ]]; then
sudo apt update && sudo apt-get install -y postgresql postgresql-client ;
sudo systemctl start postgresql ;
psql --version ;
sudo -u postgres psql -c "SELECT set_config('log_statement', 'all', false);" -U postgres ;
sudo -u postgres psql -c "CREATE USER shimmie WITH PASSWORD 'shimmie';" -U postgres ;

View File

@ -17,8 +17,7 @@
# rather than link to images/ha/hash and have an ugly filename,
# we link to images/hash/tags.ext; mod_rewrite splits things so
# that shimmie sees hash and the user sees tags.ext
RewriteRule ^_images/([0-9a-f]{2})([0-9a-f]{30}).*$ data/images/$1/$1$2 [L]
RewriteRule ^_thumbs/([0-9a-f]{2})([0-9a-f]{30}).*$ data/thumbs/$1/$1$2 [L]
RewriteRule ^_(images|thumbs)/([0-9a-f]{2})([0-9a-f]{30}).*$ data/$1/$2/$2$3 [L]
# any requests for files which don't physically exist should be handled by index.php
RewriteCond %{REQUEST_FILENAME} !-f

View File

@ -1,7 +1,17 @@
# "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
RUN apt update && apt install -y composer php7.3-gd php7.3-dom php7.3-sqlite3 imagemagick
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
COPY composer.json composer.lock /app/
WORKDIR /app
RUN composer install --no-dev
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
COPY composer.json composer.lock /app/
WORKDIR /app
RUN composer install
@ -15,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
FROM debian:stable-slim 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; \
@ -29,11 +39,11 @@ HEALTHCHECK --interval=5m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ ||
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 php-xdebug \
composer imagemagick vim zip unzip && \
php7.3-cli php7.3-gd php7.3-pgsql php7.3-mysql php7.3-sqlite3 php7.3-zip php7.3-dom php7.3-mbstring \
imagemagick zip unzip && \
rm -rf /var/lib/apt/lists/*
COPY --from=0 /app /app
COPY --from=1 /usr/local/bin/su-exec /usr/local/bin/su-exec
COPY --from=app /app /app
COPY --from=suexec /usr/local/bin/su-exec /usr/local/bin/su-exec
WORKDIR /app
CMD ["/bin/sh", "/app/tests/docker-init.sh"]

View File

@ -17,24 +17,25 @@
# Documentation
* [Install straight on disk](https://github.com/shish/shimmie2/tree/master/docs/INSTALL.md)
* [Install in docker container](https://github.com/shish/shimmie2/tree/master/docs/DOCKER.md)
* [Upgrade process](https://github.com/shish/shimmie2/tree/master/docs/UPGRADE.md)
* [Advanced config](https://github.com/shish/shimmie2/tree/master/docs/CONFIG.md)
* [Developer notes](https://github.com/shish/shimmie2/tree/master/docs/DEV.md)
* [High-performance notes](https://github.com/shish/shimmie2/tree/master/docs/SPEED.md)
* [Install straight on disk](https://github.com/shish/shimmie2/wiki/Install)
* [Install in docker container](https://github.com/shish/shimmie2/wiki/Docker)
* [Upgrade process](https://github.com/shish/shimmie2/wiki/Upgrade)
* [Basic settings](https://github.com/shish/shimmie2/wiki/Settings)
* [Advanced config](https://github.com/shish/shimmie2/wiki/Advanced-Config)
* [Developer notes](https://github.com/shish/shimmie2/wiki/Development-Info)
* [High-performance notes](https://github.com/shish/shimmie2/wiki/Performance)
# Contact
Email: webmaster at shishnet.org
Issue/Bug tracker: http://github.com/shish/shimmie2/issues
Issue/Bug tracker: https://github.com/shish/shimmie2/issues
# Licence
All code is released under the [GNU GPL Version 2](http://www.gnu.org/licenses/gpl-2.0.html) unless mentioned otherwise.
All code is released under the [GNU GPL Version 2](https://www.gnu.org/licenses/gpl-2.0.html) unless mentioned otherwise.
If you give shimmie to someone else, you have to give them the source (which
should be easy, as PHP is an interpreted language...). If you want to add

View File

@ -6,10 +6,6 @@
"minimum-stability" : "dev",
"repositories" : [
{
"type": "composer",
"url": "https://asset-packagist.org"
},
{
"type" : "package",
"package" : {
@ -25,30 +21,29 @@
],
"require" : {
"php" : ">=7.3",
"php" : "^7.3",
"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" : "dev-master",
"shish/ffsphp" : "0.0.*",
"shish/microcrud" : "dev-master",
"shish/microhtml" : "^1.0.0",
"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.*",
"bower-asset/jquery" : "1.12.*",
"bower-asset/jquery-timeago" : "1.5.*",
"bower-asset/tablesorter" : "dev-master",
"bower-asset/mediaelement" : "2.21.*",
"bower-asset/js-cookie" : "2.1.*"
},
"require-dev" : {
"phpunit/phpunit" : "8.*"
},
},
"suggest": {
"ext-memcache": "memcache caching",
@ -59,9 +54,14 @@
"ext-curl": "some extensions",
"ext-ctype": "some extensions",
"ext-json": "some extensions",
"ext-zip": "self-updater extension",
"ext-zip": "self-updater extension, bulk import/export",
"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"
}
}

1655
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ abstract class PageMode
const DATA = 'data';
const PAGE = 'page';
const FILE = 'file';
const MANUAL = 'manual';
}
/**
@ -238,16 +239,13 @@ class BasePage
// ==============================================
/**
* Display the page according to the mode and data given.
*/
public function display(): void
public function send_headers(): void
{
header("HTTP/1.0 {$this->code} Shimmie");
header("Content-type: " . $this->type);
header("X-Powered-By: Shimmie-" . VERSION);
if (!headers_sent()) {
header("HTTP/1.0 {$this->code} Shimmie");
header("Content-type: " . $this->type);
header("X-Powered-By: Shimmie-" . VERSION);
foreach ($this->http_headers as $head) {
header($head);
}
@ -257,8 +255,20 @@ class BasePage
} else {
print "Error: Headers have already been sent to the client.";
}
}
/**
* Display the page according to the mode and data given.
*/
public function display(): void
{
if ($this->mode!=PageMode::MANUAL) {
$this->send_headers();
}
switch ($this->mode) {
case PageMode::MANUAL:
break;
case PageMode::PAGE:
usort($this->blocks, "blockcmp");
$this->add_auto_html_headers();
@ -394,9 +404,7 @@ 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/tablesorter/jquery.tablesorter.min.js",
"vendor/bower-asset/js-cookie/src/js.cookie.js",
"ext/static_files/modernizr-3.3.1.custom.js",
],

View File

@ -53,7 +53,7 @@ class BaseThemelet
$h_tip = html_escape($image->get_tooltip());
$h_tags = html_escape(strtolower($image->get_tag_list()));
$extArr = array_flip(['swf', 'svg', 'mp3']); //List of thumbless filetypes
$extArr = array_flip([EXTENSION_FLASH, EXTENSION_SVG, EXTENSION_MP3]); //List of thumbless filetypes
if (!isset($extArr[$image->ext])) {
$tsize = get_thumbnail_size($image->width, $image->height);
} else {

View File

@ -60,7 +60,7 @@ class Database
$this->connect_engine();
$this->engine->init($this->db);
$this->beginTransaction();
$this->begin_transaction();
}
private function connect_engine(): void
@ -78,11 +78,14 @@ class Database
} elseif ($db_proto === DatabaseDriver::SQLITE) {
$this->engine = new SQLite();
} else {
die('Unknown PDO driver: '.$db_proto);
die_nicely(
'Unknown PDO driver: '.$db_proto,
"Please check that this is a valid driver, installing the PHP modules if needed"
);
}
}
public function beginTransaction(): void
public function begin_transaction(): void
{
if ($this->transaction === false) {
$this->db->beginTransaction();
@ -90,9 +93,14 @@ class Database
}
}
public function is_transaction_open(): bool
{
return !is_null($this->db) && $this->transaction === true;
}
public function commit(): bool
{
if (!is_null($this->db) && $this->transaction === true) {
if ($this->is_transaction_open()) {
$this->transaction = false;
return $this->db->commit();
} else {
@ -102,7 +110,7 @@ class Database
public function rollback(): bool
{
if (!is_null($this->db) && $this->transaction === true) {
if ($this->is_transaction_open()) {
$this->transaction = false;
return $this->db->rollback();
} else {
@ -139,6 +147,11 @@ class Database
return $this->engine->name;
}
public function get_version(): string
{
return $this->engine->get_version($this->db);
}
private function count_time(string $method, float $start, string $query, ?array $args): void
{
global $_tracer, $tracer_enabled;

View File

@ -31,6 +31,8 @@ abstract class DBEngine
}
abstract public function set_timeout(PDO $db, int $time);
abstract public function get_version(PDO $db): string;
}
class MySQL extends DBEngine
@ -68,12 +70,15 @@ class MySQL extends DBEngine
// 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 get_version(PDO $db): string
{
return $db->query('select version()')->fetch()[0];
}
}
class PostgreSQL extends DBEngine
{
/** @var string */
public $name = DatabaseDriver::PGSQL;
@ -87,7 +92,9 @@ class PostgreSQL extends DBEngine
} else {
$db->exec("SET application_name TO 'shimmie [local]';");
}
$this->set_timeout($db, DATABASE_TIMEOUT);
if (defined("DATABASE_TIMEOUT")) {
$this->set_timeout($db, DATABASE_TIMEOUT);
}
}
public function scoreql_to_sql(string $data): string
@ -110,6 +117,11 @@ class PostgreSQL extends DBEngine
{
$db->exec("SET statement_timeout TO ".$time.";");
}
public function get_version(PDO $db): string
{
return $db->query('select version()')->fetch()[0];
}
}
// shimmie functions for export to sqlite
@ -216,4 +228,9 @@ class SQLite extends DBEngine
{
// There doesn't seem to be such a thing for SQLite, so it does nothing
}
public function get_version(PDO $db): string
{
return $db->query('select sqlite_version()')->fetch()[0];
}
}

View File

@ -108,21 +108,26 @@ class PageRequestEvent extends Event
if ($offset >= 0 && $offset < $this->arg_count) {
return $this->args[$offset];
} else {
throw new SCoreException("Requested an invalid argument #$n");
$nm1 = $this->arg_count - 1;
throw new SCoreException("Requested an invalid page argument {$offset} / {$nm1}");
}
}
public function try_page_num(int $n): int
/**
* If page arg $n is set, then treat that as a 1-indexed page number
* and return a 0-indexed page number less than $max; else return 0
*/
public function try_page_num(int $n, ?int $max=null): int
{
if ($this->count_args() > $n) {
$i = $this->get_arg($n);
if (is_numeric($i) && int_escape($i) > 0) {
return int_escape($i);
return page_number($i, $max);
} else {
return 1;
return 0;
}
} else {
return 1;
return 0;
}
}

View File

@ -7,7 +7,7 @@
* Also loads the theme object into $this->theme if available
*
* The original concept came from Artanis's Extension extension
* --> http://github.com/Artanis/simple-extension/tree/master
* --> https://github.com/Artanis/simple-extension/tree/master
* Then re-implemented by Shish after he broke the forum and couldn't
* find the thread where the original was posted >_<
*/
@ -115,7 +115,7 @@ abstract class ExtensionInfo
// Every credit you get costs us RAM. It stops now.
public const SHISH_NAME = "Shish";
public const SHISH_EMAIL = "webmaster@shishnet.org";
public const SHIMMIE_URL = "http://code.shishnet.org/shimmie2/";
public const SHIMMIE_URL = "https://code.shishnet.org/shimmie2/";
public const SHISH_AUTHOR = [self::SHISH_NAME=>self::SHISH_EMAIL];
public const LICENSE_GPLV2 = "GPLv2";
@ -275,7 +275,7 @@ abstract class FormatterExtension extends Extension
*/
abstract class DataHandlerExtension extends Extension
{
protected $SUPPORTED_EXT = [];
protected $SUPPORTED_MIME = [];
protected function move_upload_to_archive(DataUploadEvent $event)
{
@ -298,14 +298,11 @@ abstract class DataHandlerExtension extends Extension
send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
/* Check if we are replacing an image */
if (array_key_exists('replace', $event->metadata) && isset($event->metadata['replace'])) {
if (!is_null($event->replace_id)) {
/* hax: This seems like such a dirty way to do this.. */
/* Validate things */
$image_id = int_escape($event->metadata['replace']);
/* Check to make sure the image exists. */
$existing = Image::by_id($image_id);
$existing = Image::by_id($event->replace_id);
if (is_null($existing)) {
throw new UploadException("Image to replace does not exist!");
@ -320,19 +317,25 @@ abstract class DataHandlerExtension extends Extension
if (is_null($image)) {
throw new UploadException("Data handler failed to create image object from data");
}
if (empty($image->ext)) {
throw new UploadException("Unable to determine extension for ". $event->tmpname);
}
try {
send_event(new MediaCheckPropertiesEvent($image));
} catch (MediaException $e) {
throw new UploadException("Unable to scan media properties: ".$e->getMessage());
}
send_event(new ImageReplaceEvent($image_id, $image));
$event->image_id = $image_id;
send_event(new ImageReplaceEvent($event->replace_id, $image));
$event->image_id = $event->replace_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");
}
if (empty($image->ext)) {
throw new UploadException("Unable to determine extension for ". $event->tmpname);
}
try {
send_event(new MediaCheckPropertiesEvent($image));
} catch (MediaException $e) {
@ -406,10 +409,12 @@ abstract class DataHandlerExtension extends Extension
$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(getMimeType($filename));
} else {
$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'];
}
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
$image->source = $metadata['source'];
@ -422,15 +427,20 @@ abstract class DataHandlerExtension extends Extension
protected function supported_ext(string $ext): bool
{
return in_array(strtolower($ext), $this->SUPPORTED_EXT);
return in_array(get_mime_for_extension($ext), $this->SUPPORTED_MIME);
}
public static function get_all_supported_exts(): array
{
$arr = [];
foreach (getSubclassesOf("DataHandlerExtension") as $handler) {
$arr = array_merge($arr, (new $handler())->SUPPORTED_EXT);
$handler = (new $handler());
foreach ($handler->SUPPORTED_MIME as $mime) {
$arr = array_merge($arr, get_all_extension_for_mime($mime));
}
}
$arr = array_unique($arr);
return $arr;
}
}

448
core/filetypes.php Normal file
View File

@ -0,0 +1,448 @@
<?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

@ -213,6 +213,27 @@ 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
*
@ -570,7 +591,7 @@ class Image
*/
public function get_mime_type(): string
{
return getMimeType($this->get_image_filename(), $this->get_ext());
return get_mime($this->get_image_filename(), $this->get_ext());
}
/**

View File

@ -39,7 +39,7 @@ function add_dir(string $base): array
* @param string $tags
* @throws UploadException
*/
function add_image(string $tmpname, string $filename, string $tags): void
function add_image(string $tmpname, string $filename, string $tags): int
{
assert(file_exists($tmpname));
@ -52,7 +52,11 @@ function add_image(string $tmpname, string $filename, string $tags): void
$metadata['tags'] = Tag::explode($tags);
$metadata['source'] = null;
send_event(new DataUploadEvent($tmpname, $metadata));
$due = new DataUploadEvent($tmpname, $metadata);
send_event($due);
return $due->image_id;
}
/**
@ -69,6 +73,12 @@ function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_sca
{
global $config;
$fit = $config->get_string(ImageConfig::THUMB_FIT);
if (in_array($fit, [Media::RESIZE_TYPE_FILL, Media::RESIZE_TYPE_STRETCH, Media::RESIZE_TYPE_FIT_BLUR])) {
return [$config->get_int(ImageConfig::THUMB_WIDTH), $config->get_int(ImageConfig::THUMB_HEIGHT)];
}
if ($orig_width === 0) {
$orig_width = 192;
}
@ -128,21 +138,35 @@ function get_thumbnail_max_size_scaled(): array
function create_image_thumb(string $hash, string $type, string $engine = null)
{
global $config;
$inname = warehouse_path(Image::IMAGE_DIR, $hash);
$outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
$tsize = get_thumbnail_max_size_scaled();
create_scaled_image($inname, $outname, $tsize, $type, $engine);
create_scaled_image(
$inname,
$outname,
$tsize,
$type,
$engine,
$config->get_string(ImageConfig::THUMB_FIT)
);
}
function create_scaled_image(string $inname, string $outname, array $tsize, string $type, ?string $engine)
function create_scaled_image(string $inname, string $outname, array $tsize, string $type, ?string $engine = null, ?string $resize_type = null)
{
global $config;
if (empty($engine)) {
$engine = $config->get_string(ImageConfig::THUMB_ENGINE);
}
if (empty($resize_type)) {
$resize_type = $config->get_string(ImageConfig::THUMB_FIT);
}
$output_format = $config->get_string(ImageConfig::THUMB_TYPE);
if ($output_format=="webp") {
if ($output_format==EXTENSION_WEBP) {
$output_format = Media::WEBP_LOSSY;
}
@ -153,10 +177,10 @@ function create_scaled_image(string $inname, string $outname, array $tsize, stri
$outname,
$tsize[0],
$tsize[1],
false,
$resize_type,
$output_format,
$config->get_int(ImageConfig::THUMB_QUALITY),
true,
$config->get_bool('thumb_upscale', false)
true
));
}

View File

@ -116,8 +116,6 @@ class Tag
for ($i = 0; $i < count($tags1); $i++) {
if ($tags1[$i]!==$tags2[$i]) {
var_dump($tags1);
var_dump($tags2);
return false;
}
}

View File

@ -5,8 +5,8 @@
* @package Shimmie
* @copyright Copyright (c) 2007-2015, Shish et al.
* @author Shish [webmaster at shishnet.org], jgen [jeffgenovy at gmail.com]
* @link http://code.shishnet.org/shimmie2/
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
* @link https://code.shishnet.org/shimmie2/
* @license https://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
*
* Initialise the database, check that folder
* permissions are set properly.
@ -20,23 +20,12 @@ function install()
date_default_timezone_set('UTC');
if (is_readable("data/config/shimmie.conf.php")) {
exit_with_page(
die_nicely(
"Shimmie is already installed.",
"data/config/shimmie.conf.php exists, how did you get here?"
);
}
if (!file_exists("vendor/")) {
exit_with_page("Install Error", "
<p>Shimmie is unable to find the composer vendor directory.</p>
<p>Have you followed the composer setup instructions found in the
<a href=\"https://github.com/shish/shimmie2#installation-development\">README</a>?</p>
<p>If you are not intending to do any development with Shimmie,
it is highly recommend you use one of the pre-packaged releases
found on <a href=\"https://github.com/shish/shimmie2/releases\">Github</a> instead.</p>
");
}
// Pull in necessary files
require_once "vendor/autoload.php";
global $_tracer;
@ -80,7 +69,7 @@ function do_install($dsn)
create_tables(new Database($dsn));
write_config($dsn);
} catch (InstallerException $e) {
exit_with_page($e->title, $e->body, $e->code);
die_nicely($e->title, $e->body, $e->code);
}
}
@ -128,7 +117,7 @@ function ask_questions()
$warn_msg = $warnings ? "<h3>Warnings</h3>".implode("\n<p>", $warnings) : "";
$err_msg = $errors ? "<h3>Errors</h3>".implode("\n<p>", $errors) : "";
exit_with_page(
die_nicely(
"Install Options",
<<<EOD
$warn_msg
@ -315,7 +304,7 @@ function write_config($dsn)
if (file_put_contents("data/config/shimmie.conf.php", $file_content, LOCK_EX)) {
header("Location: index.php?flash=Installation%20complete");
exit_with_page(
die_nicely(
"Installation Successful",
"<p>If you aren't redirected, <a href=\"index.php\">click here to Continue</a>."
);
@ -335,25 +324,3 @@ function write_config($dsn)
);
}
}
function exit_with_page($title, $body, $code=0)
{
print("<!DOCTYPE html>
<html lang='en'>
<head>
<title>Shimmie Installer</title>
<link rel=\"shortcut icon\" href=\"ext/static_files/static/favicon.ico\">
<link rel=\"stylesheet\" href=\"ext/static_files/style.css\" type=\"text/css\">
</head>
<body>
<div id=\"installer\">
<h1>Shimmie Installer</h1>
<h3>$title</h3>
<div class=\"container\">
$body
</div>
</div>
</body>
</html>");
exit($code);
}

View File

@ -10,6 +10,15 @@ define("SCORE_LOG_INFO", 20);
define("SCORE_LOG_DEBUG", 10);
define("SCORE_LOG_NOTSET", 0);
const LOGGING_LEVEL_NAMES = [
SCORE_LOG_NOTSET=>"Not Set",
SCORE_LOG_DEBUG=>"Debug",
SCORE_LOG_INFO=>"Info",
SCORE_LOG_WARNING=>"Warning",
SCORE_LOG_ERROR=>"Error",
SCORE_LOG_CRITICAL=>"Critical",
];
/**
* A shorthand way to send a LogEvent
*

View File

@ -18,6 +18,7 @@ abstract class Permissions
public const BAN_IP = "ban_ip";
public const CREATE_USER = "create_user";
public const CREATE_OTHER_USER = "create_other_user";
public const EDIT_USER_NAME = "edit_user_name";
public const EDIT_USER_PASSWORD = "edit_user_password";
public const EDIT_USER_INFO = "edit_user_info"; # email address, etc
@ -95,4 +96,11 @@ abstract class Permissions
public const CRON_ADMIN = "cron_admin";
public const APPROVE_IMAGE = "approve_image";
public const APPROVE_COMMENT = "approve_comment";
public const SET_PRIVATE_IMAGE = "set_private_image";
public const SET_OTHERS_PRIVATE_IMAGES = "set_others_private_images";
public const BULK_IMPORT = "bulk_import";
public const BULK_EXPORT = "bulk_export";
public const BULK_DOWNLOAD = "bulk_download";
}

View File

@ -3,6 +3,9 @@
* Things which should be in the core API *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
require_once "filetypes.php";
/**
* Return the unique elements of an array, case insensitively
*/
@ -27,7 +30,7 @@ function array_iunique(array $array): array
/**
* Figure out if an IP is in a specified range
*
* from http://uk.php.net/network
* from https://uk.php.net/network
*/
function ip_in_range(string $IP, string $CIDR): bool
{
@ -86,7 +89,7 @@ function deltree(string $f): void
/**
* Copy an entire file hierarchy
*
* from a comment on http://uk.php.net/copy
* from a comment on https://uk.php.net/copy
*/
function full_copy(string $source, string $target): void
{
@ -150,6 +153,14 @@ function list_files(string $base, string $_sub_dir=""): array
return $file_list;
}
function flush_output(): void
{
if (!defined("UNITTEST")) {
@ob_flush();
}
flush();
}
function stream_file(string $file, int $start, int $end): void
{
$fp = fopen($file, 'r');
@ -162,10 +173,7 @@ function stream_file(string $file, int $start, int $end): void
$buffer = $end - $p + 1;
}
echo fread($fp, $buffer);
if (!defined("UNITTEST")) {
@ob_flush();
}
flush();
flush_output();
// After flush, we can tell if the client browser has disconnected.
// This means we can start sending a large file, and if we detect they disappeared
@ -248,99 +256,6 @@ if (!function_exists('mb_strlen')) {
}
}
const MIME_TYPE_MAP = [
'jpg' => 'image/jpeg',
'gif' => 'image/gif',
'png' => 'image/png',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'ico' => 'image/x-icon',
'swf' => 'application/x-shockwave-flash',
'flv' => 'video/x-flv',
'svg' => 'image/svg+xml',
'pdf' => 'application/pdf',
'zip' => 'application/zip',
'gz' => 'application/x-gzip',
'tar' => 'application/x-tar',
'bz' => 'application/x-bzip',
'bz2' => 'application/x-bzip2',
'txt' => 'text/plain',
'asc' => 'text/plain',
'htm' => 'text/html',
'html' => 'text/html',
'css' => 'text/css',
'js' => 'text/javascript',
'xml' => 'text/xml',
'xsl' => 'application/xsl+xml',
'ogg' => 'application/ogg',
'mp3' => 'audio/mpeg',
'wav' => 'audio/x-wav',
'avi' => 'video/x-msvideo',
'mpg' => 'video/mpeg',
'mpeg' => 'video/mpeg',
'mov' => 'video/quicktime',
'php' => 'text/x-php',
'mp4' => 'video/mp4',
'ogv' => 'video/ogg',
'webm' => 'video/webm',
'webp' => 'image/webp',
'bmp' =>'image/x-ms-bmp',
'psd' => 'image/vnd.adobe.photoshop',
'mkv' => 'video/x-matroska'
];
/**
* Get MIME type for file
*
* The contents of this function are taken from the __getMimeType() function
* from the "Amazon S3 PHP class" which is Copyright (c) 2008, Donovan Schönknecht
* and released under the 'Simplified BSD License'.
*/
function getMimeType(string $file, string $ext=""): string
{
// Static extension lookup
$ext = strtolower($ext);
if (array_key_exists($ext, MIME_TYPE_MAP)) {
return MIME_TYPE_MAP[$ext];
}
$type = false;
// Fileinfo documentation says fileinfo_open() will use the
// MAGIC env var for the magic file
if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) {
if (($type = finfo_file($finfo, $file)) !== false) {
// Remove the charset and grab the last content-type
$type = explode(' ', str_replace('; charset=', ';charset=', $type));
$type = array_pop($type);
$type = explode(';', $type);
$type = trim(array_shift($type));
}
finfo_close($finfo);
// If anyone is still using mime_content_type()
} elseif (function_exists('mime_content_type')) {
$type = trim(mime_content_type($file));
}
if ($type !== false && strlen($type) > 0) {
return $type;
}
return 'application/octet-stream';
}
function get_extension(?string $mime_type): ?string
{
if (empty($mime_type)) {
return null;
}
$ext = array_search($mime_type, MIME_TYPE_MAP);
return ($ext ? $ext : null);
}
/** @noinspection PhpUnhandledExceptionInspection */
function getSubclassesOf(string $parent)
{
@ -380,7 +295,7 @@ function zglob(string $pattern): array
/**
* Figure out the path to the shimmie install directory.
*
* eg if shimmie is visible at http://foo.com/gallery, this
* eg if shimmie is visible at https://foo.com/gallery, this
* function should return /gallery
*
* PHP really, really sucks.
@ -406,6 +321,23 @@ function get_base_href(): string
return $dir;
}
/**
* The opposite of the standard library's parse_url
*/
function unparse_url($parsed_url)
{
$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 = ($user || $pass) ? "$pass@" : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$query = !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = !empty($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
return "$scheme$user$pass$host$port$path$query$fragment";
}
function startsWith(string $haystack, string $needle): bool
{
$length = strlen($needle);
@ -478,7 +410,7 @@ function bool_escape($input): bool
Sometimes, I don't like PHP -- this, is one of those times...
"a boolean FALSE is not considered a valid boolean value by this function."
Yay for Got'chas!
http://php.net/manual/en/filter.filters.validate.php
https://php.net/manual/en/filter.filters.validate.php
*/
if (is_bool($input)) {
return $input;
@ -511,6 +443,24 @@ function no_escape(string $input): string
return $input;
}
/**
* Given a 1-indexed numeric-ish thing, return a zero-indexed
* number between 0 and $max
*/
function page_number(string $input, ?int $max=null): int
{
if (!is_numeric($input)) {
$pageNumber = 0;
} elseif ($input <= 0) {
$pageNumber = 0;
} elseif (!is_null($max) && $input >= $max) {
$pageNumber = $max - 1;
} else {
$pageNumber = $input - 1;
}
return $pageNumber;
}
function clamp(?int $val, ?int $min=null, ?int $max=null): int
{
if (!is_numeric($val) || (!is_null($min) && $val < $min)) {
@ -570,11 +520,14 @@ function truncate(string $string, int $limit, string $break=" ", string $pad="..
*/
function parse_shorthand_int(string $limit): int
{
if (preg_match('/^([\d\.]+)([gmk])?b?$/i', (string)$limit, $m)) {
if (preg_match('/^([\d\.]+)([tgmk])?b?$/i', (string)$limit, $m)) {
$value = $m[1];
if (isset($m[2])) {
switch (strtolower($m[2])) {
/** @noinspection PhpMissingBreakStatementInspection */
case 't': $value *= 1024; // fall through
/** @noinspection PhpMissingBreakStatementInspection */
// no break
case 'g': $value *= 1024; // fall through
/** @noinspection PhpMissingBreakStatementInspection */
// no break
@ -598,7 +551,9 @@ function to_shorthand_int(int $int): string
{
assert($int >= 0);
if ($int >= pow(1024, 3)) {
if ($int >= pow(1024, 4)) {
return sprintf("%.1fTB", $int / pow(1024, 4));
} elseif ($int >= pow(1024, 3)) {
return sprintf("%.1fGB", $int / pow(1024, 3));
} elseif ($int >= pow(1024, 2)) {
return sprintf("%.1fMB", $int / pow(1024, 2));

63
core/sanitize_php.php Normal file
View File

@ -0,0 +1,63 @@
<?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>
<html lang='en'>
<head>
<title>Shimmie</title>
<link rel=\"shortcut icon\" href=\"ext/static_files/static/favicon.ico\">
<link rel=\"stylesheet\" href=\"ext/static_files/style.css\" type=\"text/css\">
</head>
<body>
<div id=\"installer\">
<h1>Shimmie</h1>
<h3>$title</h3>
<div class=\"container\">
$body
</div>
</div>
</body>
</html>");
if ($code != 0) {
http_response_code(500);
}
exit($code);
}

View File

@ -101,7 +101,7 @@ function send_event(Event $event): Event
if ($tracer_enabled) {
$_tracer->begin(get_class($event));
}
// SHIT: http://bugs.php.net/bug.php?id=35106
// SHIT: https://bugs.php.net/bug.php?id=35106
$my_event_listeners = $_shm_event_listeners[get_class($event)];
ksort($my_event_listeners);

View File

@ -18,6 +18,7 @@ function _d(string $name, $value): void
define($name, $value);
}
}
$_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
@ -25,7 +26,7 @@ _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.0'); // string shimmie version
_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)

18
core/tests/init.test.php Normal file
View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
class TestInit extends TestCase
{
public function testInitExt()
{
send_event(new InitExtEvent());
$this->assertTrue(true);
}
public function testDatabaseUpgrade()
{
send_event(new DatabaseUpgradeEvent());
$this->assertTrue(true);
}
}

View File

@ -8,15 +8,35 @@ class UrlsTest extends TestCase
{
public function test_make_link()
{
// basic
$this->assertEquals(
"/test/foo",
make_link("foo")
);
// remove leading slash from path
$this->assertEquals(
"/test/foo",
make_link("/foo")
);
// query
$this->assertEquals(
"/test/foo?a=1&b=2",
make_link("foo", "a=1&b=2")
);
// hash
$this->assertEquals(
"/test/foo#cake",
make_link("foo", null, "cake")
);
// query + hash
$this->assertEquals(
"/test/foo?a=1&b=2#cake",
make_link("foo", "a=1&b=2", "cake")
);
}
public function test_make_http()
@ -35,8 +55,47 @@ class UrlsTest extends TestCase
// absolute
$this->assertEquals(
"http://foo.com",
make_http("http://foo.com")
"https://foo.com",
make_http("https://foo.com")
);
}
public function test_modify_url()
{
$this->assertEquals(
"/foo/bar?a=3&b=2",
modify_url("/foo/bar?a=1&b=2", ["a"=>"3"])
);
$this->assertEquals(
"https://blah.com/foo/bar?b=2",
modify_url("https://blah.com/foo/bar?a=1&b=2", ["a"=>null])
);
$this->assertEquals(
"/foo/bar",
modify_url("/foo/bar?a=1&b=2", ["a"=>null, "b"=>null])
);
}
public function test_referer_or()
{
unset($_SERVER['HTTP_REFERER']);
$this->assertEquals(
"foo",
referer_or("foo")
);
$_SERVER['HTTP_REFERER'] = "cake";
$this->assertEquals(
"cake",
referer_or("foo")
);
$_SERVER['HTTP_REFERER'] = "cake";
$this->assertEquals(
"foo",
referer_or("foo", ["cake"])
);
}
}

View File

@ -1,7 +1,4 @@
<?php declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* HTML Generation *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
class Link
{
@ -26,32 +23,27 @@ class Link
*
* eg make_link("post/list") becomes "/v2/index.php?q=post/list"
*/
function make_link(?string $page=null, ?string $query=null): string
function make_link(?string $page=null, ?string $query=null, ?string $fragment=null): string
{
global $config;
if (is_null($page)) {
$page = $config->get_string(SetupConfig::MAIN_PAGE);
}
$page = trim($page, "/");
$parts = [];
$install_dir = get_base_href();
if (SPEED_HAX || $config->get_bool('nice_urls', false)) {
$base = $install_dir;
$parts['path'] = "$install_dir/$page";
} else {
$base = "$install_dir/index.php?q=";
$parts['path'] = "$install_dir/index.php";
$query = empty($query) ? "q=$page" : "q=$page&$query";
}
$parts['query'] = $query; // http_build_query($query);
$parts['fragment'] = $fragment; // http_build_query($hash);
if (is_null($query)) {
return str_replace("//", "/", $base.'/'.$page);
} else {
if (strpos($base, "?")) {
return $base .'/'. $page .'&'. $query;
} elseif (strpos($query, "#") === 0) {
return $base .'/'. $page . $query;
} else {
return $base .'/'. $page .'?'. $query;
}
}
return unparse_url($parts);
}
@ -60,43 +52,28 @@ function make_link(?string $page=null, ?string $query=null): string
*/
function modify_current_url(array $changes): string
{
return modify_url($_SERVER['QUERY_STRING'], $changes);
return modify_url($_SERVER['REQUEST_URI'], $changes);
}
function modify_url(string $url, array $changes): string
{
// SHIT: PHP is officially the worst web API ever because it does not
// have a built-in function to do this.
$parts = parse_url($url);
// SHIT: parse_str is magically retarded; not only is it a useless name, it also
// didn't return the parsed array, preferring to overwrite global variables with
// whatever data the user supplied. Thankfully, 4.0.3 added an extra option to
// give it an array to use...
$params = [];
parse_str($url, $params);
if (isset($changes['q'])) {
$base = $changes['q'];
unset($changes['q']);
} else {
$base = _get_query();
if (isset($parts['query'])) {
parse_str($parts['query'], $params);
}
if (isset($params['q'])) {
unset($params['q']);
}
foreach ($changes as $k => $v) {
if (is_null($v) and isset($params[$k])) {
unset($params[$k]);
}
$params[$k] = $v;
}
$parts['query'] = http_build_query($params);
return make_link($base, http_build_query($params));
return unparse_url($parts);
}
/**
* Turn a relative link into an absolute one, including hostname
*/
@ -116,3 +93,22 @@ function make_http(string $link): string
return $link;
}
/**
* If HTTP_REFERER is set, and not blacklisted, then return it
* Else return a default $dest
*/
function referer_or(string $dest, ?array $blacklist=null): string
{
if (empty($_SERVER['HTTP_REFERER'])) {
return $dest;
}
if ($blacklist) {
foreach ($blacklist as $b) {
if (strstr($_SERVER['HTTP_REFERER'], $b)) {
return $dest;
}
}
}
return $_SERVER['HTTP_REFERER'];
}

View File

@ -247,6 +247,9 @@ class User
public function check_auth_token(): bool
{
if (defined("UNITTEST")) {
return true;
}
return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token());
}

View File

@ -98,6 +98,8 @@ new UserClass("user", "base", [
Permissions::EDIT_FAVOURITES => true,
Permissions::SEND_PM => true,
Permissions::READ_PM => true,
Permissions::SET_PRIVATE_IMAGE => true,
Permissions::BULK_DOWNLOAD => true,
]);
new UserClass("hellbanned", "user", [
@ -118,6 +120,7 @@ new UserClass("admin", "base", [
Permissions::BAN_IP => true,
Permissions::CREATE_USER => true,
Permissions::CREATE_OTHER_USER => true,
Permissions::EDIT_USER_NAME => true,
Permissions::EDIT_USER_PASSWORD => true,
Permissions::EDIT_USER_INFO => true,
@ -196,6 +199,13 @@ new UserClass("admin", "base", [
Permissions::APPROVE_IMAGE => true,
Permissions::APPROVE_COMMENT => true,
Permissions::BULK_IMPORT =>true,
Permissions::BULK_EXPORT =>true,
Permissions::BULK_DOWNLOAD => true,
Permissions::SET_PRIVATE_IMAGE => true,
Permissions::SET_OTHERS_PRIVATE_IMAGES => true,
]);
@include_once "data/config/user-classes.conf.php";

View File

@ -104,7 +104,7 @@ function get_memory_limit(): int
Get PHP's configured memory limit.
Note that this is set to -1 for NO memory limit.
http://ca2.php.net/manual/en/ini.core.php#ini.memory-limit
https://ca2.php.net/manual/en/ini.core.php#ini.memory-limit
*/
$memory = parse_shorthand_int(ini_get("memory_limit"));
@ -393,6 +393,67 @@ function get_dir_contents(string $dir): array
);
}
function remove_empty_dirs(string $dir): bool
{
assert(!empty($dir));
$result = true;
if (!is_dir($dir)) {
return false;
}
$items = array_diff(
scandir(
$dir
),
['..', '.']
);
foreach ($items as $item) {
$path = join_path($dir, $item);
if (is_dir($path)) {
$result = $result && remove_empty_dirs($path);
} else {
$result = false;
}
}
if ($result===true) {
$result = $result && rmdir($dir);
}
return $result;
}
function get_files_recursively(string $dir): array
{
assert(!empty($dir));
if (!is_dir($dir)) {
return [];
}
$things = array_diff(
scandir(
$dir
),
['..', '.']
);
$output = [];
foreach ($things as $thing) {
$path = join_path($dir, $thing);
if (is_file($path)) {
$output[] = $path;
} else {
$output = array_merge($output, get_files_recursively($path));
}
}
return $output;
}
/**
* Returns amount of files & total size of dir.
*/
@ -493,31 +554,18 @@ function _load_theme_files()
require_all(_get_themelet_files(get_theme()));
}
function _sanitise_environment(): void
function _set_up_shimmie_environment(): void
{
global $tracer_enabled;
$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;
}
if (file_exists("images") && !file_exists("data/images")) {
die("As of Shimmie 2.7 images and thumbs should be moved to data/images and data/thumbs");
die_nicely("Upgrade error", "As of Shimmie 2.7 images and thumbs should be moved to data/images and data/thumbs");
}
if (TIMEZONE) {
date_default_timezone_set(TIMEZONE);
}
# ini_set('zend.assertions', '1'); // generate assertions
ini_set('assert.exception', '1'); // throw exceptions when failed
if (DEBUG) {
error_reporting(E_ALL);
}
@ -526,16 +574,6 @@ date and you should plan on moving elsewhere.
// so to prevent running out of memory during complex operations code that uses it should
// check if tracer output is enabled before making use of it.
$tracer_enabled = constant('TRACE_FILE')!==null;
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>";
}
}
@ -598,6 +636,7 @@ function _fatal_error(Exception $e): void
<p><b>Message:</b> '.html_escape($message).'
'.$q.'
<p><b>Version:</b> '.$version.' (on '.$phpver.')
<p><b>Stack Trace:</b></p><pre>'.$e->getTraceAsString().'</pre>
</body>
</html>
';
@ -643,7 +682,7 @@ function show_ip(string $ip, string $ban_reason): string
global $user;
$u_reason = url_escape($ban_reason);
$u_end = url_escape("+1 week");
$ban = $user->can(Permissions::BAN_IP) ? ", <a href='".make_link("ip_ban/list", "c_ip=$ip&c_reason=$u_reason&c_expires=$u_end#create")."'>Ban</a>" : "";
$ban = $user->can(Permissions::BAN_IP) ? ", <a href='".make_link("ip_ban/list", "c_ip=$ip&c_reason=$u_reason&c_expires=$u_end", "create")."'>Ban</a>" : "";
$ip = $user->can(Permissions::VIEW_IP) ? $ip.$ban : "";
return $ip;
}
@ -677,7 +716,7 @@ function SHM_FORM(string $target, string $method="POST", bool $multipart=false,
global $user;
$attrs = [
"action"=>$target,
"action"=>make_link($target),
"method"=>$method
];
@ -743,3 +782,18 @@ function human_filesize(int $bytes, $decimals = 2)
$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)
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters [rand(0, strlen($characters) - 1)];
}
return $randomString;
}

View File

@ -1,36 +0,0 @@
# Custom Configuration
Various aspects of Shimmie can be configured to suit your site specific needs
via the file `data/config/shimmie.conf.php` (created after installation).
Take a look at `core/sys_config.php` for the available options that can
be used.
# Custom User Classes
User classes can be added to or altered by placing them in
`data/config/user-classes.conf.php`.
For example, one can override the default anonymous "allow nothing"
permissions like so:
```php
new UserClass("anonymous", "base", [
Permissions::CREATE_COMMENT => True,
Permissions::EDIT_IMAGE_TAG => True,
Permissions::EDIT_IMAGE_SOURCE => True,
Permissions::CREATE_IMAGE_REPORT => True,
]);
```
For a moderator class, being a regular user who can delete images and comments:
```php
new UserClass("moderator", "user", [
Permissions::DELETE_IMAGE => True,
Permissions::DELETE_COMMENT => True,
]);
```
For a list of permissions, see `core/permissions.php`

View File

@ -1,139 +0,0 @@
# Development Info
## Themes
Theme customisation is done by creating files in `themes/<theme name>`.
The general idea with Shimmie theming is that each `Extension` will add a
set of `Block`s to the `Page`, then the `Page` is in charge of deciding
how they should be laid out, what they should look like, etc.
The overall layout is controlled by `page.class.php`, where the `render()`
function will take a look at all of the separate `Block`s and turn them
into the final rendered HTML.
Individual `Extension`s will render their content by calling functions
in `ext/<extension name>/theme.php` - for example the code in
`ext/comment/main.php` will display a list of comments by calling
`display_comment_list()` from `ext/comment/theme.php`.
If a theme wants to customise how the comment list is rendered, it would
do so by creating an override file in `themes/<theme name>/comment.theme.php`
with contents like:
```
class CustomCommentTheme extends CommentTheme {
public function display_comment_list(
array $images,
int $page_number,
int $total_pages,
bool $can_post
) {
[... render the comment list however you like here ...]
}
}
```
## Events and Extensions
An event is a little blob of data saying "something happened", possibly
"something happened, here's the specific data". Events are sent with the
`send_event()` function. Since events can store data, they can be used to
return data to the extension which sent them, for example:
```
$tfe = send_event(new TextFormattingEvent($original_text));
$formatted_text = $tfe->formatted;
```
An extension is something which is capable of reacting to events.
### Useful Variables
There are a few global variables which are pretty essential to most extensions:
* $config -- some variety of Config subclass
* $database -- a Database object used to get raw SQL access
* $page -- a Page to holds all the loose bits of extension output
* $user -- the currently logged in User
* $cache -- an optional cache for fast key / value lookups (eg Memcache)
Each of these can be imported at the start of a function with eg "global $page, $user;"
### The Hello World Extension
Here's a simple extension which listens for `PageRequestEvent`s, and each time
it sees one, it sends out a `HelloEvent`.
```
// ext/hello/main.php
public class HelloEvent extends Event {
public function __construct($username) {
$this->username = $username;
}
}
public class Hello extends Extension {
public function onPageRequest(PageRequestEvent $event) { // Every time a page request is sent
global $user; // Look at the global "currently logged in user" object
send_event(new HelloEvent($user->name)); // Broadcast a signal saying hello to that user
}
public function onHello(HelloEvent $event) { // When the "Hello" signal is recieved
$this->theme->display_hello($event->username); // Display a message on the web page
}
}
```
```
// ext/hello/theme.php
public class HelloTheme extends Themelet {
public function display_hello($username) {
global $page;
$h_user = html_escape($username); // Escape the data before adding it to the page
$block = new Block("Hello!", "Hello there $h_user"); // HTML-safe variables start with "h_"
$page->add_block($block); // Add the block to the page
}
}
```
```
// themes/mytheme/hello.theme.php
public class CustomHelloTheme extends HelloTheme { // CustomHelloTheme overrides HelloTheme
public function display_hello($username) { // the display_hello() function is customised
global $page;
$h_user = html_escape($username);
$page->add_block(new Block(
"Hello!",
"Hello there $h_user, look at my snazzy custom theme!"
);
}
}
```
## Cookies
ui-\* cookies are for the client-side scripts only; in some configurations
(eg with varnish cache) they will be stripped before they reach the server
shm-\* CSS classes are for javascript to hook into; if you're customising
themes, be careful with these, and avoid styling them, eg:
- shm-thumb = outermost element of a thumbnail
* data-tags
* data-post-id
- shm-toggler = click this to toggle elements that match the selector
* data-toggle-sel
- shm-unlocker = click this to unlock elements that match the selector
* data-unlock-sel
- shm-clink = a link to a comment, flash the target element when clicked
* data-clink-sel
## Fin
Please tell me if those docs are lacking in any way, so that they can be
improved for the next person who uses them

View File

@ -1,20 +0,0 @@
# Docker
If you just want to run shimmie inside docker, there's a pre-built image
in dockerhub - `shish2k/shimmie2` - which can be used like:
```
docker run -p 8000 -v /my/hard/drive:/app/data shish2k/shimmie2
```
There are various options settable with environment variables:
- `UID` / `GID` - which user ID to run as (default 1000/1000)
- `INSTALL_DSN` - specify a data source to install into, to skip the installer screen, eg
`-e INSTALL_DSN="pgsql:user=shimmie;password=6y5erdfg;host=127.0.0.1;dbname=shimmie"`
## Build custom image
If you want to build your own image from source:
```
docker build -t shimmie2 .
```

View File

@ -1,27 +0,0 @@
# Requirements
- These are generally based on "whatever is in Debian Stable", because that's
conservative without being TOO painfully out of date, and is a nice target
for the unit test Docker build.
- A database: PostgreSQL 11+ / MariaDB 10.3+ / SQLite 3.27+
- [Stable PHP](https://en.wikipedia.org/wiki/PHP#Release_history) (7.3+ as of writing)
- GD or ImageMagick
# Get the Code
Two main options:
1. Via Git (allows easiest updates via `git pull`):
* `git clone https://github.com/shish/shimmie2`
* Install [Composer](https://getcomposer.org/). (If you don't already have it)
* Run `composer install` in the shimmie folder.
2. Via Stable Release:
* Download the latest release under [Releases](https://github.com/shish/shimmie2/releases).
# Install
1. Create a blank database
2. Visit the install folder with a web browser
3. Enter the location of the database
4. Click "install". Hopefully you'll end up at the welcome screen; if
not, you should be given instructions on how to fix any errors~

View File

@ -1,65 +0,0 @@
Notes for any sites which require extra performance
===================================================
Image Serving
-------------
Firstly, make sure your webserver is configured properly and nice URLs are
enabled, so that images will be served straight from disk by the webserver
instead of via PHP. If you're serving images via PHP, then your site might
melt under the load of 5 concurrent users...
Add a Cache
-----------
eg installing memcached, then setting
`define("CACHE_DSN", "memcache://127.0.0.1:11211")` - a bunch of stuff will
get served from the high-speed cache instead of the SQL database.
`SPEED_HAX`
-----------
Setting this to true will make a bunch of changes which reduce the correctness
of the software and increase admin workload for the sake of speed. You almost
certainly don't want to set this, but if you do (eg you're trying to run a
site with 10,000 concurrent users on a single server), it can be a huge help.
Notable behaviour changes:
- Database schema upgrades are no longer automatic; you'll need to run
`php index.php db-upgrade` from the CLI each time you update the code.
- Mapping from Events to Extensions is cached - you'll need to delete
`data/cache/shm_event_listeners.php` after each code change, and after
enabling or disabling any extensions.
- Tag lists (eg alphabetic, popularity, map) are cached and you'll need
to delete them manually when you feel like it
- Anonymous users can only search for 3 tags at once
- We only show the first 500 pages of results for any query, except for
the most simple (no tags, or one positive tag)
- We only ever show the first 5,000 results for complex queries
- Only comments from the past 24 hours show up in /comment/list
- Web crawlers are blocked from creating too many nonsense searches
- The first 10 pages in the index get extra caching
- RSS is limited to 10 pages
- HTML for thumbnails is cached
`WH_SPLITS`
-----------
Store files as `images/ab/cd/...` instead of `images/ab/...`, which can
reduce filesystem load when you have millions of images.
Multiple Image Servers
----------------------
Image links don't have to be `/images/$hash.$ext` on the local server, they
can be full URLs, and include weighted random parts, eg:
`https://{fred=3,leo=1}.mysite.com/images/$hash.$ext` - the software will then
use consistent hashing to map 75% of the files to `fred.mysite.com` and 25% to
`leo.mysite.com` - then you can install Varnish or Squid or something as a
caching reverse-proxy.
Profiling
---------
`define()`'ing `TRACE_FILE` to a filename and `TRACE_THRESHOLD` to a number
of seconds will result in JSON event traces being dumped into that file
whenever a page takes longer than the threshold to load. These traces can
then be loaded into the chrome trace viewer (chrome://tracing/) and you'll
get a breakdown of page performance by extension, event, database, and cache
queries.

View File

@ -1,9 +0,0 @@
# Upgrade from earlier versions
I very much recommend going via each major release in turn (eg, 2.0.6
-> 2.1.3 -> 2.2.4 -> 2.3.0 rather than 2.0.6 -> 2.3.0).
While the basic database and file formats haven't changed *completely*, it's
different enough to be a pain.

View File

@ -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("text/csv");
$page->set_type(MIME_TYPE_CSV);
$page->set_filename("aliases.csv");
$page->set_data($this->get_alias_csv($database));
} elseif ($event->get_arg(0) == "import") {

View File

@ -52,6 +52,10 @@ class DeleteAutoTagEvent extends Event
}
}
class AutoTaggerException extends SCoreException
{
}
class AddAutoTagException extends SCoreException
{
}
@ -98,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("text/csv");
$page->set_type(MIME_TYPE_CSV);
$page->set_filename("auto_tag.csv");
$page->set_data($this->get_auto_tag_csv($database));
} elseif ($event->get_arg(0) == "import") {

View File

@ -20,7 +20,7 @@ class AutoComplete extends Extension
}
$page->set_mode(PageMode::DATA);
$page->set_type("application/json");
$page->set_type(MIME_TYPE_JSON);
$s = strtolower($_GET["s"]);
if (

View File

@ -15,7 +15,7 @@ class BBCodeInfo extends ExtensionInfo
" Supported tags:
<ul>
<li>[img]url[/img]
<li>[url]<a href=\"{self::SHIMMIE_URL}\">http://code.shishnet.org/</a>[/url]
<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]

View File

@ -67,12 +67,12 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
public function testURL()
{
$this->assertEquals(
$this->filter("[url]http://shishnet.org[/url]"),
"<a href=\"http://shishnet.org\">http://shishnet.org</a>"
$this->filter("[url]https://shishnet.org[/url]"),
"<a href=\"https://shishnet.org\">https://shishnet.org</a>"
);
$this->assertEquals(
$this->filter("[url=http://shishnet.org]ShishNet[/url]"),
"<a href=\"http://shishnet.org\">ShishNet</a>"
$this->filter("[url=https://shishnet.org]ShishNet[/url]"),
"<a href=\"https://shishnet.org\">ShishNet</a>"
);
$this->assertEquals(
$this->filter("[url=javascript:alert(\"owned\")]click to fail[/url]"),
@ -107,4 +107,32 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
$bb = new BBCode();
return $bb->strip($in);
}
public function testSiteLinks()
{
$this->assertEquals(
'<a class="shm-clink" data-clink-sel="" href="/test/post/view/123">&gt;&gt;123</a>',
$this->filter("&gt;&gt;123")
);
$this->assertEquals(
'<a class="shm-clink" data-clink-sel="#c456" href="/test/post/view/123#c456">&gt;&gt;123#c456</a>',
$this->filter("&gt;&gt;123#c456")
);
$this->assertEquals(
'<a class="shm-clink" data-clink-sel="" href="/test/foo/bar">foo/bar</a>',
$this->filter("[url]site://foo/bar[/url]")
);
$this->assertEquals(
'<a class="shm-clink" data-clink-sel="#c123" href="/test/foo/bar#c123">foo/bar#c123</a>',
$this->filter("[url]site://foo/bar#c123[/url]")
);
$this->assertEquals(
'<a class="shm-clink" data-clink-sel="" href="/test/foo/bar">look at my post</a>',
$this->filter("[url=site://foo/bar]look at my post[/url]")
);
$this->assertEquals(
'<a class="shm-clink" data-clink-sel="#c123" href="/test/foo/bar#c123">look at my comment</a>',
$this->filter("[url=site://foo/bar#c123]look at my comment[/url]")
);
}
}

View File

@ -12,5 +12,5 @@ class BlotterInfo extends ExtensionInfo
public $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 http://github.com/zshall/shimmie2/issues";
Development TODO at https://github.com/zshall/shimmie2/issues";
}

View File

@ -42,7 +42,7 @@ class BrowserSearch extends Extension
// And now to send it to the browser
$page->set_mode(PageMode::DATA);
$page->set_type("text/xml");
$page->set_type(MIME_TYPE_XML);
$page->set_data($xml);
} elseif ($event->page_matches("browser_search")) {
$suggestions = $config->get_string("search_suggestions_results_order");

View File

@ -1,5 +1,8 @@
<?php declare(strict_types=1);
class BulkActionException extends SCoreException
{
}
class BulkActionBlockBuildingEvent extends Event
{
/** @var array */
@ -39,6 +42,8 @@ class BulkActionEvent extends Event
public $action;
/** @var array */
public $items;
/** @var bool */
public $redirect = true;
public function __construct(String $action, Generator $items)
{
@ -164,28 +169,38 @@ class BulkActions extends Extension
$action = $_POST['bulk_action'];
$items = null;
if (isset($_POST['bulk_selected_ids']) && $_POST['bulk_selected_ids'] != "") {
$data = json_decode($_POST['bulk_selected_ids']);
if (is_array($data)&&!empty($data)) {
$items = $this->yield_items($data);
try {
$items = null;
if (isset($_POST['bulk_selected_ids']) && !empty($_POST['bulk_selected_ids'])) {
$data = json_decode($_POST['bulk_selected_ids']);
if (empty($data)) {
throw new BulkActionException("No ids specified in bulk_selected_ids");
}
if (is_array($data) && !empty($data)) {
$items = $this->yield_items($data);
}
} elseif (isset($_POST['bulk_query']) && $_POST['bulk_query'] != "") {
$query = $_POST['bulk_query'];
if ($query != null && $query != "") {
$items = $this->yield_search_results($query);
}
} else {
throw new BulkActionException("No ids selected and no query present, cannot perform bulk operation on entire collection");
}
} elseif (isset($_POST['bulk_query']) && $_POST['bulk_query'] != "") {
$query = $_POST['bulk_query'];
if ($query != null && $query != "") {
$items = $this->yield_search_results($query);
$bae = new BulkActionEvent($action, $items);
if (is_iterable($items)) {
send_event($bae);
}
} catch (BulkActionException $e) {
log_error(BulkActionsInfo::KEY, $e->getMessage(), $e->getMessage());
}
if (is_iterable($items)) {
send_event(new BulkActionEvent($action, $items));
if ($bae->redirect) {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(referer_or(make_link()));
}
$page->set_mode(PageMode::REDIRECT);
if (!isset($_SERVER['HTTP_REFERER'])) {
$_SERVER['HTTP_REFERER'] = make_link();
}
$page->set_redirect($_SERVER['HTTP_REFERER']);
}
}

View File

@ -53,6 +53,7 @@ function deactivate_bulk_selector() {
set_selected_items([]);
$('#bulk_selector_controls').hide();
$('#bulk_selector_activate').show();
$('input[name="bulk_selected_ids"]').val("");
bulk_selector_active = false;
}
@ -94,7 +95,6 @@ function deselect_item(id) {
function toggle_selection( id ) {
var data = get_selected_items();
console.log(id);
if(data.includes(id)) {
data.splice(data.indexOf(id),1);
set_selected_items(data);

View File

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
class BulkDownloadInfo extends ExtensionInfo
{
public const KEY = "bulk_download";
public $key = self::KEY;
public $name = "Bulk Download";
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public $license = self::LICENSE_WTFPL;
public $description = "Allows bulk downloading images.";
public $dependencies = [BulkActionsInfo::KEY];
}

View File

@ -0,0 +1,78 @@
<?php declare(strict_types=1);
class BulkDownloadConfig
{
public const SIZE_LIMIT = "bulk_download_size_limit";
}
class BulkDownloadException extends BulkActionException
{
}
class BulkDownload extends Extension
{
private const DOWNLOAD_ACTION_NAME = "bulk_download";
public function onInitExt(InitExtEvent $event)
{
global $config;
$config->set_default_int(BulkDownloadConfig::SIZE_LIMIT, parse_shorthand_int('100MB'));
}
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
{
global $user, $config;
if ($user->can(Permissions::BULK_DOWNLOAD)) {
$event->add_action(BulkDownload::DOWNLOAD_ACTION_NAME, "Download ZIP");
}
}
public function onSetupBuilding(SetupBuildingEvent $event)
{
$sb = new SetupBlock("Bulk Download");
$sb->start_table();
$sb->add_shorthand_int_option(BulkDownloadConfig::SIZE_LIMIT, "Size Limit", true);
$sb->end_table();
$event->panel->add_block($sb);
}
public function onBulkAction(BulkActionEvent $event)
{
global $user, $page, $config;
if ($user->can(Permissions::BULK_DOWNLOAD)&&
($event->action == BulkDownload::DOWNLOAD_ACTION_NAME)) {
$download_filename = $user->name . '-' . date('YmdHis') . '.zip';
$zip_filename = tempnam(sys_get_temp_dir(), "shimmie_bulk_download");
$zip = new ZipArchive;
$size_total = 0;
$max_size = $config->get_int(BulkDownloadConfig::SIZE_LIMIT);
if ($zip->open($zip_filename, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE) === true) {
foreach ($event->items as $image) {
$img_loc = warehouse_path(Image::IMAGE_DIR, $image->hash, false);
$size_total += filesize($img_loc);
if ($size_total>$max_size) {
throw new BulkDownloadException("Bulk download limited to ".human_filesize($max_size));
}
$filename = urldecode($image->get_nice_image_name());
$filename = str_replace(":", ";", $filename);
$zip->addFile($img_loc, $filename);
}
$zip->close();
$page->set_mode(PageMode::FILE);
$page->set_file($zip_filename, true);
$page->set_filename($download_filename);
$event->redirect = false;
}
}
}
}

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
class BulkExportEvent extends Event
{
public $image;
public $fields = [];
public function __construct(Image $image)
{
$this->image = $image;
}
}
class BulkImportEvent extends Event
{
public $image;
public $fields = [];
public function __construct(Image $image, $fields)
{
$this->image = $image;
$this->fields = $fields;
}
}

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
include_once "events.php";
class BulkImportExportInfo extends ExtensionInfo
{
public const KEY = "bulk_import_export";
public $key = self::KEY;
public $name = "Bulk Import/Export";
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public $license = self::LICENSE_WTFPL;
public $description = "Allows bulk exporting/importing of images and associated data.";
public $dependencies = [BulkActionsInfo::KEY];
}

View File

@ -0,0 +1,172 @@
<?php declare(strict_types=1);
class BulkImportExport extends DataHandlerExtension
{
const EXPORT_ACTION_NAME = "bulk_export";
const EXPORT_INFO_FILE_NAME = "export.json";
protected $SUPPORTED_MIME = [MIME_TYPE_ZIP];
public function onDataUpload(DataUploadEvent $event)
{
global $user, $database;
if ($this->supported_ext($event->type) &&
$user->can(Permissions::BULK_IMPORT)) {
$zip = new ZipArchive;
if ($zip->open($event->tmpname) === true) {
$info = $zip->getStream(self::EXPORT_INFO_FILE_NAME);
$json_data = [];
if ($info !== false) {
try {
$json_string = stream_get_contents($info);
$json_data = json_decode($json_string);
} finally {
fclose($info);
}
} else {
throw new SCoreException("Could not get " . self::EXPORT_INFO_FILE_NAME . " from archive");
}
$total = 0;
$skipped = 0;
$failed = 0;
$database->commit();
while (!empty($json_data)) {
$item = array_pop($json_data);
$database->begin_transaction();
try {
$image = Image::by_hash($item->hash);
if ($image!=null) {
$skipped++;
log_info(BulkImportExportInfo::KEY, "Image $item->hash already present, skipping");
$database->commit();
continue;
}
$tmpfile = tempnam(sys_get_temp_dir(), "shimmie_bulk_import");
$stream = $zip->getStream($item->hash);
if ($zip === false) {
throw new SCoreException("Could not import " . $item->hash . ": File not in zip");
}
file_put_contents($tmpfile, $stream);
$id = add_image($tmpfile, $item->filename, Tag::implode($item->tags));
if ($id==-1) {
throw new SCoreException("Unable to import file $item->hash");
}
$image = Image::by_id($id);
if ($image==null) {
throw new SCoreException("Unable to import file $item->hash");
}
if ($item->source!=null) {
$image->set_source($item->source);
}
send_event(new BulkImportEvent($image, $item));
$database->commit();
$total++;
} catch (Exception $ex) {
$failed++;
try {
$database->rollBack();
} catch (Exception $ex2) {
log_error(BulkImportExportInfo::KEY, "Could not roll back transaction: " . $ex2->getMessage(), "Could not import " . $item->hash . ": " . $ex->getMessage());
}
log_error(BulkImportExportInfo::KEY, "Could not import " . $item->hash . ": " . $ex->getMessage(), "Could not import " . $item->hash . ": " . $ex->getMessage());
continue;
} finally {
if (!empty($tmpfile) && is_file($tmpfile)) {
unlink($tmpfile);
}
}
}
$event->image_id = -2; // default -1 = upload wasn't handled
log_info(
BulkImportExportInfo::KEY,
"Imported $total items, skipped $skipped, $failed failed",
"Imported $total items, skipped $skipped, $failed failed"
);
} else {
throw new SCoreException("Could not open zip archive");
}
}
}
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
{
global $user, $config;
if ($user->can(Permissions::BULK_EXPORT)) {
$event->add_action(self::EXPORT_ACTION_NAME, "Export");
}
}
public function onBulkAction(BulkActionEvent $event)
{
global $user, $page;
if ($user->can(Permissions::BULK_EXPORT) &&
($event->action == self::EXPORT_ACTION_NAME)) {
$download_filename = $user->name . '-' . date('YmdHis') . '.zip';
$zip_filename = tempnam(sys_get_temp_dir(), "shimmie_bulk_export");
$zip = new ZipArchive;
$json_data = [];
if ($zip->open($zip_filename, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE) === true) {
foreach ($event->items as $image) {
$img_loc = warehouse_path(Image::IMAGE_DIR, $image->hash, false);
$export_event = new BulkExportEvent($image);
send_event($export_event);
$data = $export_event->fields;
$data["hash"] = $image->hash;
$data["tags"] = $image->get_tag_array();
$data["filename"] = $image->filename;
$data["source"] = $image->source;
array_push($json_data, $data);
$zip->addFile($img_loc, $image->hash);
}
$json_data = json_encode($json_data, JSON_PRETTY_PRINT);
$zip->addFromString(self::EXPORT_INFO_FILE_NAME, $json_data);
$zip->close();
$page->set_mode(PageMode::FILE);
$page->set_file($zip_filename, true);
$page->set_filename($download_filename);
$event->redirect = false;
}
}
}
// we don't actually do anything, just accept one upload and spawn several
protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{
}
protected function check_contents(string $tmpname): bool
{
return false;
}
protected function create_thumb(string $hash, string $type): bool
{
return false;
}
}

View File

@ -210,7 +210,7 @@ class CommentList extends Extension
$cpe = new CommentPostingEvent(int_escape($_POST['image_id']), $user, $_POST['comment']);
send_event($cpe);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/$i_iid#comment_on_$i_iid"));
$page->set_redirect(make_link("post/view/$i_iid", null, "comment_on_$i_iid"));
} catch (CommentPostingException $ex) {
$this->theme->display_error(403, "Comment Blocked", $ex->getMessage());
}
@ -226,11 +226,7 @@ class CommentList extends Extension
send_event(new CommentDeletionEvent(int_escape($event->get_arg(1))));
$page->flash("Deleted comment");
$page->set_mode(PageMode::REDIRECT);
if (!empty($_SERVER['HTTP_REFERER'])) {
$page->set_redirect($_SERVER['HTTP_REFERER']);
} else {
$page->set_redirect(make_link("post/view/" . $event->get_arg(2)));
}
$page->set_redirect(referer_or(make_link("post/view/" . $event->get_arg(2))));
}
} else {
$this->theme->display_permission_denied();
@ -264,8 +260,51 @@ class CommentList extends Extension
private function onPageRequest_list(PageRequestEvent $event)
{
$page_num = $event->try_page_num(1);
$this->build_page($page_num);
global $cache, $database, $user;
$where = SPEED_HAX ? "WHERE posted > now() - interval '24 hours'" : "";
$total_pages = $cache->get("comment_pages");
if (empty($total_pages)) {
$total_pages = (int)($database->get_one("
SELECT COUNT(c1)
FROM (SELECT COUNT(image_id) AS c1 FROM comments $where GROUP BY image_id) AS s1
") / 10);
$cache->set("comment_pages", $total_pages, 600);
}
$total_pages = max($total_pages, 1);
$current_page = $event->try_page_num(1, $total_pages);
$threads_per_page = 10;
$start = $threads_per_page * $current_page;
$result = $database->Execute("
SELECT image_id,MAX(posted) AS latest
FROM comments
$where
GROUP BY image_id
ORDER BY latest DESC
LIMIT :limit OFFSET :offset
", ["limit"=>$threads_per_page, "offset"=>$start]);
$user_ratings = Extension::is_enabled(RatingsInfo::KEY) ? Ratings::get_user_class_privs($user) : "";
$images = [];
while ($row = $result->fetch()) {
$image = Image::by_id((int)$row["image_id"]);
if (
Extension::is_enabled(RatingsInfo::KEY) && !is_null($image) &&
!in_array($image->rating, $user_ratings)
) {
$image = null; // this is "clever", I may live to regret it
}
if (!is_null($image)) {
$comments = $this->get_comments($image->id);
$images[] = [$image, $comments];
}
}
$this->theme->display_comment_list($images, $current_page+1, $total_pages, $user->can(Permissions::CREATE_COMMENT));
}
private function onPageRequest_beta_search(PageRequestEvent $event)
@ -276,9 +315,8 @@ class CommentList extends Extension
$i_comment_count = Comment::count_comments_by_user($duser);
$com_per_page = 50;
$total_pages = (int)ceil($i_comment_count / $com_per_page);
$page_num = clamp($page_num, 1, $total_pages);
$comments = $this->get_user_comments($duser->id, $com_per_page, ($page_num - 1) * $com_per_page);
$this->theme->display_all_user_comments($comments, $page_num, $total_pages, $duser);
$comments = $this->get_user_comments($duser->id, $com_per_page, $page_num * $com_per_page);
$this->theme->display_all_user_comments($comments, $page_num+1, $total_pages, $duser);
}
public function onAdminBuilding(AdminBuildingEvent $event)
@ -389,56 +427,6 @@ class CommentList extends Extension
}
}
private function build_page(int $current_page)
{
global $cache, $database, $user;
$where = SPEED_HAX ? "WHERE posted > now() - interval '24 hours'" : "";
$total_pages = $cache->get("comment_pages");
if (empty($total_pages)) {
$total_pages = (int)($database->get_one("
SELECT COUNT(c1)
FROM (SELECT COUNT(image_id) AS c1 FROM comments $where GROUP BY image_id) AS s1
") / 10);
$cache->set("comment_pages", $total_pages, 600);
}
$total_pages = max($total_pages, 1);
$current_page = clamp($current_page, 1, $total_pages);
$threads_per_page = 10;
$start = $threads_per_page * ($current_page - 1);
$result = $database->Execute("
SELECT image_id,MAX(posted) AS latest
FROM comments
$where
GROUP BY image_id
ORDER BY latest DESC
LIMIT :limit OFFSET :offset
", ["limit"=>$threads_per_page, "offset"=>$start]);
$user_ratings = Extension::is_enabled(RatingsInfo::KEY) ? Ratings::get_user_class_privs($user) : "";
$images = [];
while ($row = $result->fetch()) {
$image = Image::by_id((int)$row["image_id"]);
if (
Extension::is_enabled(RatingsInfo::KEY) && !is_null($image) &&
!in_array($image->rating, $user_ratings)
) {
$image = null; // this is "clever", I may live to regret it
}
if (!is_null($image)) {
$comments = $this->get_comments($image->id);
$images[] = [$image, $comments];
}
}
$this->theme->display_comment_list($images, $current_page, $total_pages, $user->can(Permissions::CREATE_COMMENT));
}
/**
* #return Comment[]
*/
@ -563,18 +551,9 @@ class CommentList extends Extension
'website' => '',
'body' => $text,
'permalink' => '',
];
# akismet breaks if there's no referrer in the environment; so if there
# isn't, supply one manually
if (!isset($_SERVER['HTTP_REFERER'])) {
$comment['referrer'] = 'none';
log_warning("comment", "User '{$user->name}' commented with no referrer: $text");
}
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
$comment['user_agent'] = 'none';
log_warning("comment", "User '{$user->name}' commented with no user-agent: $text");
}
'referrer' => $_SERVER['HTTP_REFERER'] ?? 'none',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'none',
];
$akismet = new Akismet(
$_SERVER['SERVER_NAME'],

View File

@ -234,7 +234,7 @@ class CommentListTheme extends Themelet
$html = "
<div class=\"comment $hb\">
$h_userlink: $h_comment
<a href=\"".make_link("post/view/$i_image_id#c$i_comment_id")."\">&gt;&gt;&gt;</a>
<a href=\"".make_link("post/view/$i_image_id", null, "c$i_comment_id")."\">&gt;&gt;&gt;</a>
</div>
";
} else {

View File

@ -3,22 +3,25 @@
abstract class CronUploaderConfig
{
const DEFAULT_PATH = "cron_uploader";
public const DEFAULT_PATH = "cron_uploader";
const KEY = "cron_uploader_key";
const COUNT = "cron_uploader_count";
const DIR = "cron_uploader_dir";
const USER = "cron_uploader_user";
public const KEY = "cron_uploader_key";
public const DIR = "cron_uploader_dir";
public const USER = "cron_uploader_user";
public const STOP_ON_ERROR = "cron_uploader_stop_on_error";
public const INCLUDE_ALL_LOGS = "cron_uploader_include_all_logs";
public const LOG_LEVEL = "cron_uploader_log_level";
public static function set_defaults(): void
{
global $config;
$config->set_default_int(self::COUNT, 1);
$config->set_default_string(self::DIR, data_path(self::DEFAULT_PATH));
$config->set_default_bool(self::INCLUDE_ALL_LOGS, false);
$config->set_default_bool(self::STOP_ON_ERROR, false);
$config->set_default_int(self::LOG_LEVEL, SCORE_LOG_INFO);
$upload_key = $config->get_string(self::KEY, "");
if (empty($upload_key)) {
$upload_key = self::generate_key();
$upload_key = generate_key();
$config->set_string(self::KEY, $upload_key);
}
@ -48,18 +51,6 @@ abstract class CronUploaderConfig
$config->set_string(self::KEY, $value);
}
public static function get_count(): int
{
global $config;
return $config->get_int(self::COUNT);
}
public static function set_count(int $value): void
{
global $config;
$config->set_int(self::COUNT, $value);
}
public static function get_dir(): string
{
global $config;
@ -76,21 +67,4 @@ abstract class CronUploaderConfig
global $config;
$config->set_string(self::DIR, $value);
}
/*
* Generates a unique key for the website to prevent unauthorized access.
*/
private static function generate_key()
{
$length = 20;
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters [rand(0, strlen($characters) - 1)];
}
return $randomString;
}
}

View File

@ -15,7 +15,7 @@ class CronUploader extends Extension
const UPLOADED_DIR = "uploaded";
const FAILED_DIR = "failed_to_upload";
public $output_buffer = [];
private static $IMPORT_RUNNING = false;
public function onInitExt(InitExtEvent $event)
{
@ -57,10 +57,18 @@ class CronUploader extends Extension
$sb = new SetupBlock("Cron Uploader");
$sb->start_table();
$sb->add_int_option(CronUploaderConfig::COUNT, "Upload per run", true);
$sb->add_text_option(CronUploaderConfig::DIR, "Root dir", true);
$sb->add_text_option(CronUploaderConfig::KEY, "Key", true);
$sb->add_choice_option(CronUploaderConfig::USER, $users, "User", true);
$sb->add_bool_option(CronUploaderConfig::STOP_ON_ERROR, "Stop On Error", true);
$sb->add_choice_option(CronUploaderConfig::LOG_LEVEL, [
LOGGING_LEVEL_NAMES[SCORE_LOG_DEBUG] => SCORE_LOG_DEBUG,
LOGGING_LEVEL_NAMES[SCORE_LOG_INFO] => SCORE_LOG_INFO,
LOGGING_LEVEL_NAMES[SCORE_LOG_WARNING] => SCORE_LOG_WARNING,
LOGGING_LEVEL_NAMES[SCORE_LOG_ERROR] => SCORE_LOG_ERROR,
LOGGING_LEVEL_NAMES[SCORE_LOG_CRITICAL] => SCORE_LOG_CRITICAL,
], "Output Log Level: ", true);
$sb->add_bool_option(CronUploaderConfig::INCLUDE_ALL_LOGS, "Include All Logs", true);
$sb->end_table();
$sb->add_label("<a href='$documentation_link'>Read the documentation</a> for cron setup instructions.");
@ -108,6 +116,24 @@ class CronUploader extends Extension
}
}
public function onLog(LogEvent $event)
{
global $config;
$all = $config->get_bool(CronUploaderConfig::INCLUDE_ALL_LOGS);
if (self::$IMPORT_RUNNING &&
$event->priority >= $config->get_int(CronUploaderConfig::LOG_LEVEL) &&
($event->section==self::NAME || $all)
) {
$output = "[" . date('Y-m-d H:i:s') . "] " . ($all ? '['. $event->section .'] ' :'') . "[" . LOGGING_LEVEL_NAMES[$event->priority] . "] " . $event->message ;
echo $output . "\r\n";
flush_output();
$log_path = $this->get_log_file();
file_put_contents($log_path, $output);
}
}
private function restage_folder(string $folder)
{
global $page;
@ -123,33 +149,46 @@ class CronUploader extends Extension
$this->prep_root_dir();
$results = get_dir_contents($queue_dir);
if (count($results) > 0) {
$page->flash("Queue folder must be empty to re-stage");
return;
}
$results = get_dir_contents($stage_dir);
$results = get_files_recursively($stage_dir);
if (count($results) == 0) {
if (rmdir($stage_dir)===false) {
if (remove_empty_dirs($stage_dir)===false) {
$page->flash("Nothing to stage from $folder, cannot remove folder");
} else {
$page->flash("Nothing to stage from $folder, removing folder");
}
return;
}
foreach ($results as $result) {
$original_path = join_path($stage_dir, $result);
$new_path = join_path($queue_dir, $result);
$new_path = join_path($queue_dir, substr($result, strlen($stage_dir)));
rename($original_path, $new_path);
if (file_exists($new_path)) {
$page->flash("File already exists in queue folder: " .$result);
return;
}
}
$page->flash("Re-staged $folder to queue");
rmdir($stage_dir);
$success = true;
foreach ($results as $result) {
$new_path = join_path($queue_dir, substr($result, strlen($stage_dir)));
$dir = dirname($new_path);
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
if (rename($result, $new_path)===false) {
$page->flash("Could not move file: " .$result);
$success = false;
}
}
if ($success===true) {
$page->flash("Re-staged $folder to queue");
if (remove_empty_dirs($stage_dir)===false) {
$page->flash("Could not remove $folder");
}
}
}
private function clear_folder($folder)
@ -265,7 +304,11 @@ class CronUploader extends Extension
*/
public function process_upload(string $key, ?int $upload_count = null): bool
{
global $database;
global $database, $config, $_shm_load_start;
$max_time = intval(ini_get('max_execution_time'))*.8;
$this->set_headers();
if ($key!=CronUploaderConfig::get_key()) {
throw new SCoreException("Cron upload key incorrect");
@ -287,24 +330,12 @@ class CronUploader extends Extension
throw new SCoreException("Cron upload process is already running");
}
self::$IMPORT_RUNNING = true;
try {
//set_time_limit(0);
// Gets amount of imgs to upload
if ($upload_count == null) {
$upload_count = CronUploaderConfig::get_count();
}
$output_subdir = date('Ymd-His', time());
$image_queue = $this->generate_image_queue(CronUploaderConfig::get_dir(), $upload_count);
// Throw exception if there's nothing in the queue
if (count($image_queue) == 0) {
$this->log_message(SCORE_LOG_WARNING, "Your queue is empty so nothing could be uploaded.");
$this->handle_log();
return false;
}
$image_queue = $this->generate_image_queue(CronUploaderConfig::get_dir());
// Randomize Images
//shuffle($this->image_queue);
@ -314,14 +345,18 @@ class CronUploader extends Extension
$failed = 0;
// Upload the file(s)
for ($i = 0; $i < $upload_count && sizeof($image_queue) > 0; $i++) {
$img = array_pop($image_queue);
foreach ($image_queue as $img) {
$execution_time = microtime(true) - $_shm_load_start;
if ($execution_time>$max_time) {
break;
}
try {
$database->beginTransaction();
$database->begin_transaction();
$this->log_message(SCORE_LOG_INFO, "Adding file: {$img[0]} - tags: {$img[2]}");
$result = $this->add_image($img[0], $img[1], $img[2]);
$database->commit();
if ($database->is_transaction_open()) {
$database->commit();
}
$this->move_uploaded($img[0], $img[1], $output_subdir, false);
if ($result->merged) {
$merged++;
@ -330,28 +365,37 @@ class CronUploader extends Extension
}
} catch (Exception $e) {
try {
$database->rollback();
if ($database->is_transaction_open()) {
$database->rollback();
}
} catch (Exception $e) {
}
$failed++;
$this->move_uploaded($img[0], $img[1], $output_subdir, true);
$this->log_message(SCORE_LOG_ERROR, "(" . gettype($e) . ") " . $e->getMessage());
$this->log_message(SCORE_LOG_ERROR, $e->getTraceAsString());
if ($config->get_bool(CronUploaderConfig::STOP_ON_ERROR)) {
break;
} else {
$this->move_uploaded($img[0], $img[1], $output_subdir, true);
}
}
}
// Throw exception if there's nothing in the queue
if ($merged+$failed+$added === 0) {
$this->log_message(SCORE_LOG_WARNING, "Your queue is empty so nothing could be uploaded.");
return false;
}
$this->log_message(SCORE_LOG_INFO, "Items added: $added");
$this->log_message(SCORE_LOG_INFO, "Items merged: $merged");
$this->log_message(SCORE_LOG_INFO, "Items failed: $failed");
// Display upload log
$this->handle_log();
return true;
} finally {
self::$IMPORT_RUNNING = false;
flock($lockfile, LOCK_UN);
fclose($lockfile);
}
@ -359,7 +403,13 @@ class CronUploader extends Extension
private function move_uploaded(string $path, string $filename, string $output_subdir, bool $corrupt = false)
{
$relativeDir = dirname(substr($path, strlen(CronUploaderConfig::get_dir()) + 7));
$rootDir = CronUploaderConfig::get_dir();
$rootLength = strlen($rootDir);
if ($rootDir[$rootLength-1]=="/"||$rootDir[$rootLength-1]=="\\") {
$rootLength--;
}
$relativeDir = dirname(substr($path, $rootLength + 7));
if ($relativeDir==".") {
$relativeDir = "";
@ -405,7 +455,7 @@ class CronUploader extends Extension
if (array_key_exists('extension', $pathinfo)) {
$metadata ['extension'] = $pathinfo ['extension'];
}
$metadata ['tags'] = $tagArray; // doesn't work when not logged in here, handled below
$metadata ['tags'] = $tagArray;
$metadata ['source'] = null;
$event = new DataUploadEvent($tmpname, $metadata);
send_event($event);
@ -420,34 +470,46 @@ class CronUploader extends Extension
}
$this->log_message(SCORE_LOG_INFO, $infomsg);
// Set tags
$img = Image::by_id($event->image_id);
$img->set_tags(array_merge($tagArray, $img->get_tag_array()));
return $event;
}
private const PARTIAL_DOWNLOAD_EXTENSIONS = ['crdownload','part'];
private const SKIPPABLE_FILES = ['.ds_store','thumbs.db'];
private const SKIPPABLE_DIRECTORIES = ['__macosx'];
private function is_skippable_file(string $path)
private function is_skippable_dir(string $path)
{
$info = pathinfo($path);
if (in_array(strtolower($info['extension']), self::PARTIAL_DOWNLOAD_EXTENSIONS)) {
if (array_key_exists("basename", $info) && in_array(strtolower($info['basename']), self::SKIPPABLE_DIRECTORIES)) {
return true;
}
return false;
}
private function generate_image_queue(string $root_dir, ?int $limit = null): array
private function is_skippable_file(string $path)
{
$info = pathinfo($path);
if (array_key_exists("basename", $info) && in_array(strtolower($info['basename']), self::SKIPPABLE_FILES)) {
return true;
}
if (array_key_exists("extension", $info) && in_array(strtolower($info['extension']), self::PARTIAL_DOWNLOAD_EXTENSIONS)) {
return true;
}
return false;
}
private function generate_image_queue(string $root_dir, ?int $limit = null): Generator
{
$base = $this->get_queue_dir();
$output = [];
if (!is_dir($base)) {
$this->log_message(SCORE_LOG_WARNING, "Image Queue Directory could not be found at \"$base\".");
return [];
return;
}
$ite = new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS);
@ -458,31 +520,19 @@ class CronUploader extends Extension
$relativePath = substr($fullpath, strlen($base));
$tags = path_to_tags($relativePath);
$img = [
yield [
0 => $fullpath,
1 => $pathinfo ["basename"],
2 => $tags
];
$output[] = $img;
if (!empty($limit) && count($output) >= $limit) {
break;
}
}
}
return $output;
}
private function log_message(int $severity, string $message): void
{
log_msg(self::NAME, $severity, $message);
$time = "[" . date('Y-m-d H:i:s') . "]";
$this->output_buffer[] = $time . " " . $message;
$log_path = $this->get_log_file();
file_put_contents($log_path, $time . " " . $message);
}
private function get_log_file(): string
@ -490,16 +540,12 @@ class CronUploader extends Extension
return join_path(CronUploaderConfig::get_dir(), "uploads.log");
}
/**
* This is run at the end to display & save the log.
*/
private function handle_log()
private function set_headers(): void
{
global $page;
// Display message
$page->set_mode(PageMode::DATA);
$page->set_type("text/plain");
$page->set_data(implode("\r\n", $this->output_buffer));
$page->set_mode(PageMode::MANUAL);
$page->set_type(MIME_TYPE_TEXT);
$page->send_headers();
}
}

View File

@ -57,6 +57,9 @@ class CronUploaderTheme extends Themelet
<br />When you create the cron job, you choose when to upload new images.</li>
</ol>";
$max_time = intval(ini_get('max_execution_time'))*.8;
$usage_html = "Upload your images you want to be uploaded to the queue directory using your FTP client or other means.
<br />(<b>{$queue_dirinfo['path']}</b>)
<ol>
@ -71,7 +74,7 @@ class CronUploaderTheme extends Themelet
<ul>
<li>If an import is already running, another cannot start until it is done.</li>
<li>Each time it runs it will import up to ".CronUploaderConfig::get_count()." file(s). This is controlled from <a href='".make_link("setup")."'>Board Config</a>.</li>
<li>Each time it runs it will import for up to ".number_format($max_time)." seconds. This is controlled by the PHP max execution time.</li>
<li>Uploaded images will be moved to the 'uploaded' directory into a subfolder named after the time the import started. It's recommended that you remove everything out of this directory from time to time. If you have admin controls enabled, this can be done from <a href='".make_link("admin")."'>Board Admin</a>.</li>
<li>If you enable the db logging extension, you can view the log output on this screen. Otherwise the log will be written to a file at ".CronUploaderConfig::get_dir().DIRECTORY_SEPARATOR."uploads.log</li>
</ul>
@ -107,7 +110,7 @@ class CronUploaderTheme extends Themelet
$html .= make_form(make_link("admin/cron_uploader_restage"));
$html .= "<table class='form'>";
$html .= "<tr><th>Failed dir</th><td><select name='failed_dir' required='required'><option></option>";
$html .= "<tr><th>Failed dir</th><td><select name='failed_dir' required='required'>";
foreach ($failed_dirs as $dir) {
$html .= "<option value='$dir'>$dir</option>";

View File

@ -10,20 +10,20 @@ class DanbooruApi extends Extension
if ($event->page_matches("api/danbooru/add_post") || $event->page_matches("api/danbooru/post/create.xml")) {
// No XML data is returned from this function
$page->set_type("text/plain");
$page->set_type(MIME_TYPE_TEXT);
$this->api_add_post();
} elseif ($event->page_matches("api/danbooru/find_posts") || $event->page_matches("api/danbooru/post/index.xml")) {
$page->set_type("application/xml");
$page->set_type(MIME_TYPE_XML_APPLICATION);
$page->set_data($this->api_find_posts());
} elseif ($event->page_matches("api/danbooru/find_tags")) {
$page->set_type("application/xml");
$page->set_type(MIME_TYPE_XML_APPLICATION);
$page->set_data($this->api_find_tags());
}
// Hackery for danbooruup 0.3.2 providing the wrong view url. This simply redirects to the proper
// Shimmie view page
// Example: danbooruup says the url is http://shimmie/api/danbooru/post/show/123
// This redirects that to http://shimmie/post/view/123
// Example: danbooruup says the url is https://shimmie/api/danbooru/post/show/123
// This redirects that to https://shimmie/post/view/123
elseif ($event->page_matches("api/danbooru/post/show")) {
$fixedlocation = make_link("post/view/" . $event->get_arg(0));
$page->set_mode(PageMode::REDIRECT);

View File

@ -9,6 +9,7 @@ class ETInfo extends ExtensionInfo
public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2;
public $core = true;
public $description = "Show various bits of system information";
public $documentation =
"Knowing the information that this extension shows can be very useful for debugging. There's also an option to send

View File

@ -10,7 +10,7 @@ class ET extends Extension
global $user;
if ($event->page_matches("system_info")) {
if ($user->can(Permissions::VIEW_SYSINTO)) {
$this->theme->display_info_page($this->get_info());
$this->theme->display_info_page($this->to_yaml($this->get_info()));
}
}
}
@ -29,20 +29,18 @@ class ET extends Extension
{
global $user;
if ($user->can(Permissions::VIEW_SYSINTO)) {
$event->add_link("System Info", make_link("system_info"));
$event->add_link("System Info", make_link("system_info"), 99);
}
}
public function onCommand(CommandEvent $event)
{
if ($event->cmd == "help") {
print "\tget-info\n";
print "\tshimmie-info\n";
print "\t\tList a bunch of info\n\n";
}
if ($event->cmd == "info") {
foreach ($this->get_info() as $k => $v) {
print("$k = $v\n");
}
if ($event->cmd == "shimmie-info") {
print($this->to_yaml($this->get_info()));
}
}
@ -53,51 +51,81 @@ class ET extends Extension
{
global $config, $database;
$info = [];
$info['site_title'] = $config->get_string(SetupConfig::TITLE);
$info['site_theme'] = $config->get_string(SetupConfig::THEME);
$info['site_url'] = "http://" . $_SERVER["HTTP_HOST"] . get_base_href();
$info['sys_shimmie'] = VERSION;
$info['sys_schema'] = $config->get_int("db_version");
$info['sys_php'] = phpversion();
$info['sys_db'] = $database->get_driver_name();
$info['sys_os'] = php_uname();
$info['sys_disk'] = to_shorthand_int((int)disk_total_space("./") - (int)disk_free_space("./")) . " / " .
to_shorthand_int((int)disk_total_space("./"));
$info['sys_server'] = isset($_SERVER["SERVER_SOFTWARE"]) ? $_SERVER["SERVER_SOFTWARE"] : 'unknown';
$info[MediaConfig::FFMPEG_PATH] = $config->get_string(MediaConfig::FFMPEG_PATH);
$info[MediaConfig::CONVERT_PATH] = $config->get_string(MediaConfig::CONVERT_PATH);
$info[MediaConfig::MEM_LIMIT] = $config->get_int(MediaConfig::MEM_LIMIT);
$info[ImageConfig::THUMB_ENGINE] = $config->get_string(ImageConfig::THUMB_ENGINE);
$info[ImageConfig::THUMB_QUALITY] = $config->get_int(ImageConfig::THUMB_QUALITY);
$info[ImageConfig::THUMB_WIDTH] = $config->get_int(ImageConfig::THUMB_WIDTH);
$info[ImageConfig::THUMB_HEIGHT] = $config->get_int(ImageConfig::THUMB_HEIGHT);
$info[ImageConfig::THUMB_SCALING] = $config->get_int(ImageConfig::THUMB_SCALING);
$info[ImageConfig::THUMB_TYPE] = $config->get_string(ImageConfig::THUMB_TYPE);
$info['stat_images'] = $database->get_one("SELECT COUNT(*) FROM images");
$info['stat_comments'] = $database->get_one("SELECT COUNT(*) FROM comments");
$info['stat_users'] = $database->get_one("SELECT COUNT(*) FROM users");
$info['stat_tags'] = $database->get_one("SELECT COUNT(*) FROM tags");
$info['stat_image_tags'] = $database->get_one("SELECT COUNT(*) FROM image_tags");
$els = [];
foreach (getSubclassesOf("Extension") as $class) {
$els[] = $class;
$core_exts = ExtensionInfo::get_core_extensions();
$extra_exts = [];
foreach (ExtensionInfo::get_all() as $info) {
if ($info->is_enabled() && !in_array($info->key, $core_exts)) {
$extra_exts[] = $info->key;
}
}
$info['sys_extensions'] = join(', ', $els);
$info['handled_extensions'] = join(', ', DataHandlerExtension::get_all_supported_exts());
$info = [
"about" => [
'title' => $config->get_string(SetupConfig::TITLE),
'theme' => $config->get_string(SetupConfig::THEME),
'url' => make_http(make_link("/")),
],
"versions" => [
'shimmie' => VERSION,
'schema' => $config->get_int("db_version"),
'php' => phpversion(),
'db' => $database->get_driver_name() . " " . $database->get_version(),
'os' => php_uname(),
'server' => $_SERVER["SERVER_SOFTWARE"] ?? 'unknown',
],
"extensions" => [
"core" => $core_exts,
"extra" => $extra_exts,
"handled_extensions" => DataHandlerExtension::get_all_supported_exts(),
],
"stats" => [
'images' => (int)$database->get_one("SELECT COUNT(*) FROM images"),
'comments' => (int)$database->get_one("SELECT COUNT(*) FROM comments"),
'users' => (int)$database->get_one("SELECT COUNT(*) FROM users"),
],
"media" => [
"memory_limit" => to_shorthand_int($config->get_int(MediaConfig::MEM_LIMIT)),
"disk_use" => to_shorthand_int((int)disk_total_space("./") - (int)disk_free_space("./")),
"disk_total" => to_shorthand_int((int)disk_total_space("./")),
],
"thumbnails" => [
"engine" => $config->get_string(ImageConfig::THUMB_ENGINE),
"quality" => $config->get_int(ImageConfig::THUMB_QUALITY),
"width" => $config->get_int(ImageConfig::THUMB_WIDTH),
"height" => $config->get_int(ImageConfig::THUMB_HEIGHT),
"scaling" => $config->get_int(ImageConfig::THUMB_SCALING),
"type" => $config->get_string(ImageConfig::THUMB_TYPE),
],
];
//$cfs = array();
//foreach($database->get_all("SELECT name, value FROM config") as $pair) {
// $cfs[] = $pair['name']."=".$pair['value'];
//}
//$info[''] = "Config: ".join(", ", $cfs);
if (file_exists(".git")) {
try {
$commitHash = trim(exec('git log --pretty="%h" -n1 HEAD'));
$commitBranch= trim(exec('git rev-parse --abbrev-ref HEAD'));
$commitOrigin= trim(exec('git config --get remote.origin.url'));
$commitOrigin= preg_replace("#//.*@#", "//xxx@", $commitOrigin);
$info['git'] = [
'commit' => $commitHash,
'branch' => $commitBranch,
'origin' => $commitOrigin,
];
} catch (Exception $e) {
}
}
return $info;
}
private function to_yaml($info)
{
$data = "";
foreach ($info as $title => $section) {
$data .= "$title:\n";
foreach ($section as $k => $v) {
$data .= " $k: " . json_encode($v, JSON_UNESCAPED_SLASHES) . "\n";
}
$data .= "\n";
}
return $data;
}
}

View File

@ -1,5 +1,10 @@
<?php declare(strict_types=1);
use function MicroHTML\FORM;
use function MicroHTML\INPUT;
use function MicroHTML\P;
use function MicroHTML\TEXTAREA;
class ETTheme extends Themelet
{
/*
@ -7,61 +12,32 @@ class ETTheme extends Themelet
*
* $info = an array of ($name => $value)
*/
public function display_info_page($info)
public function display_info_page($yaml)
{
global $page;
$page->set_title("System Info");
$page->set_heading("System Info");
$page->add_block(new NavBlock());
$page->add_block(new Block("Information:", $this->build_data_form($info)));
$page->add_block(new Block("Information:", $this->build_data_form($yaml)));
}
protected function build_data_form($info)
protected function build_data_form($yaml)
{
$data = <<<EOD
Optional:
Site title: {$info['site_title']}
Theme: {$info['site_theme']}
Genre: [describe your site here]
URL: {$info['site_url']}
System stats:
Shimmie: {$info['sys_shimmie']}
Schema: {$info['sys_schema']}
PHP: {$info['sys_php']}
OS: {$info['sys_os']}
Database: {$info['sys_db']}
Server: {$info['sys_server']}
Disk use: {$info['sys_disk']}
Media System:
Memory Limit: {$info[MediaConfig::MEM_LIMIT]}
Thumbnail Generation:
Engine: {$info[ImageConfig::THUMB_ENGINE]}
Type: {$info[ImageConfig::THUMB_TYPE]}
Quality: {$info[ImageConfig::THUMB_QUALITY]}
Width: {$info[ImageConfig::THUMB_WIDTH]}
Height: {$info[ImageConfig::THUMB_HEIGHT]}
Scaling: {$info[ImageConfig::THUMB_SCALING]}
Shimmie stats:
Images: {$info['stat_images']}
Comments: {$info['stat_comments']}
Users: {$info['stat_users']}
Tags: {$info['stat_tags']}
Applications: {$info['stat_image_tags']}
Extensions: {$info['sys_extensions']}
EOD;
return <<<EOD
<form action='https://shimmie.shishnet.org/register.php' method='POST'>
<input type='hidden' name='registration_api' value='1'>
<textarea name='data' rows='20' cols='80'>$data</textarea>
<br><input type='submit' value='Click to send to Shish'>
<br>Your stats are useful so that I know which combinations
of web servers / databases / etc I need to support.
</form>
EOD;
return (string)FORM(
["action"=>"https://shimmie.shishnet.org/register.php", "method"=>"POST"],
INPUT(["type"=>"hidden", "name"=>"registration_api", "value"=>"2"]),
P(
"Your stats are useful so that I know which combinations of ".
"web servers / databases / etc I need to support :)"
),
P(TEXTAREA(
["name"=>'data', "style"=>"width: 100%; height: 20em;"],
$yaml
)),
P(INPUT(
["type"=>'submit', "value"=>'Click to send to Shish', "style"=>"width: 100%; padding: 1em;"]
)),
);
}
}

View File

@ -30,7 +30,7 @@ class ExtManagerTheme extends Themelet
$form = SHM_SIMPLE_FORM(
"ext_manager/set",
TABLE(
["id"=>'extensions', "class"=>'zebra sortable'],
["id"=>'extensions', "class"=>'zebra'],
THEAD(TR(
$editable ? TH("Enabled") : null,
TH("Name"),

View File

@ -1,5 +1,8 @@
<?php declare(strict_types=1);
use function MicroHTML\INPUT;
use function MicroHTML\DIV;
use function MicroHTML\A;
use function MicroHTML\IMG;
class FeaturedTheme extends Themelet
{
@ -19,16 +22,21 @@ class FeaturedTheme extends Themelet
public function build_featured_html(Image $image, ?string $query=null): string
{
$i_id = $image->id;
$h_view_link = make_link("post/view/$i_id", $query);
$h_thumb_link = $image->get_thumb_link();
$h_tip = html_escape($image->get_tooltip());
$tsize = get_thumbnail_size($image->width, $image->height);
return "
<a href='$h_view_link'>
<img id='thumb_{$i_id}' title='{$h_tip}' alt='{$h_tip}' class='highlighted' style='height: {$tsize[1]}px; width: {$tsize[0]}px;' src='{$h_thumb_link}'>
</a>
";
return (string)DIV(
["style"=>"text-align: center;"],
A(
["href"=>make_link("post/view/{$image->id}", $query)],
IMG([
"id"=>"thumb_rand_{$image->id}",
"title"=>$image->get_tooltip(),
"alt"=>$image->get_tooltip(),
"class"=>'highlighted',
"style"=>"max-height: {$tsize[1]}px; max-width: 100%;",
"src"=>$image->get_thumb_link()
])
)
);
}
}

View File

@ -249,19 +249,10 @@ class Forum extends Extension
{
global $config, $database;
$threadsPerPage = $config->get_int('forumThreadsPerPage', 15);
$totalPages = ceil($database->get_one("SELECT COUNT(*) FROM forum_threads") / $threadsPerPage);
$totalPages = (int)ceil($database->get_one("SELECT COUNT(*) FROM forum_threads") / $threadsPerPage);
if ($event->count_args() >= 2) {
$pageNumber = $event->get_arg(1);
if (!is_numeric($pageNumber)) {
$pageNumber = 0;
} elseif ($pageNumber <= 0) {
$pageNumber = 0;
} elseif ($pageNumber >= $totalPages) {
$pageNumber = $totalPages - 1;
} else {
$pageNumber--;
}
$pageNumber = page_number($event->get_arg(1), $totalPages);
} else {
$pageNumber = 0;
}
@ -286,20 +277,11 @@ class Forum extends Extension
global $config, $database;
$threadID = int_escape($event->get_arg(1));
$postsPerPage = $config->get_int('forumPostsPerPage', 15);
$totalPages = ceil($database->get_one("SELECT COUNT(*) FROM forum_posts WHERE thread_id = :id", ['id'=>$threadID]) / $postsPerPage);
$totalPages = (int)ceil($database->get_one("SELECT COUNT(*) FROM forum_posts WHERE thread_id = :id", ['id'=>$threadID]) / $postsPerPage);
$threadTitle = $this->get_thread_title($threadID);
if ($event->count_args() >= 3) {
$pageNumber = $event->get_arg(2);
if (!is_numeric($pageNumber)) {
$pageNumber = 0;
} elseif ($pageNumber <= 0) {
$pageNumber = 0;
} elseif ($pageNumber >= $totalPages) {
$pageNumber = $totalPages - 1;
} else {
$pageNumber--;
}
$pageNumber = page_number($event->get_arg(2), $totalPages);
} else {
$pageNumber = 0;
}

View File

@ -9,7 +9,7 @@ class FourOhFourInfo extends ExtensionInfo
public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2;
public $visibility = self::VISIBLE_ADMIN;
public $visibility = self::VISIBLE_HIDDEN;
public $description = "If no other extension puts anything onto the page, show 404";
public $core = true;
}

View File

@ -2,7 +2,7 @@
class ArchiveFileHandler extends DataHandlerExtension
{
protected $SUPPORTED_EXT = ["zip"];
protected $SUPPORTED_MIME = [MIME_TYPE_ZIP];
public function onInitExt(InitExtEvent $event)
{

View File

@ -2,7 +2,7 @@
class CBZFileHandler extends DataHandlerExtension
{
public $SUPPORTED_EXT = ["cbz"];
public $SUPPORTED_MIME = [MIME_TYPE_COMIC_ZIP];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{
@ -27,7 +27,7 @@ class CBZFileHandler extends DataHandlerExtension
$cover,
warehouse_path(Image::THUMBNAIL_DIR, $hash),
get_thumbnail_max_size_scaled(),
get_extension(getMimeType($cover)),
get_extension(get_mime($cover)),
null
);
return true;

View File

@ -2,7 +2,7 @@
class FlashFileHandler extends DataHandlerExtension
{
protected $SUPPORTED_EXT = ["swf"];
protected $SUPPORTED_MIME = [MIME_TYPE_FLASH];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{

View File

@ -2,7 +2,7 @@
class IcoFileHandler extends DataHandlerExtension
{
protected $SUPPORTED_EXT = ["ico", "ani", "cur"];
protected $SUPPORTED_MIME = [MIME_TYPE_ICO, MIME_TYPE_ANI];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{

View File

@ -2,7 +2,7 @@
class MP3FileHandler extends DataHandlerExtension
{
protected $SUPPORTED_EXT = ["mp3"];
protected $SUPPORTED_MIME = [MIME_TYPE_MP3];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{
@ -23,6 +23,6 @@ class MP3FileHandler extends DataHandlerExtension
protected function check_contents(string $tmpname): bool
{
return getMimeType($tmpname) == 'audio/mpeg';
return get_mime($tmpname) === MIME_TYPE_MP3;
}
}

View File

@ -2,13 +2,13 @@
class PixelFileHandler extends DataHandlerExtension
{
protected $SUPPORTED_EXT = ["jpg", "jpeg", "gif", "png", "webp"];
protected $SUPPORTED_MIME = [MIME_TYPE_JPEG, MIME_TYPE_GIF, MIME_TYPE_PNG, MIME_TYPE_WEBP];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{
if (in_array($event->ext, Media::LOSSLESS_FORMATS)) {
$event->image->lossless = true;
} elseif ($event->ext=="webp") {
} elseif ($event->ext==EXTENSION_WEBP) {
$event->image->lossless = Media::is_lossless_webp($event->file_name);
}
@ -17,10 +17,10 @@ class PixelFileHandler extends DataHandlerExtension
}
$event->image->audio = false;
switch ($event->ext) {
case "gif":
case EXTENSION_GIF:
$event->image->video = Media::is_animated_gif($event->file_name);
break;
case "webp":
case EXTENSION_WEBP:
$event->image->video = Media::is_animated_webp($event->file_name);
break;
default:

View File

@ -9,7 +9,7 @@ class PixelFileHandlerTheme extends Themelet
$u_ilink = $image->get_image_link();
if ($config->get_bool(ImageConfig::SHOW_META) && function_exists(ImageIO::EXIF_READ_FUNCTION)) {
# FIXME: only read from jpegs?
$exif = @exif_read_data($image->get_image_filename(), 0, true);
$exif = @exif_read_data($image->get_image_filename(), "IFD0", true);
if ($exif) {
$head = "";
foreach ($exif as $key => $section) {

View File

@ -3,7 +3,7 @@ use enshrined\svgSanitize\Sanitizer;
class SVGFileHandler extends DataHandlerExtension
{
protected $SUPPORTED_EXT = ["svg"];
protected $SUPPORTED_MIME = [MIME_TYPE_SVG];
/** @var SVGFileHandlerTheme */
protected $theme;
@ -16,7 +16,7 @@ class SVGFileHandler extends DataHandlerExtension
$image = Image::by_id($id);
$hash = $image->hash;
$page->set_type("image/svg+xml");
$page->set_type(MIME_TYPE_SVG);
$page->set_mode(PageMode::DATA);
$sanitizer = new Sanitizer();
@ -67,7 +67,7 @@ class SVGFileHandler extends DataHandlerExtension
protected function check_contents(string $file): bool
{
if (getMimeType($file)!="image/svg+xml") {
if (get_mime($file)!==MIME_TYPE_SVG) {
return false;
}

View File

@ -1,30 +1,55 @@
<?php declare(strict_types=1);
abstract class VideoFileHandlerConfig
{
public const PLAYBACK_AUTOPLAY = "video_playback_autoplay";
public const PLAYBACK_LOOP = "video_playback_loop";
public const ENABLED_FORMATS = "video_enabled_formats";
}
class VideoFileHandler extends DataHandlerExtension
{
protected $SUPPORTED_MIME = [
'video/webm',
'video/mp4',
'video/ogg',
'video/flv',
'video/x-flv'
public const SUPPORTED_MIME = [
MIME_TYPE_ASF,
MIME_TYPE_AVI,
MIME_TYPE_FLASH_VIDEO,
MIME_TYPE_MKV,
MIME_TYPE_MP4_VIDEO,
MIME_TYPE_OGG_VIDEO,
MIME_TYPE_QUICKTIME,
MIME_TYPE_WEBM,
];
protected $SUPPORTED_EXT = ["flv", "mp4", "m4v", "ogv", "webm"];
protected $SUPPORTED_MIME = self::SUPPORTED_MIME;
public function onInitExt(InitExtEvent $event)
{
global $config;
$config->set_default_bool('video_playback_autoplay', true);
$config->set_default_bool('video_playback_loop', true);
$config->set_default_bool(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY, true);
$config->set_default_bool(VideoFileHandlerConfig::PLAYBACK_LOOP, true);
$config->set_default_array(
VideoFileHandlerConfig::ENABLED_FORMATS,
[MIME_TYPE_FLASH_VIDEO, MIME_TYPE_MP4_VIDEO, MIME_TYPE_OGG_VIDEO, MIME_TYPE_WEBM]
);
}
private function get_options(): array
{
$output = [];
foreach ($this->SUPPORTED_MIME as $format) {
$output[MIME_TYPE_MAP[$format][MIME_TYPE_MAP_NAME]] = $format;
}
return $output;
}
public function onSetupBuilding(SetupBuildingEvent $event)
{
$sb = new SetupBlock("Video Options");
$sb->add_bool_option("video_playback_autoplay", "Autoplay: ");
$sb->add_bool_option(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY, "Autoplay: ");
$sb->add_label("<br>");
$sb->add_bool_option("video_playback_loop", "Loop: ");
$sb->add_bool_option(VideoFileHandlerConfig::PLAYBACK_LOOP, "Loop: ");
$sb->add_label("<br>Enabled Formats:");
$sb->add_multichoice_option(VideoFileHandlerConfig::ENABLED_FORMATS, $this->get_options());
$event->panel->add_block($sb);
}
@ -80,6 +105,19 @@ class VideoFileHandler extends DataHandlerExtension
}
}
protected function supported_ext(string $ext): bool
{
global $config;
$enabled_formats = $config->get_array(VideoFileHandlerConfig::ENABLED_FORMATS);
foreach ($enabled_formats as $format) {
if (in_array($ext, MIME_TYPE_MAP[$format][MIME_TYPE_MAP_EXT])) {
return true;
}
}
return false;
}
protected function create_thumb(string $hash, string $type): bool
{
return Media::create_thumbnail_ffmpeg($hash);
@ -87,6 +125,18 @@ class VideoFileHandler extends DataHandlerExtension
protected function check_contents(string $tmpname): bool
{
return in_array(getMimeType($tmpname), $this->SUPPORTED_MIME);
global $config;
if (file_exists($tmpname)) {
$mime = get_mime($tmpname);
$enabled_formats = $config->get_array(VideoFileHandlerConfig::ENABLED_FORMATS);
foreach ($enabled_formats as $format) {
if (in_array($mime, MIME_TYPE_MAP[$format][MIME_TYPE_MAP_MIME])) {
return true;
}
}
}
return false;
}
}

View File

@ -9,18 +9,28 @@ class VideoFileHandlerTheme extends Themelet
$thumb_url = make_http($image->get_thumb_link()); //used as fallback image
$ext = strtolower($image->get_ext());
$full_url = make_http($ilink);
$autoplay = $config->get_bool("video_playback_autoplay");
$loop = $config->get_bool("video_playback_loop");
$autoplay = $config->get_bool(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY);
$loop = $config->get_bool(VideoFileHandlerConfig::PLAYBACK_LOOP);
$player = make_link('vendor/bower-asset/mediaelement/build/flashmediaelement.swf');
$width="auto";
if ($image->width>1) {
$width = $image->width."px";
}
$height="auto";
if ($image->height>1) {
$height = $image->height."px";
}
$html = "Video not playing? <a href='$ilink'>Click here</a> to download the file.<br/>";
//Browser media format support: https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
$supportedExts = ['mp4' => 'video/mp4', 'm4v' => 'video/mp4', 'ogv' => 'video/ogg', 'webm' => 'video/webm', 'flv' => 'video/flv'];
if (array_key_exists($ext, $supportedExts)) {
$mime = get_mime_for_extension($ext);
if (in_array($mime, VideoFileHandler::SUPPORTED_MIME)) {
//FLV isn't supported by <video>, but it should always fallback to the flash-based method.
if ($ext == "webm") {
//Several browsers still lack WebM support sadly: http://caniuse.com/#feat=webm
if ($mime == MIME_TYPE_WEBM) {
//Several browsers still lack WebM support sadly: https://caniuse.com/#feat=webm
$html .= "<!--[if IE]><p>To view webm files with IE, please <a href='https://tools.google.com/dlpage/webmmf/' target='_blank'>download this plugin</a>.</p><![endif]-->";
}
@ -40,7 +50,7 @@ class VideoFileHandlerTheme extends Themelet
<img alt='thumb' src=\"{$thumb_url}\" />
</object>";
if ($ext == "flv") {
if ($mime == MIME_TYPE_FLASH_VIDEO) {
//FLV doesn't support <video>.
$html .= $html_fallback;
} else {
@ -49,8 +59,8 @@ class VideoFileHandlerTheme extends Themelet
$html .= "
<video controls class='shm-main-image' id='main_image' alt='main image' poster='$thumb_url' {$autoplay} {$loop}
style='max-width: 100%'>
<source src='{$ilink}' type='{$supportedExts[$ext]}'>
style='height: $height; width: $width; max-width: 100%'>
<source src='{$ilink}' type='{$mime}'>
<!-- If browser doesn't support filetype, fallback to flash -->
{$html_fallback}
@ -60,7 +70,7 @@ class VideoFileHandlerTheme extends Themelet
}
} else {
//This should never happen, but just in case let's have a fallback..
$html = "Video type '$ext' not recognised";
$html = "Video type '$mime' not recognised";
}
$page->add_block(new Block("Video", $html, "main", 10));
}

View File

@ -9,5 +9,6 @@ class HelpPagesInfo extends ExtensionInfo
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public $license = self::LICENSE_WTFPL;
public $description = "Provides documentation screens";
public $visibility = self::VISIBLE_HIDDEN;
public $core = true;
}

16
ext/help_pages/test.php Normal file
View File

@ -0,0 +1,16 @@
<?php declare(strict_types=1);
class HelpPagesTest extends ShimmiePHPUnitTestCase
{
public function test_list()
{
send_event(new HelpPageListBuildingEvent());
$this->assertTrue(true);
}
public function test_page()
{
send_event(new HelpPageBuildingEvent("test"));
$this->assertTrue(true);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -5,6 +5,22 @@ class Home extends Extension
/** @var HomeTheme */
protected $theme;
private $femDimensions = array(
array(23, 64),
array(14, 86),
array(31, 63),
array(37, 100),
array(24, 90)
);
private $femTags = array(
"Hatsune_Miku",
"Monika",
"Violet_Parr",
"Keith_Kogane",
"Rin_Kagamine",
);
public function onPageRequest(PageRequestEvent $event)
{
global $config, $page;
@ -34,6 +50,27 @@ class Home extends Extension
$event->panel->add_block($sb);
}
private function addCountToBlankImage($charId, $digit)
{
$font = realpath('ext/home/vga.ttf');
$file = "ext/home/counters/femcounter/$charId.png";
$img = imagecreatefrompng($file);
$black = imagecolorallocate($img, 0, 0, 0);
$x = $this->femDimensions[$charId][0];
$y = $this->femDimensions[$charId][1];
imagettftext($img, 20, 0, $x, $y + 20, $black, $font, $digit);
imagetruecolortopalette($img, true, 16);
imagesavealpha($img, true);
imagecolortransparent($img, imagecolorat($img, 0, 0));
ob_start();
imagegif($img);
$image_data = ob_get_contents();
ob_end_clean();
$data = base64_encode($image_data);
imagedestroy($img);
return $data;
}
private function get_body()
{
@ -48,14 +85,23 @@ class Home extends Extension
$counter_dir = $config->get_string('home_counter', 'default');
$total = Image::count_images();
$streak = Image::count_upload_streak();
$strtotal = "$total";
$num_comma = number_format($total);
$streak_comma = number_format($streak);
$counter_text = "";
$length = strlen($strtotal);
for ($n=0; $n<$length; $n++) {
$cur = $strtotal[$n];
$counter_text .= " <img alt='$cur' src='$base_href/ext/home/counters/$counter_dir/$cur.gif' /> ";
if ($counter_dir === 'femcounter') {
$charId = $n % count($this->femTags);
$base64url = $this->addCountToBlankImage($charId, $cur);
$tag = $this->femTags[$charId];
$counter_text .= " <a href='$base_href/post/list/$tag/1'><img alt='$cur' title='$tag' src='data:image/gif;base64,$base64url' /></a> ";
} else {
$counter_text .= " <img alt='$cur' src='$base_href/ext/home/counters/$counter_dir/$cur.gif' /> ";
}
}
// get the homelinks and process them
@ -74,6 +120,6 @@ class Home extends Extension
$main_links = format_text($main_links);
$main_text = $config->get_string('home_text', '');
return $this->theme->build_body($sitename, $main_links, $main_text, $contact_link, $num_comma, $counter_text);
return $this->theme->build_body($sitename, $main_links, $main_text, $contact_link, $num_comma, $counter_text, $streak_comma);
}
}

View File

@ -5,7 +5,7 @@ div#front-page div#links a {margin: 0 0.5em;}
div#front-page li {list-style-type: none; margin: 0;}
@media (max-width: 800px) {
div#front-page h1 {font-size: 3em; margin-top: 0.5em; margin-bottom: 0.5em;}
#counter {display: none;}
/*#counter {display: none;}*/
}
div#front-page > #search > form { margin: 0 auto; }

View File

@ -50,7 +50,7 @@ EOD
<div class='space' id='foot'>
<small><small>
$contact_link Serving $num_comma posts &ndash;
Running <a href='http://code.shishnet.org/shimmie2/'>Shimmie2</a>
Running <a href='https://code.shishnet.org/shimmie2/'>Shimmie2</a>
</small></small>
</div>
</div>";

BIN
ext/home/vga.ttf Normal file

Binary file not shown.

View File

@ -8,6 +8,7 @@ abstract class ImageConfig
const THUMB_SCALING = 'thumb_scaling';
const THUMB_QUALITY = 'thumb_quality';
const THUMB_TYPE = 'thumb_type';
const THUMB_FIT = 'thumb_fit';
const SHOW_META = 'image_show_meta';
const ILINK = 'image_ilink';

View File

@ -10,6 +10,6 @@ class ImageIOInfo extends ExtensionInfo
public $authors = [self::SHISH_NAME=> self::SHISH_EMAIL, "jgen"=>"jgen.tech@gmail.com"];
public $license = self::LICENSE_GPLV2;
public $description = "Handle the image database";
public $visibility = self::VISIBLE_ADMIN;
public $visibility = self::VISIBLE_HIDDEN;
public $core = true;
}

View File

@ -21,8 +21,8 @@ class ImageIO extends Extension
];
const THUMBNAIL_TYPES = [
'JPEG' => "jpg",
'WEBP (Not IE/Safari compatible)' => "webp"
'JPEG' => EXTENSION_JPG,
'WEBP (Not IE/Safari compatible)' => EXTENSION_WEBP
];
public function onInitExt(InitExtEvent $event)
@ -32,7 +32,8 @@ class ImageIO extends Extension
$config->set_default_int(ImageConfig::THUMB_HEIGHT, 192);
$config->set_default_int(ImageConfig::THUMB_SCALING, 100);
$config->set_default_int(ImageConfig::THUMB_QUALITY, 75);
$config->set_default_string(ImageConfig::THUMB_TYPE, 'jpg');
$config->set_default_string(ImageConfig::THUMB_TYPE, EXTENSION_JPG);
$config->set_default_string(ImageConfig::THUMB_FIT, Media::RESIZE_TYPE_FIT);
if (function_exists(self::EXIF_READ_FUNCTION)) {
$config->set_default_bool(ImageConfig::SHOW_META, false);
@ -53,17 +54,13 @@ class ImageIO extends Extension
if ($image) {
send_event(new ImageDeletionEvent($image));
$page->set_mode(PageMode::REDIRECT);
if (isset($_SERVER['HTTP_REFERER']) && !strstr($_SERVER['HTTP_REFERER'], 'post/view')) {
$page->set_redirect($_SERVER['HTTP_REFERER']);
} else {
$page->set_redirect(make_link("post/list"));
}
$page->set_redirect(referer_or(make_link("post/list"), ['post/view']));
}
}
} elseif ($event->page_matches("image/replace")) {
global $page, $user;
if ($user->can(Permissions::REPLACE_IMAGE) && isset($_POST['image_id']) && $user->check_auth_token()) {
$image = Image::by_id($_POST['image_id']);
$image = Image::by_id(int_escape($_POST['image_id']));
if ($image) {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link('upload/replace/'.$image->id));
@ -219,35 +216,41 @@ class ImageIO extends Extension
public function onSetupBuilding(SetupBuildingEvent $event)
{
global $config;
$sb = new SetupBlock("Image Options");
$sb->start_table();
$sb->position = 30;
// advanced only
//$sb->add_text_option(ImageConfig::ILINK, "Image link: ");
//$sb->add_text_option(ImageConfig::TLINK, "<br>Thumbnail link: ");
$sb->add_text_option(ImageConfig::TIP, "Image tooltip: ");
$sb->add_choice_option(ImageConfig::UPLOAD_COLLISION_HANDLER, self::COLLISION_OPTIONS, "<br>Upload collision handler: ");
$sb->add_text_option(ImageConfig::TIP, "Image tooltip", true);
$sb->add_choice_option(ImageConfig::UPLOAD_COLLISION_HANDLER, self::COLLISION_OPTIONS, "Upload collision handler", true);
if (function_exists(self::EXIF_READ_FUNCTION)) {
$sb->add_bool_option(ImageConfig::SHOW_META, "<br>Show metadata: ");
$sb->add_bool_option(ImageConfig::SHOW_META, "Show metadata", true);
}
$sb->end_table();
$event->panel->add_block($sb);
$sb = new SetupBlock("Thumbnailing");
$sb->add_choice_option(ImageConfig::THUMB_ENGINE, self::THUMBNAIL_ENGINES, "Engine: ");
$sb->add_label("<br>");
$sb->add_choice_option(ImageConfig::THUMB_TYPE, self::THUMBNAIL_TYPES, "Filetype: ");
$sb->start_table();
$sb->add_choice_option(ImageConfig::THUMB_ENGINE, self::THUMBNAIL_ENGINES, "Engine", true);
$sb->add_choice_option(ImageConfig::THUMB_TYPE, self::THUMBNAIL_TYPES, "Filetype", true);
$sb->add_label("<br>Size ");
$sb->add_int_option(ImageConfig::THUMB_WIDTH);
$sb->add_label(" x ");
$sb->add_int_option(ImageConfig::THUMB_HEIGHT);
$sb->add_label(" px at ");
$sb->add_int_option(ImageConfig::THUMB_QUALITY);
$sb->add_label(" % quality ");
$sb->add_int_option(ImageConfig::THUMB_WIDTH, "Max Width", true);
$sb->add_int_option(ImageConfig::THUMB_HEIGHT, "Max Height", true);
$sb->add_label("<br>High-DPI scaling ");
$sb->add_int_option(ImageConfig::THUMB_SCALING);
$sb->add_label("%");
$options = [];
foreach (MediaEngine::RESIZE_TYPE_SUPPORT[$config->get_string(ImageConfig::THUMB_ENGINE)] as $type) {
$options[$type] = $type;
}
$sb->add_choice_option(ImageConfig::THUMB_FIT, $options, "Fit", true);
$sb->add_int_option(ImageConfig::THUMB_QUALITY, "Quality", true);
$sb->add_int_option(ImageConfig::THUMB_SCALING, "High-DPI Scale %", true);
$sb->end_table();
$event->panel->add_block($sb);
}
@ -275,11 +278,7 @@ class ImageIO extends Extension
if (!is_null($image)) {
if ($type == "thumb") {
$ext = $config->get_string(ImageConfig::THUMB_TYPE);
if (array_key_exists($ext, MIME_TYPE_MAP)) {
$page->set_type(MIME_TYPE_MAP[$ext]);
} else {
$page->set_type("image/jpeg");
}
$page->set_type(get_mime_for_extension($ext));
$file = $image->get_thumb_filename();
} else {
@ -287,6 +286,12 @@ class ImageIO extends Extension
$file = $image->get_image_filename();
}
if (!file_exists($file)) {
http_response_code(404);
die();
}
if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])) {
$if_modified_since = preg_replace('/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"]);
} else {

View File

@ -20,4 +20,23 @@ class ImageIOTest extends ShimmiePHPUnitTestCase
$page = $this->get_page("thumb/$image_id/moo.jpg");
$this->assertEquals(200, $page->code);
}
public function testDeleteRequest()
{
$this->log_in_as_admin();
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test");
$_POST['image_id'] = "$image_id";
send_event(new PageRequestEvent("image/delete"));
$this->assertTrue(true); // FIXME: assert image was deleted?
}
public function testReplaceRequest()
{
global $page;
$this->log_in_as_admin();
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test");
$_POST['image_id'] = "$image_id";
send_event(new PageRequestEvent("image/replace"));
$this->assertEquals("redirect", $page->mode);
}
}

View File

@ -105,7 +105,7 @@ class ImageBan extends Extension
}
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER['HTTP_REFERER']);
$page->set_redirect(referer_or(make_link()));
}
} elseif ($event->get_arg(0) == "remove") {
$user->ensure_authed();
@ -113,7 +113,7 @@ class ImageBan extends Extension
send_event(new RemoveImageHashBanEvent($input['d_hash']));
$page->flash("Image ban removed");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER['HTTP_REFERER']);
$page->set_redirect(referer_or(make_link()));
} elseif ($event->get_arg(0) == "list") {
$t = new HashBanTable($database->raw_db());
$t->token = $user->get_auth_token();

View File

@ -3,6 +3,13 @@ class ImageBanTest extends ShimmiePHPUnitTestCase
{
private $hash = "feb01bab5698a11dd87416724c7a89e3";
public function testPages()
{
$this->log_in_as_admin();
$page = $this->get_page("image_hash_ban/list");
$this->assertEquals(200, $page->code);
}
public function testBan()
{
$this->log_in_as_admin();

View File

@ -245,11 +245,18 @@ class Index extends Extension
Image::$order_sql = "images.$ord $sort";
$event->add_querylet(new Querylet("1=1")); //small hack to avoid metatag being treated as normal tag
} elseif (preg_match("/^order[=|:]random[_]([0-9]{1,4})$/i", $event->term, $matches)) {
//order[=|:]random requires a seed to avoid duplicates
//since the tag can't be changed during the parseevent, we instead generate the seed during submit using js
// requires a seed to avoid duplicates
// since the tag can't be changed during the parseevent, we instead generate the seed during submit using js
$seed = $matches[1];
Image::$order_sql = "RAND($seed)";
$event->add_querylet(new Querylet("1=1")); //small hack to avoid metatag being treated as normal tag
} elseif (preg_match("/^order[=|:]dailyshuffle$/i", $event->term, $matches)) {
// will use today's date as seed, thus allowing for a dynamic randomized list without outside intervention.
// This way the list will change every day, giving a more dynamic feel to the imageboard.
// recommended to change homepage to "post/list/order:dailyshuffle/1"
$seed = date("Ymd");
Image::$order_sql = "RAND($seed)";
$event->add_querylet(new Querylet("1=1"));
}
$this->stpen++;

View File

@ -190,4 +190,17 @@ class IndexTest extends ShimmiePHPUnitTestCase
// negative tag alone, should work
$this->assert_search_results(["-pbx"], [$image_ids[1]]);
}
// This isn't really an index thing, we just want to test this from
// SOMEWHERE because the default theme doesn't use them.
public function test_nav()
{
send_event(new UserLoginEvent(User::by_name(self::$user_name)));
send_event(new PageNavBuildingEvent());
// just a few common parents
foreach (["help", "posts", "system", "user"] as $parent) {
send_event(new PageSubNavBuildingEvent($parent));
}
$this->assertTrue(true);
}
}

View File

@ -15,7 +15,7 @@ class LinkImageTheme extends Themelet
<table><tr>
<td><fieldset>
<legend><a href='http://en.wikipedia.org/wiki/Bbcode' target='_blank'>BBCode</a></legend>
<legend><a href='https://en.wikipedia.org/wiki/Bbcode' target='_blank'>BBCode</a></legend>
<table>
".
$this->link_code("Link", $this->url($post_link, $text_link, "ubb"), "ubb_text-link").
@ -26,7 +26,7 @@ class LinkImageTheme extends Themelet
</fieldset></td>
<td><fieldset>
<legend><a href='http://en.wikipedia.org/wiki/Html' target='_blank'>HTML</a></legend>
<legend><a href='https://en.wikipedia.org/wiki/Html' target='_blank'>HTML</a></legend>
<table>
".
$this->link_code("Link", $this->url($post_link, $text_link, "html"), "html_text-link").

View File

@ -234,11 +234,11 @@ class LogDatabase extends Extension
{
$sb = new SetupBlock("Logging (Database)");
$sb->add_choice_option("log_db_priority", [
"Debug" => SCORE_LOG_DEBUG,
"Info" => SCORE_LOG_INFO,
"Warning" => SCORE_LOG_WARNING,
"Error" => SCORE_LOG_ERROR,
"Critical" => SCORE_LOG_CRITICAL,
LOGGING_LEVEL_NAMES[SCORE_LOG_DEBUG] => SCORE_LOG_DEBUG,
LOGGING_LEVEL_NAMES[SCORE_LOG_INFO] => SCORE_LOG_INFO,
LOGGING_LEVEL_NAMES[SCORE_LOG_WARNING] => SCORE_LOG_WARNING,
LOGGING_LEVEL_NAMES[SCORE_LOG_ERROR] => SCORE_LOG_ERROR,
LOGGING_LEVEL_NAMES[SCORE_LOG_CRITICAL] => SCORE_LOG_CRITICAL,
], "Debug Level: ");
$event->panel->add_block($sb);
}

View File

@ -11,8 +11,8 @@ class MediaResizeEvent extends Event
public $target_height;
public $target_quality;
public $minimize;
public $ignore_aspect_ratio;
public $allow_upscale;
public $resize_type;
public function __construct(
String $engine,
@ -21,7 +21,7 @@ class MediaResizeEvent extends Event
string $output_path,
int $target_width,
int $target_height,
bool $ignore_aspect_ratio = false,
string $resize_type = Media::RESIZE_TYPE_FIT,
string $target_format = null,
int $target_quality = 80,
bool $minimize = false,
@ -38,8 +38,8 @@ class MediaResizeEvent extends Event
$this->target_format = $target_format;
$this->target_quality = $target_quality;
$this->minimize = $minimize;
$this->ignore_aspect_ratio = $ignore_aspect_ratio;
$this->allow_upscale = $allow_upscale;
$this->resize_type = $resize_type;
}
}

View File

@ -26,28 +26,32 @@ class Media extends Extension
const LOSSLESS_FORMATS = [
self::WEBP_LOSSLESS,
"png",
"psd",
"bmp",
"ico",
"cur",
"ani",
"gif"
EXTENSION_PNG,
EXTENSION_PSD,
EXTENSION_BMP,
EXTENSION_ICO,
EXTENSION_CUR,
EXTENSION_ANI,
EXTENSION_GIF
];
const ALPHA_FORMATS = [
self::WEBP_LOSSLESS,
self::WEBP_LOSSY,
"webp",
"png",
EXTENSION_WEBP,
EXTENSION_PNG,
];
const FORMAT_ALIASES = [
"tif" => "tiff",
"jpeg" => "jpg",
EXTENSION_TIF => EXTENSION_TIFF,
EXTENSION_JPEG => EXTENSION_JPG,
];
const RESIZE_TYPE_FIT = "Fit";
const RESIZE_TYPE_FIT_BLUR = "Fit Blur";
const RESIZE_TYPE_FILL = "Fill";
const RESIZE_TYPE_STRETCH = "Stretch";
//RIFF####WEBPVP8?..............ANIM
private const WEBP_ANIMATION_HEADER =
@ -118,7 +122,7 @@ class Media extends Extension
$sb->add_text_option(MediaConfig::FFMPEG_PATH, "ffmpeg", true);
$sb->add_text_option(MediaConfig::FFPROBE_PATH, "ffprobe", true);
$sb->add_shorthand_int_option(MediaConfig::MEM_LIMIT, "Mem limit: ", true);
$sb->add_shorthand_int_option(MediaConfig::MEM_LIMIT, "Mem limit", true);
$sb->end_table();
$event->panel->add_block($sb);
@ -206,6 +210,10 @@ class Media extends Extension
*/
public function onMediaResize(MediaResizeEvent $event)
{
if (!in_array($event->resize_type, MediaEngine::RESIZE_TYPE_SUPPORT[MediaEngine::IMAGICK])) {
throw new MediaException("Resize type $event->resize_type not supported by selected media engine $event->engine");
}
switch ($event->engine) {
case MediaEngine::GD:
$info = getimagesize($event->input_path);
@ -220,7 +228,7 @@ class Media extends Extension
$event->target_height,
$event->output_path,
$event->target_format,
$event->ignore_aspect_ratio,
$event->resize_type,
$event->target_quality,
$event->allow_upscale
);
@ -236,7 +244,7 @@ class Media extends Extension
$event->target_height,
$event->output_path,
$event->target_format,
$event->ignore_aspect_ratio,
$event->resize_type,
$event->target_quality,
$event->minimize,
$event->allow_upscale
@ -321,7 +329,7 @@ class Media extends Extension
* We need to consider the size that we are GOING TO instead.
*
* The factor of 2.5 is simply a rough guideline.
* http://stackoverflow.com/questions/527532/reasonable-php-memory-limit-for-image-resize
* https://stackoverflow.com/questions/527532/reasonable-php-memory-limit-for-image-resize
*
* @param array $info The output of getimagesize() for the source file in question.
* @return int The number of bytes an image resize operation is estimated to use.
@ -356,6 +364,7 @@ class Media extends Extension
}
$inname = warehouse_path(Image::IMAGE_DIR, $hash);
$tmpname = tempnam("/tmp", "shimmie_ffmpeg_thumb");
$outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
$orig_size = self::video_size($inname);
@ -363,7 +372,7 @@ class Media extends Extension
$codec = "mjpeg";
$quality = $config->get_int(ImageConfig::THUMB_QUALITY);
if ($config->get_string(ImageConfig::THUMB_TYPE) == "webp") {
if ($config->get_string(ImageConfig::THUMB_TYPE) == EXTENSION_WEBP) {
$codec = "libwebp";
} else {
// mjpeg quality ranges from 2-31, with 2 being the best quality.
@ -376,12 +385,11 @@ class Media extends Extension
$args = [
escapeshellarg($ffmpeg),
"-y", "-i", escapeshellarg($inname),
"-vf", "thumbnail,scale={$scaled_size[0]}:{$scaled_size[1]}",
"-vf", "thumbnail",
"-f", "image2",
"-vframes", "1",
"-c:v", $codec,
"-q:v", $quality,
escapeshellarg($outname),
"-c:v", "png",
escapeshellarg($tmpname),
];
$cmd = escapeshellcmd(implode(" ", $args));
@ -390,6 +398,10 @@ class Media extends Extension
if ((int)$ret == (int)0) {
log_debug('media', "Generating thumbnail with command `$cmd`, returns $ret");
create_scaled_image($tmpname, $outname, $scaled_size, "png");
return true;
} else {
log_error('media', "Generating thumbnail with command `$cmd`, returns $ret");
@ -436,7 +448,7 @@ class Media extends Extension
switch ($format) {
case self::WEBP_LOSSLESS:
case self::WEBP_LOSSY:
return "webp";
return EXTENSION_WEBP;
default:
return $format;
}
@ -445,7 +457,7 @@ class Media extends Extension
// private static function image_save_imagick(Imagick $image, string $path, string $format, int $output_quality = 80, bool $minimize)
// {
// switch ($format) {
// case "png":
// case EXTENSION_PNG:
// $result = $image->setOption('png:compression-level', 9);
// if ($result !== true) {
// throw new GraphicsException("Could not set png compression option");
@ -555,7 +567,7 @@ class Media extends Extension
return true;
}
switch ($format) {
case "webp":
case EXTENSION_WEBP:
return self::is_lossless_webp($filename);
break;
}
@ -569,7 +581,7 @@ class Media extends Extension
int $new_height,
string $output_filename,
string $output_type = null,
bool $ignore_aspect_ratio = false,
string $resize_type = self::RESIZE_TYPE_FIT,
int $output_quality = 80,
bool $minimize = false,
bool $allow_upscale = true
@ -586,7 +598,7 @@ class Media extends Extension
$output_type = $input_type;
}
if ($output_type=="webp" && self::is_lossless($input_path, $input_type)) {
if ($output_type==EXTENSION_WEBP && self::is_lossless($input_path, $input_type)) {
$output_type = self::WEBP_LOSSLESS;
}
@ -598,36 +610,58 @@ class Media extends Extension
$input_type = $input_type . ":";
}
$resize_args = "";
$resize_suffix = "";
if (!$allow_upscale) {
$resize_args .= "\>";
$resize_suffix .= "\>";
}
if ($ignore_aspect_ratio) {
$resize_args .= "\!";
if ($resize_type==Media::RESIZE_TYPE_STRETCH) {
$resize_suffix .= "\!";
}
$args = "";
$resize_arg = "-resize";
if ($minimize) {
$args .= "-strip ";
$resize_arg = "-thumbnail";
}
$file_arg = "${input_type}\"${input_path}[0]\"";
switch ($resize_type) {
case Media::RESIZE_TYPE_FIT:
case Media::RESIZE_TYPE_STRETCH:
$args .= "${resize_arg} ${new_width}x${new_height}${resize_suffix} ${file_arg}";
break;
case Media::RESIZE_TYPE_FILL:
$args .= "${resize_arg} ${new_width}x${new_height}\^ -gravity center -extent ${new_width}x${new_height} ${file_arg}";
break;
case Media::RESIZE_TYPE_FIT_BLUR:
$blur_size = max(ceil(max($new_width, $new_height) / 25), 5);
$args .= "${file_arg} ".
"\( -clone 0 -resize ${new_width}x${new_height}\^ -gravity center -fill black -colorize 50% -extent ${new_width}x${new_height} -blur 0x${blur_size} \) ".
"\( -clone 0 -resize ${new_width}x${new_height} \) ".
"-delete 0 -gravity center -compose over -composite";
break;
}
switch ($output_type) {
case Media::WEBP_LOSSLESS:
$args .= '-define webp:lossless=true';
break;
case "png":
case EXTENSION_PNG:
$args .= '-define png:compression-level=9';
break;
}
if ($minimize) {
$args .= " -strip -thumbnail";
} else {
$args .= " -resize";
}
$args .= " -quality ${output_quality} -background ${bg}";
$output_ext = self::determine_ext($output_type);
$format = '"%s" %s %ux%u%s -quality %u -background %s %s"%s[0]" %s:"%s" 2>&1';
$cmd = sprintf($format, $convert, $args, $new_width, $new_height, $resize_args, $output_quality, $bg, $input_type, $input_path, $output_ext, $output_filename);
$format = '"%s" %s %s:"%s" 2>&1';
$cmd = sprintf($format, $convert, $args, $output_ext, $output_filename);
$cmd = str_replace("\"convert\"", "convert", $cmd); // quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
exec($cmd, $output, $ret);
if ($ret != 0) {
@ -657,7 +691,7 @@ class Media extends Extension
int $new_height,
string $output_filename,
string $output_type = null,
bool $ignore_aspect_ratio = false,
string $resize_type = self::RESIZE_TYPE_FIT,
int $output_quality = 80,
bool $allow_upscale = true
) {
@ -668,19 +702,19 @@ class Media extends Extension
/* If not specified, output to the same format as the original image */
switch ($info[2]) {
case IMAGETYPE_GIF:
$output_type = "gif";
$output_type = EXTENSION_GIF;
break;
case IMAGETYPE_JPEG:
$output_type = "jpeg";
$output_type = EXTENSION_JPEG;
break;
case IMAGETYPE_PNG:
$output_type = "png";
$output_type = EXTENSION_PNG;
break;
case IMAGETYPE_WEBP:
$output_type = "webp";
$output_type = EXTENSION_WEBP;
break;
case IMAGETYPE_BMP:
$output_type = "bmp";
$output_type = EXTENSION_BMP;
break;
default:
throw new MediaException("Failed to save the new image - Unsupported image type.");
@ -693,7 +727,7 @@ class Media extends Extension
throw new InsufficientMemoryException("The image is too large to resize given the memory limits. ($memory_use > $memory_limit)");
}
if (!$ignore_aspect_ratio) {
if ($resize_type==Media::RESIZE_TYPE_FIT) {
list($new_width, $new_height) = get_scaled_by_aspect_ratio($width, $height, $new_width, $new_height);
}
if (!$allow_upscale &&
@ -741,7 +775,7 @@ class Media extends Extension
case IMAGETYPE_PNG:
case IMAGETYPE_WEBP:
//
// More info here: http://stackoverflow.com/questions/279236/how-do-i-resize-pngs-with-transparency-in-php
// More info here: https://stackoverflow.com/questions/279236/how-do-i-resize-pngs-with-transparency-in-php
//
if (imagealphablending($image_resized, false) === false) {
throw new MediaException("Unable to disable image alpha blending");
@ -776,21 +810,21 @@ class Media extends Extension
}
switch ($output_type) {
case "bmp":
case EXTENSION_BMP:
$result = imagebmp($image_resized, $output_filename, true);
break;
case "webp":
case EXTENSION_WEBP:
case Media::WEBP_LOSSY:
$result = imagewebp($image_resized, $output_filename, $output_quality);
break;
case "jpg":
case "jpeg":
case EXTENSION_JPG:
case EXTENSION_JPEG:
$result = imagejpeg($image_resized, $output_filename, $output_quality);
break;
case "png":
case EXTENSION_PNG:
$result = imagepng($image_resized, $output_filename, 9);
break;
case "gif":
case EXTENSION_GIF:
$result = imagegif($image_resized, $output_filename);
break;
default:
@ -816,7 +850,7 @@ class Media extends Extension
$is_anim_gif = 0;
if (($fh = @fopen($image_filename, 'rb'))) {
try {
//check if gif is animated (via http://www.php.net/manual/en/function.imagecreatefromgif.php#104473)
//check if gif is animated (via https://www.php.net/manual/en/function.imagecreatefromgif.php#104473)
while (!feof($fh) && $is_anim_gif < 2) {
$chunk = fread($fh, 1024 * 100);
$is_anim_gif += preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk, $matches);
@ -904,7 +938,7 @@ class Media extends Extension
*/
public static function normalize_format(string $format, ?bool $lossless = null): ?string
{
if ($format == "webp") {
if ($format == EXTENSION_WEBP) {
if ($lossless === true) {
$format = Media::WEBP_LOSSLESS;
} else {
@ -1016,7 +1050,7 @@ class Media extends Extension
$this->set_version(MediaConfig::VERSION, 2);
$database->beginTransaction();
$database->begin_transaction();
}
}
}

View File

@ -15,63 +15,81 @@ abstract class MediaEngine
];
public const OUTPUT_SUPPORT = [
MediaEngine::GD => [
"gif",
"jpg",
"png",
"webp",
EXTENSION_GIF,
EXTENSION_JPG,
EXTENSION_PNG,
EXTENSION_WEBP,
Media::WEBP_LOSSY,
],
MediaEngine::IMAGICK => [
"gif",
"jpg",
"png",
"webp",
EXTENSION_GIF,
EXTENSION_JPG,
EXTENSION_PNG,
EXTENSION_WEBP,
Media::WEBP_LOSSY,
Media::WEBP_LOSSLESS,
],
MediaEngine::FFMPEG => [
"jpg",
"webp",
"png",
EXTENSION_JPG,
EXTENSION_WEBP,
EXTENSION_PNG,
],
MediaEngine::STATIC => [
"jpg",
EXTENSION_JPG,
],
];
public const INPUT_SUPPORT = [
MediaEngine::GD => [
"bmp",
"gif",
"jpg",
"png",
"webp",
EXTENSION_BMP,
EXTENSION_GIF,
EXTENSION_JPG,
EXTENSION_PNG,
EXTENSION_WEBP,
Media::WEBP_LOSSY,
Media::WEBP_LOSSLESS,
],
MediaEngine::IMAGICK => [
"bmp",
"gif",
"jpg",
"png",
"psd",
"tiff",
"webp",
EXTENSION_BMP,
EXTENSION_GIF,
EXTENSION_JPG,
EXTENSION_PNG,
EXTENSION_PSD,
EXTENSION_TIFF,
EXTENSION_WEBP,
Media::WEBP_LOSSY,
Media::WEBP_LOSSLESS,
"ico",
EXTENSION_ICO,
],
MediaEngine::FFMPEG => [
"avi",
"mkv",
"webm",
"mp4",
"mov",
"flv",
EXTENSION_AVI,
EXTENSION_MKV,
EXTENSION_WEBM,
EXTENSION_MP4,
EXTENSION_MOV,
EXTENSION_FLASH_VIDEO,
],
MediaEngine::STATIC => [
"jpg",
"gif",
"png",
EXTENSION_JPG,
EXTENSION_GIF,
EXTENSION_PNG,
],
];
public const RESIZE_TYPE_SUPPORT = [
MediaEngine::GD => [
Media::RESIZE_TYPE_FIT,
Media::RESIZE_TYPE_STRETCH
],
MediaEngine::IMAGICK => [
Media::RESIZE_TYPE_FIT,
Media::RESIZE_TYPE_FIT_BLUR,
Media::RESIZE_TYPE_FILL,
Media::RESIZE_TYPE_STRETCH,
],
MediaEngine::FFMPEG => [
Media::RESIZE_TYPE_FIT
],
MediaEngine::STATIC => [
Media::RESIZE_TYPE_FIT
]
];
}

View File

@ -113,7 +113,7 @@ class NotATag extends Extension
["tag"=>$input['c_tag'], "redirect"=>$input['c_redirect']]
);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER['HTTP_REFERER']);
$page->set_redirect(referer_or(make_link()));
} elseif ($event->get_arg(0) == "remove") {
$user->ensure_authed();
$input = validate_input(["d_tag"=>"string"]);
@ -123,7 +123,7 @@ class NotATag extends Extension
);
$page->flash("Image ban removed");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER['HTTP_REFERER']);
$page->set_redirect(referer_or(make_link()));
} elseif ($event->get_arg(0) == "list") {
$t = new NotATagTable($database->raw_db());
$t->token = $user->get_auth_token();

View File

@ -355,7 +355,7 @@ class Notes extends Extension
{
global $database, $config;
$pageNumber = $event->try_page_num(1) - 1;
$pageNumber = $event->try_page_num(1);
$notesPerPage = $config->get_int('notesNotesPerPage');
@ -383,7 +383,7 @@ class Notes extends Extension
{
global $config, $database;
$pageNumber = $event->try_page_num(1) - 1;
$pageNumber = $event->try_page_num(1);
$requestsPerPage = $config->get_int('notesRequestsPerPage');
@ -430,7 +430,7 @@ class Notes extends Extension
{
global $config, $database;
$pageNumber = $event->try_page_num(1) - 1;
$pageNumber = $event->try_page_num(1);
$historiesPerPage = $config->get_int('notesHistoriesPerPage');

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