Compare commits

..

10 Commits

Author SHA1 Message Date
Shish
6eae9b28e7 Merge branch 'master' into branch_2.4 2012-05-23 11:35:36 +01:00
Shish
c5aef0faea Merge branch 'master' into branch_2.4 2012-05-23 10:39:32 +01:00
Shish
88fa824d27 Merge branch 'master' into branch_2.4 2012-05-22 12:15:14 +01:00
Shish
f18c8790f2 Merge branch 'master' into branch_2.4 2012-05-14 11:13:01 +01:00
Shish
10816aa88c Merge branch 'master' into branch_2.4 2012-05-14 06:43:34 +01:00
Shish
8ba00569be Merge branch 'master' into branch_2.4 2012-05-12 10:11:05 +01:00
Shish
8f3603f6dc Merge branch 'master' into branch_2.4 2012-05-12 10:04:26 +01:00
Shish
70329fd786 example of anon with comments 2012-05-03 14:55:30 +01:00
Shish
00e2feafc3 remove alpha warning from stable branch 2012-04-28 18:38:57 +01:00
Shish
681622480e version 2012-04-01 22:06:54 +01:00
704 changed files with 52302 additions and 47506 deletions

View File

@ -1,8 +0,0 @@
vendor
.git
*.phar
data
images
thumbs
*.sqlite
Dockerfile

View File

@ -1,21 +0,0 @@
# In retrospect I'm less of a fan of tabs for indentation, because
# while they're better when they work, they're worse when they don't
# work, and so many people use terrible editors when they don't work
# that everything is inconsistent... but tabs are what Shimmie went
# with back in the 90's, so that's what we use now, and we deal with
# the pain of making sure everybody configures their editor properly
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
[*.{js,css,php}]
charset = utf-8
indent_style = space
indent_size = 4

1
.gitattributes vendored
View File

@ -1 +0,0 @@
*.php text eol=lf

View File

@ -1,28 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
---
**Server Software**
(You can get all these stats from `http://<your site>/system_info`)
- 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 '....'
**What did you expect to happen?**
A clear and concise description of what you expected to happen.
**What actually happened?**
If applicable, add screenshots to help explain your problem.

View File

@ -1,17 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

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

@ -1,95 +0,0 @@
name: Test & Publish
on:
push:
pull_request:
schedule:
- cron: '0 2 * * 0' # Weekly on Sundays at 02:00
jobs:
test:
name: PHP ${{ matrix.php }} / DB ${{ matrix.database }}
strategy:
max-parallel: 3
fail-fast: false
matrix:
php: ['7.3']
database: ['pgsql', 'mysql', 'sqlite']
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up PHP
uses: shivammathur/setup-php@master
with:
php-version: ${{ matrix.php }}
coverage: pcov
extensions: mbstring
- name: Set up database
run: |
mkdir -p data/config
if [[ "${{ matrix.database }}" == "pgsql" ]]; then
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 ;
sudo -u postgres psql -c "CREATE DATABASE shimmie WITH OWNER shimmie;" -U postgres ;
fi
if [[ "${{ matrix.database }}" == "mysql" ]]; then
sudo systemctl start mysql ;
mysql --version ;
mysql -e "SET GLOBAL general_log = 'ON';" -uroot -proot ;
mysql -e "CREATE DATABASE shimmie;" -uroot -proot ;
fi
if [[ "${{ matrix.database }}" == "sqlite" ]]; then
sudo apt update && sudo apt-get install -y sqlite3 ;
sqlite3 --version ;
fi
- name: Check versions
run: php -v && composer -V
- name: Validate composer.json and composer.lock
run: composer validate
- name: Install PHP dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Install shimmie
run: php index.php
- name: Run test suite
run: |
if [[ "${{ matrix.database }}" == "pgsql" ]]; then
export TEST_DSN="pgsql:user=shimmie;password=shimmie;host=127.0.0.1;dbname=shimmie"
fi
if [[ "${{ matrix.database }}" == "mysql" ]]; then
export TEST_DSN="mysql:user=root;password=root;host=127.0.0.1;dbname=shimmie"
fi
if [[ "${{ matrix.database }}" == "sqlite" ]]; then
export TEST_DSN="sqlite:data/shimmie.sqlite"
fi
vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover
- name: Upload coverage
run: |
wget https://scrutinizer-ci.com/ocular.phar
php ocular.phar code-coverage:upload --format=php-clover data/coverage.clover
publish:
name: Publish
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push'
steps:
- uses: actions/checkout@master
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
with:
name: shish2k/shimmie2
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
cache: ${{ github.event_name != 'schedule' }}
buildoptions: "--build-arg RUN_TESTS=false"

86
.gitignore vendored
View File

@ -2,88 +2,4 @@ backup
data data
images images
thumbs thumbs
*.phar !lib/images
*.sqlite
*.cache
.devcontainer
trace.json
#Composer
composer.phar
composer.lock
/vendor/
# Created by http://www.gitignore.io
### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
### OSX ###
.DS_Store
.AppleDouble
.LSOverride
# Icon must ends with two \r.
Icon
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes
### Linux ###
*~
# KDE directory preferences
.directory
### vim ###
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
### PhpStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
## Directory-based project format
.idea/
# if you remove the above rule, at least ignore user-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# and these sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
## File-based project format
*.ipr
*.iws
*.iml
## Additional for IntelliJ
out/
# generated by mpeltonen/sbt-idea plugin
.idea_modules/
# generated by JIRA plugin
atlassian-ide-plugin.xml
# generated by Crashlytics plugin (for Android Studio and Intellij)
com_crashlytics_export_strings.xml

View File

@ -3,65 +3,42 @@
</IfModule> </IfModule>
<FilesMatch "\.(sqlite|sdb|s3db|db)$"> <FilesMatch "\.(sqlite|sdb|s3db|db)$">
<IfModule mod_authz_host.c> Deny from all
Require all denied
</IfModule>
<IfModule !mod_authz_host.c>
Deny from all
</IfModule>
</FilesMatch> </FilesMatch>
<IfModule mod_rewrite.c> <IfModule mod_rewrite.c>
RewriteEngine On RewriteEngine on
# rather than link to images/ha/hash and have an ugly filename, # rather than link to images/ha/hash and have an ugly filename,
# we link to images/hash/tags.ext; mod_rewrite splits things so # we link to images/hash/tags.ext; mod_rewrite splits things so
# that shimmie sees hash and the user sees tags.ext # that shimmie sees hash and the user sees tags.ext
RewriteRule ^_(images|thumbs)/([0-9a-f]{2})([0-9a-f]{30}).*$ data/$1/$2/$2$3 [L] RewriteRule ^_images/([0-9a-f]{2})([0-9a-f]{30}).*$ images/$1/$1$2 [L]
RewriteRule ^_thumbs/([0-9a-f]{2})([0-9a-f]{30}).*$ thumbs/$1/$1$2 [L]
# any requests for files which don't physically exist should be handled by index.php # any requests for files which don't physically exist should be handled by index.php
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?q=$1&%{QUERY_STRING} [L] RewriteRule ^(.*)$ index.php?q=$1&%{QUERY_STRING} [L]
</IfModule> </IfModule>
<IfModule mod_php5.c>
php_flag register_globals 0
php_flag magic_quotes_gpc 0
php_flag magic_quotes_runtime 0
</IfModule>
DefaultType image/jpeg
<IfModule mod_expires.c> <IfModule mod_expires.c>
ExpiresActive On ExpiresActive On
<FilesMatch "([0-9a-f]{32}|\.(gif|jpe?g|png|webp|css|js))$"> <FilesMatch "([0-9a-f]{32}|\.(gif|jpe?g|png|css|js))$">
<IfModule mod_headers.c> Header set Cache-Control "public, max-age=2629743"
Header set Cache-Control "public, max-age=2629743"
</IfModule>
ExpiresDefault "access plus 1 month" ExpiresDefault "access plus 1 month"
</FilesMatch> </FilesMatch>
#ExpiresByType text/html "now" #ExpiresByType text/html "now"
#ExpiresByType text/plain "now" #ExpiresByType text/plain "now"
</IfModule> </IfModule>
<IfModule mod_deflate.c> <ifmodule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
AddOutputFilterByType DEFLATE application/x-javascript application/javascript AddOutputFilterByType DEFLATE application/x-javascript application/javascript
</IfModule> </ifmodule>
#EXT: handle_pixel
AddType image/jpeg jpg jpeg
AddType image/gif gif
AddType image/png png
AddType image/webp webp
#EXT: handle_ico
AddType image/x-icon ico ani cur
#EXT: handle_flash
AddType application/x-shockwave-flash swf
#EXT: handle_mp3
AddType audio/mpeg mp3
#EXT: handle_svg
AddType image/svg+xml svg svgz
#EXT: handle_video
AddType video/x-flv flv
AddType video/mp4 f4v f4p m4v mp4
AddType audio/mp4 f4a f4b m4a
AddType video/ogg ogv
AddType video/webm webm

View File

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

View File

@ -1,19 +0,0 @@
imports:
- javascript
- php
filter:
excluded_paths: [ext/*/lib/*,ext/tagger/script.js,tests/*]
build:
nodes:
analysis:
tests:
before:
- mkdir -p data/config
- cp tests/defines.php data/config/shimmie.conf.php
override:
- php-scrutinizer-run
tools:
external_code_coverage: true

View File

@ -1,49 +0,0 @@
# "Build" shimmie (composer install - done in its own stage so that we don't
# need to include all the composer fluff in the final image)
FROM debian:stable-slim AS app
RUN apt update && apt install -y composer php7.3-gd php7.3-dom php7.3-sqlite3 php-xdebug imagemagick
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
COPY . /app/
ARG RUN_TESTS=true
RUN [ $RUN_TESTS = false ] || (\
echo '=== Installing ===' && mkdir -p data/config && INSTALL_DSN="sqlite:data/shimmie.sqlite" php index.php && \
echo '=== Smoke Test ===' && php index.php get-page /post/list && \
echo '=== Unit Tests ===' && ./vendor/bin/phpunit --configuration tests/phpunit.xml && \
echo '=== Coverage ===' && ./vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-text && \
echo '=== Cleaning ===' && rm -rf data)
# Build su-exec so that our final image can be nicer
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; \
chown root:root /usr/local/bin/su-exec; \
chmod 0755 /usr/local/bin/su-exec;
# Actually run shimmie
FROM debian:stable-slim
EXPOSE 8000
HEALTHCHECK --interval=5m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1
ENV UID=1000 \
GID=1000
RUN apt update && apt install -y curl \
php7.3-cli php7.3-gd php7.3-pgsql php7.3-mysql php7.3-sqlite3 php7.3-zip php7.3-dom php7.3-mbstring \
imagemagick zip unzip && \
rm -rf /var/lib/apt/lists/*
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

@ -1,339 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@ -1,43 +0,0 @@
```
_________.__ .__ .__ ________
/ _____/| |__ |__| _____ _____ |__| ____ \_____ \
\_____ \ | | \ | | / \ / \ | |_/ __ \ / ____/
/ \| Y \| || Y Y \| Y Y \| |\ ___/ / \
/_______ /|___| /|__||__|_| /|__|_| /|__| \___ >\_______ \
\/ \/ \/ \/ \/ \/
```
# Shimmie
[![Test & Publish](https://github.com/shish/shimmie2/workflows/Test%20&%20Publish/badge.svg)](https://github.com/shish/shimmie2/actions)
[![Code Quality](https://scrutinizer-ci.com/g/shish/shimmie2/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=master)
[![Code Coverage](https://scrutinizer-ci.com/g/shish/shimmie2/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=master)
# Documentation
* [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: https://github.com/shish/shimmie2/issues
# Licence
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
customisations to your own site, then those customisations belong to you,
and you can do what you want with them.

109
README.txt Normal file
View File

@ -0,0 +1,109 @@
_________.__ .__ .__ ________
/ _____/| |__ |__| _____ _____ |__| ____ \_____ \
\_____ \ | | \ | | / \ / \ | |_/ __ \ / ____/
/ \| Y \| || Y Y \| Y Y \| |\ ___/ / \
/_______ /|___| /|__||__|_| /|__|_| /|__| \___ >\_______ \
\/ \/ \/ \/ \/ \/
_________________________________________________________________________
Requirements
~~~~~~~~~~~~
MySQL 5.1+ (with experimental support for PostgreSQL 8+ and SQLite 3)
PHP 5.2.6+
GD or ImageMagick
Installation
~~~~~~~~~~~~
1) Create a blank database
2) Unzip shimmie into a folder on the web host
3) Visit the folder with a web browser
4) Enter the location of the database
5) Click "install". Hopefully you'll end up at the welcome screen; if
not, you should be given instructions on how to fix any errors~
Upgrade from 2.3.X
~~~~~~~~~~~~~~~~~~
- Backup your current files and database!
- Unzip into a clean folder
- Copy across the images, thumbs, and data folders
- Move old/config.php to new/data/config/shimmie.conf.php
- Edit shimmie.conf.php to use the new database connection format:
OLD: $database_dsn = "<proto>://<username>:<password>@<host>/<database>";
NEW: define("DATABASE_DSN", "<proto>:user=<username>;password=<password>;host=<host>;dbname=<database>");
The rest should be automatic~
If there are any errors with the upgrade process, "in_upgrade=true" will
be left in the config table and the process will be paused for the admin
to investigate. Deleting this config entry and refreshing the page should
continue the upgrade from where it left off.
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.
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.inc.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:
new UserClass("anonymous", "base", array(
"create_comment" => True,
"edit_image_tag" => True,
"edit_image_source" => True,
"create_image_report" => True,
));
For a moderator class, being a regular user who can delete images and
comments:
new UserClass("moderator", "user", array(
"delete_image" => True,
"delete_comment" => True,
));
For a list of permissions, see core/userclass.class.php
Development Info
~~~~~~~~~~~~~~~~
http://shimmie.shishnet.org/doc/
Please tell me if those docs are lacking in any way, so that they can be
improved for the next person who uses them
Contact
~~~~~~~
#shimmie on Freenode -- IRC
webmaster at shishnet.org -- email
https://github.com/shish/shimmie2/issues -- bug tracker
Licence
~~~~~~~
All code is GPLv2 unless mentioned otherwise; ie, 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 customisations to your own
site, then those customisations belong to you, and you can do what you want
with them.

View File

@ -1,67 +0,0 @@
{
"name": "shish/shimmie2",
"description": "A tag-based image gallery",
"type" : "project",
"license" : "GPL-2.0-or-later",
"minimum-stability" : "dev",
"repositories" : [
{
"type" : "package",
"package" : {
"name" : "ifixit/php-akismet",
"version" : "1.1",
"source" : {
"url" : "https://github.com/iFixit/php-akismet.git",
"type" : "git",
"reference" : "fd4ff50eb577457c1b7b887401663e91e77625ae"
}
}
}
],
"require" : {
"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" : "^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/mediaelement" : "2.21.*",
"bower-asset/js-cookie" : "2.1.*"
},
"require-dev" : {
},
"suggest": {
"ext-memcache": "memcache caching",
"ext-memcached": "memcached caching",
"ext-apc": "apc caching",
"ext-redis": "redis caching",
"ext-dom": "some extensions",
"ext-curl": "some extensions",
"ext-ctype": "some extensions",
"ext-json": "some extensions",
"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"
}
}

461
composer.lock generated
View File

@ -1,461 +0,0 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ca096500833679c5fa819eb5cfbd1d80",
"packages": [
{
"name": "bower-asset/jquery-timeago",
"version": "v1.5.4",
"source": {
"type": "git",
"url": "git@github.com:rmm5t/jquery-timeago.git",
"reference": "180864a9c544a49e43719b457250af216d5e4c3a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rmm5t/jquery-timeago/zipball/180864a9c544a49e43719b457250af216d5e4c3a",
"reference": "180864a9c544a49e43719b457250af216d5e4c3a"
},
"require": {
"bower-asset/jquery": ">=1.4"
},
"type": "bower-asset",
"license": [
"MIT"
]
},
{
"name": "bower-asset/js-cookie",
"version": "v2.1.4",
"source": {
"type": "git",
"url": "git@github.com:js-cookie/js-cookie.git",
"reference": "8b70250875f7e07445b6a457f9c2474ead4cba44"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/js-cookie/js-cookie/zipball/8b70250875f7e07445b6a457f9c2474ead4cba44",
"reference": "8b70250875f7e07445b6a457f9c2474ead4cba44"
},
"type": "bower-asset",
"license": [
"MIT"
]
},
{
"name": "bower-asset/mediaelement",
"version": "2.21.2",
"source": {
"type": "git",
"url": "git@github.com:johndyer/mediaelement.git",
"reference": "394db3b4a2e3f5f7988cacdefe62ed973bf4a3ce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/johndyer/mediaelement/zipball/394db3b4a2e3f5f7988cacdefe62ed973bf4a3ce",
"reference": "394db3b4a2e3f5f7988cacdefe62ed973bf4a3ce"
},
"type": "bower-asset",
"license": [
"MIT"
]
},
{
"name": "dapphp/securimage",
"version": "3.6.8",
"source": {
"type": "git",
"url": "https://github.com/dapphp/securimage.git",
"reference": "5fc5953c4ffba1eb214cc83100672f238c184ca4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dapphp/securimage/zipball/5fc5953c4ffba1eb214cc83100672f238c184ca4",
"reference": "5fc5953c4ffba1eb214cc83100672f238c184ca4",
"shasum": ""
},
"require": {
"ext-gd": "*",
"php": ">=5.4"
},
"suggest": {
"ext-pdo": "For database storage support",
"ext-pdo_mysql": "For MySQL database support",
"ext-pdo_sqlite": "For SQLite3 database support"
},
"type": "library",
"autoload": {
"classmap": [
"securimage.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Drew Phillips",
"email": "drew@drew-phillips.com"
}
],
"description": "PHP CAPTCHA Library",
"homepage": "https://www.phpcaptcha.org",
"keywords": [
"Forms",
"anti-spam",
"captcha",
"security"
],
"time": "2020-05-30T09:43:22+00:00"
},
{
"name": "enshrined/svg-sanitize",
"version": "0.13.3",
"source": {
"type": "git",
"url": "https://github.com/darylldoyle/svg-sanitizer.git",
"reference": "bc66593f255b7d2613d8f22041180036979b6403"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/bc66593f255b7d2613d8f22041180036979b6403",
"reference": "bc66593f255b7d2613d8f22041180036979b6403",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*"
},
"require-dev": {
"codeclimate/php-test-reporter": "^0.1.2",
"phpunit/phpunit": "^6"
},
"type": "library",
"autoload": {
"psr-4": {
"enshrined\\svgSanitize\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"authors": [
{
"name": "Daryll Doyle",
"email": "daryll@enshrined.co.uk"
}
],
"description": "An SVG sanitizer for PHP",
"time": "2020-01-20T01:34:17+00:00"
},
{
"name": "flexihash/flexihash",
"version": "v2.0.2",
"source": {
"type": "git",
"url": "https://github.com/pda/flexihash.git",
"reference": "497ba5782606d998f8ab0b4e5942e3a799bec018"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pda/flexihash/zipball/497ba5782606d998f8ab0b4e5942e3a799bec018",
"reference": "497ba5782606d998f8ab0b4e5942e3a799bec018",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8",
"satooshi/php-coveralls": "~1.0",
"squizlabs/php_codesniffer": "^2.3",
"symfony/config": "^2.0.0",
"symfony/console": "^2.0.0",
"symfony/filesystem": "^2.0.0",
"symfony/stopwatch": "^2.0.0",
"symfony/yaml": "^2.0.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Flexihash\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Annesley",
"email": "paul@annesley.cc",
"homepage": "http://paul.annesley.cc"
},
{
"name": "Dom Morgan",
"email": "dom@d3r.com",
"homepage": "https://d3r.com"
}
],
"description": "Flexihash is a small PHP library which implements consistent hashing",
"homepage": "https://github.com/pda/flexihash",
"time": "2016-04-22T21:03:23+00:00"
},
{
"name": "google/recaptcha",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/google/recaptcha.git",
"reference": "79ccf652575e138d51742d04e78a5adbb9892f9d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/google/recaptcha/zipball/79ccf652575e138d51742d04e78a5adbb9892f9d",
"reference": "79ccf652575e138d51742d04e78a5adbb9892f9d",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.2.20|^2.15",
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^4.8.36|^5.7.27|^6.59|^7.5.11"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
},
"autoload": {
"psr-4": {
"ReCaptcha\\": "src/ReCaptcha"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.",
"homepage": "https://www.google.com/recaptcha/",
"keywords": [
"Abuse",
"captcha",
"recaptcha",
"spam"
],
"time": "2020-05-21T08:56:25+00:00"
},
{
"name": "ifixit/php-akismet",
"version": "1.1",
"source": {
"type": "git",
"url": "https://github.com/iFixit/php-akismet.git",
"reference": "fd4ff50eb577457c1b7b887401663e91e77625ae"
},
"type": "library"
},
{
"name": "shish/eventtracer-php",
"version": "v2.0.0",
"source": {
"type": "git",
"url": "https://github.com/shish/eventtracer-php.git",
"reference": "0328454c58d240667f004f3efd863b8ef521ba2a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/shish/eventtracer-php/zipball/0328454c58d240667f004f3efd863b8ef521ba2a",
"reference": "0328454c58d240667f004f3efd863b8ef521ba2a",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.3"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Shish",
"email": "webmaster@shishnet.org",
"homepage": "http://shishnet.org",
"role": "Developer"
}
],
"description": "An API to write JSON traces as used by the Chrome Trace Viewer",
"homepage": "https://github.com/shish/eventtracer-php",
"time": "2020-09-20T11:46:44+00:00"
},
{
"name": "shish/ffsphp",
"version": "v1.0.2",
"source": {
"type": "git",
"url": "https://github.com/shish/ffsphp.git",
"reference": "82d45b2691da11c82a28f85b07752334a06b8a8e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/shish/ffsphp/zipball/82d45b2691da11c82a28f85b07752334a06b8a8e",
"reference": "82d45b2691da11c82a28f85b07752334a06b8a8e",
"shasum": ""
},
"require": {
"php": "^7.3"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"FFSPHP\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Shish",
"email": "webmaster@shishnet.org",
"homepage": "http://shishnet.org",
"role": "Developer"
}
],
"description": "A collection of workarounds for stupid PHP things",
"homepage": "https://github.com/shish/ffsphp",
"time": "2020-09-20T13:15:53+00:00"
},
{
"name": "shish/microcrud",
"version": "v2.0.0",
"source": {
"type": "git",
"url": "https://github.com/shish/microcrud.git",
"reference": "1d55c02c405fc75ad6a0e952e9333552bef492f0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/shish/microcrud/zipball/1d55c02c405fc75ad6a0e952e9333552bef492f0",
"reference": "1d55c02c405fc75ad6a0e952e9333552bef492f0",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"php": "^7.3",
"shish/ffsphp": "^1.0",
"shish/microhtml": "^2.0.2"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"MicroCRUD\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Shish",
"email": "webmaster@shishnet.org",
"homepage": "http://shishnet.org",
"role": "Developer"
}
],
"description": "A minimal CRUD generating library",
"homepage": "https://github.com/shish/microcrud",
"keywords": [
"crud",
"generator"
],
"time": "2020-09-20T13:10:42+00:00"
},
{
"name": "shish/microhtml",
"version": "v2.0.2",
"source": {
"type": "git",
"url": "https://github.com/shish/microhtml.git",
"reference": "d5c393d5a47bb3b3febdfde9ebf6e91cb9b41947"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/shish/microhtml/zipball/d5c393d5a47bb3b3febdfde9ebf6e91cb9b41947",
"reference": "d5c393d5a47bb3b3febdfde9ebf6e91cb9b41947",
"shasum": ""
},
"require": {
"php": "^7.3"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"type": "library",
"autoload": {
"files": [
"src/microhtml.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Shish",
"email": "webmaster@shishnet.org",
"homepage": "http://shishnet.org",
"role": "Developer"
}
],
"description": "A minimal HTML generating library",
"homepage": "https://github.com/shish/microhtml",
"keywords": [
"generator",
"html"
],
"time": "2020-09-20T13:05:01+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^7.3",
"ext-pdo": "*",
"ext-json": "*",
"ext-fileinfo": "*"
},
"platform-dev": [],
"plugin-api-version": "1.1.0"
}

View File

@ -1,667 +0,0 @@
<?php declare(strict_types=1);
require_once "core/event.php";
abstract class PageMode
{
const REDIRECT = 'redirect';
const DATA = 'data';
const PAGE = 'page';
const FILE = 'file';
const MANUAL = 'manual';
}
/**
* Class Page
*
* A data structure for holding all the bits of data that make up a page.
*
* The various extensions all add whatever they want to this structure,
* then Layout turns it into HTML.
*/
class BasePage
{
/** @var string */
public $mode = PageMode::PAGE;
/** @var string */
private $type = "text/html; charset=utf-8";
/**
* Set what this page should do; "page", "data", or "redirect".
*/
public function set_mode(string $mode): void
{
$this->mode = $mode;
}
/**
* Set the page's MIME type.
*/
public function set_type(string $type): void
{
$this->type = $type;
}
public function __construct()
{
if (@$_GET["flash"]) {
$this->flash[] = $_GET['flash'];
unset($_GET["flash"]);
}
}
// ==============================================
/** @var string; public only for unit test */
public $data = "";
/** @var string */
private $file = null;
/** @var bool */
private $file_delete = false;
/** @var string */
private $filename = null;
private $disposition = null;
/**
* Set the raw data to be sent.
*/
public function set_data(string $data): void
{
$this->data = $data;
}
public function set_file(string $file, bool $delete = false): void
{
$this->file = $file;
$this->file_delete = $delete;
}
/**
* Set the recommended download filename.
*/
public function set_filename(string $filename, string $disposition = "attachment"): void
{
$this->filename = $filename;
$this->disposition = $disposition;
}
// ==============================================
/** @var string */
public $redirect = "";
/**
* Set the URL to redirect to (remember to use make_link() if linking
* to a page in the same site).
*/
public function set_redirect(string $redirect): void
{
$this->redirect = $redirect;
}
// ==============================================
/** @var int */
public $code = 200;
/** @var string */
public $title = "";
/** @var string */
public $heading = "";
/** @var string */
public $subheading = "";
/** @var string[] */
public $html_headers = [];
/** @var string[] */
public $http_headers = [];
/** @var string[][] */
public $cookies = [];
/** @var Block[] */
public $blocks = [];
/** @var string[] */
public $flash = [];
/**
* Set the HTTP status code
*/
public function set_code(int $code): void
{
$this->code = $code;
}
public function set_title(string $title): void
{
$this->title = $title;
}
public function set_heading(string $heading): void
{
$this->heading = $heading;
}
public function set_subheading(string $subheading): void
{
$this->subheading = $subheading;
}
public function flash(string $message): void
{
$this->flash[] = $message;
}
/**
* Add a line to the HTML head section.
*/
public function add_html_header(string $line, int $position = 50): void
{
while (isset($this->html_headers[$position])) {
$position++;
}
$this->html_headers[$position] = $line;
}
/**
* Add a http header to be sent to the client.
*/
public function add_http_header(string $line, int $position = 50): void
{
while (isset($this->http_headers[$position])) {
$position++;
}
$this->http_headers[$position] = $line;
}
/**
* The counterpart for get_cookie, this works like php's
* setcookie method, but prepends the site-wide cookie prefix to
* the $name argument before doing anything.
*/
public function add_cookie(string $name, string $value, int $time, string $path): void
{
$full_name = COOKIE_PREFIX . "_" . $name;
$this->cookies[] = [$full_name, $value, $time, $path];
}
public function get_cookie(string $name): ?string
{
$full_name = COOKIE_PREFIX . "_" . $name;
if (isset($_COOKIE[$full_name])) {
return $_COOKIE[$full_name];
} else {
return null;
}
}
/**
* Get all the HTML headers that are currently set and return as a string.
*/
public function get_all_html_headers(): string
{
$data = '';
ksort($this->html_headers);
foreach ($this->html_headers as $line) {
$data .= "\t\t" . $line . "\n";
}
return $data;
}
/**
* Add a Block of data to the page.
*/
public function add_block(Block $block): void
{
$this->blocks[] = $block;
}
/**
* Find a block which contains the given text
* (Useful for unit tests)
*/
public function find_block(string $text): ?Block
{
foreach ($this->blocks as $block) {
if ($block->header == $text) {
return $block;
}
}
return null;
}
// ==============================================
public function send_headers(): void
{
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);
}
foreach ($this->cookies as $c) {
setcookie($c[0], $c[1], $c[2], $c[3]);
}
} 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();
$this->render();
break;
case PageMode::DATA:
header("Content-Length: " . strlen($this->data));
if (!is_null($this->filename)) {
header('Content-Disposition: ' . $this->disposition . '; filename=' . $this->filename);
}
print $this->data;
break;
case PageMode::FILE:
if (!is_null($this->filename)) {
header('Content-Disposition: ' . $this->disposition . '; filename=' . $this->filename);
}
// https://gist.github.com/codler/3906826
$size = filesize($this->file); // File size
$length = $size; // Content length
$start = 0; // Start byte
$end = $size - 1; // End byte
header("Content-Length: " . $size);
header('Accept-Ranges: bytes');
if (isset($_SERVER['HTTP_RANGE'])) {
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (strpos($range, ',') !== false) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
break;
}
if ($range == '-') {
$c_start = $size - (int)substr($range, 1);
$c_end = $end;
} else {
$range = explode('-', $range);
$c_start = (int)$range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? (int)$range[1] : $size;
}
$c_end = ($c_end > $end) ? $end : $c_end;
if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
break;
}
$start = $c_start;
$end = $c_end;
$length = $end - $start + 1;
header('HTTP/1.1 206 Partial Content');
}
header("Content-Range: bytes $start-$end/$size");
header("Content-Length: " . $length);
try {
stream_file($this->file, $start, $end);
} finally {
if ($this->file_delete === true) {
unlink($this->file);
}
}
break;
case PageMode::REDIRECT:
if ($this->flash) {
$this->redirect .= (strpos($this->redirect, "?") === false) ? "?" : "&";
$this->redirect .= "flash=" . url_escape(implode("\n", $this->flash));
}
header('Location: ' . $this->redirect);
print 'You should be redirected to <a href="' . $this->redirect . '">' . $this->redirect . '</a>';
break;
default:
print "Invalid page mode";
break;
}
}
/**
* This function grabs all the CSS and JavaScript files sprinkled throughout Shimmie's folders,
* concatenates them together into two large files (one for CSS and one for JS) and then stores
* them in the /cache/ directory for serving to the user.
*
* Why do this? Two reasons:
* 1. Reduces the number of files the user's browser needs to download.
* 2. Allows these cached files to be compressed/minified by the admin.
*
* TODO: This should really be configurable somehow...
*/
public function add_auto_html_headers(): void
{
global $config;
$data_href = get_base_href();
$theme_name = $config->get_string(SetupConfig::THEME, 'default');
$this->add_html_header("<script type='text/javascript'>base_href = '$data_href';</script>", 40);
# static handler will map these to themes/foo/static/bar.ico or ext/static_files/static/bar.ico
$this->add_html_header("<link rel='icon' type='image/x-icon' href='$data_href/favicon.ico'>", 41);
$this->add_html_header("<link rel='apple-touch-icon' href='$data_href/apple-touch-icon.png'>", 42);
//We use $config_latest to make sure cache is reset if config is ever updated.
$config_latest = 0;
foreach (zglob("data/config/*") as $conf) {
$config_latest = max($config_latest, filemtime($conf));
}
/*** Generate CSS cache files ***/
$css_latest = $config_latest;
$css_files = array_merge(
zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/style.css"),
zglob("themes/$theme_name/style.css")
);
foreach ($css_files as $css) {
$css_latest = max($css_latest, filemtime($css));
}
$css_md5 = md5(serialize($css_files));
$css_cache_file = data_path("cache/style/{$theme_name}.{$css_latest}.{$css_md5}.css");
if (!file_exists($css_cache_file)) {
$css_data = "";
foreach ($css_files as $file) {
$file_data = file_get_contents($file);
$pattern = '/url[\s]*\([\s]*["\']?([^"\'\)]+)["\']?[\s]*\)/';
$replace = 'url("../../../' . dirname($file) . '/$1")';
$file_data = preg_replace($pattern, $replace, $file_data);
$css_data .= $file_data . "\n";
}
file_put_contents($css_cache_file, $css_data);
}
$this->add_html_header("<link rel='stylesheet' href='$data_href/$css_cache_file' type='text/css'>", 43);
/*** Generate JS cache files ***/
$js_latest = $config_latest;
$js_files = array_merge(
[
"vendor/bower-asset/jquery-timeago/jquery.timeago.js",
"vendor/bower-asset/js-cookie/src/js.cookie.js",
"ext/static_files/modernizr-3.3.1.custom.js",
],
zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/script.js"),
zglob("themes/$theme_name/script.js")
);
foreach ($js_files as $js) {
$js_latest = max($js_latest, filemtime($js));
}
$js_md5 = md5(serialize($js_files));
$js_cache_file = data_path("cache/script/{$theme_name}.{$js_latest}.{$js_md5}.js");
if (!file_exists($js_cache_file)) {
$js_data = "";
foreach ($js_files as $file) {
$js_data .= file_get_contents($file) . "\n";
}
file_put_contents($js_cache_file, $js_data);
}
$this->add_html_header("<script defer src='$data_href/$js_cache_file' type='text/javascript'></script>", 44);
}
protected function get_nav_links()
{
$pnbe = send_event(new PageNavBuildingEvent());
$nav_links = $pnbe->links;
$active_link = null;
// To save on event calls, we check if one of the top-level links has already been marked as active
foreach ($nav_links as $link) {
if ($link->active===true) {
$active_link = $link;
break;
}
}
$sub_links = null;
// If one is, we just query for sub-menu options under that one tab
if ($active_link!==null) {
$psnbe = send_event(new PageSubNavBuildingEvent($active_link->name));
$sub_links = $psnbe->links;
} else {
// Otherwise we query for the sub-items under each of the tabs
foreach ($nav_links as $link) {
$psnbe = send_event(new PageSubNavBuildingEvent($link->name));
// Now we check for a current link so we can identify the sub-links to show
foreach ($psnbe->links as $sub_link) {
if ($sub_link->active===true) {
$sub_links = $psnbe->links;
break;
}
}
// If the active link has been detected, we break out
if ($sub_links!==null) {
$link->active = true;
break;
}
}
}
$sub_links = $sub_links??[];
usort($nav_links, "sort_nav_links");
usort($sub_links, "sort_nav_links");
return [$nav_links, $sub_links];
}
/**
* turns the Page into HTML
*/
public function render()
{
$head_html = $this->head_html();
$body_html = $this->body_html();
print <<<EOD
<!doctype html>
<html class="no-js" lang="en">
$head_html
$body_html
</html>
EOD;
}
protected function head_html(): string
{
$html_header_html = $this->get_all_html_headers();
return "
<head>
<title>{$this->title}</title>
$html_header_html
</head>
";
}
protected function body_html(): string
{
$left_block_html = "";
$main_block_html = "";
$sub_block_html = "";
foreach ($this->blocks as $block) {
switch ($block->section) {
case "left":
$left_block_html .= $block->get_html(true);
break;
case "main":
$main_block_html .= $block->get_html(false);
break;
case "subheading":
$sub_block_html .= $block->get_html(false);
break;
default:
print "<p>error: {$block->header} using an unknown section ({$block->section})";
break;
}
}
$wrapper = "";
if (strlen($this->heading) > 100) {
$wrapper = ' style="height: 3em; overflow: auto;"';
}
$footer_html = $this->footer_html();
$flash_html = $this->flash ? "<b id='flash'>".nl2br(html_escape(implode("\n", $this->flash)))."</b>" : "";
return "
<body>
<header>
<h1$wrapper>{$this->heading}</h1>
$sub_block_html
</header>
<nav>
$left_block_html
</nav>
<article>
$flash_html
$main_block_html
</article>
<footer>
$footer_html
</footer>
</body>
";
}
protected function footer_html(): string
{
$debug = get_debug_info();
$contact_link = contact_link();
$contact = empty($contact_link) ? "" : "<br><a href='$contact_link'>Contact</a>";
return "
Images &copy; their respective owners,
<a href=\"https://code.shishnet.org/shimmie2/\">Shimmie</a> &copy;
<a href=\"https://www.shishnet.org/\">Shish</a> &amp;
<a href=\"https://github.com/shish/shimmie2/graphs/contributors\">The Team</a>
2007-2020,
based on the Danbooru concept.
$debug
$contact
";
}
}
class PageNavBuildingEvent extends Event
{
public $links = [];
public function add_nav_link(string $name, Link $link, string $desc, ?bool $active = null, int $order = 50)
{
$this->links[] = new NavLink($name, $link, $desc, $active, $order);
}
}
class PageSubNavBuildingEvent extends Event
{
public $parent;
public $links = [];
public function __construct(string $parent)
{
parent::__construct();
$this->parent= $parent;
}
public function add_nav_link(string $name, Link $link, string $desc, ?bool $active = null, int $order = 50)
{
$this->links[] = new NavLink($name, $link, $desc, $active, $order);
}
}
class NavLink
{
public $name;
public $link;
public $description;
public $order;
public $active = false;
public function __construct(String $name, Link $link, String $description, ?bool $active = null, int $order = 50)
{
global $config;
$this->name = $name;
$this->link = $link;
$this->description = $description;
$this->order = $order;
if ($active==null) {
$query = ltrim(_get_query(), "/");
if ($query === "") {
// This indicates the front page, so we check what's set as the front page
$front_page = trim($config->get_string(SetupConfig::FRONT_PAGE), "/");
if ($front_page === $link->page) {
$this->active = true;
} else {
$this->active = self::is_active([$link->page], $front_page);
}
} elseif ($query===$link->page) {
$this->active = true;
} else {
$this->active = self::is_active([$link->page]);
}
} else {
$this->active = $active;
}
}
public static function is_active(array $pages_matched, string $url = null): bool
{
/**
* Woo! We can actually SEE THE CURRENT PAGE!! (well... see it highlighted in the menu.)
*/
$url = $url??ltrim(_get_query(), "/");
$re1='.*?';
$re2='((?:[a-z][a-z_]+))';
if (preg_match_all("/".$re1.$re2."/is", $url, $matches)) {
$url=$matches[1][0];
}
$count_pages_matched = count($pages_matched);
for ($i=0; $i < $count_pages_matched; $i++) {
if ($url == $pages_matched[$i]) {
return true;
}
}
return false;
}
}
function sort_nav_links(NavLink $a, NavLink $b)
{
return $a->order - $b->order;
}

104
core/basethemelet.class.php Normal file
View File

@ -0,0 +1,104 @@
<?php
/**
* A collection of common functions for theme parts
*/
class BaseThemelet {
/**
* Generic error message display
*/
public function display_error(/*int*/ $code, /*string*/ $title, /*string*/ $message) {
global $page;
$page->add_http_header("HTTP/1.0 $code $title");
$page->set_title($title);
$page->set_heading($title);
$page->add_block(new NavBlock());
$page->add_block(new Block("Error", $message));
}
/**
* A specific, common error message
*/
public function display_permission_denied() {
$this->display_error(403, "Permission Denied", "You do not have permission to access this page");
}
/**
* Generic thumbnail code; returns HTML rather than adding
* a block since thumbs tend to go inside blocks...
*/
public function build_thumb_html(Image $image, $query=null) {
global $config;
$i_id = (int) $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());
$h_tags = strtolower($image->get_tag_list());
$base = get_base_href();
// If file is flash or svg then sets thumbnail to max size.
if($image->ext === 'swf' || $image->ext === 'svg'){
$tsize = get_thumbnail_size($config->get_int('thumb_width'), $config->get_int('thumb_height'));
}
else{
$tsize = get_thumbnail_size($image->width, $image->height);
}
return '<a href="'.$h_view_link.'" class="thumb" data-tags="'.$h_tags.'">'.
'<img id="thumb_'.$i_id.'" title="'.$h_tip.'" alt="'.$h_tip.'" height="'.$tsize[1].'" width="'.$tsize[0].'" class="lazy" data-original="'.$h_thumb_link.'" src="'.$base.'/lib/static/grey.gif">'.
'<noscript><img id="thumb_'.$i_id.'" title="'.$h_tip.'" alt="'.$h_tip.'" height="'.$tsize[1].'" width="'.$tsize[0].'" src="'.$h_thumb_link.'"></noscript>'.
"</a>\n";
}
/**
* Add a generic paginator
*/
public function display_paginator(Page $page, $base, $query, $page_number, $total_pages) {
if($total_pages == 0) $total_pages = 1;
$body = $this->build_paginator($page_number, $total_pages, $base, $query);
$page->add_block(new Block(null, $body, "main", 90, "paginator"));
}
private function gen_page_link($base_url, $query, $page, $name) {
$link = make_link($base_url.'/'.$page, $query);
return '<a href="'.$link.'">'.$name.'</a>';
}
private function gen_page_link_block($base_url, $query, $page, $current_page, $name) {
$paginator = "";
if($page == $current_page) $paginator .= "<b>";
$paginator .= $this->gen_page_link($base_url, $query, $page, $name);
if($page == $current_page) $paginator .= "</b>";
return $paginator;
}
private function build_paginator($current_page, $total_pages, $base_url, $query) {
$next = $current_page + 1;
$prev = $current_page - 1;
$rand = rand(1, $total_pages);
$at_start = ($current_page <= 1 || $total_pages <= 1);
$at_end = ($current_page >= $total_pages);
$first_html = $at_start ? "First" : $this->gen_page_link($base_url, $query, 1, "First");
$prev_html = $at_start ? "Prev" : $this->gen_page_link($base_url, $query, $prev, "Prev");
$random_html = $this->gen_page_link($base_url, $query, $rand, "Random");
$next_html = $at_end ? "Next" : $this->gen_page_link($base_url, $query, $next, "Next");
$last_html = $at_end ? "Last" : $this->gen_page_link($base_url, $query, $total_pages, "Last");
$start = $current_page-5 > 1 ? $current_page-5 : 1;
$end = $start+10 < $total_pages ? $start+10 : $total_pages;
$pages = array();
foreach(range($start, $end) as $i) {
$pages[] = $this->gen_page_link_block($base_url, $query, $i, $current_page, $i);
}
$pages_html = implode(" | ", $pages);
return $first_html.' | '.$prev_html.' | '.$random_html.' | '.$next_html.' | '.$last_html
.'<br>&lt;&lt; '.$pages_html.' &gt;&gt;';
}
}
?>

View File

@ -1,149 +0,0 @@
<?php declare(strict_types=1);
/**
* Class BaseThemelet
*
* A collection of common functions for theme parts
*/
class BaseThemelet
{
/**
* Generic error message display
*/
public function display_error(int $code, string $title, string $message): void
{
global $page;
$page->set_code($code);
$page->set_title($title);
$page->set_heading($title);
$has_nav = false;
foreach ($page->blocks as $block) {
if ($block->header == "Navigation") {
$has_nav = true;
break;
}
}
if (!$has_nav) {
$page->add_block(new NavBlock());
}
$page->add_block(new Block("Error", $message));
}
/**
* A specific, common error message
*/
public function display_permission_denied(): void
{
$this->display_error(403, "Permission Denied", "You do not have permission to access this page");
}
/**
* Generic thumbnail code; returns HTML rather than adding
* a block since thumbs tend to go inside blocks...
*/
public function build_thumb_html(Image $image): string
{
global $config;
$i_id = (int) $image->id;
$h_view_link = make_link('post/view/'.$i_id);
$h_thumb_link = $image->get_thumb_link();
$h_tip = html_escape($image->get_tooltip());
$h_tags = html_escape(strtolower($image->get_tag_list()));
$extArr = array_flip([EXTENSION_FLASH, EXTENSION_SVG, EXTENSION_MP3]); //List of thumbless filetypes
if (!isset($extArr[$image->ext])) {
$tsize = get_thumbnail_size($image->width, $image->height);
} else {
//Use max thumbnail size if using thumbless filetype
$tsize = get_thumbnail_size($config->get_int(ImageConfig::THUMB_WIDTH), $config->get_int(ImageConfig::THUMB_WIDTH));
}
$custom_classes = "";
if (class_exists("Relationships")) {
if (property_exists($image, 'parent_id') && $image->parent_id !== null) {
$custom_classes .= "shm-thumb-has_parent ";
}
if (property_exists($image, 'has_children') && bool_escape($image->has_children)) {
$custom_classes .= "shm-thumb-has_child ";
}
}
return "<a href='$h_view_link' class='thumb shm-thumb shm-thumb-link {$custom_classes}' data-tags='$h_tags' data-post-id='$i_id'>".
"<img id='thumb_$i_id' title='$h_tip' alt='$h_tip' height='{$tsize[1]}' width='{$tsize[0]}' src='$h_thumb_link'>".
"</a>\n";
}
public function display_paginator(Page $page, string $base, ?string $query, int $page_number, int $total_pages, bool $show_random = false)
{
if ($total_pages == 0) {
$total_pages = 1;
}
$body = $this->build_paginator($page_number, $total_pages, $base, $query, $show_random);
$page->add_block(new Block(null, $body, "main", 90, "paginator"));
$page->add_html_header("<link rel='first' href='".make_http(make_link($base.'/1', $query))."'>");
if ($page_number < $total_pages) {
$page->add_html_header("<link rel='prefetch' href='".make_http(make_link($base.'/'.($page_number+1), $query))."'>");
$page->add_html_header("<link rel='next' href='".make_http(make_link($base.'/'.($page_number+1), $query))."'>");
}
if ($page_number > 1) {
$page->add_html_header("<link rel='previous' href='".make_http(make_link($base.'/'.($page_number-1), $query))."'>");
}
$page->add_html_header("<link rel='last' href='".make_http(make_link($base.'/'.$total_pages, $query))."'>");
}
private function gen_page_link(string $base_url, ?string $query, int $page, string $name): string
{
$link = make_link($base_url.'/'.$page, $query);
return '<a href="'.$link.'">'.$name.'</a>';
}
private function gen_page_link_block(string $base_url, ?string $query, int $page, int $current_page, string $name): string
{
$paginator = "";
if ($page == $current_page) {
$paginator .= "<b>";
}
$paginator .= $this->gen_page_link($base_url, $query, $page, $name);
if ($page == $current_page) {
$paginator .= "</b>";
}
return $paginator;
}
private function build_paginator(int $current_page, int $total_pages, string $base_url, ?string $query, bool $show_random): string
{
$next = $current_page + 1;
$prev = $current_page - 1;
$at_start = ($current_page <= 1 || $total_pages <= 1);
$at_end = ($current_page >= $total_pages);
$first_html = $at_start ? "First" : $this->gen_page_link($base_url, $query, 1, "First");
$prev_html = $at_start ? "Prev" : $this->gen_page_link($base_url, $query, $prev, "Prev");
$random_html = "-";
if ($show_random) {
$rand = mt_rand(1, $total_pages);
$random_html = $this->gen_page_link($base_url, $query, $rand, "Random");
}
$next_html = $at_end ? "Next" : $this->gen_page_link($base_url, $query, $next, "Next");
$last_html = $at_end ? "Last" : $this->gen_page_link($base_url, $query, $total_pages, "Last");
$start = $current_page-5 > 1 ? $current_page-5 : 1;
$end = $start+10 < $total_pages ? $start+10 : $total_pages;
$pages = [];
foreach (range($start, $end) as $i) {
$pages[] = $this->gen_page_link_block($base_url, $query, $i, $current_page, (string)$i);
}
$pages_html = implode(" | ", $pages);
return $first_html.' | '.$prev_html.' | '.$random_html.' | '.$next_html.' | '.$last_html
.'<br>&lt;&lt; '.$pages_html.' &gt;&gt;';
}
}

74
core/block.class.php Normal file
View File

@ -0,0 +1,74 @@
<?php
/**
* A basic chunk of a page
*/
class Block {
/**
* The block's title
*
* @retval string
*/
var $header;
/**
* The content
*
* @retval string
*/
var $body;
/**
* Where the block should be placed. The default theme supports
* "main" and "left", other themes can add their own areas
*
* @retval string
*/
var $section;
/**
* How far down the section the block should appear, higher
* numbers appear lower. The scale is 0-100 by convention,
* though any number or string will work.
*
* @retval int
*/
var $position;
/**
*
*/
var $id;
public function __construct($header, $body, /*string*/ $section="main", /*int*/ $position=50, $id=null) {
$this->header = $header;
$this->body = $body;
$this->section = $section;
$this->position = $position;
$this->id = str_replace(' ', '_', is_null($id) ? (is_null($header) ? md5($body) : $header) . $section : $id);
}
public function get_html($hidable=false) {
$h = $this->header;
$b = $this->body;
$i = $this->id;
$html = "<section id='$i'>";
$h_toggler = $hidable ? " shm-toggler" : "";
if(!empty($h)) $html .= "<h3 data-toggle-sel='#$i' class='$h_toggler'>$h</h3>";
if(!empty($b)) $html .= "<div class='blockbody'>$b</div>";
$html .= "</section>\n";
return $html;
}
}
/**
* A generic navigation block with a link to the main page.
*
* Used because "new NavBlock()" is easier than "new Block('Navigation', ..."
*/
class NavBlock extends Block {
public function __construct() {
parent::__construct("Navigation", "<a href='".make_link()."'>Index</a>", "left", 0);
}
}
?>

View File

@ -1,105 +0,0 @@
<?php declare(strict_types=1);
/**
* Class Block
*
* A basic chunk of a page.
*/
class Block
{
/**
* The block's title.
*
* @var string
*/
public $header;
/**
* The content of the block.
*
* @var string
*/
public $body;
/**
* Where the block should be placed. The default theme supports
* "main" and "left", other themes can add their own areas.
*
* @var string
*/
public $section;
/**
* How far down the section the block should appear, higher
* numbers appear lower. The scale is 0-100 by convention,
* though any number will work.
*
* @var int
*/
public $position;
/**
* A unique ID for the block.
*
* @var string
*/
public $id;
/**
* Should this block count as content for the sake of
* the 404 handler
*
* @var boolean
*/
public $is_content = true;
public function __construct(string $header=null, string $body=null, string $section="main", int $position=50, string $id=null)
{
$this->header = $header;
$this->body = $body;
$this->section = $section;
$this->position = $position;
if (is_null($id)) {
$id = (empty($header) ? md5($body ?? '') : $header) . $section;
}
$this->id = preg_replace('/[^\w-]/', '', str_replace(' ', '_', $id));
}
/**
* Get the HTML for this block.
*/
public function get_html(bool $hidable=false): string
{
$h = $this->header;
$b = $this->body;
$i = $this->id;
$html = "<section id='$i'>";
$h_toggler = $hidable ? " shm-toggler" : "";
if (!empty($h)) {
$html .= "<h3 data-toggle-sel='#$i' class='$h_toggler'>$h</h3>";
}
if (!empty($b)) {
$html .= "<div class='blockbody'>$b</div>";
}
$html .= "</section>\n";
return $html;
}
}
/**
* Class NavBlock
*
* A generic navigation block with a link to the main page.
*
* Used because "new NavBlock()" is easier than "new Block('Navigation', ..."
*
*/
class NavBlock extends Block
{
public function __construct()
{
parent::__construct("Navigation", "<a href='".make_link()."'>Index</a>", "left", 0);
}
}

View File

@ -1,199 +0,0 @@
<?php declare(strict_types=1);
interface CacheEngine
{
public function get(string $key);
public function set(string $key, $val, int $time=0);
public function delete(string $key);
}
class NoCache implements CacheEngine
{
public function get(string $key)
{
return false;
}
public function set(string $key, $val, int $time=0)
{
}
public function delete(string $key)
{
}
}
class MemcachedCache implements CacheEngine
{
/** @var ?Memcached */
public $memcache=null;
public function __construct(string $args)
{
$hp = explode(":", $args);
$this->memcache = new Memcached;
#$this->memcache->setOption(Memcached::OPT_COMPRESSION, False);
#$this->memcache->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP);
#$this->memcache->setOption(Memcached::OPT_PREFIX_KEY, phpversion());
$this->memcache->addServer($hp[0], (int)$hp[1]);
}
public function get(string $key)
{
$key = urlencode($key);
$val = $this->memcache->get($key);
$res = $this->memcache->getResultCode();
if ($res == Memcached::RES_SUCCESS) {
return $val;
} elseif ($res == Memcached::RES_NOTFOUND) {
return false;
} else {
error_log("Memcached error during get($key): $res");
return false;
}
}
public function set(string $key, $val, int $time=0)
{
$key = urlencode($key);
$this->memcache->set($key, $val, $time);
$res = $this->memcache->getResultCode();
if ($res != Memcached::RES_SUCCESS) {
error_log("Memcached error during set($key): $res");
}
}
public function delete(string $key)
{
$key = urlencode($key);
$this->memcache->delete($key);
$res = $this->memcache->getResultCode();
if ($res != Memcached::RES_SUCCESS && $res != Memcached::RES_NOTFOUND) {
error_log("Memcached error during delete($key): $res");
}
}
}
class APCCache implements CacheEngine
{
public function __construct(string $args)
{
// $args is not used, but is passed in when APC cache is created.
}
public function get(string $key)
{
return apc_fetch($key);
}
public function set(string $key, $val, int $time=0)
{
apc_store($key, $val, $time);
}
public function delete(string $key)
{
apc_delete($key);
}
}
class RedisCache implements CacheEngine
{
private $redis=null;
public function __construct(string $args)
{
$this->redis = new Redis();
$hp = explode(":", $args);
$this->redis->pconnect($hp[0], (int)$hp[1]);
$this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
$this->redis->setOption(Redis::OPT_PREFIX, 'shm:');
}
public function get(string $key)
{
return $this->redis->get($key);
}
public function set(string $key, $val, int $time=0)
{
if ($time > 0) {
$this->redis->setEx($key, $time, $val);
} else {
$this->redis->set($key, $val);
}
}
public function delete(string $key)
{
$this->redis->del($key);
}
}
class Cache
{
public $engine;
public $hits=0;
public $misses=0;
public $time=0;
public function __construct(?string $dsn)
{
$matches = [];
$c = null;
if ($dsn && preg_match("#(.*)://(.*)#", $dsn, $matches) && !isset($_GET['DISABLE_CACHE'])) {
if ($matches[1] == "memcached") {
$c = new MemcachedCache($matches[2]);
} elseif ($matches[1] == "apc") {
$c = new APCCache($matches[2]);
} elseif ($matches[1] == "redis") {
$c = new RedisCache($matches[2]);
}
} else {
$c = new NoCache();
}
$this->engine = $c;
}
public function get(string $key)
{
global $_tracer;
$_tracer->begin("Cache Query", ["key"=>$key]);
$val = $this->engine->get($key);
if ($val !== false) {
$res = "hit";
$this->hits++;
} else {
$res = "miss";
$this->misses++;
}
$_tracer->end(null, ["result"=>$res]);
return $val;
}
public function set(string $key, $val, int $time=0)
{
global $_tracer;
$_tracer->begin("Cache Set", ["key"=>$key, "time"=>$time]);
$this->engine->set($key, $val, $time);
$_tracer->end();
}
public function delete(string $key)
{
global $_tracer;
$_tracer->begin("Cache Delete", ["key"=>$key]);
$this->engine->delete($key);
$_tracer->end();
}
public function get_hits(): int
{
return $this->hits;
}
public function get_misses(): int
{
return $this->misses;
}
}

View File

@ -1,60 +0,0 @@
<?php declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* CAPTCHA abstraction *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
use ReCaptcha\ReCaptcha;
function captcha_get_html(): string
{
global $config, $user;
if (DEBUG && ip_in_range($_SERVER['REMOTE_ADDR'], "127.0.0.0/8")) {
return "";
}
$captcha = "";
if ($user->is_anonymous() && $config->get_bool("comment_captcha")) {
$r_publickey = $config->get_string("api_recaptcha_pubkey");
if (!empty($r_publickey)) {
$captcha = "
<div class=\"g-recaptcha\" data-sitekey=\"{$r_publickey}\"></div>
<script type=\"text/javascript\" src=\"https://www.google.com/recaptcha/api.js\"></script>";
} else {
session_start();
$captcha = Securimage::getCaptchaHtml(['securimage_path' => './vendor/dapphp/securimage/']);
}
}
return $captcha;
}
function captcha_check(): bool
{
global $config, $user;
if (DEBUG && ip_in_range($_SERVER['REMOTE_ADDR'], "127.0.0.0/8")) {
return true;
}
if ($user->is_anonymous() && $config->get_bool("comment_captcha")) {
$r_privatekey = $config->get_string('api_recaptcha_privkey');
if (!empty($r_privatekey)) {
$recaptcha = new ReCaptcha($r_privatekey);
$resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']);
if (!$resp->isSuccess()) {
log_info("core", "Captcha failed (ReCaptcha): " . implode("", $resp->getErrorCodes()));
return false;
}
} else {
session_start();
$securimg = new Securimage();
if ($securimg->check($_POST['captcha_code']) === false) {
log_info("core", "Captcha failed (Securimage)");
return false;
}
}
}
return true;
}

203
core/config.class.php Normal file
View File

@ -0,0 +1,203 @@
<?php
/**
* an abstract interface for altering a name:value pair list
*/
interface Config {
/**
* Save the list of name:value pairs to wherever they came from,
* so that the next time a page is loaded it will use the new
* configuration
*/
public function save(/*string*/ $name=null);
/** @name set_*
* Set a configuration option to a new value, regardless
* of what the value is at the moment
*/
//@{
public function set_int(/*string*/ $name, $value);
public function set_string(/*string*/ $name, $value);
public function set_bool(/*string*/ $name, $value);
public function set_array(/*string*/ $name, $value);
//@}
/** @name set_default_*
* Set a configuration option to a new value, if there is no
* value currently. Extensions should generally call these
* from their InitExtEvent handlers. This has the advantage
* that the values will show up in the "advanced" setup page
* where they can be modified, while calling get_* with a
* "default" paramater won't show up.
*/
//@{
public function set_default_int(/*string*/ $name, $value);
public function set_default_string(/*string*/ $name, $value);
public function set_default_bool(/*string*/ $name, $value);
public function set_default_array(/*string*/ $name, $value);
//@}
/** @name get_*
* pick a value out of the table by name, cast to the
* appropritate data type
*/
//@{
public function get_int(/*string*/ $name, $default=null);
public function get_string(/*string*/ $name, $default=null);
public function get_bool(/*string*/ $name, $default=null);
public function get_array(/*string*/ $name, $default=array());
//@}
}
/**
* Common methods for manipulating the list, loading and saving is
* left to the concrete implementation
*/
abstract class BaseConfig implements Config {
var $values = array();
public function set_int(/*string*/ $name, $value) {
$this->values[$name] = parse_shorthand_int($value);
$this->save($name);
}
public function set_string(/*string*/ $name, $value) {
$this->values[$name] = $value;
$this->save($name);
}
public function set_bool(/*string*/ $name, $value) {
$this->values[$name] = (($value == 'on' || $value === true) ? 'Y' : 'N');
$this->save($name);
}
public function set_array(/*string*/ $name, $value) {
assert(is_array($value));
$this->values[$name] = implode(",", $value);
$this->save($name);
}
public function set_default_int(/*string*/ $name, $value) {
if(is_null($this->get($name))) {
$this->values[$name] = parse_shorthand_int($value);
}
}
public function set_default_string(/*string*/ $name, $value) {
if(is_null($this->get($name))) {
$this->values[$name] = $value;
}
}
public function set_default_bool(/*string*/ $name, $value) {
if(is_null($this->get($name))) {
$this->values[$name] = (($value == 'on' || $value === true) ? 'Y' : 'N');
}
}
public function set_default_array(/*string*/ $name, $value) {
assert(is_array($value));
if(is_null($this->get($name))) {
$this->values[$name] = implode(",", $value);
}
}
public function get_int(/*string*/ $name, $default=null) {
return (int)($this->get($name, $default));
}
public function get_string(/*string*/ $name, $default=null) {
return $this->get($name, $default);
}
public function get_bool(/*string*/ $name, $default=null) {
return bool_escape($this->get($name, $default));
}
public function get_array(/*string*/ $name, $default=array()) {
return explode(",", $this->get($name, ""));
}
private function get(/*string*/ $name, $default=null) {
if(isset($this->values[$name])) {
return $this->values[$name];
}
else {
return $default;
}
}
}
/**
* Loads the config list from a PHP file; the file should be in the format:
*
* <?php
* $config['foo'] = "bar";
* $config['baz'] = "qux";
* ?>
*/
class StaticConfig extends BaseConfig {
public function __construct($filename) {
if(file_exists($filename)) {
require_once $filename;
if(isset($config)) {
$this->values = $config;
}
else {
throw new Exception("Config file '$filename' doesn't contain any config");
}
}
else {
throw new Exception("Config file '$filename' missing");
}
}
public function save(/*string*/ $name=null) {
// static config is static
}
}
/**
* Loads the config list from a table in a given database, the table should
* be called config and have the schema:
*
* \code
* CREATE TABLE config(
* name VARCHAR(255) NOT NULL,
* value TEXT
* );
* \endcode
*/
class DatabaseConfig extends BaseConfig {
var $database = null;
/*
* Load the config table from a database
*/
public function DatabaseConfig(Database $database) {
$this->database = $database;
$cached = $this->database->cache->get("config");
if($cached) {
$this->values = $cached;
}
else {
$this->values = array();
foreach($this->database->get_all("SELECT name, value FROM config") as $row) {
$this->values[$row["name"]] = $row["value"];
}
$this->database->cache->set("config", $this->values);
}
}
/*
* Save the current values as the new config table
*/
public function save(/*string*/ $name=null) {
if(is_null($name)) {
reset($this->values); // rewind the array to the first element
foreach($this->values as $name => $value) {
$this->save(/*string*/ $name);
}
}
else {
$this->database->Execute("DELETE FROM config WHERE name = :name", array("name"=>$name));
$this->database->Execute("INSERT INTO config VALUES (:name, :value)", array("name"=>$name, "value"=>$this->values[$name]));
}
$this->database->cache->delete("config");
}
}
?>

View File

@ -1,338 +0,0 @@
<?php declare(strict_types=1);
/**
* Interface Config
*
* An abstract interface for altering a name:value pair list.
*/
interface Config
{
/**
* Save the list of name:value pairs to wherever they came from,
* so that the next time a page is loaded it will use the new
* configuration.
*/
public function save(string $name=null): void;
//@{ /*--------------------------------- SET ------------------------------------------------------*/
/**
* Set a configuration option to a new value, regardless of what the value is at the moment.
*/
public function set_int(string $name, ?int $value): void;
/**
* Set a configuration option to a new value, regardless of what the value is at the moment.
*/
public function set_float(string $name, ?float $value): void;
/**
* Set a configuration option to a new value, regardless of what the value is at the moment.
*/
public function set_string(string $name, ?string $value): void;
/**
* Set a configuration option to a new value, regardless of what the value is at the moment.
*/
public function set_bool(string $name, ?bool $value): void;
/**
* Set a configuration option to a new value, regardless of what the value is at the moment.
*/
public function set_array(string $name, array $value): void;
//@} /*--------------------------------------------------------------------------------------------*/
//@{ /*-------------------------------- SET DEFAULT -----------------------------------------------*/
/**
* Set a configuration option to a new value, if there is no value currently.
*
* Extensions should generally call these from their InitExtEvent handlers.
* This has the advantage that the values will show up in the "advanced" setup
* page where they can be modified, while calling get_* with a "default"
* parameter won't show up.
*/
public function set_default_int(string $name, int $value): void;
/**
* Set a configuration option to a new value, if there is no value currently.
*
* Extensions should generally call these from their InitExtEvent handlers.
* This has the advantage that the values will show up in the "advanced" setup
* page where they can be modified, while calling get_* with a "default"
* parameter won't show up.
*/
public function set_default_float(string $name, float $value): void;
/**
* Set a configuration option to a new value, if there is no value currently.
*
* Extensions should generally call these from their InitExtEvent handlers.
* This has the advantage that the values will show up in the "advanced" setup
* page where they can be modified, while calling get_* with a "default"
* parameter won't show up.
*/
public function set_default_string(string $name, string $value): void;
/**
* Set a configuration option to a new value, if there is no value currently.
*
* Extensions should generally call these from their InitExtEvent handlers.
* This has the advantage that the values will show up in the "advanced" setup
* page where they can be modified, while calling get_* with a "default"
* parameter won't show up.
*/
public function set_default_bool(string $name, bool $value): void;
/**
* Set a configuration option to a new value, if there is no value currently.
*
* Extensions should generally call these from their InitExtEvent handlers.
* This has the advantage that the values will show up in the "advanced" setup
* page where they can be modified, while calling get_* with a "default"
* parameter won't show up.
*/
public function set_default_array(string $name, array $value): void;
//@} /*--------------------------------------------------------------------------------------------*/
//@{ /*--------------------------------- GET ------------------------------------------------------*/
/**
* Pick a value out of the table by name, cast to the appropriate data type.
*/
public function get_int(string $name, ?int $default=null): ?int;
/**
* Pick a value out of the table by name, cast to the appropriate data type.
*/
public function get_float(string $name, ?float $default=null): ?float;
/**
* Pick a value out of the table by name, cast to the appropriate data type.
*/
public function get_string(string $name, ?string $default=null): ?string;
/**
* Pick a value out of the table by name, cast to the appropriate data type.
*/
public function get_bool(string $name, ?bool $default=null): ?bool;
/**
* Pick a value out of the table by name, cast to the appropriate data type.
*/
public function get_array(string $name, ?array $default=[]): ?array;
//@} /*--------------------------------------------------------------------------------------------*/
}
/**
* Class BaseConfig
*
* Common methods for manipulating the list, loading and saving is
* left to the concrete implementation
*/
abstract class BaseConfig implements Config
{
public $values = [];
public function set_int(string $name, ?int $value): void
{
$this->values[$name] = is_null($value) ? null : $value;
$this->save($name);
}
public function set_float(string $name, ?float $value): void
{
$this->values[$name] = $value;
$this->save($name);
}
public function set_string(string $name, ?string $value): void
{
$this->values[$name] = $value;
$this->save($name);
}
public function set_bool(string $name, ?bool $value): void
{
$this->values[$name] = $value ? 'Y' : 'N';
$this->save($name);
}
public function set_array(string $name, ?array $value): void
{
if ($value!=null) {
$this->values[$name] = implode(",", $value);
} else {
$this->values[$name] = null;
}
$this->save($name);
}
public function set_default_int(string $name, int $value): void
{
if (is_null($this->get($name))) {
$this->values[$name] = $value;
}
}
public function set_default_float(string $name, float $value): void
{
if (is_null($this->get($name))) {
$this->values[$name] = $value;
}
}
public function set_default_string(string $name, string $value): void
{
if (is_null($this->get($name))) {
$this->values[$name] = $value;
}
}
public function set_default_bool(string $name, bool $value): void
{
if (is_null($this->get($name))) {
$this->values[$name] = $value ? 'Y' : 'N';
}
}
public function set_default_array(string $name, array $value): void
{
if (is_null($this->get($name))) {
$this->values[$name] = implode(",", $value);
}
}
public function get_int(string $name, ?int $default=null): ?int
{
return (int)($this->get($name, $default));
}
public function get_float(string $name, ?float $default=null): ?float
{
return (float)($this->get($name, $default));
}
public function get_string(string $name, ?string $default=null): ?string
{
$val = $this->get($name, $default);
if (!is_string($val) && !is_null($val)) {
throw new SCoreException("$name is not a string: $val");
}
return $val;
}
public function get_bool(string $name, ?bool $default=null): ?bool
{
return bool_escape($this->get($name, $default));
}
public function get_array(string $name, ?array $default=[]): ?array
{
return explode(",", $this->get($name, ""));
}
private function get(string $name, $default=null)
{
if (isset($this->values[$name])) {
return $this->values[$name];
} else {
return $default;
}
}
}
/**
* Class DatabaseConfig
*
* Loads the config list from a table in a given database, the table should
* be called config and have the schema:
*
* \code
* CREATE TABLE config(
* name VARCHAR(255) NOT NULL,
* value TEXT
* );
* \endcode
*/
class DatabaseConfig extends BaseConfig
{
/** @var Database */
private $database = null;
private $table_name;
private $sub_column;
private $sub_value;
public function __construct(
Database $database,
string $table_name = "config",
string $sub_column = null,
string $sub_value = null
) {
global $cache;
$this->database = $database;
$this->table_name = $table_name;
$this->sub_value = $sub_value;
$this->sub_column = $sub_column;
$cache_name = "config";
if (!empty($sub_value)) {
$cache_name .= "_".$sub_value;
}
$cached = $cache->get($cache_name);
if ($cached) {
$this->values = $cached;
} else {
$this->values = [];
$query = "SELECT name, value FROM {$this->table_name}";
$args = [];
if (!empty($sub_column)&&!empty($sub_value)) {
$query .= " WHERE $sub_column = :sub_value";
$args["sub_value"] = $sub_value;
}
foreach ($this->database->get_all($query, $args) as $row) {
$this->values[$row["name"]] = $row["value"];
}
$cache->set($cache_name, $this->values);
}
}
public function save(string $name=null): void
{
global $cache;
if (is_null($name)) {
reset($this->values); // rewind the array to the first element
foreach ($this->values as $name => $value) {
$this->save($name);
}
} else {
$query = "DELETE FROM {$this->table_name} WHERE name = :name";
$args = ["name"=>$name];
$cols = ["name","value"];
$params = [":name",":value"];
if (!empty($this->sub_column)&&!empty($this->sub_value)) {
$query .= " AND $this->sub_column = :sub_value";
$args["sub_value"] = $this->sub_value;
$cols[] = $this->sub_column;
$params[] = ":sub_value";
}
$this->database->Execute($query, $args);
$args["value"] =$this->values[$name];
$this->database->Execute(
"INSERT INTO {$this->table_name} (".join(",", $cols).") VALUES (".join(",", $params).")",
$args
);
}
// rather than deleting and having some other request(s) do a thundering
// herd of race-conditioned updates, just save the updated version once here
$cache->set("config", $this->values);
}
}

445
core/database.class.php Normal file
View File

@ -0,0 +1,445 @@
<?php
/** @privatesection */
// Querylet {{{
class Querylet {
var $sql;
var $variables;
public function Querylet($sql, $variables=array()) {
$this->sql = $sql;
$this->variables = $variables;
}
public function append($querylet) {
assert(!is_null($querylet));
$this->sql .= $querylet->sql;
$this->variables = array_merge($this->variables, $querylet->variables);
}
public function append_sql($sql) {
$this->sql .= $sql;
}
public function add_variable($var) {
$this->variables[] = $var;
}
}
class TagQuerylet {
var $tag;
var $positive;
public function TagQuerylet($tag, $positive) {
$this->tag = $tag;
$this->positive = $positive;
}
}
class ImgQuerylet {
var $qlet;
var $positive;
public function ImgQuerylet($qlet, $positive) {
$this->qlet = $qlet;
$this->positive = $positive;
}
}
// }}}
// {{{ db engines
class DBEngine {
var $name = null;
public function init($db) {}
public function scoreql_to_sql($scoreql) {
return $scoreql;
}
public function create_table_sql($name, $data) {
return 'CREATE TABLE '.$name.' ('.$data.')';
}
}
class MySQL extends DBEngine {
var $name = "mysql";
public function init($db) {
$db->query("SET NAMES utf8;");
}
public function scoreql_to_sql($data) {
$data = str_replace("SCORE_AIPK", "INTEGER PRIMARY KEY auto_increment", $data);
$data = str_replace("SCORE_INET", "CHAR(45)", $data);
$data = str_replace("SCORE_BOOL_Y", "'Y'", $data);
$data = str_replace("SCORE_BOOL_N", "'N'", $data);
$data = str_replace("SCORE_BOOL", "ENUM('Y', 'N')", $data);
$data = str_replace("SCORE_DATETIME", "DATETIME", $data);
$data = str_replace("SCORE_NOW", "\"1970-01-01\"", $data);
$data = str_replace("SCORE_STRNORM", "", $data);
$data = str_replace("SCORE_ILIKE", "LIKE", $data);
return $data;
}
public function create_table_sql($name, $data) {
$data = $this->scoreql_to_sql($data);
$ctes = "ENGINE=InnoDB DEFAULT CHARSET='utf8'";
return 'CREATE TABLE '.$name.' ('.$data.') '.$ctes;
}
}
class PostgreSQL extends DBEngine {
var $name = "pgsql";
public function init($db) {
$db->query("SET application_name TO 'shimmie [{$_SERVER['REMOTE_ADDR']}]';");
}
public function scoreql_to_sql($data) {
$data = str_replace("SCORE_AIPK", "SERIAL PRIMARY KEY", $data);
$data = str_replace("SCORE_INET", "INET", $data);
$data = str_replace("SCORE_BOOL_Y", "'t'", $data);
$data = str_replace("SCORE_BOOL_N", "'f'", $data);
$data = str_replace("SCORE_BOOL", "BOOL", $data);
$data = str_replace("SCORE_DATETIME", "TIMESTAMP", $data);
$data = str_replace("SCORE_NOW", "current_time", $data);
$data = str_replace("SCORE_STRNORM", "lower", $data);
$data = str_replace("SCORE_ILIKE", "ILIKE", $data);
return $data;
}
public function create_table_sql($name, $data) {
$data = $this->scoreql_to_sql($data);
return 'CREATE TABLE '.$name.' ('.$data.')';
}
}
// shimmie functions for export to sqlite
function _unix_timestamp($date) { return strtotime($date); }
function _now() { return date("Y-m-d h:i:s"); }
function _floor($a) { return floor($a); }
function _log($a, $b=null) {
if(is_null($b)) return log($a);
else return log($a, $b);
}
function _isnull($a) { return is_null($a); }
function _md5($a) { return md5($a); }
function _concat($a, $b) { return $a . $b; }
function _lower($a) { return strtolower($a); }
class SQLite extends DBEngine {
var $name = "sqlite";
public function init($db) {
ini_set('sqlite.assoc_case', 0);
$db->execute("PRAGMA foreign_keys = ON;");
@sqlite_create_function($db->_connectionID, 'UNIX_TIMESTAMP', '_unix_timestamp', 1);
@sqlite_create_function($db->_connectionID, 'now', '_now', 0);
@sqlite_create_function($db->_connectionID, 'floor', '_floor', 1);
@sqlite_create_function($db->_connectionID, 'log', '_log');
@sqlite_create_function($db->_connectionID, 'isnull', '_isnull', 1);
@sqlite_create_function($db->_connectionID, 'md5', '_md5', 1);
@sqlite_create_function($db->_connectionID, 'concat', '_concat', 2);
@sqlite_create_function($db->_connectionID, 'lower', '_lower', 1);
}
public function create_table_sql($name, $data) {
$data = str_replace("SCORE_AIPK", "INTEGER PRIMARY KEY", $data);
$data = str_replace("SCORE_INET", "VARCHAR(45)", $data);
$data = str_replace("SCORE_BOOL_Y", "'Y'", $data);
$data = str_replace("SCORE_BOOL_N", "'N'", $data);
$data = str_replace("SCORE_BOOL", "CHAR(1)", $data);
$data = str_replace("SCORE_NOW", "\"1970-01-01\"", $data);
$data = str_replace("SCORE_STRNORM", "", $data);
$data = str_replace("SCORE_ILIKE", "LIKE", $data);
$cols = array();
$extras = "";
foreach(explode(",", $data) as $bit) {
$matches = array();
if(preg_match("/INDEX\s*\((.*)\)/", $bit, $matches)) {
$col = $matches[1];
$extras .= 'CREATE INDEX '.$name.'_'.$col.' on '.$name($col).';';
}
else {
$cols[] = $bit;
}
}
$cols_redone = implode(", ", $cols);
return 'CREATE TABLE '.$name.' ('.$cols_redone.'); '.$extras;
}
}
// }}}
// {{{ cache engines
interface CacheEngine {
public function get($key);
public function set($key, $val, $time=0);
public function delete($key);
public function get_hits();
public function get_misses();
}
class NoCache implements CacheEngine {
public function get($key) {return false;}
public function set($key, $val, $time=0) {}
public function delete($key) {}
public function get_hits() {return 0;}
public function get_misses() {return 0;}
}
class MemcacheCache implements CacheEngine {
var $memcache=null, $hits=0, $misses=0;
public function __construct($args) {
$hp = explode(":", $args);
if(class_exists("Memcache")) {
$this->memcache = new Memcache;
@$this->memcache->pconnect($hp[0], $hp[1]);
}
}
public function get($key) {
assert(!is_null($key));
$val = $this->memcache->get($key);
if($val) {
$this->hits++;
return $val;
}
else {
$this->misses++;
return false;
}
}
public function set($key, $val, $time=0) {
assert(!is_null($key));
$this->memcache->set($key, $val, false, $time);
}
public function delete($key) {
assert(!is_null($key));
$this->memcache->delete($key);
}
public function get_hits() {return $this->hits;}
public function get_misses() {return $this->misses;}
}
class APCCache implements CacheEngine {
var $hits=0, $misses=0;
public function __construct($args) {}
public function get($key) {
assert(!is_null($key));
$val = apc_fetch($key);
if($val) {
$this->hits++;
return $val;
}
else {
$this->misses++;
return false;
}
}
public function set($key, $val, $time=0) {
assert(!is_null($key));
apc_store($key, $val, $time);
}
public function delete($key) {
assert(!is_null($key));
apc_delete($key);
}
public function get_hits() {return $this->hits;}
public function get_misses() {return $this->misses;}
}
// }}}
/** @publicsection */
/**
* A class for controlled database access
*/
class Database {
/**
* The PDO database connection object, for anyone who wants direct access
*/
var $db;
/**
* Meta info about the database engine
*/
var $engine = null;
/**
* The currently active cache engine
*/
var $cache = null;
/**
* Create a new database object using connection info
* stored in the config file
*/
public function Database() {
# FIXME: detect ADODB URI, automatically translate PDO DSN
/*
* Why does the abstraction layer act differently depending on the
* back-end? Because PHP is deliberately retarded.
*
* http://stackoverflow.com/questions/237367
*/
$matches = array(); $db_user=null; $db_pass=null;
if(preg_match("/user=([^;]*)/", DATABASE_DSN, $matches)) $db_user=$matches[1];
if(preg_match("/password=([^;]*)/", DATABASE_DSN, $matches)) $db_pass=$matches[1];
$db_params = array(
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
);
if(defined("HIPHOP")) $this->db = new PDO(DATABASE_DSN, $db_user, $db_pass);
else $this->db = new PDO(DATABASE_DSN, $db_user, $db_pass, $db_params);
$db_proto = $this->db->getAttribute(PDO::ATTR_DRIVER_NAME);
if($db_proto === "mysql") {
$this->engine = new MySQL();
}
else if($db_proto === "pgsql") {
$this->engine = new PostgreSQL();
}
else if($db_proto === "sqlite") {
$this->engine = new SQLite();
}
else {
die('Unknown PDO driver: '.$db_proto);
}
$matches = array();
if( defined("CACHE_DSN") && CACHE_DSN && preg_match("#(memcache|apc)://(.*)#", CACHE_DSN, $matches)) {
if($matches[1] == "memcache") {
$this->cache = new MemcacheCache($matches[2]);
}
else if($matches[1] == "apc") {
$this->cache = new APCCache($matches[2]);
}
}
else {
$this->cache = new NoCache();
}
$this->engine->init($this->db);
}
/**
* Execute an SQL query and return an PDO resultset
*/
public function execute($query, $args=array()) {
try {
_count_execs($this->db, $query, $args);
$stmt = $this->db->prepare($query);
if (!array_key_exists(0, $args)) {
foreach($args as $name=>$value) {
if(is_numeric($value)) {
$stmt->bindValue(':'.$name, $value, PDO::PARAM_INT);
}
else {
$stmt->bindValue(':'.$name, $value, PDO::PARAM_STR);
}
}
$stmt->execute();
}
else {
$stmt->execute($args);
}
return $stmt;
}
catch(PDOException $pdoe) {
throw new SCoreException($pdoe->getMessage()."<p><b>Query:</b> ".$query);
}
}
/**
* Execute an SQL query and return a 2D array
*/
public function get_all($query, $args=array()) {
return $this->execute($query, $args)->fetchAll();
}
/**
* Execute an SQL query and return a single row
*/
public function get_row($query, $args=array()) {
$row = $this->execute($query, $args)->fetch();
return $row ? $row : null;
}
/**
* Execute an SQL query and return the first column of each row
*/
public function get_col($query, $args=array()) {
$stmt = $this->execute($query, $args);
$res = array();
foreach($stmt as $row) {
$res[] = $row[0];
}
return $res;
}
/**
* Execute an SQL query and return the the first row => the second rown
*/
public function get_pairs($query, $args=array()) {
$stmt = $this->execute($query, $args);
$res = array();
foreach($stmt as $row) {
$res[$row[0]] = $row[1];
}
return $res;
}
/**
* Execute an SQL query and return a single value
*/
public function get_one($query, $args=array()) {
$row = $this->execute($query, $args)->fetch();
return $row[0];
}
/**
* get the ID of the last inserted row
*/
public function get_last_insert_id($seq) {
if($this->engine->name == "pgsql") {
return $this->db->lastInsertId($seq);
}
else {
return $this->db->lastInsertId();
}
}
/**
* Create a table from pseudo-SQL
*/
public function create_table($name, $data) {
$this->execute($this->engine->create_table_sql($name, $data));
}
/**
* Returns the number of tables present in the current database.
*/
public function count_tables() {
if($this->engine->name === "mysql") {
return count(
$this->get_all("SHOW TABLES")
);
} else if ($this->engine->name === "pgsql") {
return count(
$this->get_all("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
);
} else if ($this->engine->name === "sqlite") {
return count(
$this->get_all(".tables")
);
} else {
// Hard to find a universal way to do this...
return NULL;
}
}
}
?>

View File

@ -1,339 +0,0 @@
<?php declare(strict_types=1);
use FFSPHP\PDO;
abstract class DatabaseDriver
{
public const MYSQL = "mysql";
public const PGSQL = "pgsql";
public const SQLITE = "sqlite";
}
/**
* A class for controlled database access
*/
class Database
{
/** @var string */
private $dsn;
/**
* The PDO database connection object, for anyone who wants direct access.
* @var null|PDO
*/
private $db = null;
/**
* @var float
*/
public $dbtime = 0.0;
/**
* Meta info about the database engine.
* @var DBEngine|null
*/
private $engine = null;
/**
* A boolean flag to track if we already have an active transaction.
* (ie: True if beginTransaction() already called)
*
* @var bool
*/
public $transaction = false;
/**
* How many queries this DB object has run
*/
public $query_count = 0;
public function __construct(string $dsn)
{
$this->dsn = $dsn;
}
private function connect_db(): void
{
$this->db = new PDO($this->dsn, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$this->connect_engine();
$this->engine->init($this->db);
$this->begin_transaction();
}
private function connect_engine(): void
{
if (preg_match("/^([^:]*)/", $this->dsn, $matches)) {
$db_proto=$matches[1];
} else {
throw new SCoreException("Can't figure out database engine");
}
if ($db_proto === DatabaseDriver::MYSQL) {
$this->engine = new MySQL();
} elseif ($db_proto === DatabaseDriver::PGSQL) {
$this->engine = new PostgreSQL();
} elseif ($db_proto === DatabaseDriver::SQLITE) {
$this->engine = new SQLite();
} else {
die_nicely(
'Unknown PDO driver: '.$db_proto,
"Please check that this is a valid driver, installing the PHP modules if needed"
);
}
}
public function begin_transaction(): void
{
if ($this->transaction === false) {
$this->db->beginTransaction();
$this->transaction = true;
}
}
public function is_transaction_open(): bool
{
return !is_null($this->db) && $this->transaction === true;
}
public function commit(): bool
{
if ($this->is_transaction_open()) {
$this->transaction = false;
return $this->db->commit();
} else {
throw new SCoreException("Unable to call commit() as there is no transaction currently open.");
}
}
public function rollback(): bool
{
if ($this->is_transaction_open()) {
$this->transaction = false;
return $this->db->rollback();
} else {
throw new SCoreException("Unable to call rollback() as there is no transaction currently open.");
}
}
public function scoreql_to_sql(string $input): string
{
if (is_null($this->engine)) {
$this->connect_engine();
}
return $this->engine->scoreql_to_sql($input);
}
public function scoresql_value_prepare($input)
{
if (is_null($this->engine)) {
$this->connect_engine();
}
if ($input===true) {
return $this->engine->BOOL_Y;
} elseif ($input===false) {
return $this->engine->BOOL_N;
}
return $input;
}
public function get_driver_name(): string
{
if (is_null($this->engine)) {
$this->connect_engine();
}
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;
$dur = microtime(true) - $start;
if ($tracer_enabled) {
$query = trim(preg_replace('/^[\t ]+/m', '', $query)); // trim leading whitespace
$_tracer->complete($start * 1000000, $dur * 1000000, "DB Query", ["query"=>$query, "args"=>$args, "method"=>$method]);
}
$this->query_count++;
$this->dbtime += $dur;
}
public function set_timeout(int $time): void
{
$this->engine->set_timeout($this->db, $time);
}
public function execute(string $query, array $args = []): PDOStatement
{
try {
if (is_null($this->db)) {
$this->connect_db();
}
return $this->db->execute(
"-- " . str_replace("%2F", "/", urlencode($_GET['q'] ?? '')). "\n" .
$query,
$args
);
} catch (PDOException $pdoe) {
throw new SCoreException($pdoe->getMessage(), $query);
}
}
/**
* Execute an SQL query and return a 2D array.
*/
public function get_all(string $query, array $args = []): array
{
$_start = microtime(true);
$data = $this->execute($query, $args)->fetchAll();
$this->count_time("get_all", $_start, $query, $args);
return $data;
}
/**
* Execute an SQL query and return a iterable object for use with generators.
*/
public function get_all_iterable(string $query, array $args = []): PDOStatement
{
$_start = microtime(true);
$data = $this->execute($query, $args);
$this->count_time("get_all_iterable", $_start, $query, $args);
return $data;
}
/**
* Execute an SQL query and return a single row.
*/
public function get_row(string $query, array $args = []): ?array
{
$_start = microtime(true);
$row = $this->execute($query, $args)->fetch();
$this->count_time("get_row", $_start, $query, $args);
return $row ? $row : null;
}
/**
* Execute an SQL query and return the first column of each row.
*/
public function get_col(string $query, array $args = []): array
{
$_start = microtime(true);
$res = $this->execute($query, $args)->fetchAll(PDO::FETCH_COLUMN);
$this->count_time("get_col", $_start, $query, $args);
return $res;
}
/**
* Execute an SQL query and return the first column of each row as a single iterable object.
*/
public function get_col_iterable(string $query, array $args = []): Generator
{
$_start = microtime(true);
$stmt = $this->execute($query, $args);
$this->count_time("get_col_iterable", $_start, $query, $args);
foreach ($stmt as $row) {
yield $row[0];
}
}
/**
* Execute an SQL query and return the the first column => the second column.
*/
public function get_pairs(string $query, array $args = []): array
{
$_start = microtime(true);
$res = $this->execute($query, $args)->fetchAll(PDO::FETCH_KEY_PAIR);
$this->count_time("get_pairs", $_start, $query, $args);
return $res;
}
/**
* Execute an SQL query and return a single value, or null.
*/
public function get_one(string $query, array $args = [])
{
$_start = microtime(true);
$row = $this->execute($query, $args)->fetch();
$this->count_time("get_one", $_start, $query, $args);
return $row ? $row[0] : null;
}
/**
* Execute an SQL query and returns a bool indicating if any data was returned
*/
public function exists(string $query, array $args = []): bool
{
$_start = microtime(true);
$row = $this->execute($query, $args)->fetch();
$this->count_time("exists", $_start, $query, $args);
if ($row==null) {
return false;
}
return true;
}
/**
* Get the ID of the last inserted row.
*/
public function get_last_insert_id(string $seq): int
{
if ($this->engine->name == DatabaseDriver::PGSQL) {
$id = $this->db->lastInsertId($seq);
} else {
$id = $this->db->lastInsertId();
}
assert(is_numeric($id));
return (int)$id;
}
/**
* Create a table from pseudo-SQL.
*/
public function create_table(string $name, string $data): void
{
if (is_null($this->engine)) {
$this->connect_engine();
}
$data = trim($data, ", \t\n\r\0\x0B"); // mysql doesn't like trailing commas
$this->execute($this->engine->create_table_sql($name, $data));
}
/**
* Returns the number of tables present in the current database.
*
* @throws SCoreException
*/
public function count_tables(): int
{
if (is_null($this->db) || is_null($this->engine)) {
$this->connect_db();
}
if ($this->engine->name === DatabaseDriver::MYSQL) {
return count(
$this->get_all("SHOW TABLES")
);
} elseif ($this->engine->name === DatabaseDriver::PGSQL) {
return count(
$this->get_all("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
);
} elseif ($this->engine->name === DatabaseDriver::SQLITE) {
return count(
$this->get_all("SELECT name FROM sqlite_master WHERE type = 'table'")
);
} else {
throw new SCoreException("Can't count tables for database type {$this->engine->name}");
}
}
public function raw_db(): PDO
{
return $this->db;
}
}

View File

@ -1,236 +0,0 @@
<?php declare(strict_types=1);
abstract class SCORE
{
const AIPK = "SCORE_AIPK";
const INET = "SCORE_INET";
const BOOL_Y = "SCORE_BOOL_Y";
const BOOL_N = "SCORE_BOOL_N";
const BOOL = "SCORE_BOOL";
}
abstract class DBEngine
{
/** @var null|string */
public $name = null;
public $BOOL_Y = null;
public $BOOL_N = null;
public function init(PDO $db)
{
}
public function scoreql_to_sql(string $scoreql): string
{
return $scoreql;
}
public function create_table_sql(string $name, string $data): string
{
return 'CREATE TABLE '.$name.' ('.$data.')';
}
abstract public function set_timeout(PDO $db, int $time);
abstract public function get_version(PDO $db): string;
}
class MySQL extends DBEngine
{
/** @var string */
public $name = DatabaseDriver::MYSQL;
public $BOOL_Y = 'Y';
public $BOOL_N = 'N';
public function init(PDO $db)
{
$db->exec("SET NAMES utf8;");
}
public function scoreql_to_sql(string $data): string
{
$data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY auto_increment", $data);
$data = str_replace(SCORE::INET, "VARCHAR(45)", $data);
$data = str_replace(SCORE::BOOL_Y, "'$this->BOOL_Y'", $data);
$data = str_replace(SCORE::BOOL_N, "'$this->BOOL_N'", $data);
$data = str_replace(SCORE::BOOL, "ENUM('Y', 'N')", $data);
return $data;
}
public function create_table_sql(string $name, string $data): string
{
$data = $this->scoreql_to_sql($data);
$ctes = "ENGINE=InnoDB DEFAULT CHARSET='utf8'";
return 'CREATE TABLE '.$name.' ('.$data.') '.$ctes;
}
public function set_timeout(PDO $db, int $time): void
{
// These only apply to read-only queries, which appears to be the best we can to mysql-wise
// $db->exec("SET SESSION MAX_EXECUTION_TIME=".$time.";");
}
public function get_version(PDO $db): string
{
return $db->query('select version()')->fetch()[0];
}
}
class PostgreSQL extends DBEngine
{
/** @var string */
public $name = DatabaseDriver::PGSQL;
public $BOOL_Y = "true";
public $BOOL_N = "false";
public function init(PDO $db)
{
if (array_key_exists('REMOTE_ADDR', $_SERVER)) {
$db->exec("SET application_name TO 'shimmie [{$_SERVER['REMOTE_ADDR']}]';");
} else {
$db->exec("SET application_name TO 'shimmie [local]';");
}
if (defined("DATABASE_TIMEOUT")) {
$this->set_timeout($db, DATABASE_TIMEOUT);
}
}
public function scoreql_to_sql(string $data): string
{
$data = str_replace(SCORE::AIPK, "INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY", $data);
$data = str_replace(SCORE::INET, "INET", $data);
$data = str_replace(SCORE::BOOL_Y, "true", $data);
$data = str_replace(SCORE::BOOL_N, "false", $data);
$data = str_replace(SCORE::BOOL, "BOOL", $data);
return $data;
}
public function create_table_sql(string $name, string $data): string
{
$data = $this->scoreql_to_sql($data);
return "CREATE TABLE $name ($data)";
}
public function set_timeout(PDO $db, int $time): void
{
$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
function _unix_timestamp($date)
{
return strtotime($date);
}
function _now()
{
return date("Y-m-d H:i:s");
}
function _floor($a)
{
return floor($a);
}
function _log($a, $b=null)
{
if (is_null($b)) {
return log($a);
} else {
return log($a, $b);
}
}
function _isnull($a)
{
return is_null($a);
}
function _md5($a)
{
return md5($a);
}
function _concat($a, $b)
{
return $a . $b;
}
function _lower($a)
{
return strtolower($a);
}
function _rand()
{
return rand();
}
function _ln($n)
{
return log($n);
}
class SQLite extends DBEngine
{
/** @var string */
public $name = DatabaseDriver::SQLITE;
public $BOOL_Y = 'Y';
public $BOOL_N = 'N';
public function init(PDO $db)
{
ini_set('sqlite.assoc_case', '0');
$db->exec("PRAGMA foreign_keys = ON;");
$db->sqliteCreateFunction('UNIX_TIMESTAMP', '_unix_timestamp', 1);
$db->sqliteCreateFunction('now', '_now', 0);
$db->sqliteCreateFunction('floor', '_floor', 1);
$db->sqliteCreateFunction('log', '_log');
$db->sqliteCreateFunction('isnull', '_isnull', 1);
$db->sqliteCreateFunction('md5', '_md5', 1);
$db->sqliteCreateFunction('concat', '_concat', 2);
$db->sqliteCreateFunction('lower', '_lower', 1);
$db->sqliteCreateFunction('rand', '_rand', 0);
$db->sqliteCreateFunction('ln', '_ln', 1);
}
public function scoreql_to_sql(string $data): string
{
$data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY", $data);
$data = str_replace(SCORE::INET, "VARCHAR(45)", $data);
$data = str_replace(SCORE::BOOL_Y, "'$this->BOOL_Y'", $data);
$data = str_replace(SCORE::BOOL_N, "'$this->BOOL_N'", $data);
$data = str_replace(SCORE::BOOL, "CHAR(1)", $data);
return $data;
}
public function create_table_sql(string $name, string $data): string
{
$data = $this->scoreql_to_sql($data);
$cols = [];
$extras = "";
foreach (explode(",", $data) as $bit) {
$matches = [];
if (preg_match("/(UNIQUE)? ?INDEX\s*\((.*)\)/", $bit, $matches)) {
$uni = $matches[1];
$col = $matches[2];
$extras .= "CREATE $uni INDEX {$name}_{$col} ON {$name}({$col});";
} else {
$cols[] = $bit;
}
}
$cols_redone = implode(", ", $cols);
return "CREATE TABLE $name ($cols_redone); $extras";
}
public function set_timeout(PDO $db, int $time): void
{
// There doesn't seem to be such a thing for SQLite, so it does nothing
}
public function get_version(PDO $db): string
{
return $db->query('select sqlite_version()')->fetch()[0];
}
}

119
core/email.class.php Normal file
View File

@ -0,0 +1,119 @@
<?php
class Email {
/**
* A generic email.
*/
var $to;
var $subject;
var $header;
var $style;
var $header_img;
var $sitename;
var $sitedomain;
var $siteemail;
var $date;
var $body;
var $footer;
public function __construct($to, $subject, $header, $body) {
global $config;
$this->to = $to;
$sub_prefix = $config->get_string("mail_sub");
if(!isset($sub_prefix)){
$this->subject = $subject;
}
else{
$this->subject = $sub_prefix." ".$subject;
}
$this->style = $config->get_string("mail_style");
$this->header = html_escape($header);
$this->header_img = $config->get_string("mail_img");
$this->sitename = $config->get_string("site_title");
$this->sitedomain = make_http(make_link(""));
$this->siteemail = $config->get_string("site_email");
$this->date = date("F j, Y");
$this->body = $body;
$this->footer = $config->get_string("mail_fot");
}
public function send() {
$headers = "From: ".$this->sitename." <".$this->siteemail.">\r\n";
$headers .= "Reply-To: ".$this->siteemail."\r\n";
$headers .= "X-Mailer: PHP/" . phpversion(). "\r\n";
$headers .= "errors-to: ".$this->siteemail."\r\n";
$headers .= "Date: " . date(DATE_RFC2822);
$headers .= 'MIME-Version: 1.0' . "\r\n";
$headers .= 'Content-type: text/html; charset=iso-8859-1' . "\r\n";
$message = '
<html>
<head>
<link rel="stylesheet" href="'.$this->style.'" type="text/css">
</head>
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0" bgcolor="#EEEEEE" >
<table width="100%" cellpadding="10" cellspacing="0" class="backgroundTable" bgcolor="#EEEEEE" >
<tr>
<td valign="top" align="center">
<table width="550" cellpadding="0" cellspacing="0">
<tr>
<td style="background-color:#FFFFFF;border-top:0px solid #333333;border-bottom:10px solid #FFFFFF;"><center><a href="'.$this->sitedomain.'"><IMG SRC="'.$this->header_img.'" alt="'.$this->sitename.'" name="Header" BORDER="0" align="center" title="'.$this->sitename.'"></a>
</center></td>
</tr>
</table>
<table width="550" cellpadding="20" cellspacing="0" bgcolor="#FFFFFF">
<tr>
<td bgcolor="#FFFFFF" valign="top" style="font-size:12px;color:#000000;line-height:150%;font-family:trebuchet ms;">
<p>
<span style="font-size:20px; font-weight:bold; color:#3399FF; font-family:arial; line-height:110%;">'.$this->header.'</span><br>
<span style="font-size:11px;font-weight:normal;color:#666666;font-style:italic;font-family:arial;">'.$this->date.'</span><br>
</p>
<p>'.$this->body.'</p>
<p>'.$this->footer.'</p>
</td>
</tr>
<tr>
<td style="background-color:#FFFFCC;border-top:10px solid #FFFFFF;" valign="top">
<span style="font-size:10px;color:#996600;line-height:100%;font-family:verdana;">
This email was sent to you since you are a member of <a href="'.$this->sitedomain.'">'.$this->sitename.'</a>. To change your email preferences, visit your <a href="'.make_http(make_link("preferences")).'">Account preferences</a>.<br />
<br />
Contact us:<br />
<a href="'.$this->siteemail.'">'.$this->siteemail.'</a><br /><br />
Copyright (C) <a href="'.$this->sitedomain.'">'.$this->sitename.'</a><br />
</span></td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
';
$sent = mail($this->to, $this->subject, $message, $headers);
if($sent){
log_info("mail", "Sent message '$this->subject' to '$this->to'");
}
else{
log_info("mail", "Error sending message '$this->subject' to '$this->to'");
}
return $sent;
}
}
?>

181
core/event.class.php Normal file
View File

@ -0,0 +1,181 @@
<?php
/**
* Generic parent class for all events.
*
* An event is anything that can be passed around via send_event($blah)
*/
abstract class Event {
public function __construct() {}
}
/**
* A wake-up call for extensions. Upon recieving an InitExtEvent an extension
* should check that it's database tables are there and install them if not,
* and set any defaults with Config::set_default_int() and such.
*/
class InitExtEvent extends Event {}
/**
* A signal that a page has been requested.
*
* User requests /view/42 -> an event is generated with $args = array("view",
* "42"); when an event handler asks $event->page_matches("view"), it returns
* true and ignores the matched part, such that $event->count_args() = 1 and
* $event->get_arg(0) = "42"
*/
class PageRequestEvent extends Event {
var $args;
var $arg_count;
var $part_count;
public function __construct($args) {
$this->args = $args;
$this->arg_count = count($args);
}
/**
* Test if the requested path matches a given pattern.
*
* If it matches, store the remaining path elements in $args
*
* @retval bool
*/
public function page_matches(/*string*/ $name) {
$parts = explode("/", $name);
$this->part_count = count($parts);
if($this->part_count > $this->arg_count) {
return false;
}
for($i=0; $i<$this->part_count; $i++) {
if($parts[$i] != $this->args[$i]) {
return false;
}
}
return true;
}
/**
* Get the n th argument of the page request (if it exists.)
* @param $n integer
* @retval The argmuent (string) or NULL
*/
public function get_arg(/*int*/ $n) {
$offset = $this->part_count + $n;
if($offset >= 0 && $offset < $this->arg_count) {
return $this->args[$offset];
}
else {
return null;
}
}
/**
* Returns the number of arguments the page request has.
* @retval int
*/
public function count_args() {
return (int)($this->arg_count - $this->part_count);
}
/*
* Many things use these functions
*/
public function get_search_terms() {
$search_terms = array();
if($this->count_args() === 2) {
$search_terms = explode(' ', $this->get_arg(0));
}
return $search_terms;
}
public function get_page_number() {
$page_number = 1;
if($this->count_args() === 1) {
$page_number = int_escape($this->get_arg(0));
}
else if($this->count_args() === 2) {
$page_number = int_escape($this->get_arg(1));
}
if($page_number === 0) $page_number = 1; // invalid -> 0
return $page_number;
}
public function get_page_size() {
global $config;
return $config->get_int('index_images');
}
}
/**
* A signal that some text needs formatting, the event carries
* both the text and the result
*/
class TextFormattingEvent extends Event {
/**
* For reference
*/
var $original;
/**
* with formatting applied
*/
var $formatted;
/**
* with formatting removed
*/
var $stripped;
public function __construct(/*string*/ $text) {
$h_text = html_escape(trim($text));
$this->original = $h_text;
$this->formatted = $h_text;
$this->stripped = $h_text;
}
}
/**
* A signal that something needs logging
*/
class LogEvent extends Event {
/**
* a category, normally the extension name
*
* @retval string
*/
var $section;
/**
* See python...
*
* @retval int
*/
var $priority = 0;
/**
* Free text to be logged
*
* @retval text
*/
var $message;
/**
* The time that the event was created
*
* @retval int
*/
var $time;
public function __construct($section, $priority, $message) {
$this->section = $section;
$this->priority = $priority;
$this->message = $message;
$this->time = time();
}
}
?>

View File

@ -1,344 +0,0 @@
<?php declare(strict_types=1);
/**
* Generic parent class for all events.
*
* An event is anything that can be passed around via send_event($blah)
*/
abstract class Event
{
public $stop_processing = false;
public function __construct()
{
}
public function __toString()
{
return var_export($this, true);
}
}
/**
* A wake-up call for extensions. Upon recieving an InitExtEvent an extension
* should check that it's database tables are there and install them if not,
* and set any defaults with Config::set_default_int() and such.
*
* This event is sent before $user is set to anything
*/
class InitExtEvent extends Event
{
}
/**
* A signal that a page has been requested.
*
* User requests /view/42 -> an event is generated with $args = array("view",
* "42"); when an event handler asks $event->page_matches("view"), it returns
* true and ignores the matched part, such that $event->count_args() = 1 and
* $event->get_arg(0) = "42"
*/
class PageRequestEvent extends Event
{
/**
* @var array
*/
public $args;
/**
* @var int
*/
public $arg_count;
/**
* @var int
*/
public $part_count;
public function __construct(string $path)
{
parent::__construct();
global $config;
// trim starting slashes
$path = ltrim($path, "/");
// if path is not specified, use the default front page
if (empty($path)) { /* empty is faster than strlen */
$path = $config->get_string(SetupConfig::FRONT_PAGE);
}
// break the path into parts
$args = explode('/', $path);
$this->args = $args;
$this->arg_count = count($args);
}
/**
* Test if the requested path matches a given pattern.
*
* If it matches, store the remaining path elements in $args
*/
public function page_matches(string $name): bool
{
$parts = explode("/", $name);
$this->part_count = count($parts);
if ($this->part_count > $this->arg_count) {
return false;
}
for ($i=0; $i<$this->part_count; $i++) {
if ($parts[$i] != $this->args[$i]) {
return false;
}
}
return true;
}
/**
* Get the n th argument of the page request (if it exists.)
*/
public function get_arg(int $n): string
{
$offset = $this->part_count + $n;
if ($offset >= 0 && $offset < $this->arg_count) {
return $this->args[$offset];
} else {
$nm1 = $this->arg_count - 1;
throw new SCoreException("Requested an invalid page argument {$offset} / {$nm1}");
}
}
/**
* 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 page_number($i, $max);
} else {
return 0;
}
} else {
return 0;
}
}
/**
* Returns the number of arguments the page request has.
*/
public function count_args(): int
{
return $this->arg_count - $this->part_count;
}
/*
* Many things use these functions
*/
public function get_search_terms(): array
{
$search_terms = [];
if ($this->count_args() === 2) {
$search_terms = Tag::explode(Tag::decaret($this->get_arg(0)));
}
return $search_terms;
}
public function get_page_number(): int
{
$page_number = 1;
if ($this->count_args() === 1) {
$page_number = int_escape($this->get_arg(0));
} elseif ($this->count_args() === 2) {
$page_number = int_escape($this->get_arg(1));
}
if ($page_number === 0) {
$page_number = 1;
} // invalid -> 0
return $page_number;
}
public function get_page_size(): int
{
global $config;
return $config->get_int(IndexConfig::IMAGES);
}
}
/**
* Sent when index.php is called from the command line
*/
class CommandEvent extends Event
{
/**
* @var string
*/
public $cmd = "help";
/**
* @var array
*/
public $args = [];
/**
* #param string[] $args
*/
public function __construct(array $args)
{
parent::__construct();
global $user;
$opts = [];
$log_level = SCORE_LOG_WARNING;
$arg_count = count($args);
for ($i=1; $i<$arg_count; $i++) {
switch ($args[$i]) {
case '-u':
$user = User::by_name($args[++$i]);
if (is_null($user)) {
die("Unknown user");
} else {
send_event(new UserLoginEvent($user));
}
break;
case '-q':
$log_level += 10;
break;
case '-v':
$log_level -= 10;
break;
default:
$opts[] = $args[$i];
break;
}
}
if (!defined("CLI_LOG_LEVEL")) {
define("CLI_LOG_LEVEL", $log_level);
}
if (count($opts) > 0) {
$this->cmd = $opts[0];
$this->args = array_slice($opts, 1);
} else {
print "\n";
print "Usage: php {$args[0]} [flags] [command]\n";
print "\n";
print "Flags:\n";
print "\t-u [username]\n";
print "\t\tLog in as the specified user\n";
print "\t-q / -v\n";
print "\t\tBe quieter / more verbose\n";
print "\t\tScale is debug - info - warning - error - critical\n";
print "\t\tDefault is to show warnings and above\n";
print "\n";
print "Currently known commands:\n";
}
}
}
/**
* A signal that some text needs formatting, the event carries
* both the text and the result
*/
class TextFormattingEvent extends Event
{
/**
* For reference
*
* @var string
*/
public $original;
/**
* with formatting applied
*
* @var string
*/
public $formatted;
/**
* with formatting removed
*
* @var string
*/
public $stripped;
public function __construct(string $text)
{
parent::__construct();
// We need to escape before formatting, instead of at display time,
// because formatters will add their own HTML tags into the mix and
// we don't want to escape those.
$h_text = html_escape(trim($text));
$this->original = $h_text;
$this->formatted = $h_text;
$this->stripped = $h_text;
}
}
/**
* A signal that something needs logging
*/
class LogEvent extends Event
{
/**
* a category, normally the extension name
*
* @var string
*/
public $section;
/**
* See python...
*
* @var int
*/
public $priority = 0;
/**
* Free text to be logged
*
* @var string
*/
public $message;
/**
* The time that the event was created
*
* @var int
*/
public $time;
/**
* Extra data to be held separate
*
* @var array
*/
public $args;
public function __construct(string $section, int $priority, string $message)
{
parent::__construct();
$this->section = $section;
$this->priority = $priority;
$this->message = $message;
$this->time = time();
}
}
class DatabaseUpgradeEvent extends Event
{
}

11
core/exceptions.class.php Normal file
View File

@ -0,0 +1,11 @@
<?php
/**
* A base exception to be caught by the upper levels
*/
class SCoreException extends Exception {}
/**
* A fairly common, generic exception
*/
class PermissionDeniedException extends SCoreException {}
?>

View File

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

230
core/extension.class.php Normal file
View File

@ -0,0 +1,230 @@
<?php
/**
* \page eande 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:
*
* \code
* $tfe = new TextFormattingEvent($original_text);
* send_event($tfe);
* $formatted_text = $tfe->formatted;
* \endcode
*
* An extension is something which is capable of reacting to events.
*
*
* \page hello The Hello World Extension
*
* \code
* // 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
* }
* }
*
* // ext/hello/test.php
* public class HelloTest extends SCoreWebTestCase {
* public function testHello() {
* $this->get_page("post/list"); // View a page, any page
* $this->assert_text("Hello there"); // Check that the specified text is in that 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!"
* );
* }
* }
* \endcode
*
*/
/**
* send_event(BlahEvent()) -> onBlah($event)
*
* 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
* Then re-implemented by Shish after he broke the forum and couldn't
* find the thread where the original was posted >_<
*/
abstract class Extension {
/** this theme's Themelet object */
var $theme;
/** @private */
var $_child;
// in PHP5.3, late static bindings can take care of this; __CLASS__
// used here will refer to the subclass
// http://php.net/manual/en/language.oop5.late-static-bindings.php
/** @private */
public function i_am(Extension $child) {
$this->_child = $child;
if(is_null($this->theme)) $this->theme = $this->get_theme_object($child, false);
}
/**
* Find the theme object for a given extension
*/
private function get_theme_object(Extension $class, $fatal=true) {
$base = get_class($class);
if(class_exists('Custom'.$base.'Theme')) {
$class = 'Custom'.$base.'Theme';
return new $class();
}
elseif ($fatal || class_exists($base.'Theme')) {
$class = $base.'Theme';
return new $class();
} else {
return false;
}
}
/**
* Override this to change the priority of the extension,
* lower numbered ones will recieve events first
*/
public function get_priority() {
return 50;
}
}
/**
* Several extensions have this in common, make a common API
*/
abstract class FormatterExtension extends Extension {
public function onTextFormatting(TextFormattingEvent $event) {
$event->formatted = $this->format($event->formatted);
$event->stripped = $this->strip($event->stripped);
}
abstract public function format(/*string*/ $text);
abstract public function strip(/*string*/ $text);
}
/**
* This too is a common class of extension with many methods in common,
* so we have a base class to extend from
*/
abstract class DataHandlerExtension extends Extension {
public function onDataUpload(DataUploadEvent $event) {
if($this->supported_ext($event->type) && $this->check_contents($event->tmpname)) {
if(!move_upload_to_archive($event)) return;
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'])) {
/* 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);
if(is_null($existing)) {
throw new UploadException("Image to replace does not exist!");
}
if ($existing->hash === $event->metadata['hash']) {
throw new UploadException("The uploaded image is the same as the one to replace.");
}
// even more hax..
$event->metadata['tags'] = $existing->get_tag_list();
$image = $this->create_image_from_data(warehouse_path("images", $event->metadata['hash']), $event->metadata);
if(is_null($image)) {
throw new UploadException("Data handler failed to create image object from data");
}
$ire = new ImageReplaceEvent($image_id, $image);
send_event($ire);
$event->image_id = $image_id;
}
else {
$image = $this->create_image_from_data(warehouse_path("images", $event->hash), $event->metadata);
if(is_null($image)) {
throw new UploadException("Data handler failed to create image object from data");
}
$iae = new ImageAdditionEvent($event->user, $image);
send_event($iae);
$event->image_id = $iae->image->id;
// Rating Stuff.
if(!empty($event->metadata['rating'])){
global $user;
$rating = $event->metadata['rating'];
send_event(new RatingSetEvent($image, $user, $rating));
}
// Locked Stuff.
if(!empty($event->metadata['locked'])){
$locked = $event->metadata['locked'];
send_event(new LockSetEvent($image, !empty($locked)));
}
}
}
}
public function onThumbnailGeneration(ThumbnailGenerationEvent $event) {
if($this->supported_ext($event->type)) {
if (method_exists($this, 'create_thumb_force') && $event->force == true) {
$this->create_thumb_force($event->hash);
}
else {
$this->create_thumb($event->hash);
}
}
}
public function onDisplayingImage(DisplayingImageEvent $event) {
global $page;
if($this->supported_ext($event->image->ext)) {
$this->theme->display_image($page, $event->image);
}
}
public function onSetupBuilding(SetupBuildingEvent $event) {
$sb = $this->setup();
if($sb) $event->panel->add_block($sb);
}
protected function setup() {}
abstract protected function supported_ext($ext);
abstract protected function check_contents($tmpname);
abstract protected function create_image_from_data($filename, $metadata);
abstract protected function create_thumb($hash);
}
?>

View File

@ -1,446 +0,0 @@
<?php declare(strict_types=1);
/**
* Class Extension
*
* send_event(BlahEvent()) -> onBlah($event)
*
* Also loads the theme object into $this->theme if available
*
* The original concept came from Artanis's Extension extension
* --> 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 >_<
*/
abstract class Extension
{
/** @var string */
public $key;
/** @var Themelet */
protected $theme;
/** @var ExtensionInfo */
public $info;
private static $enabled_extensions = [];
public function __construct($class = null)
{
$class = $class ?? get_called_class();
$this->theme = $this->get_theme_object($class);
$this->info = ExtensionInfo::get_for_extension_class($class);
if ($this->info===null) {
throw new ScoreException("Info class not found for extension $class");
}
$this->key = $this->info->key;
}
/**
* Find the theme object for a given extension.
*/
private function get_theme_object(string $base): ?Themelet
{
$custom = 'Custom'.$base.'Theme';
$normal = $base.'Theme';
if (class_exists($custom)) {
return new $custom();
} elseif (class_exists($normal)) {
return new $normal();
} else {
return null;
}
}
/**
* Override this to change the priority of the extension,
* lower numbered ones will receive events first.
*/
public function get_priority(): int
{
return 50;
}
public static function determine_enabled_extensions()
{
self::$enabled_extensions = [];
foreach (array_merge(
ExtensionInfo::get_core_extensions(),
explode(",", EXTRA_EXTS)
) as $key) {
$ext = ExtensionInfo::get_by_key($key);
if ($ext===null || !$ext->is_supported()) {
continue;
}
// FIXME: error if one of our dependencies isn't supported
self::$enabled_extensions[] = $ext->key;
if (!empty($ext->dependencies)) {
foreach ($ext->dependencies as $dep) {
self::$enabled_extensions[] = $dep;
}
}
}
}
public static function is_enabled(string $key): ?bool
{
return in_array($key, self::$enabled_extensions);
}
public static function get_enabled_extensions(): array
{
return self::$enabled_extensions;
}
public static function get_enabled_extensions_as_string(): string
{
return implode(",", self::$enabled_extensions);
}
protected function get_version(string $name): int
{
global $config;
return $config->get_int($name, 0);
}
protected function set_version(string $name, int $ver)
{
global $config;
$config->set_int($name, $ver);
log_info("upgrade", "Set version for $name to $ver");
}
}
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 = "https://code.shishnet.org/shimmie2/";
public const SHISH_AUTHOR = [self::SHISH_NAME=>self::SHISH_EMAIL];
public const LICENSE_GPLV2 = "GPLv2";
public const LICENSE_MIT = "MIT";
public const LICENSE_WTFPL = "WTFPL";
public const VISIBLE_ADMIN = "admin";
public const VISIBLE_HIDDEN = "hidden";
private const VALID_VISIBILITY = [self::VISIBLE_ADMIN, self::VISIBLE_HIDDEN];
public $key;
public $core = false;
public $beta = false;
public $name;
public $authors = [];
public $link;
public $license;
public $version;
public $dependencies = [];
public $visibility;
public $description;
public $documentation;
/** @var array which DBs this ext supports (blank for 'all') */
public $db_support = [];
/** @var bool */
private $supported = null;
/** @var string */
private $support_info = null;
public function is_supported(): bool
{
if ($this->supported===null) {
$this->check_support();
}
return $this->supported;
}
public function get_support_info(): string
{
if ($this->supported===null) {
$this->check_support();
}
return $this->support_info;
}
private static $all_info_by_key = [];
private static $all_info_by_class = [];
private static $core_extensions = [];
protected function __construct()
{
assert(!empty($this->key), "key field is required");
assert(!empty($this->name), "name field is required for extension $this->key");
assert(empty($this->visibility) || in_array($this->visibility, self::VALID_VISIBILITY), "Invalid visibility for extension $this->key");
assert(is_array($this->db_support), "db_support has to be an array for extension $this->key");
assert(is_array($this->authors), "authors has to be an array for extension $this->key");
assert(is_array($this->dependencies), "dependencies has to be an array for extension $this->key");
}
public function is_enabled(): bool
{
return Extension::is_enabled($this->key);
}
private function check_support()
{
global $database;
$this->support_info = "";
if (!empty($this->db_support) && !in_array($database->get_driver_name(), $this->db_support)) {
$this->support_info .= "Database not supported. ";
}
// Additional checks here as needed
$this->supported = empty($this->support_info);
}
public static function get_all(): array
{
return array_values(self::$all_info_by_key);
}
public static function get_all_keys(): array
{
return array_keys(self::$all_info_by_key);
}
public static function get_core_extensions(): array
{
return self::$core_extensions;
}
public static function get_by_key(string $key): ?ExtensionInfo
{
if (array_key_exists($key, self::$all_info_by_key)) {
return self::$all_info_by_key[$key];
} else {
return null;
}
}
public static function get_for_extension_class(string $base): ?ExtensionInfo
{
$normal = $base.'Info';
if (array_key_exists($normal, self::$all_info_by_class)) {
return self::$all_info_by_class[$normal];
} else {
return null;
}
}
public static function load_all_extension_info()
{
foreach (getSubclassesOf("ExtensionInfo") as $class) {
$extension_info = new $class();
if (array_key_exists($extension_info->key, self::$all_info_by_key)) {
throw new ScoreException("Extension Info $class with key $extension_info->key has already been loaded");
}
self::$all_info_by_key[$extension_info->key] = $extension_info;
self::$all_info_by_class[$class] = $extension_info;
if ($extension_info->core===true) {
self::$core_extensions[] = $extension_info->key;
}
}
}
}
/**
* Class FormatterExtension
*
* Several extensions have this in common, make a common API.
*/
abstract class FormatterExtension extends Extension
{
public function onTextFormatting(TextFormattingEvent $event)
{
$event->formatted = $this->format($event->formatted);
$event->stripped = $this->strip($event->stripped);
}
abstract public function format(string $text): string;
abstract public function strip(string $text): string;
}
/**
* Class DataHandlerExtension
*
* This too is a common class of extension with many methods in common,
* so we have a base class to extend from.
*/
abstract class DataHandlerExtension extends Extension
{
protected $SUPPORTED_MIME = [];
protected function move_upload_to_archive(DataUploadEvent $event)
{
$target = warehouse_path(Image::IMAGE_DIR, $event->hash);
if (!@copy($event->tmpname, $target)) {
$errors = error_get_last();
throw new UploadException(
"Failed to copy file from uploads ({$event->tmpname}) to archive ($target): ".
"{$errors['type']} / {$errors['message']}"
);
}
}
public function onDataUpload(DataUploadEvent $event)
{
$supported_ext = $this->supported_ext($event->type);
$check_contents = $this->check_contents($event->tmpname);
if ($supported_ext && $check_contents) {
$this->move_upload_to_archive($event);
send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
/* Check if we are replacing an image */
if (!is_null($event->replace_id)) {
/* hax: This seems like such a dirty way to do this.. */
/* Check to make sure the image exists. */
$existing = Image::by_id($event->replace_id);
if (is_null($existing)) {
throw new UploadException("Image to replace does not exist!");
}
if ($existing->hash === $event->metadata['hash']) {
throw new UploadException("The uploaded image is the same as the one to replace.");
}
// even more hax..
$event->metadata['tags'] = $existing->get_tag_list();
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->metadata['hash']), $event->metadata);
if (is_null($image)) {
throw new UploadException("Data handler failed to create image object from data");
}
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($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) {
throw new UploadException("Unable to scan media properties: ".$e->getMessage());
}
$iae = send_event(new ImageAdditionEvent($image));
$event->image_id = $iae->image->id;
$event->merged = $iae->merged;
// Rating Stuff.
if (!empty($event->metadata['rating'])) {
$rating = $event->metadata['rating'];
send_event(new RatingSetEvent($image, $rating));
}
// Locked Stuff.
if (!empty($event->metadata['locked'])) {
$locked = $event->metadata['locked'];
send_event(new LockSetEvent($image, !empty($locked)));
}
}
} elseif ($supported_ext && !$check_contents) {
// We DO support this extension - but the file looks corrupt
throw new UploadException("Invalid or corrupted file");
}
}
public function onThumbnailGeneration(ThumbnailGenerationEvent $event)
{
$result = false;
if ($this->supported_ext($event->type)) {
if ($event->force) {
$result = $this->create_thumb($event->hash, $event->type);
} else {
$outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash);
if (file_exists($outname)) {
return;
}
$result = $this->create_thumb($event->hash, $event->type);
}
}
if ($result) {
$event->generated = true;
}
}
public function onDisplayingImage(DisplayingImageEvent $event)
{
global $page;
if ($this->supported_ext($event->image->ext)) {
/** @noinspection PhpPossiblePolymorphicInvocationInspection */
$this->theme->display_image($page, $event->image);
}
}
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
if ($this->supported_ext($event->ext)) {
$this->media_check_properties($event);
}
}
protected function create_image_from_data(string $filename, array $metadata): Image
{
global $config;
$image = new Image();
$image->filesize = $metadata['size'];
$image->hash = $metadata['hash'];
$image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename'];
if ($config->get_bool("upload_use_mime")) {
$image->ext = get_extension_for_file($filename);
}
if (empty($image->ext)) {
$image->ext = (($pos = strpos($metadata['extension'], '?')) !== false) ? substr($metadata['extension'], 0, $pos) : $metadata['extension'];
}
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
$image->source = $metadata['source'];
return $image;
}
abstract protected function media_check_properties(MediaCheckPropertiesEvent $event): void;
abstract protected function check_contents(string $tmpname): bool;
abstract protected function create_thumb(string $hash, string $type): bool;
protected function supported_ext(string $ext): bool
{
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) {
$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;
}
}

View File

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

1114
core/imageboard.pack.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,149 +0,0 @@
<?php declare(strict_types=1);
/**
* An image is being added to the database.
*/
class ImageAdditionEvent extends Event
{
/** @var User */
public $user;
/** @var Image */
public $image;
public $merged = false;
/**
* Inserts a new image into the database with its associated
* information. Also calls TagSetEvent to set the tags for
* this new image.
*/
public function __construct(Image $image)
{
parent::__construct();
$this->image = $image;
}
}
class ImageAdditionException extends SCoreException
{
}
/**
* An image is being deleted.
*/
class ImageDeletionEvent extends Event
{
/** @var Image */
public $image;
/** @var bool */
public $force = false;
/**
* Deletes an image.
*
* Used by things like tags and comments handlers to
* clean out related rows in their tables.
*/
public function __construct(Image $image, bool $force = false)
{
parent::__construct();
$this->image = $image;
$this->force = $force;
}
}
/**
* An image is being replaced.
*/
class ImageReplaceEvent extends Event
{
/** @var int */
public $id;
/** @var Image */
public $image;
/**
* Replaces an image.
*
* Updates an existing ID in the database to use a new image
* file, leaving the tags and such unchanged. Also removes
* the old image file and thumbnail from the disk.
*/
public function __construct(int $id, Image $image)
{
parent::__construct();
$this->id = $id;
$this->image = $image;
}
}
class ImageReplaceException extends SCoreException
{
}
/**
* Request a thumbnail be made for an image object.
*/
class ThumbnailGenerationEvent extends Event
{
/** @var string */
public $hash;
/** @var string */
public $type;
/** @var bool */
public $force;
/** @var bool */
public $generated;
/**
* Request a thumbnail be made for an image object
*/
public function __construct(string $hash, string $type, bool $force=false)
{
parent::__construct();
$this->hash = $hash;
$this->type = $type;
$this->force = $force;
$this->generated = false;
}
}
/*
* ParseLinkTemplateEvent:
* $link -- the formatted text (with each element URL Escape'd)
* $text -- the formatted text (not escaped)
* $original -- the formatting string, for reference
* $image -- the image who's link is being parsed
*/
class ParseLinkTemplateEvent extends Event
{
/** @var string */
public $link;
/** @var string */
public $text;
/** @var string */
public $original;
/** @var Image */
public $image;
public function __construct(string $link, Image $image)
{
parent::__construct();
$this->link = $link;
$this->text = $link;
$this->original = $link;
$this->image = $image;
}
public function replace(string $needle, ?string $replace): void
{
if (!is_null($replace)) {
$this->link = str_replace($needle, url_escape($replace), $this->link);
$this->text = str_replace($needle, $replace, $this->text);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,186 +0,0 @@
<?php declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Misc functions *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/**
* Add a directory full of images
*
* @param string $base
* @return array
*/
function add_dir(string $base): array
{
$results = [];
foreach (list_files($base) as $full_path) {
$short_path = str_replace($base, "", $full_path);
$filename = basename($full_path);
$tags = path_to_tags($short_path);
$result = "$short_path (".str_replace(" ", ", ", $tags).")... ";
try {
add_image($full_path, $filename, $tags);
$result .= "ok";
} catch (UploadException $ex) {
$result .= "failed: ".$ex->getMessage();
}
$results[] = $result;
}
return $results;
}
/**
* Sends a DataUploadEvent for a file.
*
* @param string $tmpname
* @param string $filename
* @param string $tags
* @throws UploadException
*/
function add_image(string $tmpname, string $filename, string $tags): int
{
assert(file_exists($tmpname));
$pathinfo = pathinfo($filename);
$metadata = [];
$metadata['filename'] = $pathinfo['basename'];
if (array_key_exists('extension', $pathinfo)) {
$metadata['extension'] = $pathinfo['extension'];
}
$metadata['tags'] = Tag::explode($tags);
$metadata['source'] = null;
$due = new DataUploadEvent($tmpname, $metadata);
send_event($due);
return $due->image_id;
}
/**
* Given a full size pair of dimensions, return a pair scaled down to fit
* into the configured thumbnail square, with ratio intact.
* Optionally uses the High-DPI scaling setting to adjust the final resolution.
*
* @param int $orig_width
* @param int $orig_height
* @param bool $use_dpi_scaling Enables the High-DPI scaling.
* @return array
*/
function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_scaling = false): array
{
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;
}
if ($orig_height === 0) {
$orig_height = 192;
}
if ($orig_width > $orig_height * 5) {
$orig_width = $orig_height * 5;
}
if ($orig_height > $orig_width * 5) {
$orig_height = $orig_width * 5;
}
if ($use_dpi_scaling) {
list($max_width, $max_height) = get_thumbnail_max_size_scaled();
} else {
$max_width = $config->get_int(ImageConfig::THUMB_WIDTH);
$max_height = $config->get_int(ImageConfig::THUMB_HEIGHT);
}
$output = get_scaled_by_aspect_ratio($orig_width, $orig_height, $max_width, $max_height);
if ($output[2] > 1 && $config->get_bool('thumb_upscale')) {
return [(int)$orig_width, (int)$orig_height];
} else {
return $output;
}
}
function get_scaled_by_aspect_ratio(int $original_width, int $original_height, int $max_width, int $max_height) : array
{
$xscale = ($max_width/ $original_width);
$yscale = ($max_height/ $original_height);
$scale = ($yscale < $xscale) ? $yscale : $xscale ;
return [(int)($original_width*$scale), (int)($original_height*$scale), $scale];
}
/**
* Fetches the thumbnails height and width settings and applies the High-DPI scaling setting before returning the dimensions.
*
* @return array [width, height]
*/
function get_thumbnail_max_size_scaled(): array
{
global $config;
$scaling = $config->get_int(ImageConfig::THUMB_SCALING);
$max_width = $config->get_int(ImageConfig::THUMB_WIDTH) * ($scaling/100);
$max_height = $config->get_int(ImageConfig::THUMB_HEIGHT) * ($scaling/100);
return [$max_width, $max_height];
}
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,
$config->get_string(ImageConfig::THUMB_FIT)
);
}
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==EXTENSION_WEBP) {
$output_format = Media::WEBP_LOSSY;
}
send_event(new MediaResizeEvent(
$engine,
$inname,
$type,
$outname,
$tsize[0],
$tsize[1],
$resize_type,
$output_format,
$config->get_int(ImageConfig::THUMB_QUALITY),
true,
true
));
}

View File

@ -1,58 +0,0 @@
<?php declare(strict_types=1);
class Querylet
{
/** @var string */
public $sql;
/** @var array */
public $variables;
public function __construct(string $sql, array $variables=[])
{
$this->sql = $sql;
$this->variables = $variables;
}
public function append(Querylet $querylet): void
{
$this->sql .= $querylet->sql;
$this->variables = array_merge($this->variables, $querylet->variables);
}
public function append_sql(string $sql): void
{
$this->sql .= $sql;
}
public function add_variable($var): void
{
$this->variables[] = $var;
}
}
class TagCondition
{
/** @var string */
public $tag;
/** @var bool */
public $positive;
public function __construct(string $tag, bool $positive)
{
$this->tag = $tag;
$this->positive = $positive;
}
}
class ImgCondition
{
/** @var Querylet */
public $qlet;
/** @var bool */
public $positive;
public function __construct(Querylet $qlet, bool $positive)
{
$this->qlet = $qlet;
$this->positive = $positive;
}
}

View File

@ -1,218 +0,0 @@
<?php declare(strict_types=1);
/**
* Class Tag
*
* A class for organising the tag related functions.
*
* All the methods are static, one should never actually use a tag object.
*
*/
class Tag
{
public static function implode(array $tags): string
{
sort($tags);
$tags = implode(' ', $tags);
return $tags;
}
/**
* Turn a human-supplied string into a valid tag array.
*
* #return string[]
*/
public static function explode(string $tags, bool $tagme=true): array
{
global $database;
$tags = explode(' ', trim($tags));
/* sanitise by removing invisible / dodgy characters */
$tag_array = self::sanitize_array($tags);
/* if user supplied a blank string, add "tagme" */
if (count($tag_array) === 0 && $tagme) {
$tag_array = ["tagme"];
}
/* resolve aliases */
$new = [];
$i = 0;
$tag_count = count($tag_array);
while ($i<$tag_count) {
$tag = $tag_array[$i];
$negative = '';
if (!empty($tag) && ($tag[0] == '-')) {
$negative = '-';
$tag = substr($tag, 1);
}
$newtags = $database->get_one(
"
SELECT newtag
FROM aliases
WHERE LOWER(oldtag)=LOWER(:tag)
",
["tag"=>$tag]
);
if (empty($newtags)) {
//tag has no alias, use old tag
$aliases = [$tag];
} else {
$aliases = explode(" ", $newtags); // Tag::explode($newtags); - recursion can be infinite
}
foreach ($aliases as $alias) {
if (!in_array($alias, $new)) {
if ($tag == $alias) {
$new[] = $negative.$alias;
} elseif (!in_array($alias, $tag_array)) {
$tag_array[] = $negative.$alias;
$tag_count++;
}
}
}
$i++;
}
/* remove any duplicate tags */
$tag_array = array_iunique($new);
/* tidy up */
sort($tag_array);
return $tag_array;
}
public static function sanitize(string $tag): string
{
$tag = preg_replace("/\s/", "", $tag); # whitespace
$tag = preg_replace('/\x20[\x0e\x0f]/', '', $tag); # unicode RTL
$tag = preg_replace("/\.+/", ".", $tag); # strings of dots?
$tag = preg_replace("/^(\.+[\/\\\\])+/", "", $tag); # trailing slashes?
$tag = trim($tag, ", \t\n\r\0\x0B");
if ($tag == ".") {
$tag = "";
} // hard-code one bad case...
if (mb_strlen($tag, 'UTF-8') > 255) {
throw new ScoreException("The tag below is longer than 255 characters, please use a shorter tag.\n$tag\n");
}
return $tag;
}
public static function compare(array $tags1, array $tags2): bool
{
if (count($tags1)!==count($tags2)) {
return false;
}
$tags1 = array_map("strtolower", $tags1);
$tags2 = array_map("strtolower", $tags2);
natcasesort($tags1);
natcasesort($tags2);
for ($i = 0; $i < count($tags1); $i++) {
if ($tags1[$i]!==$tags2[$i]) {
return false;
}
}
return true;
}
public static function get_diff_tags(array $source, array $remove): array
{
$before = array_map('strtolower', $source);
$remove = array_map('strtolower', $remove);
$after = [];
foreach ($before as $tag) {
if (!in_array($tag, $remove)) {
$after[] = $tag;
}
}
return $after;
}
public static function sanitize_array(array $tags): array
{
global $page;
$tag_array = [];
foreach ($tags as $tag) {
try {
$tag = Tag::sanitize($tag);
} catch (Exception $e) {
$page->flash($e->getMessage());
continue;
}
if (!empty($tag)) {
$tag_array[] = $tag;
}
}
return $tag_array;
}
public static function sqlify(string $term): string
{
global $database;
if ($database->get_driver_name() === DatabaseDriver::SQLITE) {
$term = str_replace('\\', '\\\\', $term);
}
$term = str_replace('_', '\_', $term);
$term = str_replace('%', '\%', $term);
$term = str_replace('*', '%', $term);
// $term = str_replace("?", "_", $term);
return $term;
}
/**
* Kind of like urlencode, but using a custom scheme so that
* tags always fit neatly between slashes in a URL. Use this
* when you want to put an arbitrary tag into a URL.
*/
public static function caret(string $input): string
{
$to_caret = [
"^" => "^",
"/" => "s",
"\\" => "b",
"?" => "q",
"&" => "a",
"." => "d",
];
foreach ($to_caret as $from => $to) {
$input = str_replace($from, '^' . $to, $input);
}
return $input;
}
/**
* Use this when you want to get a tag out of a URL
*/
public static function decaret(string $str): string
{
$from_caret = [
"^" => "^",
"s" => "/",
"b" => "\\",
"q" => "?",
"a" => "&",
"d" => ".",
];
$out = "";
$length = strlen($str);
for ($i=0; $i<$length; $i++) {
if ($str[$i] == "^") {
$i++;
$out .= $from_caret[$str[$i]] ?? '';
} else {
$out .= $str[$i];
}
}
return $out;
}
}

View File

@ -1,326 +0,0 @@
<?php
/**
* Shimmie Installer
*
* @package Shimmie
* @copyright Copyright (c) 2007-2015, Shish et al.
* @author Shish [webmaster at shishnet.org], jgen [jeffgenovy at gmail.com]
* @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.
*
* This file should be independent of the database
* and other such things that aren't ready yet
*/
function install()
{
date_default_timezone_set('UTC');
if (is_readable("data/config/shimmie.conf.php")) {
die_nicely(
"Shimmie is already installed.",
"data/config/shimmie.conf.php exists, how did you get here?"
);
}
// Pull in necessary files
require_once "vendor/autoload.php";
global $_tracer;
$_tracer = new EventTracer();
require_once "core/exceptions.php";
require_once "core/cacheengine.php";
require_once "core/dbengine.php";
require_once "core/database.php";
require_once "core/util.php";
$dsn = get_dsn();
if ($dsn) {
do_install($dsn);
} else {
ask_questions();
}
}
function get_dsn()
{
if (getenv("INSTALL_DSN")) {
$dsn = getenv("INSTALL_DSN");
;
} elseif (@$_POST["database_type"] == DatabaseDriver::SQLITE) {
/** @noinspection PhpUnhandledExceptionInspection */
$id = bin2hex(random_bytes(5));
$dsn = "sqlite:data/shimmie.{$id}.sqlite";
} elseif (isset($_POST['database_type']) && isset($_POST['database_host']) && isset($_POST['database_user']) && isset($_POST['database_name'])) {
$dsn = "{$_POST['database_type']}:user={$_POST['database_user']};password={$_POST['database_password']};host={$_POST['database_host']};dbname={$_POST['database_name']}";
} else {
$dsn = null;
}
return $dsn;
}
function do_install($dsn)
{
try {
create_dirs();
create_tables(new Database($dsn));
write_config($dsn);
} catch (InstallerException $e) {
die_nicely($e->title, $e->body, $e->code);
}
}
function ask_questions()
{
$warnings = [];
$errors = [];
if (check_gd_version() == 0 && check_im_version() == 0) {
$errors[] = "
No thumbnailers could be found - install the imagemagick
tools (or the PHP-GD library, if imagemagick is unavailable).
";
} elseif (check_im_version() == 0) {
$warnings[] = "
The 'convert' command (from the imagemagick package)
could not be found - PHP-GD can be used instead, but
the size of thumbnails will be limited.
";
}
if (!function_exists('mb_strlen')) {
$errors[] = "
The mbstring PHP extension is missing - multibyte languages
(eg non-english languages) may not work right.
";
}
$drivers = PDO::getAvailableDrivers();
if (
!in_array(DatabaseDriver::MYSQL, $drivers) &&
!in_array(DatabaseDriver::PGSQL, $drivers) &&
!in_array(DatabaseDriver::SQLITE, $drivers)
) {
$errors[] = "
No database connection library could be found; shimmie needs
PDO with either Postgres, MySQL, or SQLite drivers
";
}
$db_m = in_array(DatabaseDriver::MYSQL, $drivers) ? '<option value="'. DatabaseDriver::MYSQL .'">MySQL</option>' : "";
$db_p = in_array(DatabaseDriver::PGSQL, $drivers) ? '<option value="'. DatabaseDriver::PGSQL .'">PostgreSQL</option>' : "";
$db_s = in_array(DatabaseDriver::SQLITE, $drivers) ? '<option value="'. DatabaseDriver::SQLITE .'">SQLite</option>' : "";
$warn_msg = $warnings ? "<h3>Warnings</h3>".implode("\n<p>", $warnings) : "";
$err_msg = $errors ? "<h3>Errors</h3>".implode("\n<p>", $errors) : "";
die_nicely(
"Install Options",
<<<EOD
$warn_msg
$err_msg
<form action="index.php" method="POST">
<table class='form' style="margin: 1em auto;">
<tr>
<th>Type:</th>
<td><select name="database_type" id="database_type" onchange="update_qs();">
$db_m
$db_p
$db_s
</select></td>
</tr>
<tr class="dbconf mysql pgsql">
<th>Host:</th>
<td><input type="text" name="database_host" size="40" value="localhost"></td>
</tr>
<tr class="dbconf mysql pgsql">
<th>Username:</th>
<td><input type="text" name="database_user" size="40"></td>
</tr>
<tr class="dbconf mysql pgsql">
<th>Password:</th>
<td><input type="password" name="database_password" size="40"></td>
</tr>
<tr class="dbconf mysql pgsql">
<th>DB&nbsp;Name:</th>
<td><input type="text" name="database_name" size="40" value="shimmie"></td>
</tr>
<tr><td colspan="2"><input type="submit" value="Go!"></td></tr>
</table>
<script>
document.addEventListener('DOMContentLoaded', update_qs);
function q(n) {
return document.querySelectorAll(n);
}
function update_qs() {
Array.prototype.forEach.call(q('.dbconf'), function(el, i){
el.style.display = 'none';
});
let seldb = q("#database_type")[0].value || "none";
Array.prototype.forEach.call(q('.'+seldb), function(el, i){
el.style.display = null;
});
}
</script>
</form>
<h3>Help</h3>
<p class="dbconf mysql pgsql">
Please make sure the database you have chosen exists and is empty.<br>
The username provided must have access to create tables within the database.
</p>
<p class="dbconf sqlite">
For SQLite the database name will be a filename on disk, relative to
where shimmie was installed.
</p>
<p class="dbconf none">
Drivers can generally be downloaded with your OS package manager;
for Debian / Ubuntu you want php-pgsql, php-mysql, or php-sqlite.
</p>
EOD
);
}
function create_dirs()
{
$data_exists = file_exists("data") || mkdir("data");
$data_writable = $data_exists && (is_writable("data") || chmod("data", 0755));
if (!$data_exists || !$data_writable) {
throw new InstallerException(
"Directory Permissions Error:",
"<p>Shimmie needs to have a 'data' folder in its directory, writable by the PHP user.</p>
<p>If you see this error, if probably means the folder is owned by you, and it needs to be writable by the web server.</p>
<p>PHP reports that it is currently running as user: ".get_current_user()." (". getmyuid() .")</p>
<p>Once you have created this folder and / or changed the ownership of the shimmie folder, hit 'refresh' to continue.</p>",
7
);
}
}
function create_tables(Database $db)
{
try {
if ($db->count_tables() > 0) {
throw new InstallerException(
"Warning: The Database schema is not empty!",
"<p>Please ensure that the database you are installing Shimmie with is empty before continuing.</p>
<p>Once you have emptied the database of any tables, please hit 'refresh' to continue.</p>",
2
);
}
$db->create_table("aliases", "
oldtag VARCHAR(128) NOT NULL,
newtag VARCHAR(128) NOT NULL,
PRIMARY KEY (oldtag)
");
$db->execute("CREATE INDEX aliases_newtag_idx ON aliases(newtag)", []);
$db->create_table("config", "
name VARCHAR(128) NOT NULL,
value TEXT,
PRIMARY KEY (name)
");
$db->create_table("users", "
id SCORE_AIPK,
name VARCHAR(32) UNIQUE NOT NULL,
pass VARCHAR(250),
joindate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
class VARCHAR(32) NOT NULL DEFAULT 'user',
email VARCHAR(128)
");
$db->execute("CREATE INDEX users_name_idx ON users(name)", []);
$db->execute("INSERT INTO users(name, pass, joindate, class) VALUES(:name, :pass, now(), :class)", ["name" => 'Anonymous', "pass" => null, "class" => 'anonymous']);
$db->execute("INSERT INTO config(name, value) VALUES(:name, :value)", ["name" => 'anon_id', "value" => $db->get_last_insert_id('users_id_seq')]);
if (check_im_version() > 0) {
$db->execute("INSERT INTO config(name, value) VALUES(:name, :value)", ["name" => 'thumb_engine', "value" => 'convert']);
}
$db->create_table("images", "
id SCORE_AIPK,
owner_id INTEGER NOT NULL,
owner_ip SCORE_INET NOT NULL,
filename VARCHAR(64) NOT NULL,
filesize INTEGER NOT NULL,
hash CHAR(32) UNIQUE NOT NULL,
ext CHAR(4) NOT NULL,
source VARCHAR(255),
width INTEGER NOT NULL,
height INTEGER NOT NULL,
posted TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
locked SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N,
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT
");
$db->execute("CREATE INDEX images_owner_id_idx ON images(owner_id)", []);
$db->execute("CREATE INDEX images_width_idx ON images(width)", []);
$db->execute("CREATE INDEX images_height_idx ON images(height)", []);
$db->execute("CREATE INDEX images_hash_idx ON images(hash)", []);
$db->create_table("tags", "
id SCORE_AIPK,
tag VARCHAR(64) UNIQUE NOT NULL,
count INTEGER NOT NULL DEFAULT 0
");
$db->execute("CREATE INDEX tags_tag_idx ON tags(tag)", []);
$db->create_table("image_tags", "
image_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
UNIQUE(image_id, tag_id),
FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
");
$db->execute("CREATE INDEX images_tags_image_id_idx ON image_tags(image_id)", []);
$db->execute("CREATE INDEX images_tags_tag_id_idx ON image_tags(tag_id)", []);
$db->execute("INSERT INTO config(name, value) VALUES('db_version', 11)");
$db->commit();
} catch (PDOException $e) {
throw new InstallerException(
"PDO Error:",
"<p>An error occurred while trying to create the database tables necessary for Shimmie.</p>
<p>Please check and ensure that the database configuration options are all correct.</p>
<p>{$e->getMessage()}</p>",
3
);
}
}
function write_config($dsn)
{
$file_content = "<" . "?php\ndefine('DATABASE_DSN', '$dsn');\n";
if (!file_exists("data/config")) {
mkdir("data/config", 0755, true);
}
if (file_put_contents("data/config/shimmie.conf.php", $file_content, LOCK_EX)) {
header("Location: index.php?flash=Installation%20complete");
die_nicely(
"Installation Successful",
"<p>If you aren't redirected, <a href=\"index.php\">click here to Continue</a>."
);
} else {
$h_file_content = htmlentities($file_content);
throw new InstallerException(
"File Permissions Error:",
"The web server isn't allowed to write to the config file; please copy
the text below, save it as 'data/config/shimmie.conf.php', and upload it into the shimmie
folder manually. Make sure that when you save it, there is no whitespace
before the \"&lt;?php\".
<p><textarea cols='80' rows='2'>$h_file_content</textarea>
<p>Once done, <a href='index.php'>click here to Continue</a>.",
0
);
}
}

View File

@ -1,82 +0,0 @@
<?php declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Logging convenience *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
define("SCORE_LOG_CRITICAL", 50);
define("SCORE_LOG_ERROR", 40);
define("SCORE_LOG_WARNING", 30);
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
*
* When parsing a user request, a flash message should give info to the user
* When taking action, a log event should be stored by the server
* Quite often, both of these happen at once, hence log_*() having $flash
*/
function log_msg(string $section, int $priority, string $message, ?string $flash=null)
{
global $page;
send_event(new LogEvent($section, $priority, $message));
$threshold = defined("CLI_LOG_LEVEL") ? CLI_LOG_LEVEL : 0;
if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && ($priority >= $threshold)) {
print date("c")." $section: $message\n";
ob_flush();
}
if (!is_null($flash)) {
$page->flash($flash);
}
}
// More shorthand ways of logging
function log_debug(string $section, string $message, ?string $flash=null)
{
log_msg($section, SCORE_LOG_DEBUG, $message, $flash);
}
function log_info(string $section, string $message, ?string $flash=null)
{
log_msg($section, SCORE_LOG_INFO, $message, $flash);
}
function log_warning(string $section, string $message, ?string $flash=null)
{
log_msg($section, SCORE_LOG_WARNING, $message, $flash);
}
function log_error(string $section, string $message, ?string $flash=null)
{
log_msg($section, SCORE_LOG_ERROR, $message, $flash);
}
function log_critical(string $section, string $message, ?string $flash=null)
{
log_msg($section, SCORE_LOG_CRITICAL, $message, $flash);
}
/**
* Get a unique ID for this request, useful for grouping log messages.
*/
function get_request_id(): string
{
static $request_id = null;
if (!$request_id) {
// not completely trustworthy, as a user can spoof this
if (@$_SERVER['HTTP_X_VARNISH']) {
$request_id = $_SERVER['HTTP_X_VARNISH'];
} else {
$request_id = "P" . uniqid();
}
}
return $request_id;
}

286
core/page.class.php Normal file
View File

@ -0,0 +1,286 @@
<?php
/**
* \page themes Themes
*
* Each extension has a theme with a specific name -- eg. the extension Setup
* which is stored in ext/setup/main.php will have a theme called SetupTheme
* stored in ext/setup/theme.php. If you want to customise it, create a class
* in the file themes/mytheme/setup.theme.php called CustomSetupTheme which
* extends SetupTheme and overrides some of its methods.
*
* Generally an extension should only deal with processing data; whenever it
* wants to display something, it should pass the data to be displayed to the
* theme object, and the theme will add the data into the global $page
* structure.
*
* A page should make sure that all the data it outputs is free from dangerous
* data by using html_escape(), url_escape(), or int_escape() as appropriate.
*
* Because some HTML can be placed anywhere according to the theme, coming up
* with the correct way to link to a page can be hard -- thus we have the
* make_link() function, which will take a path like "post/list" and turn it
* into a full and correct link, eg /myboard/post/list, /foo/index.php?q=post/list,
* etc depending on how things are set up. This should always be used to link
* to pages rather than hardcoding a path.
*
* Various other common functions are available as part of the Themelet class.
*/
/**
* A data structure for holding all the bits of data that make up a page.
*
* The various extensions all add whatever they want to this structure,
* then Layout turns it into HTML
*/
class Page {
/** @name Overall */
//@{
/** @private */
var $mode = "page";
/** @private */
var $type = "text/html; charset=utf-8";
/**
* Set what this page should do; "page", "data", or "redirect".
*/
public function set_mode($mode) {
$this->mode = $mode;
}
/**
* Set the page's MIME type
*/
public function set_type($type) {
$this->type = $type;
}
//@}
// ==============================================
/** @name "data" mode */
//@{
/** @private */
var $data = "";
/** @private */
var $filename = null;
/**
* Set the raw data to be sent
*/
public function set_data($data) {
$this->data = $data;
}
/**
* Set the recommended download filename
*/
public function set_filename($filename) {
$this->filename = $filename;
}
//@}
// ==============================================
/** @name "redirect" mode */
//@{
/** @private */
var $redirect = "";
/**
* Set the URL to redirect to (remember to use make_link() if linking
* to a page in the same site)
*/
public function set_redirect($redirect) {
$this->redirect = $redirect;
}
//@}
// ==============================================
/** @name "page" mode */
//@{
/** @privatesection */
var $title = "";
var $heading = "";
var $subheading = "";
var $quicknav = "";
var $html_headers = array();
var $http_headers = array();
var $blocks = array();
/** @publicsection */
/**
* Set the window title
*/
public function set_title($title) {
$this->title = $title;
}
/**
* Set the main heading
*/
public function set_heading($heading) {
$this->heading = $heading;
}
/**
* Set the sub heading
*/
public function set_subheading($subheading) {
$this->subheading = $subheading;
}
/**
* Add a line to the HTML head section
*/
public function add_html_header($line, $position=50) {
while(isset($this->html_headers[$position])) $position++;
$this->html_headers[$position] = $line;
}
/**
* Add a http header to be sent to the client.
*/
public function add_http_header($line, $position=50) {
while(isset($this->http_headers[$position])) $position++;
$this->http_headers[$position] = $line;
}
/**
* Get all the HTML headers that are currently set and return as a string.
*/
public function get_all_html_headers() {
$data = '';
foreach ($this->html_headers as $line) {
$data .= $line . "\n";
}
return $data;
}
/**
* Removes all currently set HTML headers. (Be careful..)
*/
public function delete_all_html_headers() {
$this->html_headers = array();
}
/**
* Add a Block of data
*/
public function add_block(Block $block) {
$this->blocks[] = $block;
}
//@}
// ==============================================
/**
* Display the page according to the mode and data given
*/
public function display() {
global $page, $user;
header("Content-type: ".$this->type);
header("X-Powered-By: SCore-".SCORE_VERSION);
if (!headers_sent()) {
foreach($this->http_headers as $head){ header($head); }
} else {
print "Error: Headers have already been sent to the client.";
}
switch($this->mode) {
case "page":
if(CACHE_HTTP) {
header("Vary: Cookie, Accept-Encoding");
if($user->is_anonymous() && $_SERVER["REQUEST_METHOD"] == "GET") {
header("Cache-control: public, max-age=600");
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 600) . ' GMT');
}
else {
#header("Cache-control: private, max-age=0");
header("Cache-control: no-cache");
header('Expires: ' . gmdate('D, d M Y H:i:s', time() - 600) . ' GMT');
}
}
#else {
# header("Cache-control: no-cache");
# header('Expires: ' . gmdate('D, d M Y H:i:s', time() - 600) . ' GMT');
#}
usort($this->blocks, "blockcmp");
$this->add_auto_html_headers();
$layout = new Layout();
$layout->display_page($page);
break;
case "data":
header("Content-Length: ".strlen($this->data));
if(!is_null($this->filename)) {
header('Content-Disposition: attachment; filename='.$this->filename);
}
print $this->data;
break;
case "redirect":
header('Location: '.$this->redirect);
print 'You should be redirected to <a href="'.$this->redirect.'">'.$this->redirect.'</a>';
break;
default:
print "Invalid page mode";
break;
}
}
protected function add_auto_html_headers() {
global $config;
$data_href = get_base_href();
$theme_name = $config->get_string('theme', 'default');
$this->add_html_header("<script type='text/javascript'>base_href = '$data_href';</script>");
# 404/static handler will map these to themes/foo/bar.ico or lib/static/bar.ico
$this->add_html_header("<link rel='icon' type='image/x-icon' href='$data_href/favicon.ico'>");
$this->add_html_header("<link rel='apple-touch-icon' href='$data_href/apple-touch-icon.png'>");
$css_files = array();
$css_latest = 0;
foreach(array_merge(zglob("lib/*.css"), zglob("ext/*/style.css"), zglob("themes/$theme_name/style.css")) as $css) {
$css_files[] = $css;
$css_latest = max($css_latest, filemtime($css));
}
$css_cache_file = data_path("cache/style.$css_latest.css");
if(!file_exists($css_cache_file)) {
$css_data = "";
foreach($css_files as $file) {
$file_data = file_get_contents($file);
$pattern = '/url[\s]*\([\s]*["\']?([^"\'\)]+)["\']?[\s]*\)/';
$replace = 'url("../../'.dirname($file).'/$1")';
$file_data = preg_replace($pattern, $replace, $file_data);
$css_data .= $file_data . "\n";
}
file_put_contents($css_cache_file, $css_data);
}
$this->add_html_header("<link rel='stylesheet' href='$data_href/$css_cache_file' type='text/css'>");
$js_files = array();
$js_latest = 0;
foreach(array_merge(zglob("lib/*.js"), zglob("ext/*/script.js"), zglob("themes/$theme_name/script.js")) as $js) {
$js_files[] = $js;
$js_latest = max($js_latest, filemtime($js));
}
$js_cache_file = data_path("cache/script.$js_latest.js");
if(!file_exists($js_cache_file)) {
$js_data = "";
foreach($js_files as $file) {
$js_data .= file_get_contents($file) . "\n";
}
file_put_contents($js_cache_file, $js_data);
}
$this->add_html_header("<script src='$data_href/$js_cache_file' type='text/javascript'></script>");
}
}
?>

View File

@ -1,106 +0,0 @@
<?php declare(strict_types=1);
// action_object_attribute
// action = create / view / edit / delete
// object = image / user / tag / setting
abstract class Permissions
{
public const CHANGE_SETTING = "change_setting"; # modify web-level settings, eg the config table
public const OVERRIDE_CONFIG = "override_config"; # modify sys-level settings, eg shimmie.conf.php
public const BIG_SEARCH = "big_search"; # search for more than 3 tags at once (speed mode only)
public const MANAGE_EXTENSION_LIST = "manage_extension_list";
public const MANAGE_ALIAS_LIST = "manage_alias_list";
public const MANAGE_AUTO_TAG = "manage_auto_tag";
public const MASS_TAG_EDIT = "mass_tag_edit";
public const VIEW_IP = "view_ip"; # view IP addresses associated with things
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
public const EDIT_USER_CLASS = "edit_user_class";
public const DELETE_USER = "delete_user";
public const CREATE_COMMENT = "create_comment";
public const DELETE_COMMENT = "delete_comment";
public const BYPASS_COMMENT_CHECKS = "bypass_comment_checks"; # spam etc
public const REPLACE_IMAGE = "replace_image";
public const CREATE_IMAGE = "create_image";
public const EDIT_IMAGE_TAG = "edit_image_tag";
public const EDIT_IMAGE_SOURCE = "edit_image_source";
public const EDIT_IMAGE_OWNER = "edit_image_owner";
public const EDIT_IMAGE_LOCK = "edit_image_lock";
public const EDIT_IMAGE_TITLE = "edit_image_title";
public const EDIT_IMAGE_RELATIONSHIPS = "edit_image_relationships";
public const EDIT_IMAGE_ARTIST = "edit_image_artist";
public const BULK_EDIT_IMAGE_TAG = "bulk_edit_image_tag";
public const BULK_EDIT_IMAGE_SOURCE = "bulk_edit_image_source";
public const DELETE_IMAGE = "delete_image";
public const BAN_IMAGE = "ban_image";
public const VIEW_EVENTLOG = "view_eventlog";
public const IGNORE_DOWNTIME = "ignore_downtime";
public const VIEW_REGISTRATIONS = "view_registrations";
public const CREATE_IMAGE_REPORT = "create_image_report";
public const VIEW_IMAGE_REPORT = "view_image_report"; # deal with reported images
public const WIKI_ADMIN = "wiki_admin";
public const EDIT_WIKI_PAGE = "edit_wiki_page";
public const DELETE_WIKI_PAGE = "delete_wiki_page";
public const MANAGE_BLOCKS = "manage_blocks";
public const MANAGE_ADMINTOOLS = "manage_admintools";
public const SEND_PM = "send_pm";
public const READ_PM = "read_pm";
public const VIEW_OTHER_PMS = "view_other_pms";
public const EDIT_FEATURE = "edit_feature";
public const BULK_EDIT_VOTE = "bulk_edit_vote";
public const EDIT_OTHER_VOTE = "edit_other_vote";
public const VIEW_SYSINTO = "view_sysinfo";
public const HELLBANNED = "hellbanned";
public const VIEW_HELLBANNED = "view_hellbanned";
public const PROTECTED = "protected"; # only admins can modify protected users (stops a moderator changing an admin's password)
public const EDIT_IMAGE_RATING = "edit_image_rating";
public const BULK_EDIT_IMAGE_RATING = "bulk_edit_image_rating";
public const VIEW_TRASH = "view_trash";
public const PERFORM_BULK_ACTIONS = "perform_bulk_actions";
public const BULK_ADD = "bulk_add";
public const EDIT_FILES = "edit_files";
public const EDIT_TAG_CATEGORIES = "edit_tag_categories";
public const RESCAN_MEDIA = "rescan_media";
public const SEE_IMAGE_VIEW_COUNTS = "see_image_view_counts";
public const EDIT_FAVOURITES = "edit_favourites";
public const ARTISTS_ADMIN = "artists_admin";
public const BLOTTER_ADMIN = "blotter_admin";
public const FORUM_ADMIN = "forum_admin";
public const NOTES_ADMIN = "notes_admin";
public const POOLS_ADMIN = "pools_admin";
public const TIPS_ADMIN = "tips_admin";
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

@ -1,777 +0,0 @@
<?php declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Things which should be in the core API *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
require_once "filetypes.php";
/**
* Return the unique elements of an array, case insensitively
*/
function array_iunique(array $array): array
{
$ok = [];
foreach ($array as $element) {
$found = false;
foreach ($ok as $existing) {
if (strtolower($element) == strtolower($existing)) {
$found = true;
break;
}
}
if (!$found) {
$ok[] = $element;
}
}
return $ok;
}
/**
* Figure out if an IP is in a specified range
*
* from https://uk.php.net/network
*/
function ip_in_range(string $IP, string $CIDR): bool
{
list($net, $mask) = explode("/", $CIDR);
$ip_net = ip2long($net);
$ip_mask = ~((1 << (32 - $mask)) - 1);
$ip_ip = ip2long($IP);
$ip_ip_net = $ip_ip & $ip_mask;
return ($ip_ip_net == $ip_net);
}
/**
* Delete an entire file heirachy
*
* from a patch by Christian Walde; only intended for use in the
* "extension manager" extension, but it seems to fit better here
*/
function deltree(string $f): void
{
//Because Windows (I know, bad excuse)
if (PHP_OS === 'WINNT') {
$real = realpath($f);
$path = realpath('./').'\\'.str_replace('/', '\\', $f);
if ($path != $real) {
rmdir($path);
} else {
foreach (glob($f.'/*') as $sf) {
if (is_dir($sf) && !is_link($sf)) {
deltree($sf);
} else {
unlink($sf);
}
}
rmdir($f);
}
} else {
if (is_link($f)) {
unlink($f);
} elseif (is_dir($f)) {
foreach (glob($f.'/*') as $sf) {
if (is_dir($sf) && !is_link($sf)) {
deltree($sf);
} else {
unlink($sf);
}
}
rmdir($f);
}
}
}
/**
* Copy an entire file hierarchy
*
* from a comment on https://uk.php.net/copy
*/
function full_copy(string $source, string $target): void
{
if (is_dir($source)) {
@mkdir($target);
$d = dir($source);
while (false !== ($entry = $d->read())) {
if ($entry == '.' || $entry == '..') {
continue;
}
$Entry = $source . '/' . $entry;
if (is_dir($Entry)) {
full_copy($Entry, $target . '/' . $entry);
continue;
}
copy($Entry, $target . '/' . $entry);
}
$d->close();
} else {
copy($source, $target);
}
}
/**
* Return a list of all the regular files in a directory and subdirectories
*/
function list_files(string $base, string $_sub_dir=""): array
{
assert(is_dir($base));
$file_list = [];
$files = [];
$dir = opendir("$base/$_sub_dir");
while ($f = readdir($dir)) {
$files[] = $f;
}
closedir($dir);
sort($files);
foreach ($files as $filename) {
$full_path = "$base/$_sub_dir/$filename";
if (!is_link($full_path) && is_dir($full_path)) {
if (!($filename == "." || $filename == "..")) {
//subdirectory found
$file_list = array_merge(
$file_list,
list_files($base, "$_sub_dir/$filename")
);
}
} else {
$full_path = str_replace("//", "/", $full_path);
$file_list[] = $full_path;
}
}
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');
try {
set_time_limit(0);
fseek($fp, $start);
$buffer = 1024 * 1024;
while (!feof($fp) && ($p = ftell($fp)) <= $end) {
if ($p + $buffer > $end) {
$buffer = $end - $p + 1;
}
echo fread($fp, $buffer);
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
// then we can just stop and not waste any more resources or bandwidth.
if (connection_status() != 0) {
break;
}
}
} finally {
fclose($fp);
}
}
if (!function_exists('http_parse_headers')) { #http://www.php.net/manual/en/function.http-parse-headers.php#112917
/**
* #return string[]
*/
function http_parse_headers(string $raw_headers): array
{
$headers = []; // $headers = [];
foreach (explode("\n", $raw_headers) as $i => $h) {
$h = explode(':', $h, 2);
if (isset($h[1])) {
if (!isset($headers[$h[0]])) {
$headers[$h[0]] = trim($h[1]);
} elseif (is_array($headers[$h[0]])) {
$tmp = array_merge($headers[$h[0]], [trim($h[1])]);
$headers[$h[0]] = $tmp;
} else {
$tmp = array_merge([$headers[$h[0]]], [trim($h[1])]);
$headers[$h[0]] = $tmp;
}
}
}
return $headers;
}
}
/**
* HTTP Headers can sometimes be lowercase which will cause issues.
* In cases like these, we need to make sure to check for them if the camelcase version does not exist.
*/
function findHeader(array $headers, string $name): ?string
{
if (!is_array($headers)) {
return null;
}
$header = null;
if (array_key_exists($name, $headers)) {
$header = $headers[$name];
} else {
$headers = array_change_key_case($headers); // convert all to lower case.
$lc_name = strtolower($name);
if (array_key_exists($lc_name, $headers)) {
$header = $headers[$lc_name];
}
}
return $header;
}
if (!function_exists('mb_strlen')) {
// TODO: we should warn the admin that they are missing multibyte support
function mb_strlen($str, $encoding)
{
return strlen($str);
}
function mb_internal_encoding($encoding)
{
}
function mb_strtolower($str)
{
return strtolower($str);
}
}
/** @noinspection PhpUnhandledExceptionInspection */
function getSubclassesOf(string $parent)
{
$result = [];
foreach (get_declared_classes() as $class) {
$rclass = new ReflectionClass($class);
if (!$rclass->isAbstract() && is_subclass_of($class, $parent)) {
$result[] = $class;
}
}
return $result;
}
/**
* Like glob, with support for matching very long patterns with braces.
*/
function zglob(string $pattern): array
{
$results = [];
if (preg_match('/(.*)\{(.*)\}(.*)/', $pattern, $matches)) {
$braced = explode(",", $matches[2]);
foreach ($braced as $b) {
$sub_pattern = $matches[1].$b.$matches[3];
$results = array_merge($results, zglob($sub_pattern));
}
return $results;
} else {
$r = glob($pattern);
if ($r) {
return $r;
} else {
return [];
}
}
}
/**
* Figure out the path to the shimmie install directory.
*
* eg if shimmie is visible at https://foo.com/gallery, this
* function should return /gallery
*
* PHP really, really sucks.
*/
function get_base_href(): string
{
if (defined("BASE_HREF") && !empty(BASE_HREF)) {
return BASE_HREF;
}
$possible_vars = ['SCRIPT_NAME', 'PHP_SELF', 'PATH_INFO', 'ORIG_PATH_INFO'];
$ok_var = null;
foreach ($possible_vars as $var) {
if (isset($_SERVER[$var]) && substr($_SERVER[$var], -4) === '.php') {
$ok_var = $_SERVER[$var];
break;
}
}
assert(!empty($ok_var));
$dir = dirname($ok_var);
$dir = str_replace("\\", "/", $dir);
$dir = str_replace("//", "/", $dir);
$dir = rtrim($dir, "/");
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);
return (substr($haystack, 0, $length) === $needle);
}
function endsWith(string $haystack, string $needle): bool
{
$length = strlen($needle);
$start = $length * -1; //negative
return (substr($haystack, $start) === $needle);
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Input / Output Sanitising *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/**
* Make some data safe for printing into HTML
*/
function html_escape(?string $input): string
{
if (is_null($input)) {
return "";
}
return htmlentities($input, ENT_QUOTES, "UTF-8");
}
/**
* Unescape data that was made safe for printing into HTML
*/
function html_unescape(string $input): string
{
return html_entity_decode($input, ENT_QUOTES, "UTF-8");
}
/**
* Make sure some data is safe to be used in integer context
*/
function int_escape(?string $input): int
{
/*
Side note, Casting to an integer is FASTER than using intval.
http://hakre.wordpress.com/2010/05/13/php-casting-vs-intval/
*/
if (is_null($input)) {
return 0;
}
return (int)$input;
}
/**
* Make sure some data is safe to be used in URL context
*/
function url_escape(?string $input): string
{
if (is_null($input)) {
return "";
}
$input = rawurlencode($input);
return $input;
}
/**
* Turn all manner of HTML / INI / JS / DB booleans into a PHP one
*/
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!
https://php.net/manual/en/filter.filters.validate.php
*/
if (is_bool($input)) {
return $input;
} elseif (is_int($input)) {
return ($input === 1);
} else {
$value = filter_var($input, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if (!is_null($value)) {
return $value;
} else {
$input = strtolower(trim($input));
return (
$input === "y" ||
$input === "yes" ||
$input === "t" ||
$input === "true" ||
$input === "on" ||
$input === "1"
);
}
}
}
/**
* Some functions require a callback function for escaping,
* but we might not want to alter the data
*/
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)) {
$val = $min;
}
if (!is_null($max) && $val > $max) {
$val = $max;
}
if (!is_null($min) && !is_null($max)) {
assert($val >= $min && $val <= $max, "$min <= $val <= $max");
}
return $val;
}
function xml_tag(string $name, array $attrs=[], array $children=[]): string
{
$xml = "<$name ";
foreach ($attrs as $k => $v) {
$xv = str_replace('&#039;', '&apos;', htmlspecialchars((string)$v, ENT_QUOTES));
$xml .= "$k=\"$xv\" ";
}
if (count($children) > 0) {
$xml .= ">\n";
foreach ($children as $child) {
$xml .= xml_tag($child);
}
$xml .= "</$name>\n";
} else {
$xml .= "/>\n";
}
return $xml;
}
/**
* Original PHP code by Chirp Internet: www.chirp.com.au
* Please acknowledge use of this code by including this header.
*/
function truncate(string $string, int $limit, string $break=" ", string $pad="..."): string
{
// return with no change if string is shorter than $limit
if (strlen($string) <= $limit) {
return $string;
}
// is $break present between $limit and the end of the string?
if (false !== ($breakpoint = strpos($string, $break, $limit))) {
if ($breakpoint < strlen($string) - 1) {
$string = substr($string, 0, $breakpoint) . $pad;
}
}
return $string;
}
/**
* Turn a human readable filesize into an integer, eg 1KB -> 1024
*/
function parse_shorthand_int(string $limit): int
{
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
case 'm': $value *= 1024; // fall through
/** @noinspection PhpMissingBreakStatementInspection */
// no break
case 'k': $value *= 1024; break;
default: $value = -1;
}
}
return (int)$value;
} else {
return -1;
}
}
/**
* Turn an integer into a human readable filesize, eg 1024 -> 1KB
*/
function to_shorthand_int(int $int): string
{
assert($int >= 0);
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));
} elseif ($int >= 1024) {
return sprintf("%.1fKB", $int / 1024);
} else {
return (string)$int;
}
}
const TIME_UNITS = ["s"=>60,"m"=>60,"h"=>24,"d"=>365,"y"=>PHP_INT_MAX];
function format_milliseconds(int $input): string
{
$output = "";
$remainder = floor($input / 1000);
foreach (TIME_UNITS as $unit=>$conversion) {
$count = $remainder % $conversion;
$remainder = floor($remainder / $conversion);
if ($count==0&&$remainder<1) {
break;
}
$output = "$count".$unit." ".$output;
}
return trim($output);
}
/**
* Turn a date into a time, a date, an "X minutes ago...", etc
*/
function autodate(string $date, bool $html=true): string
{
$cpu = date('c', strtotime($date));
$hum = date('F j, Y; H:i', strtotime($date));
return ($html ? "<time datetime='$cpu'>$hum</time>" : $hum);
}
/**
* Check if a given string is a valid date-time. ( Format: yyyy-mm-dd hh:mm:ss )
*/
function isValidDateTime(string $dateTime): bool
{
if (preg_match("/^(\d{4})-(\d{2})-(\d{2}) ([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/", $dateTime, $matches)) {
if (checkdate((int)$matches[2], (int)$matches[3], (int)$matches[1])) {
return true;
}
}
return false;
}
/**
* Check if a given string is a valid date. ( Format: yyyy-mm-dd )
*/
function isValidDate(string $date): bool
{
if (preg_match("/^(\d{4})-(\d{2})-(\d{2})$/", $date, $matches)) {
// checkdate wants (month, day, year)
if (checkdate((int)$matches[2], (int)$matches[3], (int)$matches[1])) {
return true;
}
}
return false;
}
function validate_input(array $inputs): array
{
$outputs = [];
foreach ($inputs as $key => $validations) {
$flags = explode(',', $validations);
if (in_array('bool', $flags) && !isset($_POST[$key])) {
$_POST[$key] = 'off';
}
if (in_array('optional', $flags)) {
if (!isset($_POST[$key]) || trim($_POST[$key]) == "") {
$outputs[$key] = null;
continue;
}
}
if (!isset($_POST[$key]) || trim($_POST[$key]) == "") {
throw new InvalidInput("Input '$key' not set");
}
$value = trim($_POST[$key]);
if (in_array('user_id', $flags)) {
$id = int_escape($value);
if (in_array('exists', $flags)) {
if (is_null(User::by_id($id))) {
throw new InvalidInput("User #$id does not exist");
}
}
$outputs[$key] = $id;
} elseif (in_array('user_name', $flags)) {
if (strlen($value) < 1) {
throw new InvalidInput("Username must be at least 1 character");
} elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $value)) {
throw new InvalidInput(
"Username contains invalid characters. Allowed characters are ".
"letters, numbers, dash, and underscore"
);
}
$outputs[$key] = $value;
} elseif (in_array('user_class', $flags)) {
global $_shm_user_classes;
if (!array_key_exists($value, $_shm_user_classes)) {
throw new InvalidInput("Invalid user class: ".html_escape($value));
}
$outputs[$key] = $value;
} elseif (in_array('email', $flags)) {
$outputs[$key] = trim($value);
} elseif (in_array('password', $flags)) {
$outputs[$key] = $value;
} elseif (in_array('int', $flags)) {
$value = trim($value);
if (empty($value) || !is_numeric($value)) {
throw new InvalidInput("Invalid int: ".html_escape($value));
}
$outputs[$key] = (int)$value;
} elseif (in_array('bool', $flags)) {
$outputs[$key] = bool_escape($value);
} elseif (in_array('date', $flags)) {
$outputs[$key] = date("Y-m-d H:i:s", strtotime(trim($value)));
} elseif (in_array('string', $flags)) {
if (in_array('trim', $flags)) {
$value = trim($value);
}
if (in_array('lower', $flags)) {
$value = strtolower($value);
}
if (in_array('not-empty', $flags)) {
throw new InvalidInput("$key must not be blank");
}
if (in_array('nullify', $flags)) {
if (empty($value)) {
$value = null;
}
}
$outputs[$key] = $value;
} else {
throw new InvalidInput("Unknown validation '$validations'");
}
}
return $outputs;
}
/**
* Translates all possible directory separators to the appropriate one for the current system,
* and removes any duplicate separators.
*/
function sanitize_path(string $path): string
{
return preg_replace('|[\\\\/]+|S', DIRECTORY_SEPARATOR, $path);
}
/**
* Combines all path segments specified, ensuring no duplicate separators occur,
* as well as converting all possible separators to the one appropriate for the current system.
*/
function join_path(string ...$paths): string
{
$output = "";
foreach ($paths as $path) {
if (empty($path)) {
continue;
}
$path = sanitize_path($path);
if (empty($output)) {
$output = $path;
} else {
$output = rtrim($output, DIRECTORY_SEPARATOR);
$path = ltrim($path, DIRECTORY_SEPARATOR);
$output .= DIRECTORY_SEPARATOR . $path;
}
}
return $output;
}
/**
* Perform callback on each item returned by an iterator.
*/
function iterator_map(callable $callback, iterator $iter): Generator
{
foreach ($iter as $i) {
yield call_user_func($callback, $i);
}
}
/**
* Perform callback on each item returned by an iterator and combine the result into an array.
*/
function iterator_map_to_array(callable $callback, iterator $iter): array
{
return iterator_to_array(iterator_map($callback, $iter));
}
function stringer($s)
{
if (is_array($s)) {
if (isset($s[0])) {
return "[" . implode(", ", array_map("stringer", $s)) . "]";
} else {
$pairs = [];
foreach ($s as $k=>$v) {
$pairs[] = "\"$k\"=>" . stringer($v);
}
return "[" . implode(", ", $pairs) . "]";
}
}
if (is_string($s)) {
return "\"$s\""; // FIXME: handle escaping quotes
}
return (string)$s;
}

View File

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

@ -1,128 +0,0 @@
<?php declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Event API *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/** @private */
global $_shm_event_listeners;
$_shm_event_listeners = [];
function _load_event_listeners(): void
{
global $_shm_event_listeners;
$cache_path = data_path("cache/shm_event_listeners.php");
if (SPEED_HAX && file_exists($cache_path)) {
require_once($cache_path);
} else {
_set_event_listeners();
if (SPEED_HAX) {
_dump_event_listeners($_shm_event_listeners, $cache_path);
}
}
}
function _clear_cached_event_listeners(): void
{
if (file_exists(data_path("cache/shm_event_listeners.php"))) {
unlink(data_path("cache/shm_event_listeners.php"));
}
}
function _set_event_listeners(): void
{
global $_shm_event_listeners;
$_shm_event_listeners = [];
foreach (getSubclassesOf("Extension") as $class) {
/** @var Extension $extension */
$extension = new $class();
// skip extensions which don't support our current database
if (!$extension->info->is_supported()) {
continue;
}
foreach (get_class_methods($extension) as $method) {
if (substr($method, 0, 2) == "on") {
$event = substr($method, 2) . "Event";
$pos = $extension->get_priority() * 100;
while (isset($_shm_event_listeners[$event][$pos])) {
$pos += 1;
}
$_shm_event_listeners[$event][$pos] = $extension;
}
}
}
}
function _dump_event_listeners(array $event_listeners, string $path): void
{
$p = "<"."?php\n";
foreach (getSubclassesOf("Extension") as $class) {
$p .= "\$$class = new $class(); ";
}
$p .= "\$_shm_event_listeners = array(\n";
foreach ($event_listeners as $event => $listeners) {
$p .= "\t'$event' => array(\n";
foreach ($listeners as $id => $listener) {
$p .= "\t\t$id => \$".get_class($listener).",\n";
}
$p .= "\t),\n";
}
$p .= ");\n";
file_put_contents($path, $p);
}
/** @private */
global $_shm_event_count;
$_shm_event_count = 0;
/**
* Send an event to all registered Extensions.
*/
function send_event(Event $event): Event
{
global $tracer_enabled;
global $_shm_event_listeners, $_shm_event_count, $_tracer;
if (!isset($_shm_event_listeners[get_class($event)])) {
return $event;
}
$method_name = "on".str_replace("Event", "", get_class($event));
// send_event() is performance sensitive, and with the number
// of times tracer gets called the time starts to add up
if ($tracer_enabled) {
$_tracer->begin(get_class($event));
}
// SHIT: https://bugs.php.net/bug.php?id=35106
$my_event_listeners = $_shm_event_listeners[get_class($event)];
ksort($my_event_listeners);
foreach ($my_event_listeners as $listener) {
if ($tracer_enabled) {
$_tracer->begin(get_class($listener));
}
if (method_exists($listener, $method_name)) {
$listener->$method_name($event);
}
if ($tracer_enabled) {
$_tracer->end();
}
if ($event->stop_processing===true) {
break;
}
}
$_shm_event_count++;
if ($tracer_enabled) {
$_tracer->end();
}
return $event;
}

48
core/sys_config.inc.php Normal file
View File

@ -0,0 +1,48 @@
<?php
/*
* First, load the user-specified settings
*/
@include_once "data/config/shimmie.conf.php";
@include_once "data/config/extensions.conf.php";
/**
* For any values that aren't defined in the above files, Shimmie
* will set the values to their defaults
*
* All of these can be over-ridden by placing a 'define' in data/config/shimmie.conf.php
*
* Do NOT change them in this file. These are the defaults only!
*
* Example:
* define("SPEED_HAX", true);
*
*/
function _d($name, $value) {if(!defined($name)) define($name, $value);}
_d("DATABASE_DSN", null); // string PDO database connection details
_d("CACHE_DSN", null); // string cache connection details
_d("DEBUG", false); // boolean print various debugging details
_d("DEBUG_SQL", false); // boolean dump SQL queries to data/sql.log
_d("COVERAGE", false); // boolean activate xdebug coverage monitor
_d("CONTEXT", null); // string file to log performance data into
_d("CACHE_MEMCACHE", false); // boolean store complete rendered pages in memcache
_d("CACHE_DIR", false); // boolean store complete rendered pages on disk
_d("CACHE_HTTP", false); // boolean output explicit HTTP caching headers
_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("COMPILE_ELS", false); // boolean pre-build the list of event listeners
_d("NICE_URLS", false); // boolean force niceurl mode
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
_d("VERSION", '2.4.0'); // string shimmie version
_d("TIMEZONE", null); // string timezone
_d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,comment,tag_list,index,tag_edit,alias_editor"); // extensions to always enable
_d("EXTRA_EXTS", ""); // optional extra extensions
/*
* Calculated settings - you should never need to change these
* directly, only the things they're built from
*/
_d("SCORE_VERSION", 's2hack/'.VERSION); // string SCore version
_d("ENABLED_EXTS", CORE_EXTS.",".EXTRA_EXTS);
?>

View File

@ -1,34 +0,0 @@
<?php declare(strict_types=1);
/**
* For any values that aren't defined in data/config/*.php,
* Shimmie will set the values to their defaults
*
* All of these can be over-ridden by placing a 'define' in
* data/config/shimmie.conf.php
*
* Do NOT change them in this file. These are the defaults only!
*
* Example:
* define("SPEED_HAX", true);
*/
function _d(string $name, $value): void
{
if (!defined($name)) {
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
_d("DEBUG", false); // boolean print various debugging details
_d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes
_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
_d("VERSION", "2.8.4$_g"); // string shimmie version
_d("TIMEZONE", null); // string timezone
_d("EXTRA_EXTS", ""); // string optional extra extensions
_d("BASE_HREF", null); // string force a specific base URL (default is auto-detect)
_d("TRACE_FILE", null); // string file to log performance data into
_d("TRACE_THRESHOLD", 0.0); // float log pages which take more time than this many seconds

View File

@ -1,51 +0,0 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
require_once "core/basepage.php";
class BasePageTest extends TestCase
{
public function test_page()
{
$page = new BasePage();
$page->set_mode(PageMode::PAGE);
ob_start();
$page->display();
ob_end_clean();
$this->assertTrue(true); // doesn't crash
}
public function test_file()
{
$page = new BasePage();
$page->set_mode(PageMode::FILE);
$page->set_file("tests/pbx_screenshot.jpg");
ob_start();
$page->display();
ob_end_clean();
$this->assertTrue(true); // doesn't crash
}
public function test_data()
{
$page = new BasePage();
$page->set_mode(PageMode::DATA);
$page->set_data("hello world");
ob_start();
$page->display();
ob_end_clean();
$this->assertTrue(true); // doesn't crash
}
public function test_redirect()
{
$page = new BasePage();
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect("/new/page");
ob_start();
$page->display();
ob_end_clean();
$this->assertTrue(true); // doesn't crash
}
}

View File

@ -1,17 +0,0 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
require_once "core/block.php";
class BlockTest extends TestCase
{
public function test_basic()
{
$b = new Block("head", "body");
$this->assertEquals(
"<section id='headmain'><h3 data-toggle-sel='#headmain' class=''>head</h3><div class='blockbody'>body</div></section>\n",
$b->get_html()
);
}
}

View File

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

@ -1,223 +0,0 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
require_once "core/polyfills.php";
class PolyfillsTest extends TestCase
{
public function test_html_escape()
{
$this->assertEquals(
html_escape("Foo & <waffles>"),
"Foo &amp; &lt;waffles&gt;"
);
$this->assertEquals(
html_unescape("Foo &amp; &lt;waffles&gt;"),
"Foo & <waffles>"
);
$x = "Foo &amp; &lt;waffles&gt;";
$this->assertEquals(html_escape(html_unescape($x)), $x);
}
public function test_int_escape()
{
$this->assertEquals(int_escape(""), 0);
$this->assertEquals(int_escape("1"), 1);
$this->assertEquals(int_escape("-1"), -1);
$this->assertEquals(int_escape("-1.5"), -1);
$this->assertEquals(int_escape(null), 0);
}
public function test_url_escape()
{
$this->assertEquals(url_escape("^\o/^"), "%5E%5Co%2F%5E");
$this->assertEquals(url_escape(null), "");
}
public function test_bool_escape()
{
$this->assertTrue(bool_escape(true));
$this->assertFalse(bool_escape(false));
$this->assertTrue(bool_escape("true"));
$this->assertFalse(bool_escape("false"));
$this->assertTrue(bool_escape("t"));
$this->assertFalse(bool_escape("f"));
$this->assertTrue(bool_escape("T"));
$this->assertFalse(bool_escape("F"));
$this->assertTrue(bool_escape("yes"));
$this->assertFalse(bool_escape("no"));
$this->assertTrue(bool_escape("Yes"));
$this->assertFalse(bool_escape("No"));
$this->assertTrue(bool_escape("on"));
$this->assertFalse(bool_escape("off"));
$this->assertTrue(bool_escape(1));
$this->assertFalse(bool_escape(0));
$this->assertTrue(bool_escape("1"));
$this->assertFalse(bool_escape("0"));
}
public function test_clamp()
{
$this->assertEquals(clamp(0, 5, 10), 5);
$this->assertEquals(clamp(5, 5, 10), 5);
$this->assertEquals(clamp(7, 5, 10), 7);
$this->assertEquals(clamp(10, 5, 10), 10);
$this->assertEquals(clamp(15, 5, 10), 10);
}
public function test_xml_tag()
{
$this->assertEquals(
"<test foo=\"bar\" >\n<cake />\n</test>\n",
xml_tag("test", ["foo"=>"bar"], ["cake"])
);
}
public function test_truncate()
{
$this->assertEquals(truncate("test words", 10), "test words");
$this->assertEquals(truncate("test...", 9), "test...");
$this->assertEquals(truncate("test...", 6), "test...");
$this->assertEquals(truncate("te...", 2), "te...");
}
public function test_to_shorthand_int()
{
$this->assertEquals(to_shorthand_int(1231231231), "1.1GB");
$this->assertEquals(to_shorthand_int(2), "2");
}
public function test_parse_shorthand_int()
{
$this->assertEquals(parse_shorthand_int("foo"), -1);
$this->assertEquals(parse_shorthand_int("32M"), 33554432);
$this->assertEquals(parse_shorthand_int("43.4KB"), 44441);
$this->assertEquals(parse_shorthand_int("1231231231"), 1231231231);
}
public function test_format_milliseconds()
{
$this->assertEquals("", format_milliseconds(5));
$this->assertEquals("5s", format_milliseconds(5000));
$this->assertEquals("1y 213d 16h 53m 20s", format_milliseconds(50000000000));
}
public function test_autodate()
{
$this->assertEquals(
"<time datetime='2012-06-23T16:14:22+00:00'>June 23, 2012; 16:14</time>",
autodate("2012-06-23 16:14:22")
);
}
public function test_validate_input()
{
$_POST = [
"foo" => " bar ",
"to_null" => " ",
"num" => "42",
];
$this->assertEquals(
["foo"=>"bar"],
validate_input(["foo"=>"string,trim,lower"])
);
//$this->assertEquals(
// ["to_null"=>null],
// validate_input(["to_null"=>"string,trim,nullify"])
//);
$this->assertEquals(
["num"=>42],
validate_input(["num"=>"int"])
);
}
public function test_sanitize_path()
{
$this->assertEquals(
"one",
sanitize_path("one")
);
$this->assertEquals(
"one".DIRECTORY_SEPARATOR."two",
sanitize_path("one\\two")
);
$this->assertEquals(
"one".DIRECTORY_SEPARATOR."two",
sanitize_path("one/two")
);
$this->assertEquals(
"one".DIRECTORY_SEPARATOR."two",
sanitize_path("one\\\\two")
);
$this->assertEquals(
"one".DIRECTORY_SEPARATOR."two",
sanitize_path("one//two")
);
$this->assertEquals(
"one".DIRECTORY_SEPARATOR."two",
sanitize_path("one\\\\\\two")
);
$this->assertEquals(
"one".DIRECTORY_SEPARATOR."two",
sanitize_path("one///two")
);
$this->assertEquals(
DIRECTORY_SEPARATOR."one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR,
sanitize_path("\\/one/\\/\\/two\\/")
);
}
public function test_join_path()
{
$this->assertEquals(
"one",
join_path("one")
);
$this->assertEquals(
"one".DIRECTORY_SEPARATOR."two",
join_path("one", "two")
);
$this->assertEquals(
"one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR."three",
join_path("one", "two", "three")
);
$this->assertEquals(
"one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR."three",
join_path("one/two", "three")
);
$this->assertEquals(
DIRECTORY_SEPARATOR."one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR."three".DIRECTORY_SEPARATOR,
join_path("\\/////\\\\one/\///"."\\//two\/\\//\\//", "//\/\\\/three/\\/\/")
);
}
public function test_stringer()
{
$this->assertEquals(
'["foo"=>"bar", "baz"=>[1, 2, 3], "qux"=>["a"=>"b"]]',
stringer(["foo"=>"bar", "baz"=>[1,2,3], "qux"=>["a"=>"b"]])
);
}
}

View File

@ -1,22 +0,0 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
require_once "core/imageboard/tag.php";
class TagTest extends TestCase
{
public function test_caret()
{
$this->assertEquals("foo", Tag::decaret("foo"));
$this->assertEquals("foo?", Tag::decaret("foo^q"));
$this->assertEquals("a^b/c\\d?e&f", Tag::decaret("a^^b^sc^bd^qe^af"));
}
public function test_decaret()
{
$this->assertEquals("foo", Tag::caret("foo"));
$this->assertEquals("foo^q", Tag::caret("foo?"));
$this->assertEquals("a^^b^sc^bd^qe^af", Tag::caret("a^b/c\\d?e&f"));
}
}

View File

@ -1,101 +0,0 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
require_once "core/urls.php";
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()
{
// relative to shimmie install
$this->assertEquals(
"http://<cli command>/test/foo",
make_http("foo")
);
// relative to web server
$this->assertEquals(
"http://<cli command>/foo",
make_http("/foo")
);
// absolute
$this->assertEquals(
"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,86 +0,0 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
require_once "core/util.php";
class UtilTest extends TestCase
{
public function test_warehouse_path()
{
$hash = "7ac19c10d6859415";
$this->assertEquals(
join_path(DATA_DIR, "base", $hash),
warehouse_path("base", $hash, false, 0)
);
$this->assertEquals(
join_path(DATA_DIR, "base", "7a", $hash),
warehouse_path("base", $hash, false, 1)
);
$this->assertEquals(
join_path(DATA_DIR, "base", "7a", "c1", $hash),
warehouse_path("base", $hash, false, 2)
);
$this->assertEquals(
join_path(DATA_DIR, "base", "7a", "c1", "9c", $hash),
warehouse_path("base", $hash, false, 3)
);
$this->assertEquals(
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", $hash),
warehouse_path("base", $hash, false, 4)
);
$this->assertEquals(
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", $hash),
warehouse_path("base", $hash, false, 5)
);
$this->assertEquals(
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", $hash),
warehouse_path("base", $hash, false, 6)
);
$this->assertEquals(
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", $hash),
warehouse_path("base", $hash, false, 7)
);
$this->assertEquals(
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
warehouse_path("base", $hash, false, 8)
);
$this->assertEquals(
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
warehouse_path("base", $hash, false, 9)
);
$this->assertEquals(
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
warehouse_path("base", $hash, false, 10)
);
}
public function test_load_balance_url()
{
$hash = "7ac19c10d6859415";
$ext = "jpg";
// pseudo-randomly select one of the image servers, balanced in given ratio
$this->assertEquals(
"https://baz.mycdn.com/7ac19c10d6859415.jpg",
load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash)
);
// N'th and N+1'th results should be different
$this->assertNotEquals(
load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash, 0),
load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash, 1)
);
}
}

View File

@ -1,114 +0,0 @@
<?php declare(strict_types=1);
class Link
{
public $page;
public $query;
public function __construct(?string $page=null, ?string $query=null)
{
$this->page = $page;
$this->query = $query;
}
public function make_link(): string
{
return make_link($this->page, $this->query);
}
}
/**
* Figure out the correct way to link to a page, taking into account
* things like the nice URLs setting.
*
* eg make_link("post/list") becomes "/v2/index.php?q=post/list"
*/
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)) {
$parts['path'] = "$install_dir/$page";
} else {
$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);
return unparse_url($parts);
}
/**
* Take the current URL and modify some parameters
*/
function modify_current_url(array $changes): string
{
return modify_url($_SERVER['REQUEST_URI'], $changes);
}
function modify_url(string $url, array $changes): string
{
$parts = parse_url($url);
$params = [];
if (isset($parts['query'])) {
parse_str($parts['query'], $params);
}
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 unparse_url($parts);
}
/**
* Turn a relative link into an absolute one, including hostname
*/
function make_http(string $link): string
{
if (strpos($link, "://") > 0) {
return $link;
}
if (strlen($link) > 0 && $link[0] != '/') {
$link = get_base_href() . '/' . $link;
}
$protocol = is_https_enabled() ? "https://" : "http://";
$link = $protocol . $_SERVER["HTTP_HOST"] . $link;
$link = str_replace("/./", "/", $link);
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'];
}

199
core/user.class.php Normal file
View File

@ -0,0 +1,199 @@
<?php
/** @private */
function _new_user($row) {
return new User($row);
}
/**
* An object representing a row in the "users" table.
*
* The currently logged in user will always be accessable via the global variable $user
*/
class User {
var $id;
var $name;
var $email;
var $join_date;
var $class;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Initialisation *
* *
* User objects shouldn't be created directly, they should be *
* fetched from the database like so: *
* *
* $user = User::by_name("bob"); *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/**
* One will very rarely construct a user directly, more common
* would be to use User::by_id, User::by_session, etc
*/
public function User($row) {
global $_user_classes;
$this->id = int_escape($row['id']);
$this->name = $row['name'];
$this->email = $row['email'];
$this->join_date = $row['joindate'];
$this->passhash = $row['pass'];
$this->class = $_user_classes[$row["class"]];
}
public static function by_session(/*string*/ $name, /*string*/ $session) {
global $config, $database;
if($database->engine->name === "mysql") {
$query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess";
}
else {
$query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess";
}
$row = $database->get_row($query, array("name"=>$name, "ip"=>get_session_ip($config), "sess"=>$session));
return is_null($row) ? null : new User($row);
}
public static function by_id(/*int*/ $id) {
assert(is_numeric($id));
global $database;
if($id === 1) {
$cached = $database->cache->get('user-id:'.$id);
if($cached) return new User($cached);
}
$row = $database->get_row("SELECT * FROM users WHERE id = :id", array("id"=>$id));
if($id === 1) $database->cache->set('user-id:'.$id, $row, 300);
return is_null($row) ? null : new User($row);
}
public static function by_name(/*string*/ $name) {
assert(is_string($name));
global $database;
$row = $database->get_row("SELECT * FROM users WHERE name = :name", array("name"=>$name));
return is_null($row) ? null : new User($row);
}
public static function by_name_and_hash(/*string*/ $name, /*string*/ $hash) {
assert(is_string($name));
assert(is_string($hash));
assert(strlen($hash) == 32);
global $database;
$row = $database->get_row("SELECT * FROM users WHERE name = :name AND pass = :hash", array("name"=>$name, "hash"=>$hash));
return is_null($row) ? null : new User($row);
}
public static function by_list(/*int*/ $offset, /*int*/ $limit=50) {
assert(is_numeric($offset));
assert(is_numeric($limit));
global $database;
$rows = $database->get_all("SELECT * FROM users WHERE id >= :start AND id < :end", array("start"=>$offset, "end"=>$offset+$limit));
return array_map("_new_user", $rows);
}
/*
* useful user object functions start here
*/
public function can($ability) {
return $this->class->can($ability);
}
/**
* Test if this user is anonymous (not logged in)
*
* @retval bool
*/
public function is_anonymous() {
global $config;
return ($this->id === $config->get_int('anon_id'));
}
/**
* Test if this user is logged in
*
* @retval bool
*/
public function is_logged_in() {
global $config;
return ($this->id !== $config->get_int('anon_id'));
}
/**
* Test if this user is an administrator
*
* @retval bool
*/
public function is_admin() {
return ($this->class->name === "admin");
}
public function set_class(/*string*/ $class) {
assert(is_string($class));
global $database;
$database->Execute("UPDATE users SET class=:class WHERE id=:id", array("class"=>$class, "id"=>$this->id));
log_info("core-user", 'Set class for '.$this->name.' to '.$class);
}
public function set_password(/*string*/ $password) {
global $database;
$hash = md5(strtolower($this->name) . $password);
$database->Execute("UPDATE users SET pass=:hash WHERE id=:id", array("hash"=>$hash, "id"=>$this->id));
log_info("core-user", 'Set password for '.$this->name);
}
public function set_email(/*string*/ $address) {
global $database;
$database->Execute("UPDATE users SET email=:email WHERE id=:id", array("email"=>$address, "id"=>$this->id));
log_info("core-user", 'Set email for '.$this->name);
}
/**
* Get a snippet of HTML which will render the user's avatar, be that
* a local file, a remote file, a gravatar, a something else, etc
* @retval String of HTML
*/
public function get_avatar_html() {
// FIXME: configurable
global $config;
if($config->get_string("avatar_host") === "gravatar") {
if(!empty($this->email)) {
$hash = md5(strtolower($this->email));
$s = $config->get_string("avatar_gravatar_size");
$d = urlencode($config->get_string("avatar_gravatar_default"));
$r = $config->get_string("avatar_gravatar_rating");
return "<img class=\"avatar gravatar\" src=\"http://www.gravatar.com/avatar/$hash.jpg?s=$s&d=$d&r=$r\">";
}
}
return "";
}
/**
* Get an auth token to be used in POST forms
*
* password = secret, avoid storing directly
* passhash = md5(password), so someone who gets to the database can't get passwords
* sesskey = md5(passhash . IP), so if it gets sniffed it can't be used from another IP,
* and it can't be used to get the passhash to generate new sesskeys
* authtok = md5(sesskey, salt), presented to the user in web forms, to make sure that
* the form was generated within the session. Salted and re-hashed so that
* reading a web page from the user's cache doesn't give access to the session key
*
* @retval String containing auth token (MD5sum)
*/
public function get_auth_token() {
global $config;
$salt = DATABASE_DSN;
$addr = get_session_ip($config);
return md5(md5($this->passhash . $addr) . "salty-csrf-" . $salt);
}
public function get_auth_html() {
$at = $this->get_auth_token();
return '<input type="hidden" name="auth_token" value="'.$at.'">';
}
public function check_auth_token() {
return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token());
}
}
?>

View File

@ -1,262 +0,0 @@
<?php declare(strict_types=1);
function _new_user(array $row): User
{
return new User($row);
}
/**
* Class User
*
* An object representing a row in the "users" table.
*
* The currently logged in user will always be accessible via the global variable $user.
*/
class User
{
/** @var int */
public $id;
/** @var string */
public $name;
/** @var string */
public $email;
public $join_date;
/** @var string */
public $passhash;
/** @var UserClass */
public $class;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Initialisation *
* *
* User objects shouldn't be created directly, they should be *
* fetched from the database like so: *
* *
* $user = User::by_name("bob"); *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/**
* One will very rarely construct a user directly, more common
* would be to use User::by_id, User::by_session, etc.
*
* @throws SCoreException
*/
public function __construct(array $row)
{
global $_shm_user_classes;
$this->id = int_escape((string)$row['id']);
$this->name = $row['name'];
$this->email = $row['email'];
$this->join_date = $row['joindate'];
$this->passhash = $row['pass'];
if (array_key_exists($row["class"], $_shm_user_classes)) {
$this->class = $_shm_user_classes[$row["class"]];
} else {
throw new SCoreException("User '{$this->name}' has invalid class '{$row["class"]}'");
}
}
public static function by_session(string $name, string $session): ?User
{
global $cache, $config, $database;
$row = $cache->get("user-session:$name-$session");
if (!$row) {
if ($database->get_driver_name() === DatabaseDriver::MYSQL) {
$query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess";
} else {
$query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess";
}
$row = $database->get_row($query, ["name"=>$name, "ip"=>get_session_ip($config), "sess"=>$session]);
$cache->set("user-session:$name-$session", $row, 600);
}
return is_null($row) ? null : new User($row);
}
public static function by_id(int $id): ?User
{
global $cache, $database;
if ($id === 1) {
$cached = $cache->get('user-id:'.$id);
if ($cached) {
return new User($cached);
}
}
$row = $database->get_row("SELECT * FROM users WHERE id = :id", ["id"=>$id]);
if ($id === 1) {
$cache->set('user-id:'.$id, $row, 600);
}
return is_null($row) ? null : new User($row);
}
public static function by_name(string $name): ?User
{
global $database;
$row = $database->get_row("SELECT * FROM users WHERE LOWER(name) = LOWER(:name)", ["name"=>$name]);
return is_null($row) ? null : new User($row);
}
public static function name_to_id(string $name): int
{
$u = User::by_name($name);
if (is_null($u)) {
throw new ScoreException("Can't find any user named $name");
} else {
return $u->id;
}
}
public static function by_name_and_pass(string $name, string $pass): ?User
{
$my_user = User::by_name($name);
// If user tried to log in as "foo bar" and failed, try "foo_bar"
if (!$my_user && strpos($name, " ") !== false) {
$my_user = User::by_name(str_replace(" ", "_", $name));
}
if ($my_user) {
if ($my_user->passhash == md5(strtolower($name) . $pass)) {
log_info("core-user", "Migrating from md5 to bcrypt for $name");
$my_user->set_password($pass);
}
if (password_verify($pass, $my_user->passhash)) {
log_info("core-user", "Logged in as $name ({$my_user->class->name})");
return $my_user;
} else {
log_warning("core-user", "Failed to log in as $name (Invalid password)");
}
} else {
log_warning("core-user", "Failed to log in as $name (Invalid username)");
}
return null;
}
/* useful user object functions start here */
public function can(string $ability): bool
{
return $this->class->can($ability);
}
public function is_anonymous(): bool
{
global $config;
return ($this->id === $config->get_int('anon_id'));
}
public function is_logged_in(): bool
{
global $config;
return ($this->id !== $config->get_int('anon_id'));
}
public function set_class(string $class): void
{
global $database;
$database->Execute("UPDATE users SET class=:class WHERE id=:id", ["class"=>$class, "id"=>$this->id]);
log_info("core-user", 'Set class for '.$this->name.' to '.$class);
}
public function set_name(string $name): void
{
global $database;
if (User::by_name($name)) {
throw new ScoreException("Desired username is already in use");
}
$old_name = $this->name;
$this->name = $name;
$database->Execute("UPDATE users SET name=:name WHERE id=:id", ["name"=>$this->name, "id"=>$this->id]);
log_info("core-user", "Changed username for {$old_name} to {$this->name}");
}
public function set_password(string $password): void
{
global $database;
$hash = password_hash($password, PASSWORD_BCRYPT);
if (is_string($hash)) {
$this->passhash = $hash;
$database->Execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash"=>$this->passhash, "id"=>$this->id]);
log_info("core-user", 'Set password for '.$this->name);
} else {
throw new SCoreException("Failed to hash password");
}
}
public function set_email(string $address): void
{
global $database;
$database->Execute("UPDATE users SET email=:email WHERE id=:id", ["email"=>$address, "id"=>$this->id]);
log_info("core-user", 'Set email for '.$this->name);
}
/**
* Get a snippet of HTML which will render the user's avatar, be that
* a local file, a remote file, a gravatar, a something else, etc.
*/
public function get_avatar_html(): string
{
// FIXME: configurable
global $config;
if ($config->get_string("avatar_host") === "gravatar") {
if (!empty($this->email)) {
$hash = md5(strtolower($this->email));
$s = $config->get_string("avatar_gravatar_size");
$d = urlencode($config->get_string("avatar_gravatar_default"));
$r = $config->get_string("avatar_gravatar_rating");
$cb = date("Y-m-d");
return "<img alt='avatar' class=\"avatar gravatar\" src=\"https://www.gravatar.com/avatar/$hash.jpg?s=$s&d=$d&r=$r&cacheBreak=$cb\">";
}
}
return "";
}
/**
* Get an auth token to be used in POST forms
*
* password = secret, avoid storing directly
* passhash = bcrypt(password), so someone who gets to the database can't get passwords
* sesskey = md5(passhash . IP), so if it gets sniffed it can't be used from another IP,
* and it can't be used to get the passhash to generate new sesskeys
* authtok = md5(sesskey, salt), presented to the user in web forms, to make sure that
* the form was generated within the session. Salted and re-hashed so that
* reading a web page from the user's cache doesn't give access to the session key
*/
public function get_auth_token(): string
{
global $config;
$salt = DATABASE_DSN;
$addr = get_session_ip($config);
return md5(md5($this->passhash . $addr) . "salty-csrf-" . $salt);
}
public function get_auth_html(): string
{
$at = $this->get_auth_token();
return '<input type="hidden" name="auth_token" value="'.$at.'">';
}
public function check_auth_token(): bool
{
if (defined("UNITTEST")) {
return true;
}
return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token());
}
public function ensure_authed(): void
{
if (!$this->check_auth_token()) {
die("Invalid auth token");
}
}
}

161
core/userclass.class.php Normal file
View File

@ -0,0 +1,161 @@
<?php
$_user_classes = array();
class UserClass {
var $name = null;
var $parent = null;
var $abilities = array();
public function __construct($name, $parent=null, $abilities=array()) {
global $_user_classes;
$this->name = $name;
$this->abilities = $abilities;
if(!is_null($parent)) {
$this->parent = $_user_classes[$parent];
}
$_user_classes[$name] = $this;
}
public function can(/*string*/ $ability) {
global $config;
if(array_key_exists($ability, $this->abilities)) {
$val = $this->abilities[$ability];
return $val;
}
else if(!is_null($this->parent)) {
return $this->parent->can($ability);
}
else {
global $_user_classes;
$min_dist = 9999;
$min_ability = null;
foreach($_user_classes['base']->abilities as $a => $cando) {
$v = levenshtein($ability, $a);
if($v < $min_dist) {
$min_dist = $v;
$min_ability = $a;
}
}
throw new SCoreException("Unknown ability '".html_escape($ability)."'. Did the developer mean '".html_escape($min_ability)."'?");
}
}
}
// action_object_attribute
// action = create / view / edit / delete
// object = image / user / tag / setting
new UserClass("base", null, array(
"change_setting" => False, # modify web-level settings, eg the config table
"override_config" => False, # modify sys-level settings, eg shimmie.conf.php
"big_search" => False, # search for more than 3 tags at once (speed mode only)
"manage_extension_list" => False,
"manage_alias_list" => False,
"mass_tag_edit" => False,
"view_ip" => False, # view IP addresses associated with things
"ban_ip" => False,
"edit_user_password" => False,
"edit_user_info" => False, # email address, etc
"edit_user_class" => False,
"delete_user" => False,
"create_comment" => False,
"delete_comment" => False,
"replace_image" => False,
"create_image" => False,
"edit_image_tag" => False,
"edit_image_source" => False,
"edit_image_owner" => False,
"edit_image_lock" => False,
"bulk_edit_image_tag" => False,
"bulk_edit_image_source" => False,
"delete_image" => False,
"ban_image" => False,
"view_eventlog" => False,
"ignore_downtime" => False,
"create_image_report" => False,
"view_image_report" => False, # deal with reported images
"edit_wiki_page" => False,
"delete_wiki_page" => False,
"manage_blocks" => False,
"manage_admintools" => False,
"view_other_pms" => False,
"edit_feature" => False,
"bulk_edit_vote" => False,
"edit_other_vote" => False,
"view_sysinfo" => False,
"protected" => False, # only admins can modify protected users (stops a moderator changing an admin's password)
));
new UserClass("anonymous", "base", array(
));
new UserClass("user", "base", array(
"big_search" => True,
"create_image" => True,
"create_comment" => True,
"edit_image_tag" => True,
"edit_image_source" => True,
"create_image_report" => True,
));
new UserClass("admin", "base", array(
"change_setting" => True,
"override_config" => True,
"big_search" => True,
"edit_image_lock" => True,
"view_ip" => True,
"ban_ip" => True,
"edit_user_password" => True,
"edit_user_info" => True,
"edit_user_class" => True,
"delete_user" => True,
"create_image" => True,
"delete_image" => True,
"ban_image" => True,
"create_comment" => True,
"delete_comment" => True,
"replace_image" => True,
"manage_extension_list" => True,
"manage_alias_list" => True,
"edit_image_tag" => True,
"edit_image_source" => True,
"edit_image_owner" => True,
"bulk_edit_image_tag" => True,
"bulk_edit_image_source" => True,
"mass_tag_edit" => True,
"create_image_report" => True,
"view_image_report" => True,
"edit_wiki_page" => True,
"delete_wiki_page" => True,
"view_eventlog" => True,
"manage_blocks" => True,
"manage_admintools" => True,
"ignore_downtime" => True,
"view_other_pms" => True,
"edit_feature" => True,
"bulk_edit_vote" => True,
"edit_other_vote" => True,
"view_sysinfo" => True,
"protected" => True,
));
if(file_exists("data/config/user-classes.conf.php")) {
require_once("data/config/user-classes.conf.php");
}
?>

View File

@ -1,211 +0,0 @@
<?php declare(strict_types=1);
/**
* @global UserClass[] $_shm_user_classes
*/
global $_shm_user_classes;
$_shm_user_classes = [];
/**
* Class UserClass
*/
class UserClass
{
/**
* @var ?string
*/
public $name = null;
/**
* @var ?UserClass
*/
public $parent = null;
/**
* @var array
*/
public $abilities = [];
public function __construct(string $name, string $parent = null, array $abilities = [])
{
global $_shm_user_classes;
$this->name = $name;
$this->abilities = $abilities;
if (!is_null($parent)) {
$this->parent = $_shm_user_classes[$parent];
}
$_shm_user_classes[$name] = $this;
}
/**
* Determine if this class of user can perform an action or has ability.
*
* @throws SCoreException
*/
public function can(string $ability): bool
{
if (array_key_exists($ability, $this->abilities)) {
return $this->abilities[$ability];
} elseif (!is_null($this->parent)) {
return $this->parent->can($ability);
} else {
global $_shm_user_classes;
$min_dist = 9999;
$min_ability = null;
foreach ($_shm_user_classes['base']->abilities as $a => $cando) {
$v = levenshtein($ability, $a);
if ($v < $min_dist) {
$min_dist = $v;
$min_ability = $a;
}
}
throw new SCoreException("Unknown ability '$ability'. Did the developer mean '$min_ability'?");
}
}
}
$_all_false = [];
foreach ((new ReflectionClass('Permissions'))->getConstants() as $k => $v) {
$_all_false[$v] = false;
}
new UserClass("base", null, $_all_false);
unset($_all_false);
// Ghost users can't do anything
new UserClass("ghost", "base", [
]);
// Anonymous users can't do anything by default, but
// the admin might grant them some permissions
new UserClass("anonymous", "base", [
Permissions::CREATE_USER => true,
]);
new UserClass("user", "base", [
Permissions::BIG_SEARCH => true,
Permissions::CREATE_IMAGE => true,
Permissions::CREATE_COMMENT => true,
Permissions::EDIT_IMAGE_TAG => true,
Permissions::EDIT_IMAGE_SOURCE => true,
Permissions::EDIT_IMAGE_TITLE => true,
Permissions::EDIT_IMAGE_RELATIONSHIPS => true,
Permissions::EDIT_IMAGE_ARTIST => true,
Permissions::CREATE_IMAGE_REPORT => true,
Permissions::EDIT_IMAGE_RATING => true,
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", [
Permissions::HELLBANNED => true,
]);
new UserClass("admin", "base", [
Permissions::CHANGE_SETTING => true,
Permissions::OVERRIDE_CONFIG => true,
Permissions::BIG_SEARCH => true,
Permissions::MANAGE_EXTENSION_LIST => true,
Permissions::MANAGE_ALIAS_LIST => true,
Permissions::MANAGE_AUTO_TAG => true,
Permissions::MASS_TAG_EDIT => true,
Permissions::VIEW_IP => true,
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,
Permissions::EDIT_USER_CLASS => true,
Permissions::DELETE_USER => true,
Permissions::CREATE_COMMENT => true,
Permissions::DELETE_COMMENT => true,
Permissions::BYPASS_COMMENT_CHECKS => true,
Permissions::REPLACE_IMAGE => true,
Permissions::CREATE_IMAGE => true,
Permissions::EDIT_IMAGE_TAG => true,
Permissions::EDIT_IMAGE_SOURCE => true,
Permissions::EDIT_IMAGE_OWNER => true,
Permissions::EDIT_IMAGE_LOCK => true,
Permissions::EDIT_IMAGE_TITLE => true,
Permissions::EDIT_IMAGE_RELATIONSHIPS => true,
Permissions::EDIT_IMAGE_ARTIST => true,
Permissions::BULK_EDIT_IMAGE_TAG => true,
Permissions::BULK_EDIT_IMAGE_SOURCE => true,
Permissions::DELETE_IMAGE => true,
Permissions::BAN_IMAGE => true,
Permissions::VIEW_EVENTLOG => true,
Permissions::IGNORE_DOWNTIME => true,
Permissions::VIEW_REGISTRATIONS => true,
Permissions::CREATE_IMAGE_REPORT => true,
Permissions::VIEW_IMAGE_REPORT => true,
Permissions::WIKI_ADMIN => true,
Permissions::EDIT_WIKI_PAGE => true,
Permissions::DELETE_WIKI_PAGE => true,
Permissions::MANAGE_BLOCKS => true,
Permissions::MANAGE_ADMINTOOLS => true,
Permissions::SEND_PM => true,
Permissions::READ_PM => true,
Permissions::VIEW_OTHER_PMS => true, # hm
Permissions::EDIT_FEATURE => true,
Permissions::BULK_EDIT_VOTE => true,
Permissions::EDIT_OTHER_VOTE => true,
Permissions::VIEW_SYSINTO => true,
Permissions::HELLBANNED => false,
Permissions::VIEW_HELLBANNED => true,
Permissions::PROTECTED => true,
Permissions::EDIT_IMAGE_RATING => true,
Permissions::BULK_EDIT_IMAGE_RATING => true,
Permissions::VIEW_TRASH => true,
Permissions::PERFORM_BULK_ACTIONS => true,
Permissions::BULK_ADD => true,
Permissions::EDIT_FILES => true,
Permissions::EDIT_TAG_CATEGORIES => true,
Permissions::RESCAN_MEDIA => true,
Permissions::SEE_IMAGE_VIEW_COUNTS => true,
Permissions::EDIT_FAVOURITES => true,
Permissions::ARTISTS_ADMIN => true,
Permissions::BLOTTER_ADMIN => true,
Permissions::FORUM_ADMIN => true,
Permissions::NOTES_ADMIN => true,
Permissions::POOLS_ADMIN => true,
Permissions::TIPS_ADMIN => true,
Permissions::CRON_ADMIN => true,
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";

1394
core/util.inc.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,799 +0,0 @@
<?php declare(strict_types=1);
use function MicroHTML\emptyHTML;
use function MicroHTML\rawHTML;
use function MicroHTML\FORM;
use function MicroHTML\INPUT;
use function MicroHTML\DIV;
use function MicroHTML\PRE;
use function MicroHTML\P;
use function MicroHTML\TABLE;
use function MicroHTML\THEAD;
use function MicroHTML\TFOOT;
use function MicroHTML\TR;
use function MicroHTML\TH;
use function MicroHTML\TD;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Misc *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
const DATA_DIR = "data";
function mtimefile(string $file): string
{
$data_href = get_base_href();
$mtime = filemtime($file);
return "$data_href/$file?$mtime";
}
function get_theme(): string
{
global $config;
$theme = $config->get_string(SetupConfig::THEME, "default");
if (!file_exists("themes/$theme")) {
$theme = "default";
}
return $theme;
}
function contact_link(): ?string
{
global $config;
$text = $config->get_string('contact_link');
if (is_null($text)) {
return null;
}
if (
startsWith($text, "http:") ||
startsWith($text, "https:") ||
startsWith($text, "mailto:")
) {
return $text;
}
if (strpos($text, "@")) {
return "mailto:$text";
}
if (strpos($text, "/")) {
return "http://$text";
}
return $text;
}
/**
* Check if HTTPS is enabled for the server.
*/
function is_https_enabled(): bool
{
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
}
/**
* Compare two Block objects, used to sort them before being displayed
*/
function blockcmp(Block $a, Block $b): int
{
if ($a->position == $b->position) {
return 0;
} else {
return ($a->position > $b->position) ? 1 : -1;
}
}
/**
* Figure out PHP's internal memory limit
*/
function get_memory_limit(): int
{
global $config;
// thumbnail generation requires lots of memory
$default_limit = 8*1024*1024; // 8 MB of memory is PHP's default.
$shimmie_limit = $config->get_int(MediaConfig::MEM_LIMIT);
if ($shimmie_limit < 3*1024*1024) {
// we aren't going to fit, override
$shimmie_limit = $default_limit;
}
/*
Get PHP's configured memory limit.
Note that this is set to -1 for NO memory limit.
https://ca2.php.net/manual/en/ini.core.php#ini.memory-limit
*/
$memory = parse_shorthand_int(ini_get("memory_limit"));
if ($memory == -1) {
// No memory limit.
// Return the larger of the set limits.
return max($shimmie_limit, $default_limit);
} else {
// PHP has a memory limit set.
if ($shimmie_limit > $memory) {
// Shimmie wants more memory than what PHP is currently set for.
// Attempt to set PHP's memory limit.
if (ini_set("memory_limit", "$shimmie_limit") === false) {
/* We can't change PHP's limit, oh well, return whatever its currently set to */
return $memory;
}
$memory = parse_shorthand_int(ini_get("memory_limit"));
}
// PHP's memory limit is more than Shimmie needs.
return $memory; // return the current setting
}
}
/**
* Check if PHP has the GD library installed
*/
function check_gd_version(): int
{
$gdversion = 0;
if (function_exists('gd_info')) {
$gd_info = gd_info();
if (substr_count($gd_info['GD Version'], '2.')) {
$gdversion = 2;
} elseif (substr_count($gd_info['GD Version'], '1.')) {
$gdversion = 1;
}
}
return $gdversion;
}
/**
* Check whether ImageMagick's `convert` command
* is installed and working
*/
function check_im_version(): int
{
$convert_check = exec("convert");
return (empty($convert_check) ? 0 : 1);
}
/**
* Get the currently active IP, masked to make it not change when the last
* octet or two change, for use in session cookies and such
*/
function get_session_ip(Config $config): string
{
$mask = $config->get_string("session_hash_mask", "255.255.0.0");
$addr = $_SERVER['REMOTE_ADDR'];
$addr = inet_ntop(inet_pton($addr) & inet_pton($mask));
return $addr;
}
/**
* A shorthand way to send a TextFormattingEvent and get the results.
*/
function format_text(string $string): string
{
$tfe = send_event(new TextFormattingEvent($string));
return $tfe->formatted;
}
/**
* Generates the path to a file under the data folder based on the file's hash.
* This process creates subfolders based on octet pairs from the file's hash.
* The calculated folder follows this pattern data/$base/octet_pairs/$hash
* @param string $base
* @param string $hash
* @param bool $create
* @param int $splits The number of octet pairs to split the hash into. Caps out at strlen($hash)/2.
* @return string
*/
function warehouse_path(string $base, string $hash, bool $create=true, int $splits = WH_SPLITS): string
{
$dirs =[DATA_DIR, $base];
$splits = min($splits, strlen($hash) / 2);
for ($i = 0; $i < $splits; $i++) {
$dirs[] = substr($hash, $i * 2, 2);
}
$dirs[] = $hash;
$pa = join_path(...$dirs);
if ($create && !file_exists(dirname($pa))) {
mkdir(dirname($pa), 0755, true);
}
return $pa;
}
/**
* Determines the path to the specified file in the data folder.
*/
function data_path(string $filename, bool $create = true): string
{
$filename = join_path("data", $filename);
if ($create&&!file_exists(dirname($filename))) {
mkdir(dirname($filename), 0755, true);
}
return $filename;
}
function load_balance_url(string $tmpl, string $hash, int $n=0): string
{
static $flexihashes = [];
$matches = [];
if (preg_match("/(.*){(.*)}(.*)/", $tmpl, $matches)) {
$pre = $matches[1];
$opts = $matches[2];
$post = $matches[3];
if (isset($flexihashes[$opts])) {
$flexihash = $flexihashes[$opts];
} else {
$flexihash = new Flexihash\Flexihash();
foreach (explode(",", $opts) as $opt) {
$parts = explode("=", $opt);
$parts_count = count($parts);
$opt_val = "";
$opt_weight = 0;
if ($parts_count === 2) {
$opt_val = $parts[0];
$opt_weight = $parts[1];
} elseif ($parts_count === 1) {
$opt_val = $parts[0];
$opt_weight = 1;
}
$flexihash->addTarget($opt_val, $opt_weight);
}
$flexihashes[$opts] = $flexihash;
}
// $choice = $flexihash->lookup($pre.$post);
$choices = $flexihash->lookupList($hash, $n + 1); // hash doesn't change
$choice = $choices[$n];
$tmpl = $pre . $choice . $post;
}
return $tmpl;
}
function transload(string $url, string $mfile): ?array
{
global $config;
if ($config->get_string("transload_engine") === "curl" && function_exists("curl_init")) {
$ch = curl_init($url);
$fp = fopen($mfile, "w");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_VERBOSE, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_REFERER, $url);
curl_setopt($ch, CURLOPT_USERAGENT, "Shimmie-".VERSION);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
$response = curl_exec($ch);
if ($response === false) {
log_warning("core-util", "Failed to transload $url");
throw new SCoreException("Failed to fetch $url");
}
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = http_parse_headers(implode("\n", preg_split('/\R/', rtrim(substr($response, 0, $header_size)))));
$body = substr($response, $header_size);
curl_close($ch);
fwrite($fp, $body);
fclose($fp);
return $headers;
}
if ($config->get_string("transload_engine") === "wget") {
$s_url = escapeshellarg($url);
$s_mfile = escapeshellarg($mfile);
system("wget --no-check-certificate $s_url --output-document=$s_mfile");
return file_exists($mfile) ? ["ok"=>"true"] : null;
}
if ($config->get_string("transload_engine") === "fopen") {
$fp_in = @fopen($url, "r");
$fp_out = fopen($mfile, "w");
if (!$fp_in || !$fp_out) {
return null;
}
$length = 0;
while (!feof($fp_in) && $length <= $config->get_int('upload_size')) {
$data = fread($fp_in, 8192);
$length += strlen($data);
fwrite($fp_out, $data);
}
fclose($fp_in);
fclose($fp_out);
$headers = http_parse_headers(implode("\n", $http_response_header));
return $headers;
}
return null;
}
function path_to_tags(string $path): string
{
$matches = [];
$tags = [];
if (preg_match("/\d+ - (.+)\.([a-zA-Z0-9]+)/", basename($path), $matches)) {
$tags = explode(" ", $matches[1]);
}
$path = dirname($path);
$path = str_replace(";", ":", $path);
$path = str_replace("__", " ", $path);
$category = "";
foreach (explode("/", $path) as $dir) {
$category_to_inherit = "";
foreach (explode(" ", $dir) as $tag) {
$tag = trim($tag);
if ($tag=="") {
continue;
}
if (substr_compare($tag, ":", -1) === 0) {
// This indicates a tag that ends in a colon,
// which is for inheriting to tags on the subfolder
$category_to_inherit = $tag;
} else {
if ($category!=""&&strpos($tag, ":") === false) {
// This indicates that category inheritance is active,
// and we've encountered a tag that does not specify a category.
// So we attach the inherited category to the tag.
$tag = $category.$tag;
}
$tags[] = $tag;
}
}
// Category inheritance only works on the immediate subfolder,
// so we hold a category until the next iteration, and then set
// it back to an empty string after that iteration
$category = $category_to_inherit;
}
return implode(" ", $tags);
}
function join_url(string $base, string ...$paths)
{
$output = $base;
foreach ($paths as $path) {
$output = rtrim($output, "/");
$path = ltrim($path, "/");
$output .= "/".$path;
}
return $output;
}
function get_dir_contents(string $dir): array
{
assert(!empty($dir));
if (!is_dir($dir)) {
return [];
}
return array_diff(
scandir(
$dir
),
['..', '.']
);
}
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.
*/
function scan_dir(string $path): array
{
$bytestotal = 0;
$nbfiles = 0;
$ite = new RecursiveDirectoryIterator(
$path,
FilesystemIterator::KEY_AS_PATHNAME |
FilesystemIterator::CURRENT_AS_FILEINFO |
FilesystemIterator::SKIP_DOTS
);
foreach (new RecursiveIteratorIterator($ite) as $filename => $cur) {
try {
$filesize = $cur->getSize();
$bytestotal += $filesize;
$nbfiles++;
} catch (RuntimeException $e) {
// This usually just means that the file got eaten by the import
continue;
}
}
$size_mb = $bytestotal / 1048576; // to mb
$size_mb = number_format($size_mb, 2, '.', '');
return ['path' => $path, 'total_files' => $nbfiles, 'total_mb' => $size_mb];
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Debugging functions *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
// SHIT by default this returns the time as a string. And it's not even a
// string representation of a number, it's two numbers separated by a space.
// What the fuck were the PHP developers smoking.
$_shm_load_start = microtime(true);
/**
* Collects some debug information (execution time, memory usage, queries, etc)
* and formats it to stick in the footer of the page.
*/
function get_debug_info(): string
{
global $cache, $config, $_shm_event_count, $database, $_shm_load_start;
$i_mem = sprintf("%5.2f", ((memory_get_peak_usage(true)+512)/1024)/1024);
if ($config->get_string("commit_hash", "unknown") == "unknown") {
$commit = "";
} else {
$commit = " (".$config->get_string("commit_hash").")";
}
$time = sprintf("%.2f", microtime(true) - $_shm_load_start);
$dbtime = sprintf("%.2f", $database->dbtime);
$i_files = count(get_included_files());
$hits = $cache->get_hits();
$miss = $cache->get_misses();
$debug = "<br>Took $time seconds (db:$dbtime) and {$i_mem}MB of RAM";
$debug .= "; Used $i_files files and {$database->query_count} queries";
$debug .= "; Sent $_shm_event_count events";
$debug .= "; $hits cache hits and $miss misses";
$debug .= "; Shimmie version ". VERSION . $commit;
return $debug;
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Request initialisation stuff *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/** @privatesection
* @noinspection PhpIncludeInspection
*/
function require_all(array $files): void
{
foreach ($files as $filename) {
require_once $filename;
}
}
function _load_core_files()
{
require_all(array_merge(
zglob("core/*.php"),
zglob("core/imageboard/*.php"),
zglob("ext/*/info.php")
));
}
function _load_theme_files()
{
require_all(_get_themelet_files(get_theme()));
}
function _set_up_shimmie_environment(): void
{
global $tracer_enabled;
if (file_exists("images") && !file_exists("data/images")) {
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);
}
if (DEBUG) {
error_reporting(E_ALL);
}
// The trace system has a certain amount of memory consumption every time it is used,
// 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;
}
function _get_themelet_files(string $_theme): array
{
$base_themelets = [];
$base_themelets[] = 'themes/'.$_theme.'/page.class.php';
$base_themelets[] = 'themes/'.$_theme.'/themelet.class.php';
$ext_themelets = zglob("ext/{".Extension::get_enabled_extensions_as_string()."}/theme.php");
$custom_themelets = zglob('themes/'.$_theme.'/{'.Extension::get_enabled_extensions_as_string().'}.theme.php');
return array_merge($base_themelets, $ext_themelets, $custom_themelets);
}
/**
* Used to display fatal errors to the web user.
* @noinspection PhpPossiblePolymorphicInvocationInspection
*/
function _fatal_error(Exception $e): void
{
$version = VERSION;
$message = $e->getMessage();
$phpver = phpversion();
$query = is_subclass_of($e, "SCoreException") ? $e->query : null;
//$hash = exec("git rev-parse HEAD");
//$h_hash = $hash ? "<p><b>Hash:</b> $hash" : "";
//'.$h_hash.'
if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') {
print("Trace: ");
$t = array_reverse($e->getTrace());
foreach ($t as $n => $f) {
$c = $f['class'] ?? '';
$t = $f['type'] ?? '';
$a = implode(", ", array_map("stringer", $f['args']));
print("$n: {$f['file']}({$f['line']}): {$c}{$t}{$f['function']}({$a})\n");
}
print("Message: $message\n");
if ($query) {
print("Query: {$query}\n");
}
print("Version: $version (on $phpver)\n");
} else {
$q = $query ? "" : "<p><b>Query:</b> " . html_escape($query);
header("HTTP/1.0 500 Internal Error");
echo '
<!doctype html>
<html lang="en">
<head>
<title>Internal error - SCore-'.$version.'</title>
</head>
<body>
<h1>Internal Error</h1>
<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>
';
}
}
function _get_user(): User
{
global $config, $page;
$my_user = null;
if ($page->get_cookie("user") && $page->get_cookie("session")) {
$tmp_user = User::by_session($page->get_cookie("user"), $page->get_cookie("session"));
if (!is_null($tmp_user)) {
$my_user = $tmp_user;
}
}
if (is_null($my_user)) {
$my_user = User::by_id($config->get_int("anon_id", 0));
}
assert(!is_null($my_user));
return $my_user;
}
function _get_query(): string
{
return (@$_POST["q"]?:@$_GET["q"])?:"/";
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* HTML Generation *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/**
* Give a HTML string which shows an IP (if the user is allowed to see IPs),
* and a link to ban that IP (if the user is allowed to ban IPs)
*
* FIXME: also check that IP ban ext is installed
*/
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>" : "";
$ip = $user->can(Permissions::VIEW_IP) ? $ip.$ban : "";
return $ip;
}
/**
* Make a form tag with relevant auth token and stuff
*/
function make_form(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit=""): string
{
global $user;
if ($method == "GET") {
$link = html_escape($target);
$target = make_link($target);
$extra_inputs = "<input type='hidden' name='q' value='$link'>";
} else {
$extra_inputs = $user->get_auth_html();
}
$extra = empty($form_id) ? '' : 'id="'. $form_id .'"';
if ($multipart) {
$extra .= " enctype='multipart/form-data'";
}
if ($onsubmit) {
$extra .= ' onsubmit="'.$onsubmit.'"';
}
return '<form action="'.$target.'" method="'.$method.'" '.$extra.'>'.$extra_inputs;
}
function SHM_FORM(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit="")
{
global $user;
$attrs = [
"action"=>make_link($target),
"method"=>$method
];
if ($form_id) {
$attrs["id"] = $form_id;
}
if ($multipart) {
$attrs["enctype"] = 'multipart/form-data';
}
if ($onsubmit) {
$attrs["onsubmit"] = $onsubmit;
}
return FORM(
$attrs,
INPUT(["type"=>"hidden", "name"=>"q", "value"=>$target]),
$method == "GET" ? "" : rawHTML($user->get_auth_html())
);
}
function SHM_SIMPLE_FORM($target, ...$children)
{
$form = SHM_FORM($target);
$form->appendChild(emptyHTML(...$children));
return $form;
}
function SHM_SUBMIT(string $text)
{
return INPUT(["type"=>"submit", "value"=>$text]);
}
function SHM_COMMAND_EXAMPLE(string $ex, string $desc)
{
return DIV(
["class"=>"command_example"],
PRE($ex),
P($desc)
);
}
function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot)
{
if (is_string($foot)) {
$foot = TFOOT(TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>$foot]))));
}
return SHM_SIMPLE_FORM(
$target,
P(
INPUT(["type"=>'hidden', "name"=>'id', "value"=>$duser->id]),
TABLE(
["class"=>"form"],
THEAD(TR(TH(["colspan"=>"2"], $title))),
$body,
$foot
)
)
);
}
const BYTE_DENOMINATIONS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
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,23 +0,0 @@
<?php declare(strict_types=1);
class AdminPageInfo extends ExtensionInfo
{
public const KEY = "admin";
public $key = self::KEY;
public $name = "Admin Controls";
public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2;
public $description = "Various things to make admins' lives easier";
public $documentation =
"Various moderate-level tools for admins; for advanced, obscure, and possibly dangerous tools see the shimmie2-utils script set
<p>Lowercase all tags:
<br>Set all tags to lowercase for consistency
<p>Recount tag use:
<br>If the counts of images per tag get messed up somehow, this will reset them, and remove any unused tags
<p>Database dump:
<br>Download the contents of the database in plain text format, useful for backups.
<p>Image dump:
<br>Download all the images as a .zip file (Requires ZipArchive)";
}

View File

@ -1,194 +1,191 @@
<?php /** @noinspection PhpUnusedPrivateMethodInspection */ <?php
declare(strict_types=1); /**
* Name: Admin Controls
* Author: Shish <webmaster@shishnet.org>
* Link: http://code.shishnet.org/shimmie2/
* License: GPLv2
* Description: Various things to make admins' lives easier
* Documentation:
* Various moderate-level tools for admins; for advanced, obscure, and
* possibly dangerous tools see the shimmie2-utils script set
* <p>Lowercase all tags:
* <br>Set all tags to lowercase for consistency
* <p>Recount tag use:
* <br>If the counts of images per tag get messed up somehow, this will
* reset them, and remove any unused tags
* <p>Database dump:
* <br>Download the contents of the database in plain text format, useful
* for backups.
* <p>Image dump:
* <br>Download all the images as a .zip file
*/
/** /**
* Sent when the admin page is ready to be added to * Sent when the admin page is ready to be added to
*/ */
class AdminBuildingEvent extends Event class AdminBuildingEvent extends Event {
{ var $page;
/** @var Page */ public function AdminBuildingEvent(Page $page) {
public $page; $this->page = $page;
}
public function __construct(Page $page)
{
parent::__construct();
$this->page = $page;
}
} }
class AdminActionEvent extends Event class AdminActionEvent extends Event {
{ var $action;
/** @var string */ var $redirect = true;
public $action; public function __construct(/*string*/ $action) {
/** @var bool */ $this->action = $action;
public $redirect = true; }
public function __construct(string $action)
{
parent::__construct();
$this->action = $action;
}
} }
class AdminPage extends Extension class AdminPage extends Extension {
{ public function onPageRequest(PageRequestEvent $event) {
/** @var AdminPageTheme */ global $page, $user;
protected $theme;
public function onPageRequest(PageRequestEvent $event) if($event->page_matches("admin")) {
{ if(!$user->can("manage_admintools")) {
global $page, $user; $this->theme->display_permission_denied();
}
else {
if($event->count_args() == 0) {
send_event(new AdminBuildingEvent($page));
}
else {
$action = $event->get_arg(0);
$aae = new AdminActionEvent($action);
if ($event->page_matches("admin")) { if($user->check_auth_token()) {
if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) { log_info("admin", "Util: $action");
$this->theme->display_permission_denied(); set_time_limit(0);
} else { send_event($aae);
if ($event->count_args() == 0) { }
send_event(new AdminBuildingEvent($page));
} else {
$action = $event->get_arg(0);
$aae = new AdminActionEvent($action);
if ($user->check_auth_token()) { if($aae->redirect) {
log_info("admin", "Util: $action"); $page->set_mode("redirect");
set_time_limit(0); $page->set_redirect(make_link("admin"));
send_event($aae); }
} }
}
}
}
if ($aae->redirect) { public function onAdminBuilding(AdminBuildingEvent $event) {
$page->set_mode(PageMode::REDIRECT); $this->theme->display_page();
$page->set_redirect(make_link("admin")); $this->theme->display_form();
} }
}
}
}
}
public function onCommand(CommandEvent $event) public function onUserBlockBuilding(UserBlockBuildingEvent $event) {
{ global $user;
if ($event->cmd == "help") { if($user->can("manage_admintools")) {
print "\tget-page <query string>\n"; $event->add_link("Board Admin", make_link("admin"));
print "\t\teg 'get-page post/list'\n\n"; }
print "\tpost-page <query string> <urlencoded params>\n"; }
print "\t\teg 'post-page ip_ban/delete id=1'\n\n";
print "\tget-token\n";
print "\t\tget a CSRF auth token\n\n";
print "\tregen-thumb <id / hash>\n";
print "\t\tregenerate a thumbnail\n\n";
print "\tcache [get|set|del] [key] <value>\n";
print "\t\teg 'cache get config'\n\n";
}
if ($event->cmd == "get-page") {
global $page;
if (isset($event->args[1])) {
parse_str($event->args[1], $_GET);
}
send_event(new PageRequestEvent($event->args[0]));
$page->display();
}
if ($event->cmd == "post-page") {
global $page;
$_SERVER['REQUEST_METHOD'] = "POST";
if (isset($event->args[1])) {
parse_str($event->args[1], $_POST);
}
send_event(new PageRequestEvent($event->args[0]));
$page->display();
}
if ($event->cmd == "get-token") {
global $user;
print($user->get_auth_token());
}
if ($event->cmd == "regen-thumb") {
$uid = $event->args[0];
$image = Image::by_id_or_hash($uid);
if ($image) {
send_event(new ThumbnailGenerationEvent($image->hash, $image->ext, true));
} else {
print("No post with ID '$uid'\n");
}
}
if ($event->cmd == "cache") {
global $cache;
$cmd = $event->args[0];
$key = $event->args[1];
switch ($cmd) {
case "get":
var_dump($cache->get($key));
break;
case "set":
$cache->set($key, $event->args[2], 60);
break;
case "del":
$cache->delete($key);
break;
}
}
}
public function onAdminBuilding(AdminBuildingEvent $event) public function onAdminAction(AdminActionEvent $event) {
{ $action = $event->action;
$this->theme->display_page(); if(method_exists($this, $action)) {
$this->theme->display_form(); $event->redirect = $this->$action();
} }
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) public function onPostListBuilding(PostListBuildingEvent $event) {
{ global $user;
global $user; if($user->can("manage_admintools") && !empty($event->search_terms)) {
if ($event->parent==="system") { $this->theme->display_dbq(implode(" ", $event->search_terms));
if ($user->can(Permissions::MANAGE_ADMINTOOLS)) { }
$event->add_nav_link("admin", new Link('admin'), "Board Admin"); }
}
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event) private function delete_by_query() {
{ global $page, $user;
global $user; $query = $_POST['query'];
if ($user->can(Permissions::MANAGE_ADMINTOOLS)) { assert(strlen($query) > 1);
$event->add_link("Board Admin", make_link("admin"));
}
}
public function onAdminAction(AdminActionEvent $event) log_warning("admin", "Mass deleting: $query");
{ foreach(Image::find_images(0, 1000000, Tag::explode($query)) as $image) {
$action = $event->action; send_event(new ImageDeletionEvent($image));
if (method_exists($this, $action)) { }
$event->redirect = $this->$action();
}
}
private function set_tag_case() $page->set_mode("redirect");
{ $page->set_redirect(make_link("post/list"));
global $database; return false;
$database->execute( }
"UPDATE tags SET tag=:tag1 WHERE LOWER(tag) = LOWER(:tag2)",
["tag1" => $_POST['tag'], "tag2" => $_POST['tag']]
);
log_info("admin", "Fixed the case of {$_POST['tag']}", "Fixed case");
return true;
}
private function lowercase_all_tags() private function lowercase_all_tags() {
{ global $database;
global $database; $database->execute("UPDATE tags SET tag=lower(tag)");
$database->execute("UPDATE tags SET tag=lower(tag)"); return true;
log_warning("admin", "Set all tags to lowercase", "Set all tags to lowercase"); }
return true;
}
private function recount_tag_use() private function recount_tag_use() {
{ global $database;
global $database; $database->Execute("
$database->Execute("
UPDATE tags UPDATE tags
SET count = COALESCE( SET count = COALESCE(
(SELECT COUNT(image_id) FROM image_tags WHERE tag_id=tags.id GROUP BY tag_id), (SELECT COUNT(image_id) FROM image_tags WHERE tag_id=tags.id GROUP BY tag_id),
0 0
) )
"); ");
$database->Execute("DELETE FROM tags WHERE count=0"); $database->Execute("DELETE FROM tags WHERE count=0");
log_warning("admin", "Re-counted tags", "Re-counted tags"); return true;
return true; }
}
private function database_dump() {
global $page;
$matches = array();
preg_match("#^(?P<proto>\w+)\:(?:user=(?P<user>\w+)(?:;|$)|password=(?P<password>\w+)(?:;|$)|host=(?P<host>[\w\.\-]+)(?:;|$)|dbname=(?P<dbname>[\w_]+)(?:;|$))+#", DATABASE_DSN, $matches);
$software = $matches['proto'];
$username = $matches['user'];
$password = $matches['password'];
$hostname = $matches['host'];
$database = $matches['dbname'];
switch($software) {
case 'mysql':
$cmd = "mysqldump -h$hostname -u$username -p$password $database";
break;
case 'pgsql':
putenv("PGPASSWORD=$password");
$cmd = "pg_dump -h $hostname -U $username $database";
break;
case 'sqlite':
$cmd = "sqlite3 $database .dump";
break;
}
$page->set_mode("data");
$page->set_type("application/x-unknown");
$page->set_filename('shimmie-'.date('Ymd').'.sql');
$page->set_data(shell_exec($cmd));
return false;
}
private function download_all_images() {
global $database, $page;
$zip = new ZipArchive;
$images = $database->get_all("SELECT * FROM images");
$filename = data_path('imgdump-'.date('Ymd').'.zip');
if($zip->open($filename, 1 ? ZIPARCHIVE::OVERWRITE:ZIPARCHIVE::CREATE) === TRUE){
foreach($images as $img){
$hash = $img["hash"];
preg_match("^[A-Za-z0-9]{2}^", $hash, $matches);
$img_loc = "images/".$matches[0]."/".$hash;
if(file_exists($img_loc)){
$zip->addFile($img_loc, $hash.".".$img["ext"]);
}
}
$zip->close();
}
$page->set_mode("redirect");
$page->set_redirect(make_link($filename)); //Fairly sure there is better way to do this..
//TODO: Delete file after downloaded?
return false; // we do want a redirect, but a manual one
}
} }
?>

View File

@ -1,89 +1,83 @@
<?php declare(strict_types=1); <?php
class AdminPageTest extends ShimmiePHPUnitTestCase class AdminPageTest extends ShimmieWebTestCase {
{ function testAuth() {
public function testAuth() $this->get_page('admin');
{ $this->assert_response(403);
send_event(new UserLoginEvent(User::by_name(self::$anon_name))); $this->assert_title("Permission Denied");
$page = $this->get_page('admin');
$this->assertEquals(403, $page->code);
$this->assertEquals("Permission Denied", $page->title);
send_event(new UserLoginEvent(User::by_name(self::$user_name))); $this->log_in_as_user();
$page = $this->get_page('admin'); $this->get_page('admin');
$this->assertEquals(403, $page->code); $this->assert_response(403);
$this->assertEquals("Permission Denied", $page->title); $this->assert_title("Permission Denied");
$this->log_out();
}
send_event(new UserLoginEvent(User::by_name(self::$admin_name))); function testLowercase() {
$page = $this->get_page('admin'); $ts = time(); // we need a tag that hasn't been used before
$this->assertEquals(200, $page->code);
$this->assertEquals("Admin Tools", $page->title);
}
public function testLowercaseAndSetCase() $this->log_in_as_admin();
{ $image_id_1 = $this->post_image("ext/simpletest/data/pbx_screenshot.jpg", "TeStCase$ts");
// Create a problem
$ts = time(); // we need a tag that hasn't been used before
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "TeStCase$ts");
// Validate problem $this->get_page("post/view/$image_id_1");
$page = $this->get_page("post/view/$image_id_1"); $this->assert_title("Image $image_id_1: TeStCase$ts");
$this->assertEquals("Image $image_id_1: TeStCase$ts", $page->title);
// Fix $this->get_page('admin');
send_event(new AdminActionEvent('lowercase_all_tags')); $this->assert_title("Admin Tools");
$this->click("All tags to lowercase");
// Validate fix $this->get_page("post/view/$image_id_1");
$this->get_page("post/view/$image_id_1"); $this->assert_title("Image $image_id_1: testcase$ts");
$this->assert_title("Image $image_id_1: testcase$ts");
// Change $this->delete_image($image_id_1);
$_POST["tag"] = "TestCase$ts"; $this->log_out();
send_event(new AdminActionEvent('set_tag_case')); }
// Validate change # FIXME: make sure the admin tools actually work
$this->get_page("post/view/$image_id_1"); function testRecount() {
$this->assert_title("Image $image_id_1: TestCase$ts"); $this->log_in_as_admin();
} $this->get_page('admin');
$this->assert_title("Admin Tools");
$this->click("Recount tag use");
$this->log_out();
}
# FIXME: make sure the admin tools actually work function testPurge() {
public function testRecount() $this->log_in_as_admin();
{ $this->get_page('admin');
global $database; $this->assert_title("Admin Tools");
$this->click("Purge unused tags");
$this->log_out();
}
// Create a problem function testDump() {
$ts = time(); // we need a tag that hasn't been used before $this->log_in_as_admin();
send_event(new UserLoginEvent(User::by_name(self::$admin_name))); $this->get_page('admin');
$database->execute( $this->assert_title("Admin Tools");
"INSERT INTO tags(tag, count) VALUES(:tag, :count)", $this->click("Download database contents");
["tag"=>"tes$ts", "count"=>42] $this->assert_response(200);
); $this->log_out();
}
// Fix function testDBQ() {
send_event(new AdminActionEvent('recount_tag_use')); $this->log_in_as_user();
$image_id_1 = $this->post_image("ext/simpletest/data/pbx_screenshot.jpg", "test");
$image_id_2 = $this->post_image("ext/simpletest/data/bedroom_workshop.jpg", "test2");
$image_id_3 = $this->post_image("ext/simpletest/data/favicon.png", "test");
// Validate fix $this->get_page("post/list/test/1");
$this->assertEquals( $this->click("Delete All These Images");
0,
$database->get_one(
"SELECT count FROM tags WHERE tag = :tag",
["tag"=>"tes$ts"]
)
);
}
public function testCommands() $this->get_page("post/view/$image_id_1");
{ $this->assert_response(404);
send_event(new UserLoginEvent(User::by_name(self::$admin_name))); $this->get_page("post/view/$image_id_2");
ob_start(); $this->assert_response(200);
send_event(new CommandEvent(["index.php", "help"])); $this->get_page("post/view/$image_id_3");
send_event(new CommandEvent(["index.php", "get-page", "post/list"])); $this->assert_response(404);
send_event(new CommandEvent(["index.php", "post-page", "post/list", "foo=bar"]));
send_event(new CommandEvent(["index.php", "get-token"]));
send_event(new CommandEvent(["index.php", "regen-thumb", "42"]));
ob_end_clean();
// don't crash $this->delete_image($image_id_1);
$this->assertTrue(true); $this->delete_image($image_id_2);
} $this->delete_image($image_id_3);
$this->log_out();
}
} }
?>

View File

@ -1,54 +1,61 @@
<?php declare(strict_types=1); <?php
use function MicroHTML\INPUT;
class AdminPageTheme extends Themelet class AdminPageTheme extends Themelet {
{ /*
/* * Show the basics of a page, for other extensions to add to
* Show the basics of a page, for other extensions to add to */
*/ public function display_page() {
public function display_page() global $page;
{
global $page;
$page->set_title("Admin Tools"); $page->set_title("Admin Tools");
$page->set_heading("Admin Tools"); $page->set_heading("Admin Tools");
$page->add_block(new NavBlock()); $page->add_block(new NavBlock());
} }
protected function button(string $name, string $action, bool $protected=false): string protected function button(/*string*/ $name, /*string*/ $action, /*boolean*/ $protected=false) {
{ $c_protected = $protected ? " protected" : "";
$c_protected = $protected ? " protected" : ""; $html = make_form(make_link("admin/$action"), "POST", false, false, false, "admin$c_protected");
$html = make_form(make_link("admin/$action"), "POST", false, "admin$c_protected"); if($protected) {
if ($protected) { $html .= "<input type='checkbox' onclick='$(\"#$action\").attr(\"disabled\", !$(this).is(\":checked\"))'>";
$html .= "<input type='submit' id='$action' value='$name' disabled='disabled'>"; $html .= "<input type='submit' id='$action' value='$name' disabled='true'>";
$html .= "<input type='checkbox' onclick='$(\"#$action\").attr(\"disabled\", !$(this).is(\":checked\"))'>"; }
} else { else {
$html .= "<input type='submit' id='$action' value='$name'>"; $html .= "<input type='submit' id='$action' value='$name'>";
} }
$html .= "</form>\n"; $html .= "</form>\n";
return $html; return $html;
} }
/* /*
* Show a form which links to admin_utils with POST[action] set to one of: * Show a form which links to admin_utils with POST[action] set to one of:
* 'lowercase all tags' * 'lowercase all tags'
* 'recount tag use' * 'recount tag use'
* etc * 'purge unused tags'
*/ */
public function display_form() public function display_form() {
{ global $page, $database;
global $page;
$html = ""; $html = "";
$html .= $this->button("All tags to lowercase", "lowercase_all_tags", true); $html .= $this->button("All tags to lowercase", "lowercase_all_tags", true);
$html .= $this->button("Recount tag use", "recount_tag_use", false); $html .= $this->button("Recount tag use", "recount_tag_user", false);
$page->add_block(new Block("Misc Admin Tools", $html)); $html .= $this->button("Download all images", "image_dump", false);
$html .= $this->button("Download database contents", "database_dump", false);
$html = (string)SHM_SIMPLE_FORM( $page->add_block(new Block("Misc Admin Tools", $html));
"admin/set_tag_case", }
INPUT(["type"=>'text', "name"=>'tag', "placeholder"=>'Enter tag with correct case', "class"=>'autocomplete_tags', "autocomplete"=>'off']),
SHM_SUBMIT('Set Tag Case'), public function display_dbq($terms) {
); global $page;
$page->add_block(new Block("Set Tag Case", $html));
} $h_terms = html_escape($terms);
$html = make_form(make_link("admin/delete_by_query"), "POST") . "
<input type='button' class='shm-unlocker' data-unlock-sel='#dbqsubmit' value='Unlock'>
<input type='hidden' name='query' value='$h_terms'>
<input type='submit' id='dbqsubmit' disabled='true' value='Delete All These Images'>
</form>
";
$page->add_block(new Block("List Controls", $html, "left"));
}
} }
?>

View File

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

View File

@ -1,209 +1,147 @@
<?php declare(strict_types=1); <?php
/**
* Name: Alias Editor
* Author: Shish <webmaster@shishnet.org>
* Link: http://code.shishnet.org/shimmie2/
* License: GPLv2
* Description: Edit the alias list
* Documentation:
* The list is visible at <a href="$site/alias/list">/alias/list</a>; only
* site admins can edit it, other people can view and download it
*/
use MicroCRUD\ActionColumn; class AddAliasEvent extends Event {
use MicroCRUD\TextColumn; var $oldtag;
use MicroCRUD\Table; var $newtag;
class AliasTable extends Table public function AddAliasEvent($oldtag, $newtag) {
{ $this->oldtag = $oldtag;
public function __construct(\FFSPHP\PDO $db) $this->newtag = $newtag;
{ }
parent::__construct($db);
$this->table = "aliases";
$this->base_query = "SELECT * FROM aliases";
$this->primary_key = "oldtag";
$this->size = 100;
$this->limit = 1000000;
$this->set_columns([
new TextColumn("oldtag", "Old Tag"),
new TextColumn("newtag", "New Tag"),
new ActionColumn("oldtag"),
]);
$this->order_by = ["oldtag"];
$this->table_attrs = ["class" => "zebra"];
}
} }
class AddAliasEvent extends Event class AddAliasException extends SCoreException {}
{
/** @var string */
public $oldtag;
/** @var string */
public $newtag;
public function __construct(string $oldtag, string $newtag) class AliasEditor extends Extension {
{ public function onPageRequest(PageRequestEvent $event) {
parent::__construct(); global $config, $database, $page, $user;
$this->oldtag = trim($oldtag);
$this->newtag = trim($newtag); if($event->page_matches("alias")) {
} if($event->get_arg(0) == "add") {
} if($user->can("manage_alias_list")) {
if(isset($_POST['oldtag']) && isset($_POST['newtag'])) {
class DeleteAliasEvent extends Event try {
{ $aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']);
public $oldtag; send_event($aae);
$page->set_mode("redirect");
public function __construct(string $oldtag) $page->set_redirect(make_link("alias/list"));
{ }
parent::__construct(); catch(AddAliasException $ex) {
$this->oldtag = $oldtag; $this->theme->display_error(500, "Error adding alias", $ex->getMessage());
} }
} }
}
class AddAliasException extends SCoreException }
{ else if($event->get_arg(0) == "remove") {
} if($user->can("manage_alias_list")) {
if(isset($_POST['oldtag'])) {
class AliasEditor extends Extension $database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", array("oldtag" => $_POST['oldtag']));
{ log_info("alias_editor", "Deleted alias for ".$_POST['oldtag']);
/** @var AliasEditorTheme */
protected $theme; $page->set_mode("redirect");
$page->set_redirect(make_link("alias/list"));
public function onPageRequest(PageRequestEvent $event) }
{ }
global $config, $database, $page, $user; }
else if($event->get_arg(0) == "list") {
if ($event->page_matches("alias")) { $page_number = $event->get_arg(1);
if ($event->get_arg(0) == "add") { if(is_null($page_number) || !is_numeric($page_number)) {
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) { $page_number = 0;
$user->ensure_authed(); }
$input = validate_input(["c_oldtag"=>"string", "c_newtag"=>"string"]); else if ($page_number <= 0) {
try { $page_number = 0;
send_event(new AddAliasEvent($input['c_oldtag'], $input['c_newtag'])); }
$page->set_mode(PageMode::REDIRECT); else {
$page->set_redirect(make_link("alias/list")); $page_number--;
} catch (AddAliasException $ex) { }
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
} $alias_per_page = $config->get_int('alias_items_per_page', 30);
}
} elseif ($event->get_arg(0) == "remove") { $query = "SELECT oldtag, newtag FROM aliases ORDER BY newtag ASC LIMIT :limit OFFSET :offset";
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) { $alias = $database->get_pairs($query,
$user->ensure_authed(); array("limit"=>$alias_per_page, "offset"=>$page_number * $alias_per_page)
$input = validate_input(["d_oldtag"=>"string"]); );
send_event(new DeleteAliasEvent($input['d_oldtag']));
$page->set_mode(PageMode::REDIRECT); $total_pages = ceil($database->get_one("SELECT COUNT(*) FROM aliases") / $alias_per_page);
$page->set_redirect(make_link("alias/list"));
} $this->theme->display_aliases($alias, $page_number + 1, $total_pages);
} elseif ($event->get_arg(0) == "list") { }
$t = new AliasTable($database->raw_db()); else if($event->get_arg(0) == "export") {
$t->token = $user->get_auth_token(); $page->set_mode("data");
$t->inputs = $_GET; $page->set_type("text/plain");
$t->size = $config->get_int('alias_items_per_page', 30); $page->set_data($this->get_alias_csv($database));
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) { }
$t->create_url = make_link("alias/add"); else if($event->get_arg(0) == "import") {
$t->delete_url = make_link("alias/remove"); if($user->can("manage_alias_list")) {
} if(count($_FILES) > 0) {
$this->theme->display_aliases($t->table($t->query()), $t->paginator()); $tmp = $_FILES['alias_file']['tmp_name'];
} elseif ($event->get_arg(0) == "export") { $contents = file_get_contents($tmp);
$page->set_mode(PageMode::DATA); $this->add_alias_csv($database, $contents);
$page->set_type(MIME_TYPE_CSV); $page->set_mode("redirect");
$page->set_filename("aliases.csv"); $page->set_redirect(make_link("alias/list"));
$page->set_data($this->get_alias_csv($database)); }
} elseif ($event->get_arg(0) == "import") { else {
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) { $this->theme->display_error(400, "No File Specified", "You have to upload a file");
if (count($_FILES) > 0) { }
$tmp = $_FILES['alias_file']['tmp_name']; }
$contents = file_get_contents($tmp); else {
$this->add_alias_csv($database, $contents); $this->theme->display_error(401, "Admins Only", "Only admins can edit the alias list");
log_info("alias_editor", "Imported aliases from file", "Imported aliases"); # FIXME: how many? }
$page->set_mode(PageMode::REDIRECT); }
$page->set_redirect(make_link("alias/list")); }
} else { }
$this->theme->display_error(400, "No File Specified", "You have to upload a file");
} public function onAddAlias(AddAliasEvent $event) {
} else { global $database;
$this->theme->display_error(401, "Admins Only", "Only admins can edit the alias list"); $pair = array("oldtag" => $event->oldtag, "newtag" => $event->newtag);
} if($database->get_row("SELECT * FROM aliases WHERE oldtag=:oldtag AND lower(newtag)=lower(:newtag)", $pair)) {
} throw new AddAliasException("That alias already exists");
} }
} else {
$database->execute("INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)", $pair);
public function onAddAlias(AddAliasEvent $event) log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}");
{ }
global $database; }
$row = $database->get_row( public function onUserBlockBuilding(UserBlockBuildingEvent $event) {
"SELECT * FROM aliases WHERE lower(oldtag)=lower(:oldtag)", global $user;
["oldtag"=>$event->oldtag] if($user->can("manage_alias_list")) {
); $event->add_link("Alias Editor", make_link("alias/list"));
if ($row) { }
throw new AddAliasException("{$row['oldtag']} is already an alias for {$row['newtag']}"); }
}
private function get_alias_csv(Database $database) {
$row = $database->get_row( $csv = "";
"SELECT * FROM aliases WHERE lower(oldtag)=lower(:newtag)", $aliases = $database->get_pairs("SELECT oldtag, newtag FROM aliases ORDER BY newtag");
["newtag" => $event->newtag] foreach($aliases as $old => $new) {
); $csv .= "$old,$new\n";
if ($row) { }
throw new AddAliasException("{$row['oldtag']} is itself an alias for {$row['newtag']}"); return $csv;
} }
$database->execute( private function add_alias_csv(Database $database, /*string*/ $csv) {
"INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)", $csv = str_replace("\r", "\n", $csv);
["oldtag" => $event->oldtag, "newtag" => $event->newtag] foreach(explode("\n", $csv) as $line) {
); $parts = explode(",", $line);
log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}", "Added alias"); if(count($parts) == 2) {
} $database->execute("INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)", array("oldtag" => $parts[0], "newtag" => $parts[1]));
}
public function onDeleteAlias(DeleteAliasEvent $event) }
{ }
global $database;
$database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", ["oldtag" => $event->oldtag]); // add alias *after* mass tag editing, else the MTE will
log_info("alias_editor", "Deleted alias for {$event->oldtag}", "Deleted alias"); // search for the images and be redirected to the alias,
} // missing out the images tagged with the oldtag
public function get_priority() {return 60;}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
if ($event->parent=="tags") {
$event->add_nav_link("aliases", new Link('alias/list'), "Aliases", NavLink::is_active(["alias"]));
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
$event->add_link("Alias Editor", make_link("alias/list"));
}
}
private function get_alias_csv(Database $database): string
{
$csv = "";
$aliases = $database->get_pairs("SELECT oldtag, newtag FROM aliases ORDER BY newtag");
foreach ($aliases as $old => $new) {
$csv .= "\"$old\",\"$new\"\n";
}
return $csv;
}
private function add_alias_csv(Database $database, string $csv): int
{
$csv = str_replace("\r", "\n", $csv);
$i = 0;
foreach (explode("\n", $csv) as $line) {
$parts = str_getcsv($line);
if (count($parts) == 2) {
try {
send_event(new AddAliasEvent($parts[0], $parts[1]));
$i++;
} catch (AddAliasException $ex) {
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
}
}
}
return $i;
}
/**
* Get the priority for this extension.
*
* Add alias *after* mass tag editing, else the MTE will
* search for the images and be redirected to the alias,
* missing out the images tagged with the old tag.
*/
public function get_priority(): int
{
return 60;
}
} }
?>

View File

@ -1,85 +1,74 @@
<?php declare(strict_types=1); <?php
class AliasEditorTest extends ShimmiePHPUnitTestCase class AliasEditorTest extends ShimmieWebTestCase {
{ function testAliasEditor() {
public function testAliasList() $this->get_page('alias/list');
{ $this->assert_title("Alias List");
$this->get_page('alias/list');
$this->assert_response(200);
$this->assert_title("Alias List");
}
public function testAliasListReadOnly() $this->log_in_as_admin();
{
$this->log_in_as_user();
$this->get_page('alias/list');
$this->assert_title("Alias List");
$this->assert_no_text("Add");
$this->log_out(); # test one to one
$this->get_page('alias/list'); $this->get_page('alias/list');
$this->assert_title("Alias List"); $this->assert_title("Alias List");
$this->assert_no_text("Add"); $this->set_field('oldtag', "test1");
} $this->set_field('newtag', "test2");
$this->click("Add");
$this->assert_text("test1");
public function testAliasOneToOne() $this->get_page("alias/export/aliases.csv");
{ $this->assert_text("test1,test2");
$this->log_in_as_admin();
$this->get_page("alias/export/aliases.csv"); $image_id = $this->post_image("ext/simpletest/data/pbx_screenshot.jpg", "test1");
$this->assert_no_text("test1"); $this->get_page("post/view/$image_id"); # check that the tag has been replaced
$this->assert_title("Image $image_id: test2");
$this->get_page("post/list/test1/1"); # searching for an alias should find the master tag
$this->assert_title("Image $image_id: test2");
$this->get_page("post/list/test2/1"); # check that searching for the main tag still works
$this->assert_title("Image $image_id: test2");
$this->delete_image($image_id);
send_event(new AddAliasEvent("test1", "test2")); $this->get_page('alias/list');
$this->get_page('alias/list'); $this->click("Remove");
$this->assert_text("test1"); $this->assert_title("Alias List");
$this->get_page("alias/export/aliases.csv"); $this->assert_no_text("test1");
$this->assert_text('"test1","test2"');
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1"); # test one to many
$this->get_page("post/view/$image_id"); # check that the tag has been replaced $this->get_page('alias/list');
$this->assert_title("Image $image_id: test2"); $this->assert_title("Alias List");
$this->get_page("post/list/test1/1"); # searching for an alias should find the master tag $this->set_field('oldtag', "onetag");
$this->assert_response(302); $this->set_field('newtag', "multi tag");
$this->get_page("post/list/test2/1"); # check that searching for the main tag still works $this->click("Add");
$this->assert_response(302); $this->assert_text("multi");
$this->delete_image($image_id); $this->assert_text("tag");
send_event(new DeleteAliasEvent("test1")); $this->get_page("alias/export/aliases.csv");
$this->get_page('alias/list'); $this->assert_text("onetag,multi tag");
$this->assert_title("Alias List");
$this->assert_no_text("test1");
}
public function testAliasOneToMany() $image_id_1 = $this->post_image("ext/simpletest/data/pbx_screenshot.jpg", "onetag");
{ $image_id_2 = $this->post_image("ext/simpletest/data/bedroom_workshop.jpg", "onetag");
$this->log_in_as_admin(); // FIXME: known broken
//$this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases
//$this->assert_title("onetag");
//$this->assert_no_text("No Images Found");
$this->get_page("post/list/multi/1");
$this->assert_title("multi");
$this->assert_no_text("No Images Found");
$this->get_page("post/list/multi%20tag/1");
$this->assert_title("multi tag");
$this->assert_no_text("No Images Found");
$this->delete_image($image_id_1);
$this->delete_image($image_id_2);
$this->get_page("alias/export/aliases.csv"); $this->get_page('alias/list');
$this->assert_no_text("multi"); $this->click("Remove");
$this->assert_title("Alias List");
$this->assert_no_text("test1");
send_event(new AddAliasEvent("onetag", "multi tag")); $this->log_out();
$this->get_page('alias/list');
$this->assert_text("multi");
$this->assert_text("tag");
$this->get_page("alias/export/aliases.csv");
$this->assert_text('"onetag","multi tag"');
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "onetag");
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "onetag");
$this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases
$this->assert_title("multi tag");
$this->assert_no_text("No Images Found");
$this->get_page("post/list/multi/1");
$this->assert_title("multi");
$this->assert_no_text("No Images Found");
$this->get_page("post/list/multi tag/1");
$this->assert_title("multi tag");
$this->assert_no_text("No Images Found");
$this->delete_image($image_id_1);
$this->delete_image($image_id_2);
send_event(new DeleteAliasEvent("onetag")); $this->get_page('alias/list');
$this->get_page('alias/list'); $this->assert_title("Alias List");
$this->assert_title("Alias List"); $this->assert_no_text("Add");
$this->assert_no_text("test1"); }
}
} }
?>

View File

@ -1,36 +1,77 @@
<?php declare(strict_types=1); <?php
class AliasEditorTheme extends Themelet class AliasEditorTheme extends Themelet {
{ /*
/** * Show a page of aliases:
* Show a page of aliases. *
* * $aliases = an array of ($old_tag => $new_tag)
* Note: $can_manage = whether things like "add new alias" should be shown * $can_manage = whether things like "add new alias" should be shown
*/ */
public function display_aliases($table, $paginator): void public function display_aliases($aliases, $pageNumber, $totalPages) {
{ global $page, $user;
global $page, $user;
$can_manage = $user->can(Permissions::MANAGE_ALIAS_LIST); $can_manage = $user->can("manage_alias_list");
$html = " if($can_manage) {
$table $h_action = "<th width='10%'>Action</th>";
$paginator $h_add = "
<p><a href='".make_link("alias/export/aliases.csv")."' download='aliases.csv'>Download as CSV</a></p> <tr>
".make_form(make_link("alias/add"))."
<td><input type='text' name='oldtag'></td>
<td><input type='text' name='newtag'></td>
<td><input type='submit' value='Add'></td>
</form>
</tr>
";
}
else {
$h_action = "";
$h_add = "";
}
$h_aliases = "";
$n = 0;
foreach($aliases as $old => $new) {
$h_old = html_escape($old);
$h_new = "<a href='".make_link("post/list/".url_escape($new)."/1")."'>".html_escape($new)."</a>";
$h_aliases .= "<tr><td>$h_old</td><td>$h_new</td>";
if($can_manage) {
$h_aliases .= "
<td>
".make_form(make_link("alias/remove"))."
<input type='hidden' name='oldtag' value='$h_old'>
<input type='submit' value='Remove'>
</form>
</td>
";
}
$h_aliases .= "</tr>";
}
$html = "
<table id='aliases' class='sortable zebra'>
<thead><tr><th>From</th><th>To</th>$h_action</tr></thead>
<tbody>$h_aliases</tbody>
<tfoot>$h_add</tfoot>
</table>
<p><a href='".make_link("alias/export/aliases.csv")."'>Download as CSV</a></p>
"; ";
$bulk_html = " $bulk_html = "
".make_form(make_link("alias/import"), 'post', true)." ".make_form(make_link("alias/import"), $multipart=True)."
<input type='file' name='alias_file'> <input type='file' name='alias_file'>
<input type='submit' value='Upload List'> <input type='submit' value='Upload List'>
</form> </form>
"; ";
$page->set_title("Alias List"); $page->set_title("Alias List");
$page->set_heading("Alias List"); $page->set_heading("Alias List");
$page->add_block(new NavBlock()); $page->add_block(new NavBlock());
$page->add_block(new Block("Aliases", $html)); $page->add_block(new Block("Aliases", $html));
if ($can_manage) { if($can_manage) {
$page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51)); $page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51));
} }
}
$this->display_paginator($page, "alias/list", null, $pageNumber, $totalPages);
}
} }
?>

75
ext/amazon_s3/main.php Normal file
View File

@ -0,0 +1,75 @@
<?php
/*
* Name: Amazon S3 Mirror
* Author: Shish <webmaster@shishnet.org>
* License: GPLv2
* Description: Copy uploaded files to S3
* Documentation:
*/
require_once "lib/S3.php";
class UploadS3 extends Extension {
public function onInitExt(InitExtEvent $event) {
global $config;
$config->set_default_string("amazon_s3_access", "");
$config->set_default_string("amazon_s3_secret", "");
$config->set_default_string("amazon_s3_bucket", "");
}
public function onSetupBuilding(SetupBuildingEvent $event) {
$sb = new SetupBlock("Amazon S3");
$sb->add_text_option("amazon_s3_access", "Access key: ");
$sb->add_text_option("amazon_s3_secret", "<br>Secret key: ");
$sb->add_text_option("amazon_s3_bucket", "<br>Bucket: ");
$event->panel->add_block($sb);
}
public function onImageAddition(ImageAdditionEvent $event) {
global $config;
$access = $config->get_string("amazon_s3_access");
$secret = $config->get_string("amazon_s3_secret");
$bucket = $config->get_string("amazon_s3_bucket");
if(!empty($bucket)) {
log_debug("amazon_s3", "Mirroring Image #".$event->image->id." to S3 #$bucket");
$s3 = new S3($access, $secret);
$s3->putBucket($bucket, S3::ACL_PUBLIC_READ);
$s3->putObjectFile(
warehouse_path("thumbs", $event->image->hash),
$bucket,
'thumbs/'.$event->image->hash,
S3::ACL_PUBLIC_READ,
array(),
array(
"Content-Type" => "image/jpeg",
"Content-Disposition" => "inline; filename=image-" . $event->image->id . ".jpg",
)
);
$s3->putObjectFile(
warehouse_path("images", $event->image->hash),
$bucket,
'images/'.$event->image->hash,
S3::ACL_PUBLIC_READ,
array(),
array(
"Content-Type" => $event->image->get_mime_type(),
"Content-Disposition" => "inline; filename=image-" . $event->image->id . "." . $event->image->ext,
)
);
}
}
public function onImageDeletion(ImageDeletionEvent $event) {
global $config;
$access = $config->get_string("amazon_s3_access");
$secret = $config->get_string("amazon_s3_secret");
$bucket = $config->get_string("amazon_s3_bucket");
if(!empty($bucket)) {
log_debug("amazon_s3", "Deleting Image #".$event->image->id." from S3");
$s3 = new S3($access, $secret);
$s3->deleteObject($bucket, "images/"+$event->image->hash);
$s3->deleteObject($bucket, "thumbs/"+$event->image->hash);
}
}
}
?>

View File

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

View File

@ -1,255 +0,0 @@
<?php declare(strict_types=1);
abstract class ApprovalConfig
{
const VERSION = "ext_approval_version";
const IMAGES = "approve_images";
const COMMENTS = "approve_comments";
}
class Approval extends Extension
{
/** @var ApprovalTheme */
protected $theme;
public function onInitExt(InitExtEvent $event)
{
global $config;
$config->set_default_bool(ApprovalConfig::IMAGES, false);
$config->set_default_bool(ApprovalConfig::COMMENTS, false);
Image::$bool_props[] = "approved";
}
public function onPageRequest(PageRequestEvent $event)
{
global $page, $user;
if ($event->page_matches("approve_image") && $user->can(Permissions::APPROVE_IMAGE)) {
// Try to get the image ID
$image_id = int_escape($event->get_arg(0));
if (empty($image_id)) {
$image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
}
if (empty($image_id)) {
throw new SCoreException("Can not approve image: No valid Image ID given.");
}
self::approve_image($image_id);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/" . $image_id));
}
if ($event->page_matches("disapprove_image") && $user->can(Permissions::APPROVE_IMAGE)) {
// Try to get the image ID
$image_id = int_escape($event->get_arg(0));
if (empty($image_id)) {
$image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
}
if (empty($image_id)) {
throw new SCoreException("Can not disapprove image: No valid Image ID given.");
}
self::disapprove_image($image_id);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/".$image_id));
}
}
public function onSetupBuilding(SetupBuildingEvent $event)
{
$this->theme->display_admin_block($event);
}
public function onAdminBuilding(AdminBuildingEvent $event)
{
$this->theme->display_admin_form();
}
public function onAdminAction(AdminActionEvent $event)
{
global $database, $user;
$action = $event->action;
$event->redirect = true;
if ($action==="approval") {
$approval_action = $_POST["approval_action"];
switch ($approval_action) {
case "approve_all":
$database->set_timeout(300000); // These updates can take a little bit
$database->execute(
"UPDATE images SET approved = :true, approved_by_id = :approved_by_id WHERE approved = :false",
["approved_by_id"=>$user->id, "true"=>true, "false"=>false]
);
break;
case "disapprove_all":
$database->set_timeout(300000); // These updates can take a little bit
$database->execute(
"UPDATE images SET approved = :false, approved_by_id = NULL WHERE approved = :true",
["true"=>true, "false"=>false]
);
break;
default:
break;
}
}
}
public function onDisplayingImage(DisplayingImageEvent $event)
{
global $user, $page, $config;
if ($config->get_bool(ApprovalConfig::IMAGES) && $event->image->approved===false && !$user->can(Permissions::APPROVE_IMAGE)) {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/list"));
}
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
global $user;
if ($event->parent=="posts") {
if ($user->can(Permissions::APPROVE_IMAGE)) {
$event->add_nav_link("posts_unapproved", new Link('/post/list/approved%3Ano/1'), "Pending Approval", null, 60);
}
}
}
const SEARCH_REGEXP = "/^approved:(yes|no)/";
public function onSearchTermParse(SearchTermParseEvent $event)
{
global $user, $database, $config;
if ($config->get_bool(ApprovalConfig::IMAGES)) {
$matches = [];
if (is_null($event->term) && $this->no_approval_query($event->context)) {
$event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
}
if (is_null($event->term)) {
return;
}
if (preg_match(self::SEARCH_REGEXP, strtolower($event->term), $matches)) {
if ($user->can(Permissions::APPROVE_IMAGE) && $matches[1] == "no") {
$event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_N ")));
} else {
$event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
}
}
}
}
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
{
global $user, $config;
if ($event->key===HelpPages::SEARCH) {
if ($user->can(Permissions::APPROVE_IMAGE) && $config->get_bool(ApprovalConfig::IMAGES)) {
$block = new Block();
$block->header = "Approval";
$block->body = $this->theme->get_help_html();
$event->add_block($block);
}
}
}
private function no_approval_query(array $context): bool
{
foreach ($context as $term) {
if (preg_match(self::SEARCH_REGEXP, $term)) {
return false;
}
}
return true;
}
public static function approve_image($image_id)
{
global $database, $user;
$database->execute(
"UPDATE images SET approved = :true, approved_by_id = :approved_by_id WHERE id = :id AND approved = :false",
["approved_by_id"=>$user->id, "id"=>$image_id, "true"=>true, "false"=>false]
);
}
public static function disapprove_image($image_id)
{
global $database;
$database->execute(
"UPDATE images SET approved = :false, approved_by_id = NULL WHERE id = :id AND approved = :true",
["id"=>$image_id, "true"=>true, "false"=>false]
);
}
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
{
global $user, $config;
if ($user->can(Permissions::APPROVE_IMAGE) && $config->get_bool(ApprovalConfig::IMAGES)) {
$event->add_part($this->theme->get_image_admin_html($event->image));
}
}
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
{
global $user, $config;
if ($user->can(Permissions::APPROVE_IMAGE)&& $config->get_bool(ApprovalConfig::IMAGES)) {
if (in_array("approved:no", $event->search_terms)) {
$event->add_action("bulk_approve_image", "Approve", "a");
} else {
$event->add_action("bulk_disapprove_image", "Disapprove");
}
}
}
public function onBulkAction(BulkActionEvent $event)
{
global $page, $user;
switch ($event->action) {
case "bulk_approve_image":
if ($user->can(Permissions::APPROVE_IMAGE)) {
$total = 0;
foreach ($event->items as $image) {
self::approve_image($image->id);
$total++;
}
$page->flash("Approved $total items");
}
break;
case "bulk_disapprove_image":
if ($user->can(Permissions::APPROVE_IMAGE)) {
$total = 0;
foreach ($event->items as $image) {
self::disapprove_image($image->id);
$total++;
}
$page->flash("Disapproved $total items");
}
break;
}
}
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{
global $database;
if ($this->get_version(ApprovalConfig::VERSION) < 1) {
$database->execute($database->scoreql_to_sql(
"ALTER TABLE images ADD COLUMN approved SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N"
));
$database->execute(
"ALTER TABLE images ADD COLUMN approved_by_id INTEGER NULL"
);
$database->execute("CREATE INDEX images_approved_idx ON images(approved)");
$this->set_version(ApprovalConfig::VERSION, 1);
}
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,8 @@
<?php declare(strict_types=1); <?php
class ArtistsTest extends ShimmiePHPUnitTestCase class ArtistTest extends ShimmieWebTestCase {
{ function testSearch() {
public function testSearch() # FIXME: check that the results are there
{ $this->get_page("post/list/author=bob/1");
global $user; }
$this->log_in_as_user();
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot");
$image = Image::by_id($image_id);
send_event(new AuthorSetEvent($image, $user, "bob"));
$this->assert_search_results(["author=bob"], [$image_id]);
}
} }
?>

View File

@ -1,10 +1,9 @@
<?php declare(strict_types=1); <?php
class ArtistsTheme extends Themelet class ArtistsTheme extends Themelet {
{
public function get_author_editor_html(string $author): string public function get_author_editor_html(/*string*/ $author) {
{ $h_author = html_escape($author);
$h_author = html_escape($author); return "
return "
<tr> <tr>
<th>Author</th> <th>Author</th>
<td> <td>
@ -13,104 +12,110 @@ class ArtistsTheme extends Themelet
</td> </td>
</tr> </tr>
"; ";
} }
public function sidebar_options(string $mode, ?int $artistID=null, $is_admin=false): void public function display_artists(){
{ global $page;
global $page, $user;
$html = ""; $page->set_title("Artists");
$page->set_heading("Artists");
$page->add_block(new Block("Artists", $html, "main", 10));
if ($mode == "neutral") { //$this->display_paginator($page, "artist/list", null, $pageNumber, $totalPages);
$html = "<form method='post' action='".make_link("artist/new_artist")."'> }
public function sidebar_options(/*string*/ $mode, $artistID=NULL, $is_admin=FALSE){
global $page;
if($mode == "neutral"){
$html = "<form method='post' action='".make_link("artist/new_artist")."'>
".$user->get_auth_html()." ".$user->get_auth_html()."
<input type='submit' name='edit' id='edit' value='New Artist'/> <input type='submit' name='edit' id='edit' value='New Artist'/>
</form>"; </form>";
} }
if ($mode == "editor") { if($mode == "editor"){
$html = "<form method='post' action='".make_link("artist/new_artist")."'> $html = "<form method='post' action='".make_link("artist/new_artist")."'>
".$user->get_auth_html()." ".$user->get_auth_html()."
<input type='submit' name='edit' value='New Artist'/> <input type='submit' name='edit' id='edit' value='New Artist'/>
</form> </form>
<form method='post' action='".make_link("artist/edit_artist")."'> <form method='post' action='".make_link("artist/edit_artist")."'>
".$user->get_auth_html()." ".$user->get_auth_html()."
<input type='submit' name='edit' value='Edit Artist'/> <input type='submit' name='edit' id='edit' value='Edit Artist'/>
<input type='hidden' name='artist_id' value='".$artistID."'> <input type='hidden' name='artist_id' value='".$artistID."'>
</form>"; </form>";
if ($is_admin) { if($is_admin){
$html .= "<form method='post' action='".make_link("artist/nuke_artist")."'> $html .= "<form method='post' action='".make_link("artist/nuke_artist")."'>
".$user->get_auth_html()." ".$user->get_auth_html()."
<input type='submit' name='edit' value='Delete Artist'/> <input type='submit' name='edit' id='edit' value='Delete Artist'/>
<input type='hidden' name='artist_id' value='".$artistID."'> <input type='hidden' name='artist_id' value='".$artistID."'>
</form>"; </form>";
} }
$html .= "<form method='post' action='".make_link("artist/add_alias")."'> $html .= "<form method='post' action='".make_link("artist/add_alias")."'>
".$user->get_auth_html()." ".$user->get_auth_html()."
<input type='submit' name='edit' value='Add Alias'/> <input type='submit' name='edit' id='edit' value='Add Alias'/>
<input type='hidden' name='artist_id' value='".$artistID."'> <input type='hidden' name='artist_id' value='".$artistID."'>
</form> </form>
<form method='post' action='".make_link("artist/add_member")."'> <form method='post' action='".make_link("artist/add_member")."'>
".$user->get_auth_html()." ".$user->get_auth_html()."
<input type='submit' name='edit' value='Add Member'/> <input type='submit' name='edit' id='edit' value='Add Member'/>
<input type='hidden' name='artist_id' value='".$artistID."'> <input type='hidden' name='artist_id' value='".$artistID."'>
</form> </form>
<form method='post' action='".make_link("artist/add_url")."'> <form method='post' action='".make_link("artist/add_url")."'>
".$user->get_auth_html()." ".$user->get_auth_html()."
<input type='submit' name='edit' value='Add Url'/> <input type='submit' name='edit' id='edit' value='Add Url'/>
<input type='hidden' name='artist_id' value='".$artistID."'> <input type='hidden' name='artist_id' value='".$artistID."'>
</form>"; </form>";
} }
$page->add_block(new Block("Manage Artists", $html, "left", 10));
}
if ($html) { public function show_artist_editor($artist, $aliases, $members, $urls)
$page->add_block(new Block("Manage Artists", $html, "left", 10)); {
} $artistName = $artist['name'];
} $artistNotes = $artist['notes'];
$artistID = $artist['id'];
public function show_artist_editor($artist, $aliases, $members, $urls) // aliases
{ $aliasesString = "";
global $user; $aliasesIDsString = "";
foreach ($aliases as $alias)
{
$aliasesString .= $alias["alias_name"]." ";
$aliasesIDsString .= $alias["alias_id"]." ";
}
$aliasesString = rtrim($aliasesString);
$aliasesIDsString = rtrim($aliasesIDsString);
$artistName = $artist['name']; // members
$artistNotes = $artist['notes']; $membersString = "";
$artistID = $artist['id']; $membersIDsString = "";
foreach ($members as $member)
{
$membersString .= $member["name"]." ";
$membersIDsString .= $member["id"]." ";
}
$membersString = rtrim($membersString);
$membersIDsString = rtrim($membersIDsString);
// aliases // urls
$aliasesString = ""; $urlsString = "";
$aliasesIDsString = ""; $urlsIDsString = "";
foreach ($aliases as $alias) { foreach ($urls as $url)
$aliasesString .= $alias["alias_name"]." "; {
$aliasesIDsString .= $alias["alias_id"]." "; $urlsString .= $url["url"]."\n";
} $urlsIDsString .= $url["id"]." ";
$aliasesString = rtrim($aliasesString); }
$aliasesIDsString = rtrim($aliasesIDsString); $urlsString = substr($urlsString, 0, strlen($urlsString) -1);
$urlsIDsString = rtrim($urlsIDsString);
// members $html =
$membersString = ""; '
$membersIDsString = "";
foreach ($members as $member) {
$membersString .= $member["name"]." ";
$membersIDsString .= $member["id"]." ";
}
$membersString = rtrim($membersString);
$membersIDsString = rtrim($membersIDsString);
// urls
$urlsString = "";
$urlsIDsString = "";
foreach ($urls as $url) {
$urlsString .= $url["url"]."\n";
$urlsIDsString .= $url["id"]." ";
}
$urlsString = substr($urlsString, 0, strlen($urlsString) -1);
$urlsIDsString = rtrim($urlsIDsString);
$html = '
<form method="POST" action="'.make_link("artist/edited/".$artist['id']).'"> <form method="POST" action="'.make_link("artist/edited/".$artist['id']).'">
'.$user->get_auth_html().' '.$user->get_auth_html().'
<table> <table>
@ -126,118 +131,112 @@ class ArtistsTheme extends Themelet
<tr><td colspan="2"><input type="submit" value="Submit" /></td></tr> <tr><td colspan="2"><input type="submit" value="Submit" /></td></tr>
</table> </table>
</form> </form>
';
global $page; ';
$page->add_block(new Block("Edit artist", $html, "main", 10));
}
public function new_artist_composer() global $page;
{ $page->add_block(new Block("Edit artist", $html, "main", 10));
global $page, $user;
$html = "<form action=".make_link("artist/create")." method='POST'>
".$user->get_auth_html()."
<table>
<tr><td>Name:</td><td><input type='text' name='name' /></td></tr>
<tr><td>Aliases:</td><td><input type='text' name='aliases' /></td></tr>
<tr><td>Members:</td><td><input type='text' name='members' /></td></tr>
<tr><td>URLs:</td><td><textarea name='urls'></textarea></td></tr>
<tr><td>Notes:</td><td><textarea name='notes'></textarea></td></tr>
<tr><td colspan='2'><input type='submit' value='Submit' /></td></tr>
</table>
";
$page->set_title("Artists");
$page->set_heading("Artists");
$page->add_block(new Block("Artists", $html, "main", 10));
}
public function list_artists($artists, $pageNumber, $totalPages)
{
global $user, $page;
$html = "<table id='poolsList' class='zebra'>".
"<thead><tr>".
"<th>Name</th>".
"<th>Type</th>".
"<th>Last updater</th>".
"<th>Posts</th>";
if (!$user->is_anonymous()) {
$html .= "<th colspan='2'>Action</th>";
} // space for edit link
$html .= "</tr></thead>";
$deletionLinkActionArray = [
'artist' => 'artist/nuke/',
'alias' => 'artist/alias/delete/',
'member' => 'artist/member/delete/',
];
$editionLinkActionArray = [
'artist' => 'artist/edit/',
'alias' => 'artist/alias/edit/',
'member' => 'artist/member/edit/',
];
$typeTextArray = [
'artist' => 'Artist',
'alias' => 'Alias',
'member' => 'Member',
];
foreach ($artists as $artist) {
if ($artist['type'] != 'artist') {
$artist['name'] = str_replace("_", " ", $artist['name']);
}
$elementLink = "<a href='".make_link("artist/view/".$artist['artist_id'])."'>".str_replace("_", " ", $artist['name'])."</a>";
//$artist_link = "<a href='".make_link("artist/view/".$artist['artist_id'])."'>".str_replace("_", " ", $artist['artist_name'])."</a>";
$user_link = "<a href='".make_link("user/".$artist['user_name'])."'>".$artist['user_name']."</a>";
$edit_link = "<a href='".make_link($editionLinkActionArray[$artist['type']].$artist['id'])."'>Edit</a>";
$del_link = "<a href='".make_link($deletionLinkActionArray[$artist['type']].$artist['id'])."'>Delete</a>";
$html .= "<tr>".
"<td class='left'>".$elementLink;
//if ($artist['type'] == 'member')
// $html .= " (member of ".$artist_link.")";
//if ($artist['type'] == 'alias')
// $html .= " (alias for ".$artist_link.")";
$html .= "</td>".
"<td>".$typeTextArray[$artist['type']]."</td>".
"<td>".$user_link."</td>".
"<td>".$artist['posts']."</td>";
if (!$user->is_anonymous()) {
$html .= "<td>".$edit_link."</td>";
}
if ($user->can(Permissions::ARTISTS_ADMIN)) {
$html .= "<td>".$del_link."</td>";
}
$html .= "</tr>";
} }
$html .= "</tbody></table>"; public function new_artist_composer()
{
global $page;
$page->set_title("Artists"); $html = "<form action=".make_link("artist/create")." method='POST'>
$page->set_heading("Artists"); ".$user->get_auth_html()."
$page->add_block(new Block("Artists", $html, "main", 10)); <table>
<tr><td>Name:</td><td><input type='text' name='name' /></td></tr>
<tr><td>Aliases:</td><td><input type='text' name='aliases' /></td></tr>
<tr><td>Members:</td><td><input type='text' name='members' /></td></tr>
<tr><td>URLs:</td><td><textarea name='urls'></textarea></td></tr>
<tr><td>Notes:</td><td><textarea name='notes'></textarea></td></tr>
<tr><td colspan='2'><input type='submit' value='Submit' /></td></tr>
</table>
";
$this->display_paginator($page, "artist/list", null, $pageNumber, $totalPages); $page->set_title("Artists");
} $page->set_heading("Artists");
$page->add_block(new Block("Artists", $html, "main", 10));
}
public function show_new_alias_composer($artistID) public function list_artists($artists, $pageNumber, $totalPages)
{ {
global $user; global $user, $page;
$html = ' $html = "<table id='poolsList' class='zebra'>".
<form method="POST" action='.make_link("artist/alias/add").'> "<thead><tr>".
"<th>Name</th>".
"<th>Type</th>".
"<th>Last updater</th>".
"<th>Posts</th>";
if(!$user->is_anonymous()) $html .= "<th colspan='2'>Action</th>"; // space for edit link
$html .= "</tr></thead>";
$n = 0;
$deletionLinkActionArray =
array('artist' => 'artist/nuke/'
, 'alias' => 'artist/alias/delete/'
, 'member' => 'artist/member/delete/'
);
$editionLinkActionArray =
array('artist' => 'artist/edit/'
, 'alias' => 'artist/alias/edit/'
, 'member' => 'artist/member/edit/'
);
$typeTextArray =
array('artist' => 'Artist'
, 'alias' => 'Alias'
, 'member' => 'Member'
);
foreach ($artists as $artist) {
if ($artist['type'] != 'artist')
$artist['name'] = str_replace("_", " ", $artist['name']);
$elementLink = "<a href='".make_link("artist/view/".$artist['artist_id'])."'>".str_replace("_", " ", $artist['name'])."</a>";
$artist_link = "<a href='".make_link("artist/view/".$artist['artist_id'])."'>".str_replace("_", " ", $artist['artist_name'])."</a>";
$user_link = "<a href='".make_link("user/".$artist['user_name'])."'>".$artist['user_name']."</a>";
$edit_link = "<a href='".make_link($editionLinkActionArray[$artist['type']].$artist['id'])."'>Edit</a>";
$del_link = "<a href='".make_link($deletionLinkActionArray[$artist['type']].$artist['id'])."'>Delete</a>";
$html .= "<tr>".
"<td class='left'>".$elementLink;
//if ($artist['type'] == 'member')
// $html .= " (member of ".$artist_link.")";
//if ($artist['type'] == 'alias')
// $html .= " (alias for ".$artist_link.")";
$html .= "</td>".
"<td>".$typeTextArray[$artist['type']]."</td>".
"<td>".$user_link."</td>".
"<td>".$artist['posts']."</td>";
if(!$user->is_anonymous()) $html .= "<td>".$edit_link."</td>";
if($user->is_admin()) $html .= "<td>".$del_link."</td>";
$html .= "</tr>";
}
$html .= "</tbody></table>";
$page->set_title("Artists");
$page->set_heading("Artists");
$page->add_block(new Block("Artists", $html, "main", 10));
$this->display_paginator($page, "artist/list", null, $pageNumber, $totalPages);
}
public function show_new_alias_composer($artistID)
{
$html =
'<form method="POST" action='.make_link("artist/alias/add").'>
'.$user->get_auth_html().' '.$user->get_auth_html().'
<table> <table>
<tr><td>Alias:</td><td><input type="text" name="aliases" /> <tr><td>Alias:</td><td><input type="text" name="aliases" />
@ -245,314 +244,271 @@ class ArtistsTheme extends Themelet
<tr><td colspan="2"><input type="submit" value="Submit" /></td></tr> <tr><td colspan="2"><input type="submit" value="Submit" /></td></tr>
</table> </table>
</form> </form>
'; ';
global $page; global $page;
$page->add_block(new Block("Artist Aliases", $html, "main", 20)); $page->add_block(new Block("Artist Aliases", $html, "main", 20));
} }
public function show_new_member_composer($artistID)
public function show_new_member_composer($artistID) {
{ $html =
global $user; ' <form method="POST" action='.make_link("artist/member/add").'>
$html = '
<form method="POST" action='.make_link("artist/member/add").'>
'.$user->get_auth_html().' '.$user->get_auth_html().'
<table> <table>
<tr><td>Members:</td><td><input type="text" name="members" /> <tr><td>Members:</td><td><input type="text" name="members" />
<input type="hidden" name="artistID" value='.$artistID.' /></td></tr> <input type="hidden" name="artistID" value='.$artistID.' /></td></tr>
<tr><td colspan="2"><input type="submit" value="Submit" /></td></tr> <tr><td colspan="2"><input type="submit" value="Submit" /></td></tr>
</table> </table>
</form> </form>
'; ';
global $page; global $page;
$page->add_block(new Block("Artist members", $html, "main", 30)); $page->add_block(new Block("Artist members", $html, "main", 30));
} }
public function show_new_url_composer($artistID) public function show_new_url_composer($artistID)
{ {
global $user; $html =
' <form method="POST" action='.make_link("artist/url/add").'>
$html = '
<form method="POST" action='.make_link("artist/url/add").'>
'.$user->get_auth_html().' '.$user->get_auth_html().'
<table> <table>
<tr><td>URL:</td><td><textarea name="urls"></textarea> <tr><td>URL:</td><td><textarea name="urls"></textarea>
<input type="hidden" name="artistID" value='.$artistID.' /></td></tr> <input type="hidden" name="artistID" value='.$artistID.' /></td></tr>
<tr><td colspan="2"><input type="submit" value="Submit" /></td></tr> <tr><td colspan="2"><input type="submit" value="Submit" /></td></tr>
</table> </table>
</form> </form>
'; ';
global $page; global $page;
$page->add_block(new Block("Artist URLs", $html, "main", 40)); $page->add_block(new Block("Artist URLs", $html, "main", 40));
}
public function show_alias_editor($alias)
{
global $user;
$html = '
<form method="POST" action="'.make_link("artist/alias/edited/".$alias['id']).'">
'.$user->get_auth_html().'
<label for="alias">Alias:</label>
<input type="text" name="alias" id="alias" value="'.$alias['alias'].'" />
<input type="hidden" name="aliasID" value="'.$alias['id'].'" />
<input type="submit" value="Submit" />
</form>
';
global $page;
$page->add_block(new Block("Edit Alias", $html, "main", 10));
}
public function show_url_editor($url)
{
global $user;
$html = '
<form method="POST" action="'.make_link("artist/url/edited/".$url['id']).'">
'.$user->get_auth_html().'
<label for="url">URL:</label>
<input type="text" name="url" id="url" value="'.$url['url'].'" />
<input type="hidden" name="urlID" value="'.$url['id'].'" />
<input type="submit" value="Submit" />
</form>
';
global $page;
$page->add_block(new Block("Edit URL", $html, "main", 10));
}
public function show_member_editor($member)
{
global $user;
$html = '
<form method="POST" action="'.make_link("artist/member/edited/".$member['id']).'">
'.$user->get_auth_html().'
<label for="name">Member name:</label>
<input type="text" name="name" id="name" value="'.$member['name'].'" />
<input type="hidden" name="memberID" value="'.$member['id'].'" />
<input type="submit" value="Submit" />
</form>
';
global $page;
$page->add_block(new Block("Edit Member", $html, "main", 10));
}
public function show_artist($artist, $aliases, $members, $urls, $images, $userIsLogged, $userIsAdmin)
{
global $page;
$artist_link = "<a href='".make_link("post/list/".$artist['name']."/1")."'>".str_replace("_", " ", $artist['name'])."</a>";
$html = "<table id='poolsList' class='zebra'>
<thead>
<tr>
<th></th>
<th></th>";
if ($userIsLogged) {
$html .= "<th></th>";
}
if ($userIsAdmin) {
$html .= "<th></th>";
} }
$html .= " <tr> public function show_alias_editor($alias)
</thead> {
$html =
'
<form method="POST" action="'.make_link("artist/alias/edited/".$alias['id']).'">
'.$user->get_auth_html().'
<label for="alias">Alias:</label>
<input type="text" name="alias" value="'.$alias['alias'].'" />
<input type="hidden" name="aliasID" value="'.$alias['id'].'" />
<input type="submit" value="Submit" />
</form>
';
<tr> global $page;
<td class='left'>Name:</td> $page->add_block(new Block("Edit Alias", $html, "main", 10));
<td class='left'>".$artist_link."</td>";
if ($userIsLogged) {
$html .= "<td></td>";
}
if ($userIsAdmin) {
$html .= "<td></td>";
}
$html .= "</tr>";
$html .= $this->render_aliases($aliases, $userIsLogged, $userIsAdmin);
$html .= $this->render_members($members, $userIsLogged, $userIsAdmin);
$html .= $this->render_urls($urls, $userIsLogged, $userIsAdmin);
$html .= "<tr>
<td class='left'>Notes:</td>
<td class='left'>".$artist["notes"]."</td>";
if ($userIsLogged) {
$html .= "<td></td>";
}
if ($userIsAdmin) {
$html .= "<td></td>";
}
//TODO how will notes be edited? On edit artist? (should there be an editartist?) or on a editnotes?
//same question for deletion
$html .= "</tr>
</table>";
$page->set_title("Artist");
$page->set_heading("Artist");
$page->add_block(new Block("Artist", $html, "main", 10));
//we show the images for the artist
$artist_images = "";
foreach ($images as $image) {
$thumb_html = $this->build_thumb_html($image);
$artist_images .= '<span class="thumb">'.
'<a href="$image_link">'.$thumb_html.'</a>'.
'</span>';
} }
$page->add_block(new Block("Artist Images", $artist_images, "main", 20)); public function show_url_editor($url)
} {
$html =
'
<form method="POST" action="'.make_link("artist/url/edited/".$url['id']).'">
'.$user->get_auth_html().'
<label for="url">URL:</label>
<input type="text" name="url" value="'.$url['url'].'" />
<input type="hidden" name="urlID" value="'.$url['id'].'" />
<input type="submit" value="Submit" />
</form>
';
private function render_aliases(array $aliases, bool $userIsLogged, bool $userIsAdmin): string global $page;
{ $page->add_block(new Block("Edit URL", $html, "main", 10));
$html = ""; }
if (count($aliases) > 0) {
$aliasViewLink = str_replace("_", " ", $aliases[0]['alias_name']); // no link anymore
$aliasEditLink = "<a href='" . make_link("artist/alias/edit/" . $aliases[0]['alias_id']) . "'>Edit</a>";
$aliasDeleteLink = "<a href='" . make_link("artist/alias/delete/" . $aliases[0]['alias_id']) . "'>Delete</a>";
$html .= "<tr> public function show_member_editor($member)
<td class='left'>Aliases:</td> {
<td class='left'>" . $aliasViewLink . "</td>"; $html =
'
<form method="POST" action="'.make_link("artist/member/edited/".$member['id']).'">
'.$user->get_auth_html().'
<label for="member">Member name:</label>
<input type="text" name="name" value="'.$member['name'].'" />
<input type="hidden" name="memberID" value="'.$member['id'].'" />
<input type="submit" value="Submit" />
</form>
';
if ($userIsLogged) { global $page;
$html .= "<td class='left'>" . $aliasEditLink . "</td>"; $page->add_block(new Block("Edit Member", $html, "main", 10));
} }
if ($userIsAdmin) { public function show_artist($artist, $aliases, $members, $urls, $images, $userIsLogged, $userIsAdmin)
$html .= "<td class='left'>" . $aliasDeleteLink . "</td>"; {
} global $user, $event, $page;
$artist_link = "<a href='".make_link("post/list/".$artist['name']."/1")."'>".str_replace("_", " ", $artist['name'])."</a>";
$n = 0;
$html = "<table id='poolsList' class='zebra'>
<thead>
<tr>
<th></th>
<th></th>";
if ($userIsLogged)
$html .= "<th></th>";
if ($userIsAdmin)
$html .= "<th></th>";
$html .= " <tr>
</thead>
<tr>
<td class='left'>Name:</td>
<td class='left'>".$artist_link."</td>";
if ($userIsLogged) $html .= "<td></td>";
if ($userIsAdmin) $html .= "<td></td>";
$html .= "</tr>"; $html .= "</tr>";
if (count($aliases) > 1) { if (count($aliases) > 0)
for ($i = 1; $i < count($aliases); $i++) { {
$aliasViewLink = str_replace("_", " ", $aliases[$i]['alias_name']); // no link anymore $aliasViewLink = str_replace("_", " ", $aliases[0]['alias_name']); // no link anymore
$aliasEditLink = "<a href='" . make_link("artist/alias/edit/" . $aliases[$i]['alias_id']) . "'>Edit</a>"; $aliasEditLink = "<a href='".make_link("artist/alias/edit/".$aliases[0]['alias_id'])."'>Edit</a>";
$aliasDeleteLink = "<a href='" . make_link("artist/alias/delete/" . $aliases[$i]['alias_id']) . "'>Delete</a>"; $aliasDeleteLink = "<a href='".make_link("artist/alias/delete/".$aliases[0]['alias_id'])."'>Delete</a>";
$html .= "<tr> $html .= "<tr>
<td class='left'>&nbsp;</td> <td class='left'>Aliases:</td>
<td class='left'>" . $aliasViewLink . "</td>"; <td class='left'>".$aliasViewLink."</td>";
if ($userIsLogged) {
$html .= "<td class='left'>" . $aliasEditLink . "</td>";
}
if ($userIsAdmin) {
$html .= "<td class='left'>" . $aliasDeleteLink . "</td>";
}
$html .= "</tr>"; if ($userIsLogged)
$html .= "<td class='left'>".$aliasEditLink."</td>";
if ($userIsAdmin)
$html .= "<td class='left'>".$aliasDeleteLink."</td>";
$html .= "</tr>";
if (count($aliases) > 1)
{
for ($i = 1; $i < count($aliases); $i++)
{
$aliasViewLink = str_replace("_", " ", $aliases[$i]['alias_name']); // no link anymore
$aliasEditLink = "<a href='".make_link("artist/alias/edit/".$aliases[$i]['alias_id'])."'>Edit</a>";
$aliasDeleteLink = "<a href='".make_link("artist/alias/delete/".$aliases[$i]['alias_id'])."'>Delete</a>";
$html .= "<tr>
<td class='left'>&nbsp;</td>
<td class='left'>".$aliasViewLink."</td>";
if ($userIsLogged)
$html .= "<td class='left'>".$aliasEditLink."</td>";
if ($userIsAdmin)
$html .= "<td class='left'>".$aliasDeleteLink."</td>";
$html .= "</tr>";
}
} }
} }
}
return $html;
}
private function render_members(array $members, bool $userIsLogged, bool $userIsAdmin): string if (count($members) > 0)
{ {
$html = ""; $memberViewLink = str_replace("_", " ", $members[0]['name']); // no link anymore
if (count($members) > 0) { $memberEditLink = "<a href='".make_link("artist/member/edit/".$members[0]['id'])."'>Edit</a>";
$memberViewLink = str_replace("_", " ", $members[0]['name']); // no link anymore $memberDeleteLink = "<a href='".make_link("artist/member/delete/".$members[0]['id'])."'>Delete</a>";
$memberEditLink = "<a href='" . make_link("artist/member/edit/" . $members[0]['id']) . "'>Edit</a>";
$memberDeleteLink = "<a href='" . make_link("artist/member/delete/" . $members[0]['id']) . "'>Delete</a>";
$html .= "<tr> $html .= "<tr>
<td class='left'>Members:</td> <td class='left'>Members:</td>
<td class='left'>" . $memberViewLink . "</td>"; <td class='left'>".$memberViewLink."</td>";
if ($userIsLogged) { if ($userIsLogged)
$html .= "<td class='left'>" . $memberEditLink . "</td>"; $html .= "<td class='left'>".$memberEditLink."</td>";
} if ($userIsAdmin)
if ($userIsAdmin) { $html .= "<td class='left'>".$memberDeleteLink."</td>";
$html .= "<td class='left'>" . $memberDeleteLink . "</td>";
}
$html .= "</tr>"; $html .= "</tr>";
if (count($members) > 1) { if (count($members) > 1)
for ($i = 1; $i < count($members); $i++) { {
$memberViewLink = str_replace("_", " ", $members[$i]['name']); // no link anymore for ($i = 1; $i < count($members); $i++)
$memberEditLink = "<a href='" . make_link("artist/member/edit/" . $members[$i]['id']) . "'>Edit</a>"; {
$memberDeleteLink = "<a href='" . make_link("artist/member/delete/" . $members[$i]['id']) . "'>Delete</a>"; $memberViewLink = str_replace("_", " ", $members[$i]['name']); // no link anymore
$memberEditLink = "<a href='".make_link("artist/member/edit/".$members[$i]['id'])."'>Edit</a>";
$memberDeleteLink = "<a href='".make_link("artist/member/delete/".$members[$i]['id'])."'>Delete</a>";
$html .= "<tr> $html .= "<tr>
<td class='left'>&nbsp;</td> <td class='left'>&nbsp;</td>
<td class='left'>" . $memberViewLink . "</td>"; <td class='left'>".$memberViewLink."</td>";
if ($userIsLogged) { if ($userIsLogged)
$html .= "<td class='left'>" . $memberEditLink . "</td>"; $html .= "<td class='left'>".$memberEditLink."</td>";
if ($userIsAdmin)
$html .= "<td class='left'>".$memberDeleteLink."</td>";
$html .= "</tr>";
} }
if ($userIsAdmin) {
$html .= "<td class='left'>" . $memberDeleteLink . "</td>";
}
$html .= "</tr>";
} }
} }
}
return $html;
}
private function render_urls(array $urls, bool $userIsLogged, bool $userIsAdmin): string if (count($urls) > 0)
{ {
$html = ""; $urlViewLink = "<a href='".str_replace("_", " ", $urls[0]['url'])."' target='_blank'>".str_replace("_", " ", $urls[0]['url'])."</a>";
if (count($urls) > 0) { $urlEditLink = "<a href='".make_link("artist/url/edit/".$urls[0]['id'])."'>Edit</a>";
$urlViewLink = "<a href='" . str_replace("_", " ", $urls[0]['url']) . "' target='_blank'>" . str_replace("_", " ", $urls[0]['url']) . "</a>"; $urlDeleteLink = "<a href='".make_link("artist/url/delete/".$urls[0]['id'])."'>Delete</a>";
$urlEditLink = "<a href='" . make_link("artist/url/edit/" . $urls[0]['id']) . "'>Edit</a>";
$urlDeleteLink = "<a href='" . make_link("artist/url/delete/" . $urls[0]['id']) . "'>Delete</a>";
$html .= "<tr> $html .= "<tr>
<td class='left'>URLs:</td> <td class='left'>URLs:</td>
<td class='left'>" . $urlViewLink . "</td>"; <td class='left'>".$urlViewLink."</td>";
if ($userIsLogged) { if ($userIsLogged)
$html .= "<td class='left'>" . $urlEditLink . "</td>"; $html .= "<td class='left'>".$urlEditLink."</td>";
}
if ($userIsAdmin) { if ($userIsAdmin)
$html .= "<td class='left'>" . $urlDeleteLink . "</td>"; $html .= "<td class='left'>".$urlDeleteLink."</td>";
}
$html .= "</tr>"; $html .= "</tr>";
if (count($urls) > 1) { if (count($urls) > 1)
for ($i = 1; $i < count($urls); $i++) { {
$urlViewLink = "<a href='" . str_replace("_", " ", $urls[$i]['url']) . "' target='_blank'>" . str_replace("_", " ", $urls[$i]['url']) . "</a>"; for ($i = 1; $i < count($urls); $i++)
$urlEditLink = "<a href='" . make_link("artist/url/edit/" . $urls[$i]['id']) . "'>Edit</a>"; {
$urlDeleteLink = "<a href='" . make_link("artist/url/delete/" . $urls[$i]['id']) . "'>Delete</a>"; $urlViewLink = "<a href='".str_replace("_", " ", $urls[$i]['url'])."' target='_blank'>".str_replace("_", " ", $urls[$i]['url'])."</a>";
$urlEditLink = "<a href='".make_link("artist/url/edit/".$urls[$i]['id'])."'>Edit</a>";
$urlDeleteLink = "<a href='".make_link("artist/url/delete/".$urls[$i]['id'])."'>Delete</a>";
$html .= "<tr> $html .= "<tr>
<td class='left'>&nbsp;</td> <td class='left'>&nbsp;</td>
<td class='left'>" . $urlViewLink . "</td>"; <td class='left'>".$urlViewLink."</td>";
if ($userIsLogged) { if ($userIsLogged)
$html .= "<td class='left'>" . $urlEditLink . "</td>"; $html .= "<td class='left'>".$urlEditLink."</td>";
if ($userIsAdmin)
$html .= "<td class='left'>".$urlDeleteLink."</td>";
$html .= "</tr>";
} }
if ($userIsAdmin) {
$html .= "<td class='left'>" . $urlDeleteLink . "</td>";
}
$html .= "</tr>";
} }
return $html;
} }
}
return $html;
}
public function get_help_html() $html .=
{ "<tr>
return '<p>Search for images with a particular artist.</p> <td class='left'>Notes:</td>
<div class="command_example"> <td class='left'>".$artist["notes"]."</td>";
<pre>artist=leonardo</pre> if ($userIsLogged) $html .= "<td></td>";
<p>Returns images with the artist "leonardo".</p> if ($userIsAdmin) $html .= "<td></td>";
</div> //TODO how will notes be edited? On edit artist? (should there be an editartist?) or on a editnotes?
'; //same question for deletion
} $html .= "</tr>
</table>";
$page->set_title("Artist");
$page->set_heading("Artist");
$page->add_block(new Block("Artist", $html, "main", 10));
//we show the images for the artist
$artist_images = "";
foreach($images as $image) {
$thumb_html = $this->build_thumb_html($image);
$artist_images .= '<span class="thumb">'.
'<a href="$image_link">'.$thumb_html.'</a>'.
'</span>';
}
$page->add_block(new Block("Artist Images", $artist_images, "main", 20));
}
} }
?>

View File

@ -1,7 +0,0 @@
<?php declare(strict_types=1);
abstract class AutoTaggerConfig
{
public const VERSION = "ext_auto_tagger_ver";
public const ITEMS_PER_PAGE = "auto_tagger_items_per_page";
}

View File

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

View File

@ -1,335 +0,0 @@
<?php declare(strict_types=1);
require_once 'config.php';
use MicroCRUD\ActionColumn;
use MicroCRUD\TextColumn;
use MicroCRUD\Table;
class AutoTaggerTable extends Table
{
public function __construct(\FFSPHP\PDO $db)
{
parent::__construct($db);
$this->table = "auto_tagger";
$this->base_query = "SELECT * FROM auto_tag";
$this->primary_key = "tag";
$this->size = 100;
$this->limit = 1000000;
$this->set_columns([
new TextColumn("tag", "Tag"),
new TextColumn("additional_tags", "Additional Tags"),
new ActionColumn("tag"),
]);
$this->order_by = ["tag"];
$this->table_attrs = ["class" => "zebra"];
}
}
class AddAutoTagEvent extends Event
{
/** @var string */
public $tag;
/** @var string */
public $additional_tags;
public function __construct(string $tag, string $additional_tags)
{
parent::__construct();
$this->tag = trim($tag);
$this->additional_tags = trim($additional_tags);
}
}
class DeleteAutoTagEvent extends Event
{
public $tag;
public function __construct(string $tag)
{
parent::__construct();
$this->tag = $tag;
}
}
class AutoTaggerException extends SCoreException
{
}
class AddAutoTagException extends SCoreException
{
}
class AutoTagger extends Extension
{
/** @var AutoTaggerTheme */
protected $theme;
public function onPageRequest(PageRequestEvent $event)
{
global $config, $database, $page, $user;
if ($event->page_matches("auto_tag")) {
if ($event->get_arg(0) == "add") {
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
$user->ensure_authed();
$input = validate_input(["c_tag"=>"string", "c_additional_tags"=>"string"]);
try {
send_event(new AddAutoTagEvent($input['c_tag'], $input['c_additional_tags']));
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("auto_tag/list"));
} catch (AddAutoTagException $ex) {
$this->theme->display_error(500, "Error adding auto-tag", $ex->getMessage());
}
}
} elseif ($event->get_arg(0) == "remove") {
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
$user->ensure_authed();
$input = validate_input(["d_tag"=>"string"]);
send_event(new DeleteAutoTagEvent($input['d_tag']));
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("auto_tag/list"));
}
} elseif ($event->get_arg(0) == "list") {
$t = new AutoTaggerTable($database->raw_db());
$t->token = $user->get_auth_token();
$t->inputs = $_GET;
$t->size = $config->get_int(AutoTaggerConfig::ITEMS_PER_PAGE, 30);
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
$t->create_url = make_link("auto_tag/add");
$t->delete_url = make_link("auto_tag/remove");
}
$this->theme->display_auto_tagtable($t->table($t->query()), $t->paginator());
} elseif ($event->get_arg(0) == "export") {
$page->set_mode(PageMode::DATA);
$page->set_type(MIME_TYPE_CSV);
$page->set_filename("auto_tag.csv");
$page->set_data($this->get_auto_tag_csv($database));
} elseif ($event->get_arg(0) == "import") {
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
if (count($_FILES) > 0) {
$tmp = $_FILES['auto_tag_file']['tmp_name'];
$contents = file_get_contents($tmp);
$count = $this->add_auto_tag_csv($database, $contents);
log_info(AutoTaggerInfo::KEY, "Imported $count auto-tag definitions from file from file", "Imported $count auto-tag definitions");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("auto_tag/list"));
} else {
$this->theme->display_error(400, "No File Specified", "You have to upload a file");
}
} else {
$this->theme->display_error(401, "Admins Only", "Only admins can edit the auto-tag list");
}
}
}
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
if ($event->parent=="tags") {
$event->add_nav_link("auto_tag", new Link('auto_tag/list'), "Auto-Tag", NavLink::is_active(["auto_tag"]));
}
}
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{
global $database;
// Create the database tables
if ($this->get_version(AutoTaggerConfig::VERSION) < 1) {
$database->create_table("auto_tag", "
tag VARCHAR(128) NOT NULL PRIMARY KEY,
additional_tags VARCHAR(2000) NOT NULL
");
if ($database->get_driver_name() == DatabaseDriver::PGSQL) {
$database->execute('CREATE INDEX auto_tag_lower_tag_idx ON auto_tag ((lower(tag)))');
}
$this->set_version(AutoTaggerConfig::VERSION, 1);
log_info(AutoTaggerInfo::KEY, "extension installed");
}
}
public function onTagSet(TagSetEvent $event)
{
$results = $this->apply_auto_tags($event->tags);
if (!empty($results)) {
$event->tags = $results;
}
}
public function onAddAutoTag(AddAutoTagEvent $event)
{
global $page;
$this->add_auto_tag($event->tag, $event->additional_tags);
$page->flash("Added Auto-Tag");
}
public function onDeleteAutoTag(DeleteAutoTagEvent $event)
{
$this->remove_auto_tag($event->tag);
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
$event->add_link("Auto-Tag Editor", make_link("auto_tag/list"));
}
}
private function get_auto_tag_csv(Database $database): string
{
$csv = "";
$pairs = $database->get_pairs("SELECT tag, additional_tags FROM auto_tag ORDER BY tag");
foreach ($pairs as $old => $new) {
$csv .= "\"$old\",\"$new\"\n";
}
return $csv;
}
private function add_auto_tag_csv(Database $database, string $csv): int
{
$csv = str_replace("\r", "\n", $csv);
$i = 0;
foreach (explode("\n", $csv) as $line) {
$parts = str_getcsv($line);
if (count($parts) == 2) {
try {
send_event(new AddAutoTagEvent($parts[0], $parts[1]));
$i++;
} catch (AddAutoTagException $ex) {
$this->theme->display_error(500, "Error adding auto-tags", $ex->getMessage());
}
}
}
return $i;
}
private function add_auto_tag(string $tag, string $additional_tags)
{
global $database;
if ($database->exists("SELECT * FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag"=>$tag])) {
throw new AutoTaggerException("Auto-Tag is already set for that tag");
} else {
$tag = Tag::sanitize($tag);
$additional_tags = Tag::explode($additional_tags);
$database->execute(
"INSERT INTO auto_tag(tag, additional_tags) VALUES(:tag, :additional_tags)",
["tag"=>$tag, "additional_tags"=>Tag::implode($additional_tags)]
);
log_info(
AutoTaggerInfo::KEY,
"Added auto-tag for {$tag} -> {".implode(" ", $additional_tags)."}"
);
// Now we apply it to existing items
$this->apply_new_auto_tag($tag);
}
}
private function update_auto_tag(string $tag, string $additional_tags): bool
{
global $database;
$result = $database->get_row("SELECT * FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag"=>$tag]);
if ($result===null) {
throw new AutoTaggerException("Auto-tag not set for $tag, can't update");
} else {
$additional_tags = Tag::explode($additional_tags);
$current_additional_tags = Tag::explode($result["additional_tags"]);
if (!Tag::compare($additional_tags, $current_additional_tags)) {
$database->execute(
"UPDATE auto_tag SET additional_tags = :additional_tags WHERE LOWER(tag)=LOWER(:tag)",
["tag"=>$tag, "additional_tags"=>Tag::implode($additional_tags)]
);
log_info(
AutoTaggerInfo::KEY,
"Updated auto-tag for {$tag} -> {".implode(" ", $additional_tags)."}",
"Updated Auto-Tag"
);
// Now we apply it to existing items
$this->apply_new_auto_tag($tag);
return true;
}
}
return false;
}
private function apply_new_auto_tag(string $tag)
{
global $database;
$tag_id = $database->get_one("SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)", ["tag"=>$tag]);
if (!empty($tag_id)) {
$image_ids = $database->get_col_iterable("SELECT image_id FROM image_tags WHERE tag_id = :tag_id", ["tag_id"=>$tag_id]);
foreach ($image_ids as $image_id) {
$image = Image::by_id($image_id);
$event = new TagSetEvent($image, $image->get_tag_array());
send_event($event);
}
}
}
private function remove_auto_tag(String $tag)
{
global $database;
$database->execute("DELETE FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag" => $tag]);
}
/**
* #param string[] $tags_mixed
*/
private function apply_auto_tags(array $tags_mixed): ?array
{
global $database;
while (true) {
$new_tags = [];
foreach ($tags_mixed as $tag) {
$additional_tags = $database->get_one(
"SELECT additional_tags FROM auto_tag WHERE LOWER(tag) = LOWER(:input)",
["input" => $tag]
);
if (!empty($additional_tags)) {
$additional_tags = explode(" ", $additional_tags);
$new_tags = array_merge(
$new_tags,
array_udiff($additional_tags, $tags_mixed, 'strcasecmp')
);
}
}
if (empty($new_tags)) {
break;
}
$tags_mixed = array_merge($tags_mixed, $new_tags);
}
$results = array_intersect_key(
$tags_mixed,
array_unique(array_map('strtolower', $tags_mixed))
);
return $results;
}
/**
* Get the priority for this extension.
*
*/
public function get_priority(): int
{
return 30;
}
}

View File

@ -1,58 +0,0 @@
<?php declare(strict_types=1);
class AutoTaggerTest extends ShimmiePHPUnitTestCase
{
public function testAutoTaggerList()
{
$this->get_page('auto_tag/list');
$this->assert_response(200);
$this->assert_title("Auto-Tag");
}
public function testAutoTaggerListReadOnly()
{
$this->log_in_as_user();
$this->get_page('auto_tag/list');
$this->assert_title("Auto-Tag");
$this->assert_no_text("value=\"Add\"");
$this->log_out();
$this->get_page('auto_tag/list');
$this->assert_title("Auto-Tag");
$this->assert_no_text("value=\"Add\"");
}
public function testAutoTagger()
{
$this->log_in_as_admin();
$this->get_page("auto_tag/export/auto_tag.csv");
$this->assert_no_text("test1");
send_event(new AddAutoTagEvent("test1", "test2"));
$this->get_page('auto_tag/list');
$this->assert_text("test1");
$this->assert_text("test2");
$this->get_page("auto_tag/export/auto_tag.csv");
$this->assert_text('"test1","test2"');
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
$this->get_page("post/view/$image_id"); # check that the tag has been replaced
$this->assert_title("Image $image_id: test1 test2");
$this->delete_image($image_id);
send_event(new AddAutoTagEvent("test2", "test3"));
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
$this->get_page("post/view/$image_id"); # check that the tag has been replaced
$this->assert_title("Image $image_id: test1 test2 test3");
$this->delete_image($image_id);
send_event(new DeleteAutoTagEvent("test1"));
send_event(new DeleteAutoTagEvent("test2"));
$this->get_page('auto_tag/list');
$this->assert_title("Auto-Tag");
$this->assert_no_text("test1");
$this->assert_no_text("test2");
$this->assert_no_text("test3");
}
}

View File

@ -1,36 +0,0 @@
<?php declare(strict_types=1);
class AutoTaggerTheme extends Themelet
{
/**
* Show a page of auto-tag definitions.
*
* Note: $can_manage = whether things like "add new alias" should be shown
*/
public function display_auto_tagtable($table, $paginator): void
{
global $page, $user;
$can_manage = $user->can(Permissions::MANAGE_AUTO_TAG);
$html = "
$table
$paginator
<p><a href='".make_link("auto_tag/export/auto_tag.csv")."' download='auto_tag.csv'>Download as CSV</a></p>
";
$bulk_html = "
".make_form(make_link("auto_tag/import"), 'post', true)."
<input type='file' name='auto_tag_file'>
<input type='submit' value='Upload List'>
</form>
";
$page->set_title("Auto-Tag List");
$page->set_heading("Auto-Tag List");
$page->add_block(new NavBlock());
$page->add_block(new Block("Auto-Tag", $html));
if ($can_manage) {
$page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51));
}
}
}

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,69 +0,0 @@
ul.tagit {
padding: 1px 5px;
overflow: auto;
margin-left: inherit; /* usually we don't want the regular ul margins. */
margin-right: inherit;
}
ul.tagit li {
display: block;
float: left;
margin: 2px 5px 2px 0;
}
ul.tagit li.tagit-choice {
position: relative;
line-height: inherit;
}
input.tagit-hidden-field {
display: none;
}
ul.tagit li.tagit-choice-read-only {
padding: .2em .5em .2em .5em;
}
ul.tagit li.tagit-choice-editable {
padding: .2em 18px .2em .5em;
}
ul.tagit li.tagit-new {
padding: .25em 4px .25em 0;
}
ul.tagit li.tagit-choice a.tagit-label {
cursor: pointer;
text-decoration: none;
}
ul.tagit li.tagit-choice .tagit-close {
cursor: pointer;
position: absolute;
right: .1em;
top: 50%;
margin-top: -8px;
line-height: 17px;
}
/* used for some custom themes that don't need image icons */
ul.tagit li.tagit-choice .tagit-close .text-icon {
display: none;
}
ul.tagit li.tagit-choice input {
display: block;
float: left;
margin: 2px 5px 2px 0;
}
ul.tagit input[type="text"] {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
border: none;
margin: 0;
padding: 0;
width: inherit;
background-color: inherit;
outline: none;
}

View File

@ -1,18 +0,0 @@
//Removed TAB keybind
;(function(b){b.widget("ui.tagit",{options:{allowDuplicates:!1,caseSensitive:!0,fieldName:"tags",placeholderText:null,readOnly:!1,removeConfirmation:!1,tagLimit:null,availableTags:[],autocomplete:{},showAutocompleteOnFocus:!1,allowSpaces:!1,singleField:!1,singleFieldDelimiter:",",singleFieldNode:null,animate:!0,tabIndex:null,beforeTagAdded:null,afterTagAdded:null,beforeTagRemoved:null,afterTagRemoved:null,onTagClicked:null,onTagLimitExceeded:null,onTagAdded:null,onTagRemoved:null,tagSource:null},_create:function(){var a=
this;this.element.is("input")?(this.tagList=b("<ul></ul>").insertAfter(this.element),this.options.singleField=!0,this.options.singleFieldNode=this.element,this.element.addClass("tagit-hidden-field")):this.tagList=this.element.find("ul, ol").andSelf().last();this.tagInput=b('<input type="text" />').addClass("ui-widget-content");this.options.readOnly&&this.tagInput.attr("disabled","disabled");this.options.tabIndex&&this.tagInput.attr("tabindex",this.options.tabIndex);this.options.placeholderText&&this.tagInput.attr("placeholder",
this.options.placeholderText);this.options.autocomplete.source||(this.options.autocomplete.source=function(a,e){var d=a.term.toLowerCase(),c=b.grep(this.options.availableTags,function(a){return 0===a.toLowerCase().indexOf(d)});this.options.allowDuplicates||(c=this._subtractArray(c,this.assignedTags()));e(c)});this.options.showAutocompleteOnFocus&&(this.tagInput.focus(function(b,d){a._showAutocomplete()}),"undefined"===typeof this.options.autocomplete.minLength&&(this.options.autocomplete.minLength=
0));b.isFunction(this.options.autocomplete.source)&&(this.options.autocomplete.source=b.proxy(this.options.autocomplete.source,this));b.isFunction(this.options.tagSource)&&(this.options.tagSource=b.proxy(this.options.tagSource,this));this.tagList.addClass("tagit").addClass("ui-widget ui-widget-content ui-corner-all").append(b('<li class="tagit-new"></li>').append(this.tagInput)).click(function(d){var c=b(d.target);c.hasClass("tagit-label")?(c=c.closest(".tagit-choice"),c.hasClass("removed")||a._trigger("onTagClicked",
d,{tag:c,tagLabel:a.tagLabel(c)})):a.tagInput.focus()});var c=!1;if(this.options.singleField)if(this.options.singleFieldNode){var d=b(this.options.singleFieldNode),f=d.val().split(this.options.singleFieldDelimiter);d.val("");b.each(f,function(b,d){a.createTag(d,null,!0);c=!0})}else this.options.singleFieldNode=b('<input type="hidden" style="display:none;" value="" name="'+this.options.fieldName+'" />'),this.tagList.after(this.options.singleFieldNode);c||this.tagList.children("li").each(function(){b(this).hasClass("tagit-new")||
(a.createTag(b(this).text(),b(this).attr("class"),!0),b(this).remove())});this.tagInput.keydown(function(c){if(c.which==b.ui.keyCode.BACKSPACE&&""===a.tagInput.val()){var d=a._lastTag();!a.options.removeConfirmation||d.hasClass("remove")?a.removeTag(d):a.options.removeConfirmation&&d.addClass("remove ui-state-highlight")}else a.options.removeConfirmation&&a._lastTag().removeClass("remove ui-state-highlight");if(c.which===b.ui.keyCode.COMMA&&!1===c.shiftKey||c.which===b.ui.keyCode.ENTER||c.which==
c.which==b.ui.keyCode.SPACE&&!0!==a.options.allowSpaces&&('"'!=b.trim(a.tagInput.val()).replace(/^s*/,"").charAt(0)||'"'==b.trim(a.tagInput.val()).charAt(0)&&'"'==b.trim(a.tagInput.val()).charAt(b.trim(a.tagInput.val()).length-1)&&0!==b.trim(a.tagInput.val()).length-1))c.which===b.ui.keyCode.ENTER&&""===a.tagInput.val()||c.preventDefault(),a.options.autocomplete.autoFocus&&a.tagInput.data("autocomplete-open")||(a.tagInput.autocomplete("close"),a.createTag(a._cleanedInput()))}).blur(function(b){a.tagInput.data("autocomplete-open")||
a.createTag(a._cleanedInput())});if(this.options.availableTags||this.options.tagSource||this.options.autocomplete.source)d={select:function(b,c){a.createTag(c.item.value);return!1}},b.extend(d,this.options.autocomplete),d.source=this.options.tagSource||d.source,this.tagInput.autocomplete(d).bind("autocompleteopen.tagit",function(b,c){a.tagInput.data("autocomplete-open",!0)}).bind("autocompleteclose.tagit",function(b,c){a.tagInput.data("autocomplete-open",!1)}),this.tagInput.autocomplete("widget").addClass("tagit-autocomplete")},
destroy:function(){b.Widget.prototype.destroy.call(this);this.element.unbind(".tagit");this.tagList.unbind(".tagit");this.tagInput.removeData("autocomplete-open");this.tagList.removeClass("tagit ui-widget ui-widget-content ui-corner-all tagit-hidden-field");this.element.is("input")?(this.element.removeClass("tagit-hidden-field"),this.tagList.remove()):(this.element.children("li").each(function(){b(this).hasClass("tagit-new")?b(this).remove():(b(this).removeClass("tagit-choice ui-widget-content ui-state-default ui-state-highlight ui-corner-all remove tagit-choice-editable tagit-choice-read-only"),
b(this).text(b(this).children(".tagit-label").text()))}),this.singleFieldNode&&this.singleFieldNode.remove());return this},_cleanedInput:function(){return b.trim(this.tagInput.val().replace(/^"(.*)"$/,"$1"))},_lastTag:function(){return this.tagList.find(".tagit-choice:last:not(.removed)")},_tags:function(){return this.tagList.find(".tagit-choice:not(.removed)")},assignedTags:function(){var a=this,c=[];this.options.singleField?(c=b(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter),
""===c[0]&&(c=[])):this._tags().each(function(){c.push(a.tagLabel(this))});return c},_updateSingleTagsField:function(a){b(this.options.singleFieldNode).val(a.join(this.options.singleFieldDelimiter)).trigger("change")},_subtractArray:function(a,c){for(var d=[],f=0;f<a.length;f++)-1==b.inArray(a[f],c)&&d.push(a[f]);return d},tagLabel:function(a){return this.options.singleField?b(a).find(".tagit-label:first").text():b(a).find("input:first").val()},_showAutocomplete:function(){this.tagInput.autocomplete("search",
"")},_findTagByLabel:function(a){var c=this,d=null;this._tags().each(function(f){if(c._formatStr(a)==c._formatStr(c.tagLabel(this)))return d=b(this),!1});return d},_isNew:function(a){return!this._findTagByLabel(a)},_formatStr:function(a){return this.options.caseSensitive?a:b.trim(a.toLowerCase())},_effectExists:function(a){return Boolean(b.effects&&(b.effects[a]||b.effects.effect&&b.effects.effect[a]))},createTag:function(a,c,d){var f=this;a=b.trim(a);this.options.preprocessTag&&(a=this.options.preprocessTag(a));
if(""===a)return!1;if(!this.options.allowDuplicates&&!this._isNew(a))return a=this._findTagByLabel(a),!1!==this._trigger("onTagExists",null,{existingTag:a,duringInitialization:d})&&this._effectExists("highlight")&&a.effect("highlight"),!1;if(this.options.tagLimit&&this._tags().length>=this.options.tagLimit)return this._trigger("onTagLimitExceeded",null,{duringInitialization:d}),!1;var g=b(this.options.onTagClicked?'<a class="tagit-label"></a>':'<span class="tagit-label"></span>').text(a),e=b("<li></li>").addClass("tagit-choice ui-widget-content ui-state-default ui-corner-all").addClass(c).append(g);
this.options.readOnly?e.addClass("tagit-choice-read-only"):(e.addClass("tagit-choice-editable"),c=b("<span></span>").addClass("ui-icon ui-icon-close"),c=b('<a><span class="text-icon">\u00d7</span></a>').addClass("tagit-close").append(c).click(function(a){f.removeTag(e)}),e.append(c));this.options.singleField||(g=g.html(),e.append('<input type="hidden" value="'+g+'" name="'+this.options.fieldName+'" class="tagit-hidden-field" />'));!1!==this._trigger("beforeTagAdded",null,{tag:e,tagLabel:this.tagLabel(e),
duringInitialization:d})&&(this.options.singleField&&(g=this.assignedTags(),g.push(a),this._updateSingleTagsField(g)),this._trigger("onTagAdded",null,e),this.tagInput.val(""),this.tagInput.parent().before(e),this._trigger("afterTagAdded",null,{tag:e,tagLabel:this.tagLabel(e),duringInitialization:d}),this.options.showAutocompleteOnFocus&&!d&&setTimeout(function(){f._showAutocomplete()},0))},removeTag:function(a,c){c="undefined"===typeof c?this.options.animate:c;a=b(a);this._trigger("onTagRemoved",
null,a);if(!1!==this._trigger("beforeTagRemoved",null,{tag:a,tagLabel:this.tagLabel(a)})){if(this.options.singleField){var d=this.assignedTags(),f=this.tagLabel(a),d=b.grep(d,function(a){return a!=f});this._updateSingleTagsField(d)}if(c){a.addClass("removed");var d=this._effectExists("blind")?["blind",{direction:"horizontal"},"fast"]:["fast"],g=this;d.push(function(){a.remove();g._trigger("afterTagRemoved",null,{tag:a,tagLabel:g.tagLabel(a)})});a.fadeOut("fast").hide.apply(a,d).dequeue()}else a.remove(),
this._trigger("afterTagRemoved",null,{tag:a,tagLabel:this.tagLabel(a)})}},removeTagByLabel:function(a,b){var d=this._findTagByLabel(a);if(!d)throw"No such tag exists with the name '"+a+"'";this.removeTag(d,b)},removeAll:function(){var a=this;this._tags().each(function(b,d){a.removeTag(d,!1)})}})})(jQuery);

View File

@ -1,97 +0,0 @@
/* Optional scoped theme for tag-it which mimics the zendesk widget. */
ul.tagit {
border-style: solid;
border-width: 1px;
border-color: #C6C6C6;
background: inherit;
}
ul.tagit li.tagit-choice {
-moz-border-radius: 6px;
border-radius: 6px;
-webkit-border-radius: 6px;
border: 1px solid #CAD8F3;
background: #DEE7F8 none;
font-weight: normal;
}
ul.tagit li.tagit-choice .tagit-label:not(a) {
color: #555;
}
ul.tagit li.tagit-choice a.tagit-close {
text-decoration: none;
}
ul.tagit li.tagit-choice .tagit-close {
right: .4em;
}
ul.tagit li.tagit-choice .ui-icon {
display: none;
}
ul.tagit li.tagit-choice .tagit-close .text-icon {
display: inline;
font-family: arial, sans-serif;
font-size: 16px;
line-height: 16px;
color: #777;
}
ul.tagit li.tagit-choice:hover, ul.tagit li.tagit-choice.remove {
background-color: #bbcef1;
border-color: #6d95e0;
}
ul.tagit li.tagit-choice a.tagLabel:hover,
ul.tagit li.tagit-choice a.tagit-close .text-icon:hover {
color: #222;
}
ul.tagit input[type="text"] {
color: #333333;
background: none;
}
.ui-widget {
font-size: 1.1em;
}
/* Forked from a jQuery UI theme, so that we don't require the jQuery UI CSS as a dependency. */
.tagit-autocomplete.ui-autocomplete { position: absolute; cursor: default; }
* html .tagit-autocomplete.ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
.tagit-autocomplete.ui-menu {
list-style:none;
padding: 2px;
margin: 0;
display:block;
float: left;
}
.tagit-autocomplete.ui-menu .ui-menu {
margin-top: -3px;
}
.tagit-autocomplete.ui-menu .ui-menu-item {
margin:0;
padding: 0;
zoom: 1;
float: left;
clear: left;
width: 100%;
}
.tagit-autocomplete.ui-menu .ui-menu-item a {
text-decoration:none;
display:block;
padding:.2em .4em;
line-height:1.5;
zoom:1;
}
.tagit-autocomplete .ui-menu .ui-menu-item a.ui-state-hover,
.tagit-autocomplete .ui-menu .ui-menu-item a.ui-state-active {
font-weight: normal;
margin: -1px;
}
.tagit-autocomplete.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff 50% 50% repeat-x; color: #222222; }
.tagit-autocomplete.ui-corner-all, .tagit-autocomplete .ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; -khtml-border-radius: 4px; border-radius: 4px; }
.tagit-autocomplete .ui-state-hover, .tagit-autocomplete .ui-state-focus { border: 1px solid #999999; background: #dadada; font-weight: normal; color: #212121; }
.tagit-autocomplete .ui-state-active { border: 1px solid #aaaaaa; }
.tagit-autocomplete .ui-widget-content { border: 1px solid #aaaaaa; }
.tagit .ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px,1px,1px,1px); }

View File

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

View File

@ -1,81 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
var metatags = ['order:id', 'order:width', 'order:height', 'order:filesize', 'order:filename'];
$('[name="search"]').tagit({
singleFieldDelimiter: ' ',
beforeTagAdded: function(event, ui) {
if(metatags.indexOf(ui.tagLabel) !== -1) {
ui.tag.addClass('tag-metatag');
} else {
console.log(ui.tagLabel);
// give special class to negative tags
if(ui.tagLabel[0] === '-') {
ui.tag.addClass('tag-negative');
}else{
ui.tag.addClass('tag-positive');
}
}
},
autocomplete : ({
source: function (request, response) {
var ac_metatags = $.map(
$.grep(metatags, function(s) {
// Only show metatags for strings longer than one character
return (request.term.length > 1 && s.indexOf(request.term) === 0);
}),
function(item) {
return {
label : item + ' [metatag]',
value : item
};
}
);
var isNegative = (request.term[0] === '-');
$.ajax({
url: base_href + '/api/internal/autocomplete',
data: {'s': (isNegative ? request.term.substring(1) : request.term)},
dataType : 'json',
type : 'GET',
success : function (data) {
response(
$.merge(ac_metatags,
$.map(data, function (count, item) {
item = (isNegative ? '-'+item : item);
return {
label : item + ' ('+count+')',
value : item
};
})
)
);
},
error : function (request, status, error) {
console.log(error);
}
});
},
minLength: 1
})
});
$('.ui-autocomplete-input').keydown(function(e) {
var keyCode = e.keyCode || e.which;
//Stop tags containing space.
if(keyCode == 32) {
e.preventDefault();
$('.autocomplete_tags').tagit('createTag', $(this).val());
$(this).autocomplete('close');
} else if (keyCode == 9) {
e.preventDefault();
var tag = $('.tagit-autocomplete[style*=\"display: block\"] > li:focus, .tagit-autocomplete[style*=\"display: block\"] > li:first').first();
if(tag.length){
$(tag).click();
$('.ui-autocomplete-input').val(''); //If tag already exists, make sure to remove duplicate.
}
}
});
});

View File

@ -1,9 +0,0 @@
#Navigationleft .blockbody { overflow: visible; }
.tagit { background: white !important; border: 1px solid grey !important; cursor: text; }
.tagit-choice { cursor: initial; }
input[name=search] ~ input[type=submit] { display: inline-block !important; }
.tag-negative { background: #ff8080 !important; }
.tag-positive { background: #40bf40 !important; }
.tag-metatag { background: #eaa338 !important; }

View File

@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
class AutoCompleteTest extends ShimmiePHPUnitTestCase
{
public function testAuth()
{
send_event(new UserLoginEvent(User::by_name(self::$anon_name)));
$page = $this->get_page('api/internal/autocomplete', ["s"=>"not-a-tag"]);
$this->assertEquals(200, $page->code);
$this->assertEquals(PageMode::DATA, $page->mode);
$this->assertEquals("[]", $page->data);
}
}

View File

@ -1,15 +0,0 @@
<?php declare(strict_types=1);
class AutoCompleteTheme extends Themelet
{
public function build_autocomplete(Page $page)
{
$base_href = get_base_href();
// TODO: AJAX test and fallback.
$page->add_html_header("<script defer src='$base_href/ext/autocomplete/lib/jquery-ui.min.js' type='text/javascript'></script>");
$page->add_html_header("<script defer src='$base_href/ext/autocomplete/lib/tag-it.min.js' type='text/javascript'></script>");
$page->add_html_header('<link rel="stylesheet" type="text/css" href="//ajax.googleapis.com/ajax/libs/jqueryui/1/themes/flick/jquery-ui.css">');
$page->add_html_header("<link rel='stylesheet' type='text/css' href='$base_href/ext/autocomplete/lib/jquery.tagit.css' />");
}
}

View File

@ -1,26 +0,0 @@
<?php declare(strict_types=1);
class BanWordsInfo extends ExtensionInfo
{
public const KEY = "ban_words";
public $key = self::KEY;
public $name = "Comment Word Ban";
public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2;
public $description = "For stopping spam and other comment abuse";
public $documentation =
"Allows an administrator to ban certain words
from comments. This can be a very simple but effective way
of stopping spam; just add \"viagra\", \"porn\", etc to the
banned words list.
<p>Regex bans are also supported, allowing more complicated
bans like <code>/http:.*\.cn\//</code> to block links to
chinese websites, or <code>/.*?http.*?http.*?http.*?http.*?/</code>
to block comments with four (or more) links in.
<p>Note that for non-regex matches, only whole words are
matched, eg banning \"sex\" would block the comment \"get free
sex call this number\", but allow \"This is a photo of Bob
from Essex\"";
}

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