diff --git a/.buildpath b/.buildpath deleted file mode 100644 index 8bcb4b5f..00000000 --- a/.buildpath +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..828d2ff8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +vendor +.git +*.phar +data +images +thumbs +*.sqlite diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..15d2c086 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# 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 + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..24d11dfb --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.php text eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..cd370b01 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Server Software, if you're the server admin (please complete the following information):** + - Shimmie version + - Database [mysql, postgres, ...] + - Web server [apache, nginx, ...] + +**Client Software (please complete the following information):** + - Device [e.g. iphone, windows desktop] + - Browser [e.g. chrome, safari] diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..066b2d92 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +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. diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..4947c19a --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,13 @@ +name: Docker Push +on: [push] +jobs: + build: + runs-on: ubuntu-latest + 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 }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..12be62d6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,76 @@ +name: Unit Tests + +on: [push, pull_request] + +jobs: + build: + 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 apt update && sudo apt-get install -y postgresql postgresql-client ; + 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 DSN="pgsql:user=shimmie;password=shimmie;host=127.0.0.1;dbname=shimmie" + fi + if [[ "${{ matrix.database }}" == "mysql" ]]; then + export DSN="mysql:user=root;password=root;host=127.0.0.1;dbname=shimmie" + fi + if [[ "${{ matrix.database }}" == "sqlite" ]]; then + export 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 diff --git a/.gitignore b/.gitignore index 4002f868..4f3b64dc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,11 @@ backup data images thumbs -!lib/images *.phar *.sqlite -/lib/vendor/ +*.cache +.devcontainer +trace.json #Composer composer.phar @@ -54,7 +55,6 @@ Icon *.un~ Session.vim .netrwhist -*~ ### PhpStorm ### diff --git a/.htaccess b/.htaccess index d6a43797..e2f1ca28 100644 --- a/.htaccess +++ b/.htaccess @@ -17,8 +17,8 @@ # rather than link to images/ha/hash and have an ugly filename, # we link to images/hash/tags.ext; mod_rewrite splits things so # that shimmie sees hash and the user sees tags.ext - RewriteRule ^_images/([0-9a-f]{2})([0-9a-f]{30}).*$ images/$1/$1$2 [L] - RewriteRule ^_thumbs/([0-9a-f]{2})([0-9a-f]{30}).*$ thumbs/$1/$1$2 [L] + RewriteRule ^_images/([0-9a-f]{2})([0-9a-f]{30}).*$ data/images/$1/$1$2 [L] + RewriteRule ^_thumbs/([0-9a-f]{2})([0-9a-f]{30}).*$ data/thumbs/$1/$1$2 [L] # any requests for files which don't physically exist should be handled by index.php RewriteCond %{REQUEST_FILENAME} !-f @@ -27,7 +27,7 @@ ExpiresActive On - + Header set Cache-Control "public, max-age=2629743" @@ -46,6 +46,7 @@ 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 diff --git a/.php_cs.dist b/.php_cs.dist new file mode 100644 index 00000000..c36c9cd2 --- /dev/null +++ b/.php_cs.dist @@ -0,0 +1,19 @@ +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) +; + +?> diff --git a/.project b/.project deleted file mode 100644 index 092bd500..00000000 --- a/.project +++ /dev/null @@ -1,23 +0,0 @@ - - - Shimmie 2 - - - - - - org.eclipse.wst.validation.validationbuilder - - - - - org.eclipse.dltk.core.scriptbuilder - - - - - - org.eclipse.php.core.PHPNature - - - diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 3dba09a6..8a3a5db6 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -3,7 +3,17 @@ imports: - php filter: - excluded_paths: [lib/*,ext/*/lib/*,ext/tagger/script.js,ext/chatbox/*] + 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 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0743ff0d..00000000 --- a/.travis.yml +++ /dev/null @@ -1,62 +0,0 @@ -language: php -php: - - 5.6 - - 7.0 - - 7.1 - -sudo: false - -env: - matrix: - - DB=mysql - - DB=pgsql - - DB=sqlite - allow_failures: - - DB=sqlite - -cache: - directories: - - vendor - - $HOME/.composer/cache - -before_install: - - travis_retry composer self-update && composer --version #travis is bad at updating composer - - if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi; - -install: - - mkdir -p data/config - - | - if [[ "$DB" == "pgsql" ]]; then - psql -c "SELECT set_config('log_statement', 'all', false);" -U postgres ; - psql -c "CREATE DATABASE shimmie;" -U postgres ; - echo ' data/config/auto_install.conf.php ; - fi - - | - if [[ "$DB" == "mysql" ]]; then - mysql -e "SET GLOBAL general_log = 'ON';" -uroot ; - mysql -e "CREATE DATABASE shimmie;" -uroot ; - echo ' data/config/auto_install.conf.php ; - fi - - if [[ "$DB" == "sqlite" ]]; then echo ' data/config/auto_install.conf.php ; fi - - composer install - - php install.php - -script: - - vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover - -after_failure: - - head -n 100 data/config/* - - ls /var/run/mysql* - # All of the below commands require sudo, which we can't use without losing some speed & caching. - # SEE: https://docs.travis-ci.com/user/workers/container-based-infrastructure/ - # - ls /var/log/*mysql* - # - cat /var/log/mysql.err - # - cat /var/log/mysql.log - # - cat /var/log/mysql/error.log - # - cat /var/log/mysql/slow.log - # - ls /var/log/postgresql - # - cat /var/log/postgresql/postgresql* - -after_script: - - wget https://scrutinizer-ci.com/ocular.phar - - php ocular.phar code-coverage:upload --format=php-clover data/coverage.clover diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..c8c9d64f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM debian:stable-slim +ENV DEBIAN_FRONTEND=noninteractive +EXPOSE 8000 +RUN apt update && apt install -y curl +HEALTHCHECK --interval=5m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1 + +RUN apt install -y php7.3-cli php7.3-gd php7.3-pgsql php7.3-mysql php7.3-sqlite3 php7.3-zip php7.3-dom php7.3-mbstring php-xdebug +RUN apt install -y composer imagemagick vim zip unzip + +COPY composer.json composer.lock /app/ +WORKDIR /app +RUN composer install + +COPY . /app/ +RUN echo '=== Installing ===' && mkdir -p data/config && echo " data/config/auto_install.conf.php && 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 +RUN chmod +x /app/tests/docker-init.sh +CMD "/app/tests/docker-init.sh" diff --git a/README.markdown b/README.md similarity index 58% rename from README.markdown rename to README.md index 7660134c..6b0a1d54 100644 --- a/README.markdown +++ b/README.md @@ -10,15 +10,9 @@ # Shimmie -[![Build Status](https://travis-ci.org/shish/shimmie2.svg?branch=master)](https://travis-ci.org/shish/shimmie2) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/shish/shimmie2/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=master) +[![Unit Tests](https://github.com/shish/shimmie2/workflows/Unit%20Tests/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) -(master) - -[![Build Status](https://travis-ci.org/shish/shimmie2.svg?branch=develop)](https://travis-ci.org/shish/shimmie2) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/shish/shimmie2/badges/quality-score.png?b=develop)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=develop) -[![Code Coverage](https://scrutinizer-ci.com/g/shish/shimmie2/badges/coverage.png?b=develop)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=develop) -(develop) This is the main branch of Shimmie, if you know anything at all about running websites, this is the version to use. @@ -28,8 +22,11 @@ check out one of the versioned branches. # Requirements -- MySQL/MariaDB 5.1+ (with experimental support for PostgreSQL 9+ and SQLite 3) -- [Stable PHP](https://en.wikipedia.org/wiki/PHP#Release_history) (5.6+ as of writing) +- These are generally based on "whatever is in Debian Stable", because that's + conservative without being TOO painfully out of date, and is a nice target + for the unit test Docker build. +- A database: PostgreSQL 11+ / MariaDB 10.3+ / SQLite 3.27+ +- [Stable PHP](https://en.wikipedia.org/wiki/PHP#Release_history) (7.3+ as of writing) - GD or ImageMagick # Installation @@ -44,46 +41,41 @@ check out one of the versioned branches. # Installation (Development) -1. Download shimmie via the "Download Zip" button on the [develop](https://github.com/shish/shimmie2/tree/develop) branch. +1. Download shimmie via the "Download Zip" button on the [master](https://github.com/shish/shimmie2/tree/master) branch. 2. Unzip shimmie into a folder on the web host 3. Install [Composer](https://getcomposer.org/). (If you don't already have it) 4. Run `composer install` in the shimmie folder. 5. Follow instructions noted in "Installation" starting from step 3. -## Upgrade from 2.3.X +# Docker -1. Backup your current files and database! -2. Unzip into a clean folder -3. Copy across the images, thumbs, and data folders -4. Move `old/config.php` to `new/data/config/shimmie.conf.php` -5. Edit `shimmie.conf.php` to use the new database connection format: +Useful for testing in a known-good environment, this command will build a +simple debian image and run all the unit tests inside it: -OLD Format: -```php -$database_dsn = "://:@/"; +``` +docker build -t shimmie . ``` -NEW Format: -```php -define("DATABASE_DSN", ":user=;password=;host=;dbname="); +Once you have an image which has passed all tests, you can then run it to get +a live system: + +``` +docker run -p 0.0.0.0:8123:8000 shimmie ``` -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. +Then you can visit your server on port 8123 to see the site. +Note that the docker image is entirely self-contained and has no persistence +(assuming you use the sqlite database); each `docker run` will give a clean +un-installed image. ### 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. +While the basic database and file formats haven't changed *completely*, it's +different enough to be a pain. ## Custom Configuration @@ -91,7 +83,7 @@ enough to be a pain. 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 +Take a look at `core/sys_config.php` for the available options that can be used. @@ -100,35 +92,36 @@ be used. 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: +For example, one can override the default anonymous "allow nothing" +permissions like so: ```php -new UserClass("anonymous", "base", array( - "create_comment" => True, - "edit_image_tag" => True, - "edit_image_source" => True, - "create_image_report" => True, -)); +new UserClass("anonymous", "base", [ + Permissions::CREATE_COMMENT => True, + Permissions::EDIT_IMAGE_TAG => True, + Permissions::EDIT_IMAGE_SOURCE => True, + Permissions::CREATE_IMAGE_REPORT => True, +]); ``` For a moderator class, being a regular user who can delete images and comments: ```php -new UserClass("moderator", "user", array( - "delete_image" => True, - "delete_comment" => True, -)); +new UserClass("moderator", "user", [ + Permissions::DELETE_IMAGE => True, + Permissions::DELETE_COMMENT => True, +]); ``` -For a list of permissions, see `core/userclass.class.php` +For a list of permissions, see `core/permissions.php` # Development Info -ui-* cookies are for the client-side scripts only; in some configurations +ui-\* cookies are for the client-side scripts only; in some configurations (eg with varnish cache) they will be stripped before they reach the server -shm-* CSS classes are for javascript to hook into; if you're customising +shm-\* CSS classes are for javascript to hook into; if you're customising themes, be careful with these, and avoid styling them, eg: - shm-thumb = outermost element of a thumbnail @@ -141,16 +134,12 @@ themes, be careful with these, and avoid styling them, eg: - shm-clink = a link to a comment, flash the target element when clicked * data-clink-sel -Documentation: 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 -IRC: `#shimmie` on [Freenode](irc.freenode.net) - Email: webmaster at shishnet.org Issue/Bug tracker: http://github.com/shish/shimmie2/issues diff --git a/SPEED.md b/SPEED.md new file mode 100644 index 00000000..c98fe559 --- /dev/null +++ b/SPEED.md @@ -0,0 +1,65 @@ +Notes for any sites which require extra performance +=================================================== + +Image Serving +------------- +Firstly, make sure your webserver is configured properly and nice URLs are +enabled, so that images will be served straight from disk by the webserver +instead of via PHP. If you're serving images via PHP, then your site might +melt under the load of 5 concurrent users... + +Add a Cache +----------- +eg installing memcached, then setting +`define("CACHE_DSN", "memcache://127.0.0.1:11211")` - a bunch of stuff will +get served from the high-speed cache instead of the SQL database. + +`SPEED_HAX` +----------- +Setting this to true will make a bunch of changes which reduce the correctness +of the software and increase admin workload for the sake of speed. You almost +certainly don't want to set this, but if you do (eg you're trying to run a +site with 10,000 concurrent users on a single server), it can be a huge help. + +Notable behaviour changes: + +- Database schema upgrades are no longer automatic; you'll need to run + `php index.php db-upgrade` from the CLI each time you update the code. +- Mapping from Events to Extensions is cached - you'll need to delete + `data/cache/shm_event_listeners.php` after each code change, and after + enabling or disabling any extensions. +- Tag lists (eg alphabetic, popularity, map) are cached and you'll need + to delete them manually when you feel like it +- Anonymous users can only search for 3 tags at once +- We only show the first 500 pages of results for any query, except for + the most simple (no tags, or one positive tag) +- We only ever show the first 5,000 results for complex queries +- Only comments from the past 24 hours show up in /comment/list +- Web crawlers are blocked from creating too many nonsense searches +- The first 10 pages in the index get extra caching +- RSS is limited to 10 pages +- HTML for thumbnails is cached + +`WH_SPLITS` +----------- +Store files as `images/ab/cd/...` instead of `images/ab/...`, which can +reduce filesystem load when you have millions of images. + +Multiple Image Servers +---------------------- +Image links don't have to be `/images/$hash.$ext` on the local server, they +can be full URLs, and include weighted random parts, eg: + +`https://{fred=3,leo=1}.mysite.com/images/$hash.$ext` - the software will then +use consistent hashing to map 75% of the files to `fred.mysite.com` and 25% to +`leo.mysite.com` - then you can install Varnish or Squid or something as a +caching reverse-proxy. + +Profiling +--------- +`define()`'ing `TRACE_FILE` to a filename and `TRACE_THRESHOLD` to a number +of seconds will result in JSON event traces being dumped into that file +whenever a page takes longer than the threshold to load. These traces can +then be loaded into the chrome trace viewer (chrome://tracing/) and you'll +get a breakdown of page performance by extension, event, database, and cache +queries. diff --git a/composer.json b/composer.json index 73b7cd7a..097f2d37 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,8 @@ { + "name": "shish/shimmie2", + "description": "A tag-based image gallery", "type" : "project", - "license" : "GPL-2.0", + "license" : "GPL-2.0-or-later", "minimum-stability" : "dev", "repositories" : [ @@ -23,46 +25,43 @@ ], "require" : { - "php" : ">=5.6", + "php" : ">=7.3", + "ext-pdo": "*", + "ext-json": "*", "flexihash/flexihash" : "^2.0.0", "ifixit/php-akismet" : "1.*", "google/recaptcha" : "~1.1", "dapphp/securimage" : "3.6.*", + "shish/eventtracer-php" : "dev-master", + "shish/ffsphp" : "0.0.*", + "shish/microcrud" : "dev-master", + "shish/microhtml" : "^1.0.0", + "enshrined/svg-sanitize" : "0.13.*", - "bower-asset/jquery" : "1.12.3", - "bower-asset/jquery-timeago" : "1.5.2", + "bower-asset/jquery" : "1.12.*", + "bower-asset/jquery-timeago" : "1.5.*", "bower-asset/tablesorter" : "dev-master", - "bower-asset/mediaelement" : "2.21.1", - "bower-asset/js-cookie" : "2.1.1" - }, + "bower-asset/mediaelement" : "2.21.*", + "bower-asset/js-cookie" : "2.1.*" + }, "require-dev" : { - "phpunit/phpunit" : "5.*" + "phpunit/phpunit" : "8.*" }, - "vendor-copy": { - "vendor/bower-asset/jquery/dist/jquery.min.js" : "lib/vendor/js/jquery-1.12.3.min.js", - "vendor/bower-asset/jquery/dist/jquery.min.map" : "lib/vendor/js/jquery-1.12.3.min.map", - "vendor/bower-asset/jquery-timeago/jquery.timeago.js" : "lib/vendor/js/jquery.timeago.js", - "vendor/bower-asset/tablesorter/jquery.tablesorter.min.js" : "lib/vendor/js/jquery.tablesorter.min.js", - "vendor/bower-asset/mediaelement/build/flashmediaelement.swf" : "lib/vendor/swf/flashmediaelement.swf", - "vendor/bower-asset/js-cookie/src/js.cookie.js" : "lib/vendor/js/js.cookie.js" - }, - - "scripts": { - "pre-install-cmd" : [ - "php -r \"array_map('unlink', array_merge(glob('lib/vendor/js/j*.{js,map}', GLOB_BRACE), glob('lib/vendor/css/*.css'), glob('lib/vendor/swf/*.swf')));\"" - ], - "pre-update-cmd" : [ - "php -r \"array_map('unlink', array_merge(glob('lib/vendor/js/j*.{js,map}', GLOB_BRACE), glob('lib/vendor/css/*.css'), glob('lib/vendor/swf/*.swf')));\"" - ], - - "post-install-cmd" : [ - "php -r \"array_map('copy', array_keys(json_decode(file_get_contents('composer.json'), TRUE)['vendor-copy']), json_decode(file_get_contents('composer.json'), TRUE)['vendor-copy']);\"" - ], - "post-update-cmd" : [ - "php -r \"array_map('copy', array_keys(json_decode(file_get_contents('composer.json'), TRUE)['vendor-copy']), json_decode(file_get_contents('composer.json'), TRUE)['vendor-copy']);\"" - ] + "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", + "ext-zlib": "anti-spam", + "ext-xml": "some extensions", + "ext-gd": "GD-based thumbnailing" } } diff --git a/composer.lock b/composer.lock index 0be19b19..62a5cd41 100644 --- a/composer.lock +++ b/composer.lock @@ -1,24 +1,23 @@ { "_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#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "040335a85a560b3bdd3dcf55490c98a1", + "content-hash": "3719624fc2c580e06fe8595e6bf6cb71", "packages": [ { "name": "bower-asset/jquery", - "version": "1.12.3", + "version": "1.12.4", "source": { "type": "git", "url": "https://github.com/jquery/jquery-dist.git", - "reference": "3a43d7e563314bf32970b773dd31ecf2b90813dd" + "reference": "5e89585e0121e72ff47de177c5ef604f3089a53d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/3a43d7e563314bf32970b773dd31ecf2b90813dd", - "reference": "3a43d7e563314bf32970b773dd31ecf2b90813dd", - "shasum": null + "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/5e89585e0121e72ff47de177c5ef604f3089a53d", + "reference": "5e89585e0121e72ff47de177c5ef604f3089a53d" }, "type": "bower-asset", "license": [ @@ -27,17 +26,16 @@ }, { "name": "bower-asset/jquery-timeago", - "version": "v1.5.2", + "version": "v1.5.4", "source": { "type": "git", - "url": "https://github.com/rmm5t/jquery-timeago.git", - "reference": "67c11951ae9b6020341c1056a42b5406162db40c" + "url": "git@github.com:rmm5t/jquery-timeago.git", + "reference": "180864a9c544a49e43719b457250af216d5e4c3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rmm5t/jquery-timeago/zipball/67c11951ae9b6020341c1056a42b5406162db40c", - "reference": "67c11951ae9b6020341c1056a42b5406162db40c", - "shasum": null + "url": "https://api.github.com/repos/rmm5t/jquery-timeago/zipball/180864a9c544a49e43719b457250af216d5e4c3a", + "reference": "180864a9c544a49e43719b457250af216d5e4c3a" }, "require": { "bower-asset/jquery": ">=1.4" @@ -49,17 +47,16 @@ }, { "name": "bower-asset/js-cookie", - "version": "v2.1.1", + "version": "v2.1.4", "source": { "type": "git", - "url": "https://github.com/js-cookie/js-cookie.git", - "reference": "5c830fb71a2bd3acce9cb733d692e13316991891" + "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/5c830fb71a2bd3acce9cb733d692e13316991891", - "reference": "5c830fb71a2bd3acce9cb733d692e13316991891", - "shasum": null + "url": "https://api.github.com/repos/js-cookie/js-cookie/zipball/8b70250875f7e07445b6a457f9c2474ead4cba44", + "reference": "8b70250875f7e07445b6a457f9c2474ead4cba44" }, "type": "bower-asset", "license": [ @@ -68,17 +65,16 @@ }, { "name": "bower-asset/mediaelement", - "version": "2.21.1", + "version": "2.21.2", "source": { "type": "git", - "url": "https://github.com/mediaelement/mediaelement.git", - "reference": "6e80b260172f4ddc3b0bbee046775d2ba4c6f9b7" + "url": "git@github.com:johndyer/mediaelement.git", + "reference": "394db3b4a2e3f5f7988cacdefe62ed973bf4a3ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mediaelement/mediaelement/zipball/6e80b260172f4ddc3b0bbee046775d2ba4c6f9b7", - "reference": "6e80b260172f4ddc3b0bbee046775d2ba4c6f9b7", - "shasum": null + "url": "https://api.github.com/repos/johndyer/mediaelement/zipball/394db3b4a2e3f5f7988cacdefe62ed973bf4a3ce", + "reference": "394db3b4a2e3f5f7988cacdefe62ed973bf4a3ce" }, "type": "bower-asset", "license": [ @@ -91,37 +87,36 @@ "source": { "type": "git", "url": "https://github.com/christianbach/tablesorter.git", - "reference": "774576308e8a25aa9d68b7fe3069b79543992d7a" + "reference": "07e0918254df3c2057d6d8e4653a0769f1881412" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/christianbach/tablesorter/zipball/774576308e8a25aa9d68b7fe3069b79543992d7a", - "reference": "774576308e8a25aa9d68b7fe3069b79543992d7a", - "shasum": null + "url": "https://api.github.com/repos/christianbach/tablesorter/zipball/07e0918254df3c2057d6d8e4653a0769f1881412", + "reference": "07e0918254df3c2057d6d8e4653a0769f1881412" }, "type": "bower-asset", "license": [ "MIT,GPL" ], - "time": "2015-12-03T01:22:52+00:00" + "time": "2017-12-20T18:16:21+00:00" }, { "name": "dapphp/securimage", - "version": "3.6.5", + "version": "3.6.7", "source": { "type": "git", "url": "https://github.com/dapphp/securimage.git", - "reference": "3f5a84fd80b1a35d58332896c944142713a7e802" + "reference": "1ecb884797c66e01a875c058def46c85aecea45b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dapphp/securimage/zipball/3f5a84fd80b1a35d58332896c944142713a7e802", - "reference": "3f5a84fd80b1a35d58332896c944142713a7e802", + "url": "https://api.github.com/repos/dapphp/securimage/zipball/1ecb884797c66e01a875c058def46c85aecea45b", + "reference": "1ecb884797c66e01a875c058def46c85aecea45b", "shasum": "" }, "require": { "ext-gd": "*", - "php": ">=5.2.0" + "php": ">=5.4" }, "suggest": { "ext-pdo": "For database storage support", @@ -136,7 +131,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD" + "BSD-3-Clause" ], "authors": [ { @@ -147,10 +142,53 @@ "description": "PHP CAPTCHA Library", "homepage": "https://www.phpcaptcha.org", "keywords": [ + "Forms", + "anti-spam", "captcha", "security" ], - "time": "2016-12-04T17:45:57+00:00" + "time": "2018-03-09T06:07:41+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", @@ -211,24 +249,26 @@ "source": { "type": "git", "url": "https://github.com/google/recaptcha.git", - "reference": "6990961e664372ddbed7ebc1cd673da7077552e5" + "reference": "2ccff6a4fde9a975b70975567c2793bdf72d3085" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/google/recaptcha/zipball/6990961e664372ddbed7ebc1cd673da7077552e5", - "reference": "6990961e664372ddbed7ebc1cd673da7077552e5", + "url": "https://api.github.com/repos/google/recaptcha/zipball/2ccff6a4fde9a975b70975567c2793bdf72d3085", + "reference": "2ccff6a4fde9a975b70975567c2793bdf72d3085", "shasum": "" }, "require": { "php": ">=5.5" }, "require-dev": { - "phpunit/phpunit": "^4.8" + "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.1.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -240,15 +280,15 @@ "license": [ "BSD-3-Clause" ], - "description": "Client library for reCAPTCHA, a free service that protect websites from spam and abuse.", - "homepage": "http://www.google.com/recaptcha/", + "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": "2017-03-09T18:57:45+00:00" + "time": "2019-09-10T21:42:39+00:00" }, { "name": "ifixit/php-akismet", @@ -259,37 +299,220 @@ "reference": "fd4ff50eb577457c1b7b887401663e91e77625ae" }, "type": "library" + }, + { + "name": "shish/eventtracer-php", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/shish/eventtracer-php.git", + "reference": "57c1eb94028d8973907ee859455d9756b7cc597d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shish/eventtracer-php/zipball/57c1eb94028d8973907ee859455d9756b7cc597d", + "reference": "57c1eb94028d8973907ee859455d9756b7cc597d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-posix": "*", + "php": "^7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8" + }, + "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-03-16T15:07:22+00:00" + }, + { + "name": "shish/ffsphp", + "version": "v0.0.2", + "source": { + "type": "git", + "url": "https://github.com/shish/ffsphp.git", + "reference": "16c98d57c80bb4848f20253c8c1e5fe7f6c5823f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shish/ffsphp/zipball/16c98d57c80bb4848f20253c8c1e5fe7f6c5823f", + "reference": "16c98d57c80bb4848f20253c8c1e5fe7f6c5823f", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "phpunit/phpunit": "8.*" + }, + "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": "2019-11-29T12:00:09+00:00" + }, + { + "name": "shish/microcrud", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/shish/microcrud.git", + "reference": "60d501a7b21d88652af40bc0ddd964153bc16697" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shish/microcrud/zipball/60d501a7b21d88652af40bc0ddd964153bc16697", + "reference": "60d501a7b21d88652af40bc0ddd964153bc16697", + "shasum": "" + }, + "require": { + "ext-pdo": "*", + "php": ">=7.2", + "shish/ffsphp": "0.0.*", + "shish/microhtml": "^1.0.0" + }, + "require-dev": { + "phpunit/phpunit": "8.*" + }, + "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-03-18T17:19:10+00:00" + }, + { + "name": "shish/microhtml", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/shish/microhtml.git", + "reference": "b942fe1da33cd8889252290469ce3ed2ea329491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shish/microhtml/zipball/b942fe1da33cd8889252290469ce3ed2ea329491", + "reference": "b942fe1da33cd8889252290469ce3ed2ea329491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "phpunit/phpunit": "8.*" + }, + "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": "2019-12-09T14:15:35+00:00" } ], "packages-dev": [ { "name": "doctrine/instantiator", - "version": "1.0.x-dev", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + "reference": "6a1471ddbf2f448b35f3a8e390c903435e6dd5de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/6a1471ddbf2f448b35f3a8e390c903435e6dd5de", + "reference": "6a1471ddbf2f448b35f3a8e390c903435e6dd5de", "shasum": "" }, "require": { - "php": ">=5.3,<8.0-DEV" + "php": "^7.1" }, "require-dev": { - "athletic/athletic": "~0.1.8", + "doctrine/coding-standard": "^6.0", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.4.x-dev" } }, "autoload": { @@ -309,12 +532,12 @@ } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ "constructor", "instantiate" ], - "time": "2015-06-14T21:17:01+00:00" + "time": "2019-12-23T19:18:31+00:00" }, { "name": "myclabs/deep-copy", @@ -322,33 +545,39 @@ "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102" + "reference": "a491d65139e2411c75704e871dd02bdddf5a4bdc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/a491d65139e2411c75704e871dd02bdddf5a4bdc", + "reference": "a491d65139e2411c75704e871dd02bdddf5a4bdc", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": "^7.1" + }, + "replace": { + "myclabs/deep-copy": "self.version" }, "require-dev": { - "doctrine/collections": "1.*", - "phpunit/phpunit": "~4.1" + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" }, "type": "library", "autoload": { "psr-4": { "DeepCopy\\": "src/DeepCopy/" - } + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "description": "Create deep copies (clones) of your objects", - "homepage": "https://github.com/myclabs/DeepCopy", "keywords": [ "clone", "copy", @@ -356,27 +585,31 @@ "object", "object graph" ], - "time": "2017-04-12T18:52:22+00:00" + "time": "2020-03-12T21:49:07+00:00" }, { - "name": "phpdocumentor/reflection-common", + "name": "phar-io/manifest", "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + "url": "https://github.com/phar-io/manifest.git", + "reference": "3d94e3b6eb309e921a100a4992f72314299bb03f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/3d94e3b6eb309e921a100a4992f72314299bb03f", + "reference": "3d94e3b6eb309e921a100a4992f72314299bb03f", "shasum": "" }, "require": { - "php": ">=5.5" + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^2.0", + "php": "^7.2" }, "require-dev": { - "phpunit/phpunit": "^4.6" + "phpunit/phpunit": "^8.2" }, "type": "library", "extra": { @@ -384,11 +617,111 @@ "dev-master": "1.0.x-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2019-12-29T10:29:09+00:00" + }, + { + "name": "phar-io/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2018-07-08T19:19:57+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "b0843c8cbcc2dc5eda5158e583c7199a5e44c86d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/b0843c8cbcc2dc5eda5158e583c7199a5e44c86d", + "reference": "b0843c8cbcc2dc5eda5158e583c7199a5e44c86d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] + "phpDocumentor\\Reflection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -410,86 +743,92 @@ "reflection", "static analysis" ], - "time": "2017-09-11T18:02:19+00:00" + "time": "2019-12-20T12:45:35+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.2.2", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157" + "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", + "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", "shasum": "" }, "require": { - "php": ">=5.5", - "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.3.0", - "webmozart/assert": "^1.0" + "ext-filter": "^7.1", + "php": "^7.2", + "phpdocumentor/reflection-common": "^2.0", + "phpdocumentor/type-resolver": "^1.0", + "webmozart/assert": "^1" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-08-08T06:39:58+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "0.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fb3933512008d8162b3cdf9e18dba9309b7c3773", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" + "doctrine/instantiator": "^1", + "mockery/mockery": "^1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "5.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2020-02-22T12:28:44+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "d6b5291650d058fe1162a54fee9d923de19bcda2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/d6b5291650d058fe1162a54fee9d923de19bcda2", + "reference": "d6b5291650d058fe1162a54fee9d923de19bcda2", + "shasum": "" + }, + "require": { + "php": "^7.2", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "^7.2", + "mockery/mockery": "~1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -502,7 +841,8 @@ "email": "me@mikevanriel.com" } ], - "time": "2017-06-03T08:32:36+00:00" + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2020-03-06T17:11:40+00:00" }, { "name": "phpspec/prophecy", @@ -510,34 +850,34 @@ "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6" + "reference": "451c3cd1418cf640de218914901e51b064abb093" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", - "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", + "reference": "451c3cd1418cf640de218914901e51b064abb093", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0" + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", + "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" }, "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8 || ^5.6.5" + "phpspec/phpspec": "^2.5 || ^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.10.x-dev" } }, "autoload": { - "psr-0": { - "Prophecy\\": "src/" + "psr-4": { + "Prophecy\\": "src/Prophecy" } }, "notification-url": "https://packagist.org/downloads/", @@ -565,44 +905,44 @@ "spy", "stub" ], - "time": "2017-09-04T11:05:03+00:00" + "time": "2020-03-05T15:02:03+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "4.0.x-dev", + "version": "7.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" + "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf", + "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^5.6 || ^7.0", - "phpunit/php-file-iterator": "^1.3", - "phpunit/php-text-template": "^1.2", - "phpunit/php-token-stream": "^1.4.2 || ^2.0", - "sebastian/code-unit-reverse-lookup": "^1.0", - "sebastian/environment": "^1.3.2 || ^2.0", - "sebastian/version": "^1.0 || ^2.0" + "php": "^7.2", + "phpunit/php-file-iterator": "^2.0.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.1.1", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^4.2.2", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1.3" }, "require-dev": { - "ext-xdebug": "^2.1.4", - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^8.2.2" }, "suggest": { - "ext-xdebug": "^2.5.1" + "ext-xdebug": "^2.7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-master": "7.0-dev" } }, "autoload": { @@ -617,7 +957,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -628,29 +968,32 @@ "testing", "xunit" ], - "time": "2017-04-02T07:44:40+00:00" + "time": "2019-11-20T13:55:58+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "dev-master", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + "reference": "050bedf145a257b1ff02746c31894800e5122946" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", + "reference": "050bedf145a257b1ff02746c31894800e5122946", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -665,7 +1008,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -675,7 +1018,7 @@ "filesystem", "iterator" ], - "time": "2016-10-03T07:40:28+00:00" + "time": "2018-09-13T20:33:42+00:00" }, { "name": "phpunit/php-text-template", @@ -720,28 +1063,28 @@ }, { "name": "phpunit/php-timer", - "version": "dev-master", + "version": "2.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "d107f347d368dd8a384601398280c7c608390ab7" + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/d107f347d368dd8a384601398280c7c608390ab7", - "reference": "d107f347d368dd8a384601398280c7c608390ab7", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -756,7 +1099,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -765,33 +1108,33 @@ "keywords": [ "timer" ], - "time": "2017-03-07T15:42:04+00:00" + "time": "2019-06-07T04:22:29+00:00" }, { "name": "phpunit/php-token-stream", - "version": "1.4.x-dev", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "958103f327daef5dd0bb328dec53e0a9e43cfaf7" + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/958103f327daef5dd0bb328dec53e0a9e43cfaf7", - "reference": "958103f327daef5dd0bb328dec53e0a9e43cfaf7", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=5.3.3" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -814,55 +1157,56 @@ "keywords": [ "tokenizer" ], - "time": "2017-03-07T08:21:50+00:00" + "time": "2019-09-17T06:23:10+00:00" }, { "name": "phpunit/phpunit", - "version": "5.7.x-dev", + "version": "8.5.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4eba3374803c6c0903145e8940844e6f1d665c07" + "reference": "014b4c58184c41572c9ae4297c73d9efe2bc9bd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4eba3374803c6c0903145e8940844e6f1d665c07", - "reference": "4eba3374803c6c0903145e8940844e6f1d665c07", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/014b4c58184c41572c9ae4297c73d9efe2bc9bd3", + "reference": "014b4c58184c41572c9ae4297c73d9efe2bc9bd3", "shasum": "" }, "require": { + "doctrine/instantiator": "^1.2.0", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", - "myclabs/deep-copy": "~1.3", - "php": "^5.6 || ^7.0", - "phpspec/prophecy": "^1.6.2", - "phpunit/php-code-coverage": "^4.0.4", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "^3.2", - "sebastian/comparator": "^1.2.4", - "sebastian/diff": "^1.4.3", - "sebastian/environment": "^1.3.4 || ^2.0", - "sebastian/exporter": "~2.0", - "sebastian/global-state": "^1.1", - "sebastian/object-enumerator": "~2.0", - "sebastian/resource-operations": "~1.0", - "sebastian/version": "~1.0.3|~2.0", - "symfony/yaml": "~2.1|~3.0" - }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2" + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.9.1", + "phar-io/manifest": "^1.0.3", + "phar-io/version": "^2.0.1", + "php": "^7.2", + "phpspec/prophecy": "^1.8.1", + "phpunit/php-code-coverage": "^7.0.7", + "phpunit/php-file-iterator": "^2.0.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1.2", + "sebastian/comparator": "^3.0.2", + "sebastian/diff": "^3.0.2", + "sebastian/environment": "^4.2.2", + "sebastian/exporter": "^3.1.1", + "sebastian/global-state": "^3.0.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0.1", + "sebastian/type": "^1.1.3", + "sebastian/version": "^2.0.1" }, "require-dev": { "ext-pdo": "*" }, "suggest": { + "ext-soap": "*", "ext-xdebug": "*", - "phpunit/php-invoker": "~1.1" + "phpunit/php-invoker": "^2.0.0" }, "bin": [ "phpunit" @@ -870,7 +1214,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.7.x-dev" + "dev-master": "8.5-dev" } }, "autoload": { @@ -896,79 +1240,20 @@ "testing", "xunit" ], - "time": "2017-09-01T08:38:37+00:00" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "3.4.x-dev", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "a23b761686d50a560cc56233b9ecf49597cc9118" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118", - "reference": "a23b761686d50a560cc56233b9ecf49597cc9118", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.6 || ^7.0", - "phpunit/php-text-template": "^1.2", - "sebastian/exporter": "^1.2 || ^2.0" - }, - "conflict": { - "phpunit/phpunit": "<5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.4" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "time": "2017-06-30T09:13:00+00:00" + "time": "2020-03-11T07:08:11+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "dev-master", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "3488be0a7b346cd6e5361510ed07e88f9bea2e88" + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/3488be0a7b346cd6e5361510ed07e88f9bea2e88", - "reference": "3488be0a7b346cd6e5361510ed07e88f9bea2e88", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", "shasum": "" }, "require": { @@ -1000,34 +1285,34 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2017-03-04T10:23:55+00:00" + "time": "2017-03-04T06:30:41+00:00" }, { "name": "sebastian/comparator", - "version": "1.2.x-dev", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "18a5d97c25f408f48acaf6d1b9f4079314c5996a" + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/18a5d97c25f408f48acaf6d1b9f4079314c5996a", - "reference": "18a5d97c25f408f48acaf6d1b9f4079314c5996a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", "shasum": "" }, "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2 || ~2.0" + "php": "^7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1058,38 +1343,39 @@ } ], "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", + "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ "comparator", "compare", "equality" ], - "time": "2017-03-07T10:34:43+00:00" + "time": "2018-07-12T15:12:46+00:00" }, { "name": "sebastian/diff", - "version": "1.4.x-dev", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1114,34 +1400,40 @@ "description": "Diff implementation", "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ - "diff" + "diff", + "udiff", + "unidiff", + "unified diff" ], - "time": "2017-05-22T07:24:03+00:00" + "time": "2019-02-04T06:01:07+00:00" }, { "name": "sebastian/environment", - "version": "2.0.x-dev", + "version": "4.2.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368", + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^5.0" + "phpunit/phpunit": "^7.5" + }, + "suggest": { + "ext-posix": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -1166,34 +1458,34 @@ "environment", "hhvm" ], - "time": "2016-11-26T07:53:53+00:00" + "time": "2019-11-20T08:46:58+00:00" }, { "name": "sebastian/exporter", - "version": "2.0.x-dev", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "5e8e30670c3f36481e75211dbbcfd029a41ebf07" + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/5e8e30670c3f36481e75211dbbcfd029a41ebf07", - "reference": "5e8e30670c3f36481e75211dbbcfd029a41ebf07", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0", - "sebastian/recursion-context": "^2.0" + "php": "^7.0", + "sebastian/recursion-context": "^3.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.1.x-dev" } }, "autoload": { @@ -1206,6 +1498,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -1214,17 +1510,13 @@ "name": "Volker Dusch", "email": "github@wallbash.com" }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, { "name": "Adam Harvey", "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], "description": "Provides the functionality to export PHP variables for visualization", @@ -1233,27 +1525,30 @@ "export", "exporter" ], - "time": "2017-03-07T10:36:49+00:00" + "time": "2019-09-14T09:02:43+00:00" }, { "name": "sebastian/global-state", - "version": "1.1.x-dev", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "cea85a84b00f2795341ebbbca4fa396347f2494e" + "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/cea85a84b00f2795341ebbbca4fa396347f2494e", - "reference": "cea85a84b00f2795341ebbbca4fa396347f2494e", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", + "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.2", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { - "phpunit/phpunit": "~4.2|~5.0" + "ext-dom": "*", + "phpunit/phpunit": "^8.0" }, "suggest": { "ext-uopz": "*" @@ -1261,7 +1556,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1284,33 +1579,34 @@ "keywords": [ "global state" ], - "time": "2017-02-23T14:11:06+00:00" + "time": "2019-02-01T05:30:01+00:00" }, { "name": "sebastian/object-enumerator", - "version": "2.0.x-dev", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "c956fe7a68318639f694fc6bba0c89b7cdf1b08c" + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/c956fe7a68318639f694fc6bba0c89b7cdf1b08c", - "reference": "c956fe7a68318639f694fc6bba0c89b7cdf1b08c", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", - "sebastian/recursion-context": "^2.0" + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -1330,32 +1626,77 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-03-07T10:37:45+00:00" + "time": "2017-08-03T12:35:26+00:00" }, { - "name": "sebastian/recursion-context", - "version": "2.0.x-dev", + "name": "sebastian/object-reflector", + "version": "1.1.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "7e4d7c56f6e65d215f71ad913a5256e5439aca1c" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/7e4d7c56f6e65d215f71ad913a5256e5439aca1c", - "reference": "7e4d7c56f6e65d215f71ad913a5256e5439aca1c", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -1383,29 +1724,29 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2017-03-08T08:21:15+00:00" + "time": "2017-03-03T06:23:57+00:00" }, { "name": "sebastian/resource-operations", - "version": "dev-master", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "fadc83f7c41fb2924e542635fea47ae546816ece" + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/fadc83f7c41fb2924e542635fea47ae546816ece", - "reference": "fadc83f7c41fb2924e542635fea47ae546816ece", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", "shasum": "" }, "require": { - "php": ">=5.6.0" + "php": "^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1425,11 +1766,57 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2016-10-03T07:43:09+00:00" + "time": "2018-10-04T04:07:39+00:00" + }, + { + "name": "sebastian/type", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3", + "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3", + "shasum": "" + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "time": "2019-07-02T08:10:15+00:00" }, { "name": "sebastian/version", - "version": "dev-master", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", @@ -1471,43 +1858,37 @@ "time": "2016-10-03T07:35:21+00:00" }, { - "name": "symfony/yaml", - "version": "3.4.x-dev", + "name": "symfony/polyfill-ctype", + "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "a0e15688972f012156cf1ffa076fe1203bce6bc9" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/a0e15688972f012156cf1ffa076fe1203bce6bc9", - "reference": "a0e15688972f012156cf1ffa076fe1203bce6bc9", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14", + "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8" - }, - "conflict": { - "symfony/console": "<3.4" - }, - "require-dev": { - "symfony/console": "~3.4|~4.0" + "php": ">=5.3.3" }, "suggest": { - "symfony/console": "For validating YAML files using the lint command" + "ext-ctype": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "1.15-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\Yaml\\": "" + "Symfony\\Polyfill\\Ctype\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "bootstrap.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1516,45 +1897,89 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Yaml Component", + "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", - "time": "2017-09-17T10:10:45+00:00" + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2020-02-27T09:26:54+00:00" }, { - "name": "webmozart/assert", - "version": "dev-master", + "name": "theseer/tokenizer", + "version": "1.1.3", "source": { "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "4a8bf11547e139e77b651365113fc12850c43d9a" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/4a8bf11547e139e77b651365113fc12850c43d9a", - "reference": "4a8bf11547e139e77b651365113fc12850c43d9a", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } + "autoload": { + "classmap": [ + "src/" + ] }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "time": "2019-06-13T22:48:21+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "aed98a490f9a8f78468232db345ab9cf606cf598" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598", + "reference": "aed98a490f9a8f78468232db345ab9cf606cf598", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "vimeo/psalm": "<3.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", "autoload": { "psr-4": { "Webmozart\\Assert\\": "src/" @@ -1576,18 +2001,22 @@ "check", "validate" ], - "time": "2016-11-23T20:04:41+00:00" + "time": "2020-02-14T12:15:55+00:00" } ], "aliases": [], "minimum-stability": "dev", "stability-flags": { + "shish/eventtracer-php": 20, + "shish/microcrud": 20, "bower-asset/tablesorter": 20 }, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=5.6" + "php": ">=7.3", + "ext-pdo": "*", + "ext-json": "*" }, "platform-dev": [] } diff --git a/core/_bootstrap.inc.php b/core/_bootstrap.inc.php deleted file mode 100644 index 47be22aa..00000000 --- a/core/_bootstrap.inc.php +++ /dev/null @@ -1,51 +0,0 @@ -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; + } + + // ============================================== + + /** + * Display the page according to the mode and data given. + */ + public function display(): void + { + header("HTTP/1.0 {$this->code} Shimmie"); + header("Content-type: " . $this->type); + header("X-Powered-By: Shimmie-" . VERSION); + + if (!headers_sent()) { + 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."; + } + + switch ($this->mode) { + 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 ' . $this->redirect . ''; + 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("", 40); + + # static handler will map these to themes/foo/static/bar.ico or ext/static_files/static/bar.ico + $this->add_html_header("", 41); + $this->add_html_header("", 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("", 43); + + /*** Generate JS cache files ***/ + $js_latest = $config_latest; + $js_files = array_merge( + [ + "vendor/bower-asset/jquery/dist/jquery.min.js", + "vendor/bower-asset/jquery-timeago/jquery.timeago.js", + "vendor/bower-asset/tablesorter/jquery.tablesorter.min.js", + "vendor/bower-asset/js-cookie/src/js.cookie.js", + "ext/static_files/modernizr-3.3.1.custom.js", + ], + 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("", 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 << + + $head_html + $body_html + +EOD; + } + + protected function head_html(): string + { + $html_header_html = $this->get_all_html_headers(); + + return " + + {$this->title} + $html_header_html + + "; + } + + 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 "

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 ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + return " + +

+ {$this->heading} + $sub_block_html +
+ +
+ $flash_html + $main_block_html +
+
+ $footer_html +
+ + "; + } + + protected function footer_html(): string + { + $debug = get_debug_info(); + $contact_link = contact_link(); + $contact = empty($contact_link) ? "" : "
Contact"; + + return " + Images © their respective owners, + Shimmie © + Shish & + The Team + 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; +} diff --git a/core/basethemelet.class.php b/core/basethemelet.class.php deleted file mode 100644 index 71ce4288..00000000 --- a/core/basethemelet.class.php +++ /dev/null @@ -1,166 +0,0 @@ -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() { - $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... - * - * @param Image $image - * @return string - */ - public function build_thumb_html(Image $image) { - 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(array('swf', 'svg', '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('thumb_width'), $config->get_int('thumb_height')); - } - - $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 "". - "$h_tip". - "\n"; - } - - /** - * Add a generic paginator. - * - * @param Page $page - * @param string $base - * @param string $query - * @param int $page_number - * @param int $total_pages - * @param bool $show_random - */ - public function display_paginator(Page $page, $base, $query, $page_number, $total_pages, $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")); - } - - /** - * Generate a single HTML link. - * - * @param string $base_url - * @param string $query - * @param string $page - * @param string $name - * @return string - */ - private function gen_page_link($base_url, $query, $page, $name) { - $link = make_link($base_url.'/'.$page, $query); - return ''.$name.''; - } - - /** - * @param string $base_url - * @param string $query - * @param string $page - * @param int $current_page - * @param string $name - * @return string - */ - private function gen_page_link_block($base_url, $query, $page, $current_page, $name) { - $paginator = ""; - if($page == $current_page) $paginator .= ""; - $paginator .= $this->gen_page_link($base_url, $query, $page, $name); - if($page == $current_page) $paginator .= ""; - return $paginator; - } - - /** - * Build the paginator. - * - * @param int $current_page - * @param int $total_pages - * @param string $base_url - * @param string $query - * @param bool $show_random - * @return string - */ - private function build_paginator($current_page, $total_pages, $base_url, $query, $show_random) { - $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 = 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 - .'
<< '.$pages_html.' >>'; - } -} - diff --git a/core/basethemelet.php b/core/basethemelet.php new file mode 100644 index 00000000..cc493bb8 --- /dev/null +++ b/core/basethemelet.php @@ -0,0 +1,149 @@ +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(['swf', 'svg', '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 "". + "$h_tip". + "\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(""); + if ($page_number < $total_pages) { + $page->add_html_header(""); + $page->add_html_header(""); + } + if ($page_number > 1) { + $page->add_html_header(""); + } + $page->add_html_header(""); + } + + private function gen_page_link(string $base_url, ?string $query, int $page, string $name): string + { + $link = make_link($base_url.'/'.$page, $query); + return ''.$name.''; + } + + 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 .= ""; + } + $paginator .= $this->gen_page_link($base_url, $query, $page, $name); + if ($page == $current_page) { + $paginator .= ""; + } + 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 + .'
<< '.$pages_html.' >>'; + } +} diff --git a/core/block.class.php b/core/block.class.php deleted file mode 100644 index 0fffb41f..00000000 --- a/core/block.class.php +++ /dev/null @@ -1,108 +0,0 @@ -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. - * - * @param bool $hidable - * @return string - */ - public function get_html($hidable=false) { - $h = $this->header; - $b = $this->body; - $i = $this->id; - $html = "
"; - $h_toggler = $hidable ? " shm-toggler" : ""; - if(!empty($h)) $html .= "

$h

"; - if(!empty($b)) $html .= "
$b
"; - $html .= "
\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", "Index", "left", 0); - } -} diff --git a/core/block.php b/core/block.php new file mode 100644 index 00000000..2fdaa091 --- /dev/null +++ b/core/block.php @@ -0,0 +1,105 @@ +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 = "
"; + $h_toggler = $hidable ? " shm-toggler" : ""; + if (!empty($h)) { + $html .= "

$h

"; + } + if (!empty($b)) { + $html .= "
$b
"; + } + $html .= "
\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", "Index", "left", 0); + } +} diff --git a/core/cacheengine.php b/core/cacheengine.php new file mode 100644 index 00000000..75b7891e --- /dev/null +++ b/core/cacheengine.php @@ -0,0 +1,199 @@ +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; + } +} diff --git a/core/captcha.php b/core/captcha.php new file mode 100644 index 00000000..c731ab6e --- /dev/null +++ b/core/captcha.php @@ -0,0 +1,60 @@ +is_anonymous() && $config->get_bool("comment_captcha")) { + $r_publickey = $config->get_string("api_recaptcha_pubkey"); + if (!empty($r_publickey)) { + $captcha = " +
+ "; + } 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; +} diff --git a/core/config.class.php b/core/config.class.php deleted file mode 100644 index dc7e85ba..00000000 --- a/core/config.class.php +++ /dev/null @@ -1,429 +0,0 @@ -values[$name] = parse_shorthand_int($value); - $this->save($name); - } - - /** - * @param string $name - * @param null|string $value - * @return void - */ - public function set_string(/*string*/ $name, $value) { - $this->values[$name] = $value; - $this->save($name); - } - - /** - * @param string $name - * @param bool|null|string $value - * @return void - */ - public function set_bool(/*string*/ $name, $value) { - $this->values[$name] = (($value == 'on' || $value === true) ? 'Y' : 'N'); - $this->save($name); - } - - /** - * @param string $name - * @param array $value - * @return void - */ - public function set_array(/*string*/ $name, $value) { - assert(isset($value) && is_array($value)); - $this->values[$name] = implode(",", $value); - $this->save($name); - } - - /** - * @param string $name - * @param int $value - * @return void - */ - public function set_default_int(/*string*/ $name, $value) { - if(is_null($this->get($name))) { - $this->values[$name] = parse_shorthand_int($value); - } - } - - /** - * @param string $name - * @param null|string $value - * @return void - */ - public function set_default_string(/*string*/ $name, $value) { - if(is_null($this->get($name))) { - $this->values[$name] = $value; - } - } - - /** - * @param string $name - * @param bool $value - * @return void - */ - public function set_default_bool(/*string*/ $name, /*bool*/ $value) { - if(is_null($this->get($name))) { - $this->values[$name] = (($value == 'on' || $value === true) ? 'Y' : 'N'); - } - } - - /** - * @param string $name - * @param array $value - * @return void - */ - public function set_default_array(/*string*/ $name, $value) { - assert(isset($value) && is_array($value)); - if(is_null($this->get($name))) { - $this->values[$name] = implode(",", $value); - } - } - - /** - * @param string $name - * @param null|int $default - * @return int - */ - public function get_int(/*string*/ $name, $default=null) { - return (int)($this->get($name, $default)); - } - - /** - * @param string $name - * @param null|string $default - * @return null|string - */ - public function get_string(/*string*/ $name, $default=null) { - return $this->get($name, $default); - } - - /** - * @param string $name - * @param null|bool|string $default - * @return bool - */ - public function get_bool(/*string*/ $name, $default=null) { - return bool_escape($this->get($name, $default)); - } - - /** - * @param string $name - * @param array $default - * @return array - */ - public function get_array(/*string*/ $name, $default=array()) { - return explode(",", $this->get($name, "")); - } - - /** - * @param string $name - * @param null|mixed $default - * @return null|mixed - */ - private function get(/*string*/ $name, $default=null) { - if(isset($this->values[$name])) { - return $this->values[$name]; - } - else { - return $default; - } - } -} - - -/** - * Class HardcodeConfig - * - * For testing, mostly. - */ -class HardcodeConfig extends BaseConfig { - public function __construct($dict) { - $this->values = $dict; - } - - /** - * @param null|string $name - * @return mixed|void - */ - public function save(/*string*/ $name=null) { - // static config is static - } -} - - -/** - * Class StaticConfig - * - * Loads the config list from a PHP file; the file should be in the format: - * - * - */ -class StaticConfig extends BaseConfig { - /** - * @param string $filename - * @throws Exception - */ - public function __construct($filename) { - if(file_exists($filename)) { - $config = array(); - require_once $filename; - if(!empty($config)) { - $this->values = $config; - } - else { - throw new Exception("Config file '$filename' doesn't contain any config"); - } - } - else { - throw new Exception("Config file '$filename' missing"); - } - } - - /** - * @param null|string $name - * @return mixed|void - */ - public function save(/*string*/ $name=null) { - // static config is static - } -} - - -/** - * 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; - - /** - * Load the config table from a database. - * - * @param Database $database - */ - public function __construct(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. - * - * @param null|string $name - * @return mixed|void - */ - 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])); - } - // rather than deleting and having some other request(s) do a thundering - // herd of race-conditioned updates, just save the updated version once here - $this->database->cache->set("config", $this->values); - } -} - -/** - * Class MockConfig - */ -class MockConfig extends HardcodeConfig { - /** - * @param array $config - */ - public function __construct($config=array()) { - $config["db_version"] = "999"; - $config["anon_id"] = "0"; - parent::__construct($config); - } -} - diff --git a/core/config.php b/core/config.php new file mode 100644 index 00000000..a538da27 --- /dev/null +++ b/core/config.php @@ -0,0 +1,338 @@ +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); + } +} diff --git a/core/database.class.php b/core/database.class.php deleted file mode 100644 index ec1a7ce2..00000000 --- a/core/database.class.php +++ /dev/null @@ -1,963 +0,0 @@ -sql = $sql; - $this->variables = $variables; - } - - /** - * @param \Querylet $querylet - */ - public function append($querylet) { - assert('!is_null($querylet)'); - $this->sql .= $querylet->sql; - $this->variables = array_merge($this->variables, $querylet->variables); - } - - /** - * @param string $sql - */ - public function append_sql($sql) { - $this->sql .= $sql; - } - - /** - * @param mixed $var - */ - public function add_variable($var) { - $this->variables[] = $var; - } -} - -class TagQuerylet { - /** @var string */ - public $tag; - /** @var bool */ - public $positive; - - /** - * @param string $tag - * @param bool $positive - */ - public function __construct($tag, $positive) { - $this->tag = $tag; - $this->positive = $positive; - } -} - -class ImgQuerylet { - /** @var \Querylet */ - public $qlet; - /** @var bool */ - public $positive; - - /** - * @param \Querylet $qlet - * @param bool $positive - */ - public function __construct($qlet, $positive) { - $this->qlet = $qlet; - $this->positive = $positive; - } -} -// }}} -// {{{ db engines -class DBEngine { - /** @var null|string */ - public $name = null; - - /** - * @param \PDO $db - */ - public function init($db) {} - - /** - * @param string $scoreql - * @return string - */ - public function scoreql_to_sql($scoreql) { - return $scoreql; - } - - /** - * @param string $name - * @param string $data - * @return string - */ - public function create_table_sql($name, $data) { - return 'CREATE TABLE '.$name.' ('.$data.')'; - } -} -class MySQL extends DBEngine { - /** @var string */ - public $name = "mysql"; - - /** - * @param \PDO $db - */ - public function init($db) { - $db->exec("SET NAMES utf8;"); - } - - /** - * @param string $data - * @return string - */ - public function scoreql_to_sql($data) { - $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", "'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; - } - - /** - * @param string $name - * @param string $data - * @return string - */ - 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 string */ - public $name = "pgsql"; - - /** - * @param \PDO $db - */ - public function init($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]';"); - } - $db->exec("SET statement_timeout TO 10000;"); - } - - /** - * @param string $data - * @return string - */ - 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_timestamp", $data); - $data = str_replace("SCORE_STRNORM", "lower", $data); - $data = str_replace("SCORE_ILIKE", "ILIKE", $data); - return $data; - } - - /** - * @param string $name - * @param string $data - * @return string - */ - 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); } -function _rand() { return rand(); } -function _ln($n) { return log($n); } - -class SQLite extends DBEngine { - /** @var string */ - public $name = "sqlite"; - - /** - * @param \PDO $db - */ - public function init($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); - } - - /** - * @param string $data - * @return string - */ - public function scoreql_to_sql($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", "lower", $data); - $data = str_replace("SCORE_ILIKE", "LIKE", $data); - return $data; - } - - /** - * @param string $name - * @param string $data - * @return string - */ - public function create_table_sql($name, $data) { - $data = $this->scoreql_to_sql($data); - $cols = array(); - $extras = ""; - foreach(explode(",", $data) as $bit) { - $matches = array(); - 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"; - } -} -// }}} -// {{{ cache engines -interface CacheEngine { - - /** - * @param string $key - * @return mixed - */ - public function get($key); - - /** - * @param string $key - * @param mixed $val - * @param integer $time - * @return void - */ - public function set($key, $val, $time=0); - - /** - * @return void - */ - public function delete($key); - - /** - * @return integer - */ - public function get_hits(); - - /** - * @return integer - */ - 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 */ - public $memcache=null; - /** @var int */ - private $hits=0; - /** @var int */ - private $misses=0; - - /** - * @param string $args - */ - public function __construct($args) { - $hp = explode(":", $args); - $this->memcache = new Memcache; - @$this->memcache->pconnect($hp[0], $hp[1]); - } - - /** - * @param string $key - * @return array|bool|string - */ - public function get($key) { - assert('!is_null($key)'); - $val = $this->memcache->get($key); - if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) { - $hit = $val === false ? "miss" : "hit"; - file_put_contents("data/cache.log", "Cache $hit: $key\n", FILE_APPEND); - } - if($val !== false) { - $this->hits++; - return $val; - } - else { - $this->misses++; - return false; - } - } - - /** - * @param string $key - * @param mixed $val - * @param integer $time - */ - public function set($key, $val, $time=0) { - assert('!is_null($key)'); - $this->memcache->set($key, $val, false, $time); - if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) { - file_put_contents("data/cache.log", "Cache set: $key ($time)\n", FILE_APPEND); - } - } - - /** - * @param string $key - */ - public function delete($key) { - assert('!is_null($key)'); - $this->memcache->delete($key); - if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) { - file_put_contents("data/cache.log", "Cache delete: $key\n", FILE_APPEND); - } - } - - /** - * @return int - */ - public function get_hits() {return $this->hits;} - - /** - * @return int - */ - public function get_misses() {return $this->misses;} -} -class MemcachedCache implements CacheEngine { - /** @var \Memcached|null */ - public $memcache=null; - /** @var int */ - private $hits=0; - /** @var int */ - private $misses=0; - - /** - * @param string $args - */ - public function __construct($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], $hp[1]); - } - - /** - * @param string $key - * @return array|bool|string - */ - public function get($key) { - assert('!is_null($key)'); - $key = urlencode($key); - - $val = $this->memcache->get($key); - $res = $this->memcache->getResultCode(); - - if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) { - $hit = $res == Memcached::RES_SUCCESS ? "hit" : "miss"; - file_put_contents("data/cache.log", "Cache $hit: $key\n", FILE_APPEND); - } - if($res == Memcached::RES_SUCCESS) { - $this->hits++; - return $val; - } - else if($res == Memcached::RES_NOTFOUND) { - $this->misses++; - return false; - } - else { - error_log("Memcached error during get($key): $res"); - } - } - - /** - * @param string $key - * @param mixed $val - * @param int $time - */ - public function set($key, $val, $time=0) { - assert('!is_null($key)'); - $key = urlencode($key); - - $this->memcache->set($key, $val, $time); - $res = $this->memcache->getResultCode(); - if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) { - file_put_contents("data/cache.log", "Cache set: $key ($time)\n", FILE_APPEND); - } - if($res != Memcached::RES_SUCCESS) { - error_log("Memcached error during set($key): $res"); - } - } - - /** - * @param string $key - */ - public function delete($key) { - assert('!is_null($key)'); - $key = urlencode($key); - - $this->memcache->delete($key); - $res = $this->memcache->getResultCode(); - if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) { - file_put_contents("data/cache.log", "Cache delete: $key\n", FILE_APPEND); - } - if($res != Memcached::RES_SUCCESS && $res != Memcached::RES_NOTFOUND) { - error_log("Memcached error during delete($key): $res"); - } - } - - /** - * @return int - */ - public function get_hits() {return $this->hits;} - - /** - * @return int - */ - public function get_misses() {return $this->misses;} -} - -class APCCache implements CacheEngine { - public $hits=0, $misses=0; - - public function __construct($args) { - // $args is not used, but is passed in when APC cache is created. - } - - 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 null|PDO - */ - private $db = null; - - /** - * @var float - */ - public $dbtime = 0.0; - - /** - * Meta info about the database engine. - * @var DBEngine|null - */ - private $engine = null; - - /** - * The currently active cache engine. - * @var CacheEngine|null - */ - public $cache = 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; - - /** - * For now, only connect to the cache, as we will pretty much certainly - * need it. There are some pages where all the data is in cache, so the - * DB connection is on-demand. - */ - public function __construct() { - $this->connect_cache(); - } - - private function connect_cache() { - $matches = array(); - if(defined("CACHE_DSN") && CACHE_DSN && preg_match("#(memcache|memcached|apc)://(.*)#", CACHE_DSN, $matches)) { - if($matches[1] == "memcache") { - $this->cache = new MemcacheCache($matches[2]); - } - else if($matches[1] == "memcached") { - $this->cache = new MemcachedCache($matches[2]); - } - else if($matches[1] == "apc") { - $this->cache = new APCCache($matches[2]); - } - } - else { - $this->cache = new NoCache(); - } - } - - private function connect_db() { - # 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]; - - // https://bugs.php.net/bug.php?id=70221 - $ka = DATABASE_KA; - if(version_compare(PHP_VERSION, "6.9.9") == 1 && $this->get_driver_name() == "sqlite") { - $ka = false; - } - - $db_params = array( - PDO::ATTR_PERSISTENT => $ka, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - ); - $this->db = new PDO(DATABASE_DSN, $db_user, $db_pass, $db_params); - - $this->connect_engine(); - $this->engine->init($this->db); - - $this->beginTransaction(); - } - - private function connect_engine() { - if(preg_match("/^([^:]*)/", DATABASE_DSN, $matches)) $db_proto=$matches[1]; - else throw new SCoreException("Can't figure out database engine"); - - 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); - } - } - - public function beginTransaction() { - if ($this->transaction === false) { - $this->db->beginTransaction(); - $this->transaction = true; - } - } - - /** - * @return boolean|null - * @throws SCoreException - */ - public function commit() { - if(!is_null($this->db)) { - if ($this->transaction === true) { - $this->transaction = false; - return $this->db->commit(); - } - else { - throw new SCoreException("

Database Transaction Error: Unable to call commit() as there is no transaction currently open."); - } - } - } - - /** - * @return boolean|null - * @throws SCoreException - */ - public function rollback() { - if(!is_null($this->db)) { - if ($this->transaction === true) { - $this->transaction = false; - return $this->db->rollback(); - } - else { - throw new SCoreException("

Database Transaction Error: Unable to call rollback() as there is no transaction currently open."); - } - } - } - - /** - * @param string $input - * @return string - */ - public function escape($input) { - if(is_null($this->db)) $this->connect_db(); - return $this->db->Quote($input); - } - - /** - * @param string $input - * @return string - */ - public function scoreql_to_sql($input) { - if(is_null($this->engine)) $this->connect_engine(); - return $this->engine->scoreql_to_sql($input); - } - - /** - * @return null|string - */ - public function get_driver_name() { - if(is_null($this->engine)) $this->connect_engine(); - return $this->engine->name; - } - - /** - * @param null|PDO $db - * @param string $sql - */ - private function count_execs($db, $sql, $inputarray) { - if((DEBUG_SQL === true) || (is_null(DEBUG_SQL) && @$_GET['DEBUG_SQL'])) { - $sql = trim(preg_replace('/\s+/msi', ' ', $sql)); - if(isset($inputarray) && is_array($inputarray) && !empty($inputarray)) { - $text = $sql." -- ".join(", ", $inputarray)."\n"; - } - else { - $text = $sql."\n"; - } - file_put_contents("data/sql.log", $text, FILE_APPEND); - } - if(!is_array($inputarray)) $this->query_count++; - # handle 2-dimensional input arrays - else if(is_array(reset($inputarray))) $this->query_count += sizeof($inputarray); - else $this->query_count++; - } - - private function count_time($method, $start) { - if((DEBUG_SQL === true) || (is_null(DEBUG_SQL) && @$_GET['DEBUG_SQL'])) { - $text = $method.":".(microtime(true) - $start)."\n"; - file_put_contents("data/sql.log", $text, FILE_APPEND); - } - $this->dbtime += microtime(true) - $start; - } - - /** - * Execute an SQL query and return an PDO result-set. - * - * @param string $query - * @param array $args - * @return PDOStatement - * @throws SCoreException - */ - public function execute($query, $args=array()) { - try { - if(is_null($this->db)) $this->connect_db(); - $this->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()."

Query: ".$query); - } - } - - /** - * Execute an SQL query and return a 2D array. - * - * @param string $query - * @param array $args - * @return array - */ - public function get_all($query, $args=array()) { - $_start = microtime(true); - $data = $this->execute($query, $args)->fetchAll(); - $this->count_time("get_all", $_start); - return $data; - } - - /** - * Execute an SQL query and return a single row. - * - * @param string $query - * @param array $args - * @return array|null - */ - public function get_row($query, $args=array()) { - $_start = microtime(true); - $row = $this->execute($query, $args)->fetch(); - $this->count_time("get_row", $_start); - return $row ? $row : null; - } - - /** - * Execute an SQL query and return the first column of each row. - * - * @param string $query - * @param array $args - * @return array - */ - public function get_col($query, $args=array()) { - $_start = microtime(true); - $stmt = $this->execute($query, $args); - $res = array(); - foreach($stmt as $row) { - $res[] = $row[0]; - } - $this->count_time("get_col", $_start); - return $res; - } - - /** - * Execute an SQL query and return the the first row => the second rown. - * - * @param string $query - * @param array $args - * @return array - */ - public function get_pairs($query, $args=array()) { - $_start = microtime(true); - $stmt = $this->execute($query, $args); - $res = array(); - foreach($stmt as $row) { - $res[$row[0]] = $row[1]; - } - $this->count_time("get_pairs", $_start); - return $res; - } - - /** - * Execute an SQL query and return a single value. - * - * @param string $query - * @param array $args - * @return mixed - */ - public function get_one($query, $args=array()) { - $_start = microtime(true); - $row = $this->execute($query, $args)->fetch(); - $this->count_time("get_one", $_start); - return $row[0]; - } - - /** - * Get the ID of the last inserted row. - * - * @param string|null $seq - * @return int - */ - 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. - * - * @param string $name - * @param string $data - */ - public function create_table($name, $data) { - 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. - * - * @return int|null - */ - public function count_tables() { - - if(is_null($this->db) || is_null($this->engine)) $this->connect_db(); - - 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("SELECT name FROM sqlite_master WHERE type = 'table'") - ); - } else { - // Hard to find a universal way to do this... - return NULL; - } - } -} - -class MockDatabase extends Database { - /** @var int */ - private $query_id = 0; - /** @var array */ - private $responses = array(); - /** @var \NoCache|null */ - public $cache = null; - - /** - * @param array $responses - */ - public function __construct($responses = array()) { - $this->cache = new NoCache(); - $this->responses = $responses; - } - - /** - * @param string $query - * @param array $params - * @return PDOStatement - */ - public function execute($query, $params=array()) { - log_debug("mock-database", - "QUERY: " . $query . - "\nARGS: " . var_export($params, true) . - "\nRETURN: " . var_export($this->responses[$this->query_id], true) - ); - return $this->responses[$this->query_id++]; - } - - /** - * @param string $query - * @param array $args - * @return PDOStatement - */ - public function get_all($query, $args=array()) {return $this->execute($query, $args);} - - /** - * @param string $query - * @param array $args - * @return PDOStatement - */ - public function get_row($query, $args=array()) {return $this->execute($query, $args);} - - /** - * @param string $query - * @param array $args - * @return PDOStatement - */ - public function get_col($query, $args=array()) {return $this->execute($query, $args);} - - /** - * @param string $query - * @param array $args - * @return PDOStatement - */ - public function get_pairs($query, $args=array()) {return $this->execute($query, $args);} - - /** - * @param string $query - * @param array $args - * @return PDOStatement - */ - public function get_one($query, $args=array()) {return $this->execute($query, $args);} - - /** - * @param null|string $seq - * @return int|string - */ - public function get_last_insert_id($seq) {return $this->query_id;} - - /** - * @param string $sql - * @return string - */ - public function scoreql_to_sql($sql) {return $sql;} - public function create_table($name, $def) {} - public function connect_engine() {} -} - diff --git a/core/database.php b/core/database.php new file mode 100644 index 00000000..4f6643a0 --- /dev/null +++ b/core/database.php @@ -0,0 +1,326 @@ +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->beginTransaction(); + } + + 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('Unknown PDO driver: '.$db_proto); + } + } + + public function beginTransaction(): void + { + if ($this->transaction === false) { + $this->db->beginTransaction(); + $this->transaction = true; + } + } + + public function commit(): bool + { + if (!is_null($this->db) && $this->transaction === true) { + $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 (!is_null($this->db) && $this->transaction === true) { + $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; + } + + 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; + } +} diff --git a/core/dbengine.php b/core/dbengine.php new file mode 100644 index 00000000..c974ab34 --- /dev/null +++ b/core/dbengine.php @@ -0,0 +1,219 @@ +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.";"); + } +} + +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]';"); + } + $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.";"); + } +} + +// 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 + } +} diff --git a/core/email.class.php b/core/email.class.php deleted file mode 100644 index d43e4dc4..00000000 --- a/core/email.class.php +++ /dev/null @@ -1,138 +0,0 @@ -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 = ' - - - - - - - - - - - -
- - - - - - - - -
'.$this->sitename.' -
- - - - - - - - - - -
- -

-'.$this->header.'
-'.$this->date.'
-

-

'.$this->body.'

-

'.$this->footer.'

-
- -This email was sent to you since you are a member of '.$this->sitename.'. To change your email preferences, visit your Account preferences.
- -
-Contact us:
-'.$this->siteemail.'

-Copyright (C) '.$this->sitename.'
-
- -
- - - - '; - $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; - } -} - diff --git a/core/event.class.php b/core/event.class.php deleted file mode 100644 index 37511e29..00000000 --- a/core/event.class.php +++ /dev/null @@ -1,325 +0,0 @@ - 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; - - /** - * @param string $path - */ - public function __construct($path) { - 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('front_page'); - } - - // break the path into parts - $args = explode('/', $path); - - // voodoo so that an arg can contain a slash; is - // this still needed? - if(strpos($path, "^") !== FALSE) { - $unescaped = array(); - foreach($args as $part) { - $unescaped[] = _decaret($part); - } - $args = $unescaped; - } - - $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 - * - * @param string $name - * @return 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 int $n - * @return string|null The argument (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. - * @return int - */ - public function count_args() { - return int_escape($this->arg_count - $this->part_count); - } - - /* - * Many things use these functions - */ - - /** - * @return array - */ - public function get_search_terms() { - $search_terms = array(); - if($this->count_args() === 2) { - $search_terms = Tag::explode($this->get_arg(0)); - } - return $search_terms; - } - - /** - * @return int - */ - 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; - } - - /** - * @return int - */ - public function get_page_size() { - global $config; - return $config->get_int('index_images'); - } -} - - -/** - * Sent when index.php is called from the command line - */ -class CommandEvent extends Event { - /** - * @var string - */ - public $cmd = "help"; - - /** - * @var array - */ - public $args = array(); - - /** - * @param string[] $args - */ - public function __construct(/*array(string)*/ $args) { - global $user; - - $opts = array(); - $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"); - } - break; - case '-q': - $log_level += 10; - break; - case '-v': - $log_level -= 10; - break; - default: - $opts[] = $args[$i]; - break; - } - } - - 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 " -u [username]\n"; - print " Log in as the specified user\n"; - print " -q / -v\n"; - print " Be quieter / more verbose\n"; - print " Scale is debug - info - warning - error - critical\n"; - print " Default 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; - - /** - * @param string $text - */ - 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 - * - * @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; - - /** - * @param string $section - * @param int $priority - * @param string $message - * @param array $args - */ - public function __construct($section, $priority, $message, $args) { - $this->section = $section; - $this->priority = $priority; - $this->message = $message; - $this->args = $args; - $this->time = time(); - } -} - diff --git a/core/event.php b/core/event.php new file mode 100644 index 00000000..57f9c2dc --- /dev/null +++ b/core/event.php @@ -0,0 +1,339 @@ + 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 { + throw new SCoreException("Requested an invalid argument #$n"); + } + } + + public function try_page_num(int $n): int + { + if ($this->count_args() > $n) { + $i = $this->get_arg($n); + if (is_numeric($i) && int_escape($i) > 0) { + return int_escape($i); + } else { + return 1; + } + } else { + return 1; + } + } + + /** + * 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 +{ +} diff --git a/core/exceptions.class.php b/core/exceptions.class.php deleted file mode 100644 index d2400893..00000000 --- a/core/exceptions.class.php +++ /dev/null @@ -1,29 +0,0 @@ -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 +{ +} diff --git a/core/extension.class.php b/core/extension.class.php deleted file mode 100644 index dc8a1ccd..00000000 --- a/core/extension.class.php +++ /dev/null @@ -1,297 +0,0 @@ -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 SCorePHPUnitTestCase { - * 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 - * - */ - -/** - * 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 - * --> 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 { - /** @var array which DBs this ext supports (blank for 'all') */ - protected $db_support = []; - - /** @var Themelet this theme's Themelet object */ - public $theme; - - public function __construct() { - $this->theme = $this->get_theme_object(get_called_class()); - } - - /** - * @return boolean - */ - public function is_live() { - global $database; - return ( - empty($this->db_support) || - in_array($database->get_driver_name(), $this->db_support) - ); - } - - /** - * Find the theme object for a given extension. - * - * @param string $base - * @return Themelet - */ - private function get_theme_object($base) { - $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 recieve events first. - * - * @return int - */ - public function get_priority() { - return 50; - } -} - -/** - * Class FormatterExtension - * - * Several extensions have this in common, make a common API. - */ -abstract class FormatterExtension extends Extension { - /** - * @param TextFormattingEvent $event - */ - public function onTextFormatting(TextFormattingEvent $event) { - $event->formatted = $this->format($event->formatted); - $event->stripped = $this->strip($event->stripped); - } - - /** - * @param string $text - * @return string - */ - abstract public function format(/*string*/ $text); - - /** - * @param string $text - * @return string - */ - abstract public function strip(/*string*/ $text); -} - -/** - * 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 { - /** - * @param DataUploadEvent $event - * @throws UploadException - */ - public function onDataUpload(DataUploadEvent $event) { - $supported_ext = $this->supported_ext($event->type); - $check_contents = $this->check_contents($event->tmpname); - if($supported_ext && $check_contents) { - move_upload_to_archive($event); - 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($image); - send_event($iae); - $event->image_id = $iae->image->id; - - // 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){ - throw new UploadException("Invalid or corrupted file"); - } - } - - /** - * @param ThumbnailGenerationEvent $event - */ - 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); - } - } - } - - /** - * @param DisplayingImageEvent $event - */ - 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() {} - */ - - /** - * @param string $ext - * @return bool - */ - abstract protected function supported_ext($ext); - - /** - * @param string $tmpname - * @return bool - */ - abstract protected function check_contents($tmpname); - - /** - * @param string $filename - * @param array $metadata - * @return Image|null - */ - abstract protected function create_image_from_data($filename, $metadata); - - /** - * @param string $hash - * @return bool - */ - abstract protected function create_thumb($hash); -} - diff --git a/core/extension.php b/core/extension.php new file mode 100644 index 00000000..d464b578 --- /dev/null +++ b/core/extension.php @@ -0,0 +1,505 @@ +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 SCorePHPUnitTestCase { + * 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 + * + */ + +/** + * 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 + * --> 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 +{ + /** @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 = "http://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_EXT = []; + + 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 (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(Image::IMAGE_DIR, $event->metadata['hash']), $event->metadata); + if (is_null($image)) { + throw new UploadException("Data handler failed to create image object from data"); + } + try { + send_event(new MediaCheckPropertiesEvent($image)); + } catch (MediaException $e) { + throw new UploadException("Unable to scan media properties: ".$e->getMessage()); + } + + send_event(new ImageReplaceEvent($image_id, $image)); + $event->image_id = $image_id; + } 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"); + } + 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(getMimeType($filename)); + } else { + $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(strtolower($ext), $this->SUPPORTED_EXT); + } + + public static function get_all_supported_exts(): array + { + $arr = []; + foreach (getSubclassesOf("DataHandlerExtension") as $handler) { + $arr = array_merge($arr, (new $handler())->SUPPORTED_EXT); + } + return $arr; + } +} diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php deleted file mode 100644 index 0ee89a09..00000000 --- a/core/imageboard.pack.php +++ /dev/null @@ -1,1292 +0,0 @@ - image ID list - * translators, eg: - * - * \li the item "fred" will search the image_tags table to find image IDs with the fred tag - * \li the item "size=640x480" will search the images table to find image IDs of 640x480 images - * - * So the search "fred size=640x480" will calculate two lists and take the - * intersection. (There are some optimisations in there making it more - * complicated behind the scenes, but as long as you can turn a single word - * into a list of image IDs, making a search plugin should be simple) - */ - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ -* Classes * -\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -/** - * Class Image - * - * An object representing an entry in the images table. - * - * As of 2.2, this no longer necessarily represents an - * image per se, but could be a video, sound file, or any - * other supported upload type. - */ -class Image { - private static $tag_n = 0; // temp hack - public static $order_sql = null; // this feels ugly - - /** @var null|int */ - public $id = null; - - /** @var int */ - public $height; - - /** @var int */ - public $width; - - /** @var string */ - public $hash; - - public $filesize; - - /** @var string */ - public $filename; - - /** @var string */ - public $ext; - - /** @var string[]|null */ - public $tag_array; - - /** @var int */ - public $owner_id; - - /** @var string */ - public $owner_ip; - - /** @var string */ - public $posted; - - /** @var string */ - public $source; - - /** @var boolean */ - public $locked; - - /** - * One will very rarely construct an image directly, more common - * would be to use Image::by_id, Image::by_hash, etc. - * - * @param null|mixed $row - */ - public function __construct($row=null) { - assert('is_null($row) || is_array($row)'); - - if(!is_null($row)) { - foreach($row as $name => $value) { - // some databases use table.name rather than name - $name = str_replace("images.", "", $name); - $this->$name = $value; // hax, this is likely the cause of much scrutinizer-ci complaints. - } - $this->locked = bool_escape($this->locked); - - assert(is_numeric($this->id)); - assert(is_numeric($this->height)); - assert(is_numeric($this->width)); - } - } - - /** - * Find an image by ID. - * - * @param int $id - * @return Image - */ - public static function by_id(/*int*/ $id) { - assert('is_numeric($id)'); - global $database; - $row = $database->get_row("SELECT * FROM images WHERE images.id=:id", array("id"=>$id)); - return ($row ? new Image($row) : null); - } - - /** - * Find an image by hash. - * - * @param string $hash - * @return Image - */ - public static function by_hash(/*string*/ $hash) { - assert('is_string($hash)'); - global $database; - $row = $database->get_row("SELECT images.* FROM images WHERE hash=:hash", array("hash"=>$hash)); - return ($row ? new Image($row) : null); - } - - /** - * Pick a random image out of a set. - * - * @param string[] $tags - * @return Image - */ - public static function by_random($tags=array()) { - assert('is_array($tags)'); - $max = Image::count_images($tags); - if ($max < 1) return null; // From Issue #22 - opened by HungryFeline on May 30, 2011. - $rand = mt_rand(0, $max-1); - $set = Image::find_images($rand, 1, $tags); - if(count($set) > 0) return $set[0]; - else return null; - } - - /** - * Search for an array of images - * - * @param int $start - * @param int $limit - * @param string[] $tags - * @throws SCoreException - * @return Image[] - */ - public static function find_images(/*int*/ $start, /*int*/ $limit, $tags=array()) { - assert('is_numeric($start)'); - assert('is_numeric($limit)'); - assert('is_array($tags)'); - global $database, $user, $config; - - $images = array(); - - if($start < 0) $start = 0; - if($limit < 1) $limit = 1; - - if(SPEED_HAX) { - if(!$user->can("big_search") and count($tags) > 3) { - throw new SCoreException("Anonymous users may only search for up to 3 tags at a time"); - } - } - - $result = null; - if(SEARCH_ACCEL) { - $result = Image::get_accelerated_result($tags, $start, $limit); - } - - if(!$result) { - $querylet = Image::build_search_querylet($tags); - $querylet->append(new Querylet(" ORDER BY ".(Image::$order_sql ?: "images.".$config->get_string("index_order")))); - $querylet->append(new Querylet(" LIMIT :limit OFFSET :offset", array("limit"=>$limit, "offset"=>$start))); - #var_dump($querylet->sql); var_dump($querylet->variables); - $result = $database->execute($querylet->sql, $querylet->variables); - } - - while($row = $result->fetch()) { - $images[] = new Image($row); - } - Image::$order_sql = null; - return $images; - } - - /** - * @param string[] $tags - * @return boolean - */ - public static function validate_accel($tags) { - $yays = 0; - $nays = 0; - foreach($tags as $tag) { - if(!preg_match("/^-?[a-zA-Z0-9_]+$/", $tag)) { - return false; - } - if($tag[0] == "-") $nays++; - else $yays++; - } - return ($yays > 1 || $nays > 0); - } - - /** - * @param string[] $tags - * @param int $offset - * @param int $limit - * @return null|PDOStatement - * @throws SCoreException - */ - public static function get_accelerated_result($tags, $offset, $limit) { - global $database; - - if(!Image::validate_accel($tags)) { - return null; - } - - $yays = array(); - $nays = array(); - foreach($tags as $tag) { - if($tag[0] == "-") { - $nays[] = substr($tag, 1); - } - else { - $yays[] = $tag; - } - } - $req = array( - "yays" => $yays, - "nays" => $nays, - "offset" => $offset, - "limit" => $limit, - ); - - $fp = fsockopen("127.0.0.1", 21212); - if (!$fp) { - return null; - } - fwrite($fp, json_encode($req)); - $data = fgets($fp, 1024); - fclose($fp); - - $response = json_decode($data); - $list = implode(",", $response); - if($list) { - $result = $database->execute("SELECT * FROM images WHERE id IN ($list) ORDER BY images.id DESC"); - } - else { - $result = $database->execute("SELECT * FROM images WHERE 1=0 ORDER BY images.id DESC"); - } - return $result; - } - - /* - * Image-related utility functions - */ - - /** - * Count the number of image results for a given search - * - * @param string[] $tags - * @return int - */ - public static function count_images($tags=array()) { - assert('is_array($tags)'); - global $database; - $tag_count = count($tags); - - if($tag_count === 0) { - $total = $database->cache->get("image-count"); - if(!$total) { - $total = $database->get_one("SELECT COUNT(*) FROM images"); - $database->cache->set("image-count", $total, 600); - } - return $total; - } - else if($tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) { - return $database->get_one( - $database->scoreql_to_sql("SELECT count FROM tags WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag)"), - array("tag"=>$tags[0])); - } - else { - $querylet = Image::build_search_querylet($tags); - return $database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables); - } - } - - /** - * Count the number of pages for a given search - * - * @param string[] $tags - * @return float - */ - public static function count_pages($tags=array()) { - assert('is_array($tags)'); - global $config; - return ceil(Image::count_images($tags) / $config->get_int('index_images')); - } - - /* - * Accessors & mutators - */ - - /** - * Find the next image in the sequence. - * - * Rather than simply $this_id + 1, one must take into account - * deleted images and search queries - * - * @param string[] $tags - * @param bool $next - * @return Image - */ - public function get_next($tags=array(), $next=true) { - assert('is_array($tags)'); - assert('is_bool($next)'); - global $database; - - if($next) { - $gtlt = "<"; - $dir = "DESC"; - } - else { - $gtlt = ">"; - $dir = "ASC"; - } - - if(count($tags) === 0) { - $row = $database->get_row(' - SELECT images.* - FROM images - WHERE images.id '.$gtlt.' '.$this->id.' - ORDER BY images.id '.$dir.' - LIMIT 1 - '); - } - else { - $tags[] = 'id'. $gtlt . $this->id; - $querylet = Image::build_search_querylet($tags); - $querylet->append_sql(' ORDER BY images.id '.$dir.' LIMIT 1'); - $row = $database->get_row($querylet->sql, $querylet->variables); - } - - return ($row ? new Image($row) : null); - } - - /** - * The reverse of get_next - * - * @param string[] $tags - * @return Image - */ - public function get_prev($tags=array()) { - return $this->get_next($tags, false); - } - - /** - * Find the User who owns this Image - * - * @return User - */ - public function get_owner() { - return User::by_id($this->owner_id); - } - - /** - * Set the image's owner. - * - * @param User $owner - */ - public function set_owner(User $owner) { - global $database; - if($owner->id != $this->owner_id) { - $database->execute(" - UPDATE images - SET owner_id=:owner_id - WHERE id=:id - ", array("owner_id"=>$owner->id, "id"=>$this->id)); - log_info("core_image", "Owner for Image #{$this->id} set to {$owner->name}", false, array("image_id" => $this->id)); - } - } - - /** - * Get this image's tags as an array. - * - * @return string[] - */ - public function get_tag_array() { - global $database; - if(!isset($this->tag_array)) { - $this->tag_array = $database->get_col(" - SELECT tag - FROM image_tags - JOIN tags ON image_tags.tag_id = tags.id - WHERE image_id=:id - ORDER BY tag - ", array("id"=>$this->id)); - } - return $this->tag_array; - } - - /** - * Get this image's tags as a string. - * - * @return string - */ - public function get_tag_list() { - return Tag::implode($this->get_tag_array()); - } - - /** - * Get the URL for the full size image - * - * @return string - */ - public function get_image_link() { - return $this->get_link('image_ilink', '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext'); - } - - /** - * Get the URL for the thumbnail - * - * @return string - */ - public function get_thumb_link() { - return $this->get_link('image_tlink', '_thumbs/$hash/thumb.jpg', 'thumb/$id.jpg'); - } - - /** - * Check configured template for a link, then try nice URL, then plain URL - * - * @param string $template - * @param string $nice - * @param string $plain - * @return string - */ - private function get_link($template, $nice, $plain) { - global $config; - - $image_link = $config->get_string($template); - - if(!empty($image_link)) { - if(!(strpos($image_link, "://") > 0) && !startsWith($image_link, "/")) { - $image_link = make_link($image_link); - } - return $this->parse_link_template($image_link); - } - else if($config->get_bool('nice_urls', false)) { - return $this->parse_link_template(make_link($nice)); - } - else { - return $this->parse_link_template(make_link($plain)); - } - } - - /** - * Get the tooltip for this image, formatted according to the - * configured template. - * - * @return string - */ - public function get_tooltip() { - global $config; - $tt = $this->parse_link_template($config->get_string('image_tip'), "no_escape"); - - // Removes the size tag if the file is an mp3 - if($this->ext === 'mp3'){ - $iitip = $tt; - $mp3tip = array("0x0"); - $h_tip = str_replace($mp3tip, " ", $iitip); - - // Makes it work with a variation of the default tooltips (I.E $tags // $filesize // $size) - $justincase = array(" //", "// ", " //", "// ", " "); - if(strstr($h_tip, " ")) { - $h_tip = html_escape(str_replace($justincase, "", $h_tip)); - }else{ - $h_tip = html_escape($h_tip); - } - return $h_tip; - } - else { - return $tt; - } - } - - /** - * Figure out where the full size image is on disk. - * - * @return string - */ - public function get_image_filename() { - return warehouse_path("images", $this->hash); - } - - /** - * Figure out where the thumbnail is on disk. - * - * @return string - */ - public function get_thumb_filename() { - return warehouse_path("thumbs", $this->hash); - } - - /** - * Get the original filename. - * - * @return string - */ - public function get_filename() { - return $this->filename; - } - - /** - * Get the image's mime type. - * - * @return string - */ - public function get_mime_type() { - return getMimeType($this->get_image_filename(), $this->get_ext()); - } - - /** - * Get the image's filename extension - * - * @return string - */ - public function get_ext() { - return $this->ext; - } - - /** - * Get the image's source URL - * - * @return string - */ - public function get_source() { - return $this->source; - } - - /** - * Set the image's source URL - * - * @param string $new_source - */ - public function set_source(/*string*/ $new_source) { - global $database; - $old_source = $this->source; - if(empty($new_source)) $new_source = null; - if($new_source != $old_source) { - $database->execute("UPDATE images SET source=:source WHERE id=:id", array("source"=>$new_source, "id"=>$this->id)); - log_info("core_image", "Source for Image #{$this->id} set to: $new_source (was $old_source)", false, array("image_id" => $this->id)); - } - } - - /** - * Check if the image is locked. - * @return bool - */ - public function is_locked() { - return $this->locked; - } - - /** - * @param bool $tf - * @throws SCoreException - */ - public function set_locked($tf) { - global $database; - $ln = $tf ? "Y" : "N"; - $sln = $database->scoreql_to_sql('SCORE_BOOL_'.$ln); - $sln = str_replace("'", "", $sln); - $sln = str_replace('"', "", $sln); - if(bool_escape($sln) !== $this->locked) { - $database->execute("UPDATE images SET locked=:yn WHERE id=:id", array("yn"=>$sln, "id"=>$this->id)); - log_info("core_image", "Setting Image #{$this->id} lock to: $ln", false, array("image_id" => $this->id)); - } - } - - /** - * Delete all tags from this image. - * - * Normally in preparation to set them to a new set. - */ - public function delete_tags_from_image() { - global $database; - if($database->get_driver_name() == "mysql") { - //mysql < 5.6 has terrible subquery optimization, using EXISTS / JOIN fixes this - $database->execute(" - UPDATE tags t - INNER JOIN image_tags it ON t.id = it.tag_id - SET count = count - 1 - WHERE it.image_id = :id", - array("id"=>$this->id) - ); - } else { - $database->execute(" - UPDATE tags - SET count = count - 1 - WHERE id IN ( - SELECT tag_id - FROM image_tags - WHERE image_id = :id - ) - ", array("id"=>$this->id)); - } - $database->execute(" - DELETE - FROM image_tags - WHERE image_id=:id - ", array("id"=>$this->id)); - } - - /** - * Set the tags for this image. - * - * @param string[] $tags - * @throws Exception - */ - public function set_tags($tags) { - assert('is_array($tags) && count($tags) > 0', var_export($tags, true)); - global $database; - - if(count($tags) <= 0) { - throw new SCoreException('Tried to set zero tags'); - } - - if(implode(" ", $tags) != $this->get_tag_list()) { - // delete old - $this->delete_tags_from_image(); - // insert each new tags - foreach($tags as $tag) { - if(mb_strlen($tag, 'UTF-8') > 255){ - flash_message("The tag below is longer than 255 characters, please use a shorter tag.\n$tag\n"); - continue; - } - - $id = $database->get_one( - $database->scoreql_to_sql(" - SELECT id - FROM tags - WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag) - "), - array("tag"=>$tag) - ); - if(empty($id)) { - // a new tag - $database->execute( - "INSERT INTO tags(tag) VALUES (:tag)", - array("tag"=>$tag)); - $database->execute( - "INSERT INTO image_tags(image_id, tag_id) - VALUES(:id, (SELECT id FROM tags WHERE tag = :tag))", - array("id"=>$this->id, "tag"=>$tag)); - } - else { - // user of an existing tag - $database->execute(" - INSERT INTO image_tags(image_id, tag_id) - VALUES(:iid, :tid) - ", array("iid"=>$this->id, "tid"=>$id)); - } - $database->execute( - $database->scoreql_to_sql(" - UPDATE tags - SET count = count + 1 - WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag) - "), - array("tag"=>$tag) - ); - } - - log_info("core_image", "Tags for Image #{$this->id} set to: ".implode(" ", $tags), null, array("image_id" => $this->id)); - $database->cache->delete("image-{$this->id}-tags"); - } - } - - /** - * Send list of metatags to be parsed. - * - * @param string[] $metatags - * @param int $image_id - */ - public function parse_metatags($metatags, $image_id) { - foreach($metatags as $tag) { - $ttpe = new TagTermParseEvent($tag, $image_id, TRUE); - send_event($ttpe); - } - } - - /** - * Delete this image from the database and disk - */ - public function delete() { - global $database; - $this->delete_tags_from_image(); - $database->execute("DELETE FROM images WHERE id=:id", array("id"=>$this->id)); - log_info("core_image", 'Deleted Image #'.$this->id.' ('.$this->hash.')', false, array("image_id" => $this->id)); - - unlink($this->get_image_filename()); - unlink($this->get_thumb_filename()); - } - - /** - * This function removes an image (and thumbnail) from the DISK ONLY. - * It DOES NOT remove anything from the database. - */ - public function remove_image_only() { - log_info("core_image", 'Removed Image File ('.$this->hash.')', false, array("image_id" => $this->id)); - @unlink($this->get_image_filename()); - @unlink($this->get_thumb_filename()); - } - - /** - * Someone please explain this - * - * @param string $tmpl - * @param string $_escape - * @return string - */ - public function parse_link_template($tmpl, $_escape="url_escape") { - global $config; - - // don't bother hitting the database if it won't be used... - $tags = ""; - if(strpos($tmpl, '$tags') !== false) { // * stabs dynamically typed languages with a rusty spoon * - $tags = $this->get_tag_list(); - $tags = str_replace("/", "", $tags); - $tags = preg_replace("/^\.+/", "", $tags); - } - - $base_href = $config->get_string('base_href'); - $fname = $this->get_filename(); - $base_fname = strpos($fname, '.') ? substr($fname, 0, strrpos($fname, '.')) : $fname; - - $tmpl = str_replace('$id', $this->id, $tmpl); - $tmpl = str_replace('$hash_ab', substr($this->hash, 0, 2), $tmpl); - $tmpl = str_replace('$hash_cd', substr($this->hash, 2, 2), $tmpl); - $tmpl = str_replace('$hash', $this->hash, $tmpl); - $tmpl = str_replace('$tags', $_escape($tags), $tmpl); - $tmpl = str_replace('$base', $base_href, $tmpl); - $tmpl = str_replace('$ext', $this->ext, $tmpl); - $tmpl = str_replace('$size', "{$this->width}x{$this->height}", $tmpl); - $tmpl = str_replace('$filesize', to_shorthand_int($this->filesize), $tmpl); - $tmpl = str_replace('$filename', $_escape($base_fname), $tmpl); - $tmpl = str_replace('$title', $_escape($config->get_string("title")), $tmpl); - $tmpl = str_replace('$date', $_escape(autodate($this->posted, false)), $tmpl); - - // nothing seems to use this, sending the event out to 50 exts is a lot of overhead - if(!SPEED_HAX) { - $plte = new ParseLinkTemplateEvent($tmpl, $this); - send_event($plte); - $tmpl = $plte->link; - } - - static $flexihash = null; - static $fh_last_opts = null; - $matches = array(); - if(preg_match("/(.*){(.*)}(.*)/", $tmpl, $matches)) { - $pre = $matches[1]; - $opts = $matches[2]; - $post = $matches[3]; - - if($opts != $fh_last_opts) { - $fh_last_opts = $opts; - $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); - } - } - - $choice = $flexihash->lookup($pre.$post); - $tmpl = $pre.$choice.$post; - } - - return $tmpl; - } - - /** - * @param string[] $terms - * @return \Querylet - */ - private static function build_search_querylet($terms) { - assert('is_array($terms)'); - global $database; - - $tag_querylets = array(); - $img_querylets = array(); - $positive_tag_count = 0; - $negative_tag_count = 0; - - /* - * Turn a bunch of strings into a bunch of TagQuerylet - * and ImgQuerylet objects - */ - $stpe = new SearchTermParseEvent(null, $terms); - send_event($stpe); - if ($stpe->is_querylet_set()) { - foreach ($stpe->get_querylets() as $querylet) { - $img_querylets[] = new ImgQuerylet($querylet, true); - } - } - - foreach ($terms as $term) { - $positive = true; - if (is_string($term) && !empty($term) && ($term[0] == '-')) { - $positive = false; - $term = substr($term, 1); - } - if (strlen($term) === 0) { - continue; - } - - $stpe = new SearchTermParseEvent($term, $terms); - send_event($stpe); - if ($stpe->is_querylet_set()) { - foreach ($stpe->get_querylets() as $querylet) { - $img_querylets[] = new ImgQuerylet($querylet, $positive); - } - } - else { - // if the whole match is wild, skip this; - // if not, translate into SQL - if(str_replace("*", "", $term) != "") { - $term = str_replace('_', '\_', $term); - $term = str_replace('%', '\%', $term); - $term = str_replace('*', '%', $term); - $tag_querylets[] = new TagQuerylet($term, $positive); - if ($positive) $positive_tag_count++; - else $negative_tag_count++; - } - } - } - - /* - * Turn a bunch of Querylet objects into a base query - * - * Must follow the format - * - * SELECT images.* - * FROM (...) AS images - * WHERE (...) - * - * ie, return a set of images.* columns, and end with a WHERE - */ - - // no tags, do a simple search - if($positive_tag_count === 0 && $negative_tag_count === 0) { - $query = new Querylet(" - SELECT images.* - FROM images - WHERE 1=1 - "); - } - - // one positive tag (a common case), do an optimised search - else if($positive_tag_count === 1 && $negative_tag_count === 0) { - # "LIKE" to account for wildcards - $query = new Querylet($database->scoreql_to_sql(" - SELECT * - FROM ( - SELECT images.* - FROM images - JOIN image_tags ON images.id=image_tags.image_id - JOIN tags ON image_tags.tag_id=tags.id - WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:tag) - GROUP BY images.id - ) AS images - WHERE 1=1 - "), array("tag"=>$tag_querylets[0]->tag)); - } - - // more than one positive tag, or more than zero negative tags - else { - if($database->get_driver_name() === "mysql") - $query = Image::build_ugly_search_querylet($tag_querylets); - else - $query = Image::build_accurate_search_querylet($tag_querylets); - } - - /* - * Merge all the image metadata searches into one generic querylet - * and append to the base querylet with "AND blah" - */ - if(!empty($img_querylets)) { - $n = 0; - $img_sql = ""; - $img_vars = array(); - foreach ($img_querylets as $iq) { - if ($n++ > 0) $img_sql .= " AND"; - if (!$iq->positive) $img_sql .= " NOT"; - $img_sql .= " (" . $iq->qlet->sql . ")"; - $img_vars = array_merge($img_vars, $iq->qlet->variables); - } - $query->append_sql(" AND "); - $query->append(new Querylet($img_sql, $img_vars)); - } - - return $query; - } - - /** - * WARNING: this description is no longer accurate, though it does get across - * the general idea - the actual method has a few extra optimisations - * - * "foo bar -baz user=foo" becomes - * - * SELECT * FROM images WHERE - * images.id IN (SELECT image_id FROM image_tags WHERE tag='foo') - * AND images.id IN (SELECT image_id FROM image_tags WHERE tag='bar') - * AND NOT images.id IN (SELECT image_id FROM image_tags WHERE tag='baz') - * AND images.id IN (SELECT id FROM images WHERE owner_name='foo') - * - * This is: - * A) Incredibly simple: - * Each search term maps to a list of image IDs - * B) Runs really fast on a good database: - * These lists are calculated once, and the set intersection taken - * C) Runs really slow on bad databases: - * All the subqueries are executed every time for every row in the - * images table. Yes, MySQL does suck this much. - * - * @param TagQuerylet[] $tag_querylets - * @return Querylet - */ - private static function build_accurate_search_querylet($tag_querylets) { - global $database; - - $positive_tag_id_array = array(); - $negative_tag_id_array = array(); - - foreach ($tag_querylets as $tq) { - $tag_ids = $database->get_col( - $database->scoreql_to_sql(" - SELECT id - FROM tags - WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:tag) - "), - array("tag" => $tq->tag) - ); - if ($tq->positive) { - $positive_tag_id_array = array_merge($positive_tag_id_array, $tag_ids); - if (count($tag_ids) == 0) { - # one of the positive tags had zero results, therefor there - # can be no results; "where 1=0" should shortcut things - return new Querylet(" - SELECT images.* - FROM images - WHERE 1=0 - "); - } - } else { - $negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids); - } - } - - assert('$positive_tag_id_array || $negative_tag_id_array', @$_GET['q']); - $wheres = array(); - if (!empty($positive_tag_id_array)) { - $positive_tag_id_list = join(', ', $positive_tag_id_array); - $wheres[] = "tag_id IN ($positive_tag_id_list)"; - } - if (!empty($negative_tag_id_array)) { - $negative_tag_id_list = join(', ', $negative_tag_id_array); - $wheres[] = "tag_id NOT IN ($negative_tag_id_list)"; - } - $wheres_str = join(" AND ", $wheres); - return new Querylet(" - SELECT images.* - FROM images - WHERE images.id IN ( - SELECT image_id - FROM image_tags - WHERE $wheres_str - GROUP BY image_id - HAVING COUNT(image_id) >= :search_score - ) - ", array("search_score"=>count($positive_tag_id_array))); - } - - /** - * this function exists because mysql is a turd, see the docs for - * build_accurate_search_querylet() for a full explanation - * - * @param TagQuerylet[] $tag_querylets - * @return Querylet - */ - private static function build_ugly_search_querylet($tag_querylets) { - global $database; - - $positive_tag_count = 0; - foreach($tag_querylets as $tq) { - if($tq->positive) $positive_tag_count++; - } - - // only negative tags - shortcut to fail - if($positive_tag_count == 0) { - // TODO: This isn't currently implemented. - // SEE: https://github.com/shish/shimmie2/issues/66 - return new Querylet(" - SELECT images.* - FROM images - WHERE 1=0 - "); - } - - // merge all the tag querylets into one generic one - $sql = "0"; - $terms = array(); - foreach($tag_querylets as $tq) { - $sign = $tq->positive ? "+" : "-"; - $sql .= ' '.$sign.' IF(SUM(tag LIKE :tag'.Image::$tag_n.'), 1, 0)'; - $terms['tag'.Image::$tag_n] = $tq->tag; - Image::$tag_n++; - } - $tag_search = new Querylet($sql, $terms); - - $tag_id_array = array(); - - foreach($tag_querylets as $tq) { - $tag_ids = $database->get_col( - $database->scoreql_to_sql(" - SELECT id - FROM tags - WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:tag) - "), - array("tag" => $tq->tag) - ); - $tag_id_array = array_merge($tag_id_array, $tag_ids); - - if($tq->positive && count($tag_ids) == 0) { - # one of the positive tags had zero results, therefor there - # can be no results; "where 1=0" should shortcut things - return new Querylet(" - SELECT images.* - FROM images - WHERE 1=0 - "); - } - } - - Image::$tag_n = 0; - return new Querylet(' - SELECT * - FROM ( - SELECT images.*, ('.$tag_search->sql.') AS score - FROM images - LEFT JOIN image_tags ON image_tags.image_id = images.id - JOIN tags ON image_tags.tag_id = tags.id - WHERE tags.id IN (' . join(', ', $tag_id_array) . ') - GROUP BY images.id - HAVING score = :score - ) AS images - WHERE 1=1 - ', array_merge( - $tag_search->variables, - array("score"=>$positive_tag_count) - )); - } -} - -/** - * 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 { - /** - * @param string[] $tags - * @return string - */ - public static function implode($tags) { - assert('is_array($tags)'); - - sort($tags); - $tags = implode(' ', $tags); - - return $tags; - } - - /** - * Turn a human-supplied string into a valid tag array. - * - * @param string $tags - * @param bool $tagme add "tagme" if the string is empty - * @return string[] - */ - public static function explode($tags, $tagme=true) { - global $database; - assert('is_string($tags)'); - - $tags = explode(' ', trim($tags)); - - /* sanitise by removing invisible / dodgy characters */ - $tag_array = array(); - foreach($tags as $tag) { - $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(mb_strlen($tag, 'UTF-8') > 255){ - flash_message("The tag below is longer than 255 characters, please use a shorter tag.\n$tag\n"); - continue; - } - - if(!empty($tag)) { - $tag_array[] = $tag; - } - } - - /* if user supplied a blank string, add "tagme" */ - if(count($tag_array) === 0 && $tagme) { - $tag_array = array("tagme"); - } - - /* resolve aliases */ - $new = array(); - $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( - $database->scoreql_to_sql(" - SELECT newtag - FROM aliases - WHERE SCORE_STRNORM(oldtag)=SCORE_STRNORM(:tag) - "), - array("tag"=>$tag) - ); - if(empty($newtags)) { - //tag has no alias, use old tag - $aliases = array($tag); - } - else { - $aliases = Tag::explode($newtags); - } - - 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; - } -} - - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ -* Misc functions * -\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -/** - * Move a file from PHP's temporary area into shimmie's image storage - * hierarchy, or throw an exception trying. - * - * @param DataUploadEvent $event - * @throws UploadException - */ -function move_upload_to_archive(DataUploadEvent $event) { - $target = warehouse_path("images", $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']}" - ); - } -} - -/** - * Add a directory full of images - * - * @param $base string - * @return array|string[] - */ -function add_dir($base) { - $results = array(); - - 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; -} - -/** - * @param string $tmpname - * @param string $filename - * @param string $tags - * @throws UploadException - */ -function add_image($tmpname, $filename, $tags) { - assert(file_exists($tmpname)); - - $pathinfo = pathinfo($filename); - if(!array_key_exists('extension', $pathinfo)) { - throw new UploadException("File has no extension"); - } - $metadata = array(); - $metadata['filename'] = $pathinfo['basename']; - $metadata['extension'] = $pathinfo['extension']; - $metadata['tags'] = Tag::explode($tags); - $metadata['source'] = null; - $event = new DataUploadEvent($tmpname, $metadata); - send_event($event); - if($event->image_id == -1) { - throw new UploadException("File type not recognised"); - } -} - -/** - * Given a full size pair of dimensions, return a pair scaled down to fit - * into the configured thumbnail square, with ratio intact - * - * @param int $orig_width - * @param int $orig_height - * @return integer[] - */ -function get_thumbnail_size(/*int*/ $orig_width, /*int*/ $orig_height) { - global $config; - - 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; - - $max_width = $config->get_int('thumb_width'); - $max_height = $config->get_int('thumb_height'); - - $xscale = ($max_height / $orig_height); - $yscale = ($max_width / $orig_width); - $scale = ($xscale < $yscale) ? $xscale : $yscale; - - if($scale > 1 && $config->get_bool('thumb_upscale')) { - return array((int)$orig_width, (int)$orig_height); - } - else { - return array((int)($orig_width*$scale), (int)($orig_height*$scale)); - } -} - diff --git a/core/imageboard/event.php b/core/imageboard/event.php new file mode 100644 index 00000000..27a00f96 --- /dev/null +++ b/core/imageboard/event.php @@ -0,0 +1,149 @@ +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); + } + } +} diff --git a/core/imageboard/image.php b/core/imageboard/image.php new file mode 100644 index 00000000..513ddbc1 --- /dev/null +++ b/core/imageboard/image.php @@ -0,0 +1,983 @@ + $value) { + // some databases use table.name rather than name + $name = str_replace("images.", "", $name); + + // hax, this is likely the cause of much scrutinizer-ci complaints. + if (is_null($value)) { + $this->$name = null; + } elseif (in_array($name, self::$bool_props)) { + $this->$name = bool_escape((string)$value); + } elseif (in_array($name, self::$int_props)) { + $this->$name = int_escape((string)$value); + } else { + $this->$name = $value; + } + } + } + } + + public static function by_id(int $id): ?Image + { + global $database; + $row = $database->get_row("SELECT * FROM images WHERE images.id=:id", ["id"=>$id]); + return ($row ? new Image($row) : null); + } + + public static function by_hash(string $hash): ?Image + { + global $database; + $hash = strtolower($hash); + $row = $database->get_row("SELECT images.* FROM images WHERE hash=:hash", ["hash"=>$hash]); + return ($row ? new Image($row) : null); + } + + public static function by_id_or_hash(string $id): ?Image + { + return (is_numeric($id) && strlen($id) != 32) ? Image::by_id((int)$id) : Image::by_hash($id); + } + + public static function by_random(array $tags=[], int $limit_range=0): ?Image + { + $max = Image::count_images($tags); + if ($max < 1) { + return null; + } // From Issue #22 - opened by HungryFeline on May 30, 2011. + if ($limit_range > 0 && $max > $limit_range) { + $max = $limit_range; + } + $rand = mt_rand(0, $max-1); + $set = Image::find_images($rand, 1, $tags); + if (count($set) > 0) { + return $set[0]; + } else { + return null; + } + } + + private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags=[]): iterable + { + global $database, $user, $config; + + if ($start < 0) { + $start = 0; + } + if ($limit!=null && $limit < 1) { + $limit = 1; + } + + if (SPEED_HAX) { + if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > 3) { + throw new SCoreException("Anonymous users may only search for up to 3 tags at a time"); + } + } + + $order = (Image::$order_sql ?: "images.".$config->get_string(IndexConfig::ORDER)); + $querylet = Image::build_search_querylet($tags, $order, $limit, $start); + $result = $database->get_all_iterable($querylet->sql, $querylet->variables); + + Image::$order_sql = null; + + return $result; + } + + /** + * Search for an array of images + * + * #param string[] $tags + * #return Image[] + */ + public static function find_images(int $start, ?int $limit = null, array $tags=[]): array + { + $result = self::find_images_internal($start, $limit, $tags); + + $images = []; + foreach ($result as $row) { + $images[] = new Image($row); + } + return $images; + } + + /** + * Search for an array of images, returning a iterable object of Image + */ + public static function find_images_iterable(int $start = 0, ?int $limit = null, array $tags=[]): Generator + { + $result = self::find_images_internal($start, $limit, $tags); + foreach ($result as $row) { + yield new Image($row); + } + } + + /* + * Image-related utility functions + */ + + public static function count_total_images(): int + { + global $cache, $database; + $total = $cache->get("image-count"); + if (!$total) { + $total = (int)$database->get_one("SELECT COUNT(*) FROM images"); + $cache->set("image-count", $total, 600); + } + return $total; + } + + public static function count_tag(string $tag): int + { + global $database; + return (int)$database->get_one( + "SELECT count FROM tags WHERE LOWER(tag) = LOWER(:tag)", + ["tag"=>$tag] + ); + } + + /** + * Count the number of image results for a given search + * + * #param string[] $tags + */ + public static function count_images(array $tags=[]): int + { + global $cache, $database; + $tag_count = count($tags); + + if ($tag_count === 0) { + // total number of images in the DB + $total = self::count_total_images(); + } elseif ($tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) { + if (!startsWith($tags[0], "-")) { + // one tag - we can look that up directly + $total = self::count_tag($tags[0]); + } else { + // one negative tag - subtract from the total + $total = self::count_total_images() - self::count_tag(substr($tags[0], 1)); + } + } else { + // complex query + // implode(tags) can be too long for memcache... + $cache_key = "image-count:" . md5(Tag::implode($tags)); + $total = $cache->get($cache_key); + if (!$total) { + if (Extension::is_enabled(RatingsInfo::KEY)) { + $tags[] = "rating:*"; + } + $querylet = Image::build_search_querylet($tags); + $total = (int)$database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables); + if (SPEED_HAX && $total > 5000) { + // when we have a ton of images, the count + // won't change dramatically very often + $cache->set($cache_key, $total, 3600); + } + } + } + if (is_null($total)) { + return 0; + } + return $total; + } + + /** + * Count the number of pages for a given search + * + * #param string[] $tags + */ + public static function count_pages(array $tags=[]): int + { + global $config; + return (int)ceil(Image::count_images($tags) / $config->get_int(IndexConfig::IMAGES)); + } + + private static function terms_to_conditions(array $terms): array + { + $tag_conditions = []; + $img_conditions = []; + + /* + * Turn a bunch of strings into a bunch of TagCondition + * and ImgCondition objects + */ + $stpe = send_event(new SearchTermParseEvent(null, $terms)); + if ($stpe->is_querylet_set()) { + foreach ($stpe->get_querylets() as $querylet) { + $img_conditions[] = new ImgCondition($querylet, true); + } + } + + foreach ($terms as $term) { + $positive = true; + if (is_string($term) && !empty($term) && ($term[0] == '-')) { + $positive = false; + $term = substr($term, 1); + } + if (strlen($term) === 0) { + continue; + } + + $stpe = send_event(new SearchTermParseEvent($term, $terms)); + if ($stpe->is_querylet_set()) { + foreach ($stpe->get_querylets() as $querylet) { + $img_conditions[] = new ImgCondition($querylet, $positive); + } + } else { + // if the whole match is wild, skip this + if (str_replace("*", "", $term) != "") { + $tag_conditions[] = new TagCondition($term, $positive); + } + } + } + return [$tag_conditions, $img_conditions]; + } + + /* + * Accessors & mutators + */ + + /** + * Find the next image in the sequence. + * + * Rather than simply $this_id + 1, one must take into account + * deleted images and search queries + * + * #param string[] $tags + */ + public function get_next(array $tags=[], bool $next=true): ?Image + { + global $database; + + if ($next) { + $gtlt = "<"; + $dir = "DESC"; + } else { + $gtlt = ">"; + $dir = "ASC"; + } + + if (count($tags) === 0) { + $row = $database->get_row(' + SELECT images.* + FROM images + WHERE images.id '.$gtlt.' '.$this->id.' + ORDER BY images.id '.$dir.' + LIMIT 1 + '); + } else { + $tags[] = 'id'. $gtlt . $this->id; + $querylet = Image::build_search_querylet($tags); + $querylet->append_sql(' ORDER BY images.id '.$dir.' LIMIT 1'); + $row = $database->get_row($querylet->sql, $querylet->variables); + } + + return ($row ? new Image($row) : null); + } + + /** + * The reverse of get_next + * + * #param string[] $tags + */ + public function get_prev(array $tags=[]): ?Image + { + return $this->get_next($tags, false); + } + + /** + * Find the User who owns this Image + */ + public function get_owner(): User + { + return User::by_id($this->owner_id); + } + + /** + * Set the image's owner. + */ + public function set_owner(User $owner): void + { + global $database; + if ($owner->id != $this->owner_id) { + $database->execute(" + UPDATE images + SET owner_id=:owner_id + WHERE id=:id + ", ["owner_id"=>$owner->id, "id"=>$this->id]); + log_info("core_image", "Owner for Image #{$this->id} set to {$owner->name}"); + } + } + + public function save_to_db() + { + global $database, $user; + $cut_name = substr($this->filename, 0, 255); + + if (is_null($this->id)) { + $database->execute( + "INSERT INTO images( + owner_id, owner_ip, + filename, filesize, + hash, ext, + width, height, + posted, source + ) + VALUES ( + :owner_id, :owner_ip, + :filename, :filesize, + :hash, :ext, + 0, 0, + now(), :source + )", + [ + "owner_id" => $user->id, "owner_ip" => $_SERVER['REMOTE_ADDR'], + "filename" => $cut_name, "filesize" => $this->filesize, + "hash" => $this->hash, "ext" => strtolower($this->ext), + "source" => $this->source + ] + ); + $this->id = $database->get_last_insert_id('images_id_seq'); + } else { + $database->execute( + "UPDATE images SET ". + "filename = :filename, filesize = :filesize, hash = :hash, ". + "ext = :ext, width = 0, height = 0, source = :source ". + "WHERE id = :id", + [ + "filename" => $cut_name, + "filesize" => $this->filesize, + "hash" => $this->hash, + "ext" => strtolower($this->ext), + "source" => $this->source, + "id" => $this->id, + ] + ); + } + + $database->execute( + "UPDATE images SET ". + "lossless = :lossless, ". + "video = :video, audio = :audio,image = :image, ". + "height = :height, width = :width, ". + "length = :length WHERE id = :id", + [ + "id" => $this->id, + "width" => $this->width ?? 0, + "height" => $this->height ?? 0, + "lossless" => $database->scoresql_value_prepare($this->lossless), + "video" => $database->scoresql_value_prepare($this->video), + "image" => $database->scoresql_value_prepare($this->image), + "audio" => $database->scoresql_value_prepare($this->audio), + "length" => $this->length + ] + ); + } + + /** + * Get this image's tags as an array. + * + * #return string[] + */ + public function get_tag_array(): array + { + global $database; + if (!isset($this->tag_array)) { + $this->tag_array = $database->get_col(" + SELECT tag + FROM image_tags + JOIN tags ON image_tags.tag_id = tags.id + WHERE image_id=:id + ORDER BY tag + ", ["id"=>$this->id]); + } + return $this->tag_array; + } + + /** + * Get this image's tags as a string. + */ + public function get_tag_list(): string + { + return Tag::implode($this->get_tag_array()); + } + + /** + * Get the URL for the full size image + */ + public function get_image_link(): string + { + return $this->get_link(ImageConfig::ILINK, '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext'); + } + + /** + * Get the nicely formatted version of the file name + */ + public function get_nice_image_name(): string + { + $plte = new ParseLinkTemplateEvent('$id - $tags.$ext', $this); + send_event($plte); + return $plte->text; + } + + /** + * Get the URL for the thumbnail + */ + public function get_thumb_link(): string + { + global $config; + $ext = $config->get_string(ImageConfig::THUMB_TYPE); + return $this->get_link(ImageConfig::TLINK, '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext); + } + + /** + * Check configured template for a link, then try nice URL, then plain URL + */ + private function get_link(string $template, string $nice, string $plain): string + { + global $config; + + $image_link = $config->get_string($template); + + if (!empty($image_link)) { + if (!(strpos($image_link, "://") > 0) && !startsWith($image_link, "/")) { + $image_link = make_link($image_link); + } + $chosen = $image_link; + } elseif ($config->get_bool('nice_urls', false)) { + $chosen = make_link($nice); + } else { + $chosen = make_link($plain); + } + return $this->parse_link_template($chosen); + } + + /** + * Get the tooltip for this image, formatted according to the + * configured template. + */ + public function get_tooltip(): string + { + global $config; + $plte = new ParseLinkTemplateEvent($config->get_string(ImageConfig::TIP), $this); + send_event($plte); + return $plte->text; + } + + /** + * Figure out where the full size image is on disk. + */ + public function get_image_filename(): string + { + return warehouse_path(self::IMAGE_DIR, $this->hash); + } + + /** + * Figure out where the thumbnail is on disk. + */ + public function get_thumb_filename(): string + { + return warehouse_path(self::THUMBNAIL_DIR, $this->hash); + } + + /** + * Get the original filename. + */ + public function get_filename(): string + { + return $this->filename; + } + + /** + * Get the image's mime type. + */ + public function get_mime_type(): string + { + return getMimeType($this->get_image_filename(), $this->get_ext()); + } + + /** + * Get the image's filename extension + */ + public function get_ext(): string + { + return $this->ext; + } + + /** + * Get the image's source URL + */ + public function get_source(): ?string + { + return $this->source; + } + + /** + * Set the image's source URL + */ + public function set_source(string $new_source): void + { + global $database; + $old_source = $this->source; + if (empty($new_source)) { + $new_source = null; + } + if ($new_source != $old_source) { + $database->execute("UPDATE images SET source=:source WHERE id=:id", ["source"=>$new_source, "id"=>$this->id]); + log_info("core_image", "Source for Image #{$this->id} set to: $new_source (was $old_source)"); + } + } + + /** + * Check if the image is locked. + */ + public function is_locked(): bool + { + return $this->locked; + } + + public function set_locked(bool $tf): void + { + global $database; + $ln = $tf ? "Y" : "N"; + $sln = $database->scoreql_to_sql('SCORE_BOOL_'.$ln); + $sln = str_replace("'", "", $sln); + $sln = str_replace('"', "", $sln); + if (bool_escape($sln) !== $this->locked) { + $database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn"=>$sln, "id"=>$this->id]); + log_info("core_image", "Setting Image #{$this->id} lock to: $ln"); + } + } + + /** + * Delete all tags from this image. + * + * Normally in preparation to set them to a new set. + */ + public function delete_tags_from_image(): void + { + global $database; + if ($database->get_driver_name() == DatabaseDriver::MYSQL) { + //mysql < 5.6 has terrible subquery optimization, using EXISTS / JOIN fixes this + $database->execute( + " + UPDATE tags t + INNER JOIN image_tags it ON t.id = it.tag_id + SET count = count - 1 + WHERE it.image_id = :id", + ["id"=>$this->id] + ); + } else { + $database->execute(" + UPDATE tags + SET count = count - 1 + WHERE id IN ( + SELECT tag_id + FROM image_tags + WHERE image_id = :id + ) + ", ["id"=>$this->id]); + } + $database->execute(" + DELETE + FROM image_tags + WHERE image_id=:id + ", ["id"=>$this->id]); + } + + /** + * Set the tags for this image. + */ + public function set_tags(array $unfiltered_tags): void + { + global $cache, $database, $page; + + $unfiltered_tags = array_unique($unfiltered_tags); + + $tags = []; + foreach ($unfiltered_tags as $tag) { + if (mb_strlen($tag, 'UTF-8') > 255) { + $page->flash("Can't set a tag longer than 255 characters"); + continue; + } + if (startsWith($tag, "-")) { + $page->flash("Can't set a tag which starts with a minus"); + continue; + } + + $tags[] = $tag; + } + + if (count($tags) <= 0) { + throw new SCoreException('Tried to set zero tags'); + } + + if (Tag::implode($tags) != $this->get_tag_list()) { + // delete old + $this->delete_tags_from_image(); + + $written_tags = []; + + // insert each new tags + foreach ($tags as $tag) { + $id = $database->get_one( + " + SELECT id + FROM tags + WHERE LOWER(tag) = LOWER(:tag) + ", + ["tag"=>$tag] + ); + if (empty($id)) { + // a new tag + $database->execute( + "INSERT INTO tags(tag) VALUES (:tag)", + ["tag"=>$tag] + ); + $database->execute( + "INSERT INTO image_tags(image_id, tag_id) + VALUES(:id, (SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)))", + ["id"=>$this->id, "tag"=>$tag] + ); + } else { + // check if tag has already been written + if (in_array($id, $written_tags)) { + continue; + } + + $database->execute(" + INSERT INTO image_tags(image_id, tag_id) + VALUES(:iid, :tid) + ", ["iid"=>$this->id, "tid"=>$id]); + + array_push($written_tags, $id); + } + $database->execute( + " + UPDATE tags + SET count = count + 1 + WHERE LOWER(tag) = LOWER(:tag) + ", + ["tag"=>$tag] + ); + } + + log_info("core_image", "Tags for Image #{$this->id} set to: ".Tag::implode($tags)); + $cache->delete("image-{$this->id}-tags"); + } + } + + /** + * Delete this image from the database and disk + */ + public function delete(): void + { + global $database; + $this->delete_tags_from_image(); + $database->execute("DELETE FROM images WHERE id=:id", ["id"=>$this->id]); + log_info("core_image", 'Deleted Image #'.$this->id.' ('.$this->hash.')'); + + unlink($this->get_image_filename()); + unlink($this->get_thumb_filename()); + } + + /** + * This function removes an image (and thumbnail) from the DISK ONLY. + * It DOES NOT remove anything from the database. + */ + public function remove_image_only(): void + { + log_info("core_image", 'Removed Image File ('.$this->hash.')'); + @unlink($this->get_image_filename()); + @unlink($this->get_thumb_filename()); + } + + public function parse_link_template(string $tmpl, int $n=0): string + { + $plte = send_event(new ParseLinkTemplateEvent($tmpl, $this)); + $tmpl = $plte->link; + return load_balance_url($tmpl, $this->hash, $n); + } + + private static function tag_or_wildcard_to_ids(string $tag): array + { + global $database; + $sq = "SELECT id FROM tags WHERE LOWER(tag) LIKE LOWER(:tag)"; + if ($database->get_driver_name() === DatabaseDriver::SQLITE) { + $sq .= "ESCAPE '\\'"; + } + return $database->get_col($sq, ["tag" => Tag::sqlify($tag)]); + } + + /** + * #param string[] $terms + */ + private static function build_search_querylet( + array $tags, + ?string $order=null, + ?int $limit=null, + ?int $offset=null + ): Querylet { + list($tag_conditions, $img_conditions) = self::terms_to_conditions($tags); + + $positive_tag_count = 0; + $negative_tag_count = 0; + foreach ($tag_conditions as $tq) { + if ($tq->positive) { + $positive_tag_count++; + } else { + $negative_tag_count++; + } + } + + /* + * Turn a bunch of Querylet objects into a base query + * + * Must follow the format + * + * SELECT images.* + * FROM (...) AS images + * WHERE (...) + * + * ie, return a set of images.* columns, and end with a WHERE + */ + + // no tags, do a simple search + if ($positive_tag_count === 0 && $negative_tag_count === 0) { + $query = new Querylet("SELECT images.* FROM images WHERE 1=1"); + } + + // one tag sorted by ID - we can fetch this from the image_tags table, + // and do the offset / limit there, which is 10x faster than fetching + // all the image_tags and doing the offset / limit on the result. + elseif ( + ( + ($positive_tag_count === 1 && $negative_tag_count === 0) + || ($positive_tag_count === 0 && $negative_tag_count === 1) + ) + && empty($img_conditions) + && ($order == "id DESC" || $order == "images.id DESC") + && !is_null($offset) + && !is_null($limit) + ) { + $in = $positive_tag_count === 1 ? "IN" : "NOT IN"; + // IN (SELECT id FROM tags) is 100x slower than doing a separate + // query and then a second query for IN(first_query_results)?? + $tag_array = self::tag_or_wildcard_to_ids($tag_conditions[0]->tag); + if (count($tag_array) == 0) { + if ($positive_tag_count == 1) { + $query = new Querylet("SELECT images.* FROM images WHERE 1=0"); + } else { + $query = new Querylet("SELECT images.* FROM images WHERE 1=1"); + } + } else { + $set = implode(', ', $tag_array); + $query = new Querylet(" + SELECT images.* + FROM images INNER JOIN ( + SELECT it.image_id + FROM image_tags it + WHERE it.tag_id $in ($set) + ORDER BY it.image_id DESC + LIMIT :limit OFFSET :offset + ) a on a.image_id = images.id + ORDER BY images.id DESC + ", ["limit"=>$limit, "offset"=>$offset]); + // don't offset at the image level because + // we already offset at the image_tags level + $order = null; + $limit = null; + $offset = null; + } + } + + // more than one positive tag, or more than zero negative tags + else { + $positive_tag_id_array = []; + $positive_wildcard_id_array = []; + $negative_tag_id_array = []; + + foreach ($tag_conditions as $tq) { + $tag_ids = self::tag_or_wildcard_to_ids($tq->tag); + $tag_count = count($tag_ids); + + if ($tq->positive) { + if ($tag_count== 0) { + # one of the positive tags had zero results, therefor there + # can be no results; "where 1=0" should shortcut things + return new Querylet("SELECT images.* FROM images WHERE 1=0"); + } elseif ($tag_count==1) { + // All wildcard terms that qualify for a single tag can be treated the same as non-wildcards + $positive_tag_id_array[] = $tag_ids[0]; + } else { + // Terms that resolve to multiple tags act as an OR within themselves + // and as an AND in relation to all other terms, + $positive_wildcard_id_array[] = $tag_ids; + } + } else { + // Unlike positive criteria, negative criteria are all handled in an OR fashion, + // so we can just compile them all into a single sub-query. + $negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids); + } + } + + assert($positive_tag_id_array || $positive_wildcard_id_array || $negative_tag_id_array, @$_GET['q']); + if (!empty($positive_tag_id_array) || !empty($positive_wildcard_id_array)) { + $inner_joins = []; + if (!empty($positive_tag_id_array)) { + foreach ($positive_tag_id_array as $tag) { + $inner_joins[] = "= $tag"; + } + } + if (!empty($positive_wildcard_id_array)) { + foreach ($positive_wildcard_id_array as $tags) { + $positive_tag_id_list = join(', ', $tags); + $inner_joins[] = "IN ($positive_tag_id_list)"; + } + } + + $first = array_shift($inner_joins); + $sub_query = "SELECT it.image_id FROM image_tags it "; + $i = 0; + foreach ($inner_joins as $inner_join) { + $i++; + $sub_query .= " INNER JOIN image_tags it$i ON it$i.image_id = it.image_id AND it$i.tag_id $inner_join "; + } + if (!empty($negative_tag_id_array)) { + $negative_tag_id_list = join(', ', $negative_tag_id_array); + $sub_query .= " LEFT JOIN image_tags negative ON negative.image_id = it.image_id AND negative.tag_id IN ($negative_tag_id_list) "; + } + $sub_query .= "WHERE it.tag_id $first "; + if (!empty($negative_tag_id_array)) { + $sub_query .= " AND negative.image_id IS NULL"; + } + $sub_query .= " GROUP BY it.image_id "; + + $query = new Querylet(" + SELECT images.* + FROM images + INNER JOIN ($sub_query) a on a.image_id = images.id + "); + } elseif (!empty($negative_tag_id_array)) { + $negative_tag_id_list = join(', ', $negative_tag_id_array); + $query = new Querylet(" + SELECT images.* + FROM images + LEFT JOIN image_tags negative ON negative.image_id = images.id AND negative.tag_id in ($negative_tag_id_list) + WHERE negative.image_id IS NULL + "); + } else { + throw new SCoreException("No criteria specified"); + } + } + + /* + * Merge all the image metadata searches into one generic querylet + * and append to the base querylet with "AND blah" + */ + if (!empty($img_conditions)) { + $n = 0; + $img_sql = ""; + $img_vars = []; + foreach ($img_conditions as $iq) { + if ($n++ > 0) { + $img_sql .= " AND"; + } + if (!$iq->positive) { + $img_sql .= " NOT"; + } + $img_sql .= " (" . $iq->qlet->sql . ")"; + $img_vars = array_merge($img_vars, $iq->qlet->variables); + } + $query->append_sql(" AND "); + $query->append(new Querylet($img_sql, $img_vars)); + } + + if (!is_null($order)) { + $query->append(new Querylet(" ORDER BY ".$order)); + } + if (!is_null($limit)) { + $query->append(new Querylet(" LIMIT :limit ", ["limit" => $limit])); + $query->append(new Querylet(" OFFSET :offset ", ["offset" => $offset])); + } + + return $query; + } +} diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php new file mode 100644 index 00000000..393e0f94 --- /dev/null +++ b/core/imageboard/misc.php @@ -0,0 +1,162 @@ +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): void +{ + 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; + send_event(new DataUploadEvent($tmpname, $metadata)); +} + +/** + * 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; + + 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) +{ + $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); +} + +function create_scaled_image(string $inname, string $outname, array $tsize, string $type, ?string $engine) +{ + global $config; + if (empty($engine)) { + $engine = $config->get_string(ImageConfig::THUMB_ENGINE); + } + + $output_format = $config->get_string(ImageConfig::THUMB_TYPE); + if ($output_format=="webp") { + $output_format = Media::WEBP_LOSSY; + } + + send_event(new MediaResizeEvent( + $engine, + $inname, + $type, + $outname, + $tsize[0], + $tsize[1], + false, + $output_format, + $config->get_int(ImageConfig::THUMB_QUALITY), + true, + $config->get_bool('thumb_upscale', false) + )); +} diff --git a/core/imageboard/search.php b/core/imageboard/search.php new file mode 100644 index 00000000..30456459 --- /dev/null +++ b/core/imageboard/search.php @@ -0,0 +1,58 @@ +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; + } +} diff --git a/core/imageboard/tag.php b/core/imageboard/tag.php new file mode 100644 index 00000000..5db83ffd --- /dev/null +++ b/core/imageboard/tag.php @@ -0,0 +1,218 @@ +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]) { + var_dump($tags1); + var_dump($tags2); + 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; + } +} diff --git a/core/install.php b/core/install.php new file mode 100644 index 00000000..e85299e4 --- /dev/null +++ b/core/install.php @@ -0,0 +1,364 @@ +Shimmie is unable to find the composer vendor directory.

+

Have you followed the composer setup instructions found in the + README?

+

If you are not intending to do any development with Shimmie, + it is highly recommend you use one of the pre-packaged releases + found on Github instead.

+ "); + } + + // 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 (file_exists("data/config/auto_install.conf.php")) { + $dsn = null; + /** @noinspection PhpIncludeInspection */ + require_once "data/config/auto_install.conf.php"; + } 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) { + exit_with_page($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) ? '' : ""; + $db_p = in_array(DatabaseDriver::PGSQL, $drivers) ? '' : ""; + $db_s = in_array(DatabaseDriver::SQLITE, $drivers) ? '' : ""; + + $warn_msg = $warnings ? "

Warnings

".implode("\n

", $warnings) : ""; + $err_msg = $errors ? "

Errors

".implode("\n

", $errors) : ""; + + exit_with_page( + "Install Options", + <<Database Install +

+
+ + + + + + + + + + + + + + + + + + + + + + +
Type:
Host:
Username:
Password:
DB Name:
+
+ +
+ +

Help

+

+ Please make sure the database you have chosen exists and is empty.
+ The username provided must have access to create tables within the database. +

+

+ For SQLite the database name will be a filename on disk, relative to + where shimmie was installed. +

+

+ Drivers can generally be downloaded with your OS package manager; + for Debian / Ubuntu you want php-pgsql, php-mysql, or php-sqlite. +

+EOD + ); +} + + +function create_dirs() +{ + $data_exists = file_exists("data") || mkdir("data"); + $data_writable = is_writable("data") || chmod("data", 0755); + + if (!$data_exists || !$data_writable) { + throw new InstallerException( + "Directory Permissions Error:", + "

Shimmie needs to have a 'data' folder in its directory, writable by the PHP user.

+

If you see this error, if probably means the folder is owned by you, and it needs to be writable by the web server.

+

PHP reports that it is currently running as user: ".$_ENV["USER"]." (". $_SERVER["USER"] .")

+

Once you have created this folder and / or changed the ownership of the shimmie folder, hit 'refresh' to continue.

", + 7 + ); + } +} + +function create_tables(Database $db) +{ + try { + if ($db->count_tables() > 0) { + throw new InstallerException( + "Warning: The Database schema is not empty!", + "

Please ensure that the database you are installing Shimmie with is empty before continuing.

+

Once you have emptied the database of any tables, please hit 'refresh' to continue.

", + 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:", + "

An error occurred while trying to create the database tables necessary for Shimmie.

+

Please check and ensure that the database configuration options are all correct.

+

{$e->getMessage()}

", + 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"); + exit_with_page( + "Installation Successful", + "

If you aren't redirected, click here to Continue." + ); + } 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 \"<?php\". + +

+ +

Once done, click here to Continue.", + 0 + ); + } +} + +function exit_with_page($title, $body, $code=0) +{ + print(" + + + Shimmie Installer + + + + +

+

Shimmie Installer

+

$title

+
+ $body +
+
+ +"); + exit($code); +} diff --git a/core/logging.php b/core/logging.php new file mode 100644 index 00000000..ae76d01c --- /dev/null +++ b/core/logging.php @@ -0,0 +1,73 @@ += $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; +} diff --git a/core/page.class.php b/core/page.class.php deleted file mode 100644 index 3d02bbe6..00000000 --- a/core/page.class.php +++ /dev/null @@ -1,424 +0,0 @@ -mode = $mode; - } - - /** - * Set the page's MIME type. - * @param string $type - */ - public function set_type($type) { - $this->type = $type; - } - - - //@} - // ============================================== - /** @name "data" mode */ - //@{ - - /** @var string; public only for unit test */ - public $data = ""; - - /** @var string; public only for unit test */ - public $filename = null; - - /** - * Set the raw data to be sent. - * @param string $data - */ - public function set_data($data) { - $this->data = $data; - } - - /** - * Set the recommended download filename. - * @param string $filename - */ - public function set_filename($filename) { - $this->filename = $filename; - } - - - //@} - // ============================================== - /** @name "redirect" mode */ - //@{ - - /** @var string */ - private $redirect = ""; - - /** - * Set the URL to redirect to (remember to use make_link() if linking - * to a page in the same site). - * @param string $redirect - */ - public function set_redirect($redirect) { - $this->redirect = $redirect; - } - - - //@} - // ============================================== - /** @name "page" mode */ - //@{ - - /** @var int */ - public $code = 200; - - /** @var string */ - public $title = ""; - - /** @var string */ - public $heading = ""; - - /** @var string */ - public $subheading = ""; - - /** @var string */ - public $quicknav = ""; - - /** @var string[] */ - public $html_headers = array(); - - /** @var string[] */ - public $http_headers = array(); - - /** @var string[][] */ - public $cookies = array(); - - /** @var Block[] */ - public $blocks = array(); - - /** - * Set the HTTP status code - * @param int $code - */ - public function set_code($code) { - $this->code = $code; - } - - /** - * Set the window title. - * @param string $title - */ - public function set_title($title) { - $this->title = $title; - } - - /** - * Set the main heading. - * @param string $heading - */ - public function set_heading($heading) { - $this->heading = $heading; - } - - /** - * Set the sub heading. - * @param string $subheading - */ - public function set_subheading($subheading) { - $this->subheading = $subheading; - } - - /** - * Add a line to the HTML head section. - * @param string $line - * @param int $position - */ - 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. - * @param string $line - * @param int $position - */ - public function add_http_header($line, $position=50) { - 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. - * - * @param string $name - * @param string $value - * @param int $time - * @param string $path - */ - public function add_cookie($name, $value, $time, $path) { - $full_name = COOKIE_PREFIX."_".$name; - $this->cookies[] = array($full_name, $value, $time, $path); - } - - /** - * @param string $name - * @return string|null - */ - public function get_cookie(/*string*/ $name) { - $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. - * @return string - */ - public function get_all_html_headers() { - $data = ''; - ksort($this->html_headers); - foreach ($this->html_headers as $line) { - $data .= "\t\t" . $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 to the page. - * @param Block $block - */ - 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("HTTP/1.0 {$this->code} Shimmie"); - header("Content-type: ".$this->type); - header("X-Powered-By: SCore-".SCORE_VERSION); - - if (!headers_sent()) { - 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."; - } - - 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'); - #} - if($this->get_cookie("flash_message") !== null) { - $this->add_cookie("flash_message", "", -1, "/"); - } - 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 '.$this->redirect.''; - 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() { - global $config; - - $data_href = get_base_href(); - $theme_name = $config->get_string('theme', 'default'); - - $this->add_html_header("", 40); - - # 404/static handler will map these to themes/foo/bar.ico or lib/static/bar.ico - $this->add_html_header("", 41); - $this->add_html_header("", 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_lib_latest = $config_latest; - $css_lib_files = zglob("lib/vendor/css/*.css"); - foreach($css_lib_files as $css) { - $css_lib_latest = max($css_lib_latest, filemtime($css)); - } - $css_lib_md5 = md5(serialize($css_lib_files)); - $css_lib_cache_file = data_path("cache/style.lib.{$theme_name}.{$css_lib_latest}.{$css_lib_md5}.css"); - if(!file_exists($css_lib_cache_file)) { - $css_lib_data = ""; - foreach($css_lib_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_lib_data .= $file_data . "\n"; - } - file_put_contents($css_lib_cache_file, $css_lib_data); - } - $this->add_html_header("", 43); - - $css_latest = $config_latest; - $css_files = array_merge(zglob("lib/shimmie.css"), zglob("ext/{".ENABLED_EXTS."}/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.main.{$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("", 100); - - /*** Generate JS cache files ***/ - $js_lib_latest = $config_latest; - $js_lib_files = zglob("lib/vendor/js/*.js"); - foreach($js_lib_files as $js) { - $js_lib_latest = max($js_lib_latest, filemtime($js)); - } - $js_lib_md5 = md5(serialize($js_lib_files)); - $js_lib_cache_file = data_path("cache/script.lib.{$theme_name}.{$js_lib_latest}.{$js_lib_md5}.js"); - if(!file_exists($js_lib_cache_file)) { - $js_data = ""; - foreach($js_lib_files as $file) { - $js_data .= file_get_contents($file) . "\n"; - } - file_put_contents($js_lib_cache_file, $js_data); - } - $this->add_html_header("", 45); - - $js_latest = $config_latest; - $js_files = array_merge(zglob("lib/shimmie.js"), zglob("ext/{".ENABLED_EXTS."}/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.main.{$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("", 100); - } -} - -class MockPage extends Page { -} diff --git a/core/permissions.php b/core/permissions.php new file mode 100644 index 00000000..1b514a46 --- /dev/null +++ b/core/permissions.php @@ -0,0 +1,94 @@ +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 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); + if (!defined("UNITTEST")) { + @ob_flush(); + } + flush(); + + // 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); + } +} + +const MIME_TYPE_MAP = [ + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'ico' => 'image/x-icon', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', + 'svg' => 'image/svg+xml', + 'pdf' => 'application/pdf', + 'zip' => 'application/zip', + 'gz' => 'application/x-gzip', + 'tar' => 'application/x-tar', + 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', + 'txt' => 'text/plain', + 'asc' => 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'css' => 'text/css', + 'js' => 'text/javascript', + 'xml' => 'text/xml', + 'xsl' => 'application/xsl+xml', + 'ogg' => 'application/ogg', + 'mp3' => 'audio/mpeg', + 'wav' => 'audio/x-wav', + 'avi' => 'video/x-msvideo', + 'mpg' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mov' => 'video/quicktime', + 'php' => 'text/x-php', + 'mp4' => 'video/mp4', + 'ogv' => 'video/ogg', + 'webm' => 'video/webm', + 'webp' => 'image/webp', + 'bmp' =>'image/x-ms-bmp', + 'psd' => 'image/vnd.adobe.photoshop', + 'mkv' => 'video/x-matroska' +]; + +/** + * Get MIME type for file + * + * The contents of this function are taken from the __getMimeType() function + * from the "Amazon S3 PHP class" which is Copyright (c) 2008, Donovan Schönknecht + * and released under the 'Simplified BSD License'. + */ +function getMimeType(string $file, string $ext=""): string +{ + // Static extension lookup + $ext = strtolower($ext); + + if (array_key_exists($ext, MIME_TYPE_MAP)) { + return MIME_TYPE_MAP[$ext]; + } + + $type = false; + // Fileinfo documentation says fileinfo_open() will use the + // MAGIC env var for the magic file + if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && + ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) { + if (($type = finfo_file($finfo, $file)) !== false) { + // Remove the charset and grab the last content-type + $type = explode(' ', str_replace('; charset=', ';charset=', $type)); + $type = array_pop($type); + $type = explode(';', $type); + $type = trim(array_shift($type)); + } + finfo_close($finfo); + + // If anyone is still using mime_content_type() + } elseif (function_exists('mime_content_type')) { + $type = trim(mime_content_type($file)); + } + + if ($type !== false && strlen($type) > 0) { + return $type; + } + + return 'application/octet-stream'; +} + +function get_extension(?string $mime_type): ?string +{ + if (empty($mime_type)) { + return null; + } + + $ext = array_search($mime_type, MIME_TYPE_MAP); + return ($ext ? $ext : null); +} + +/** @noinspection PhpUnhandledExceptionInspection */ +function getSubclassesOf(string $parent) +{ + $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 http://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; +} + +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! + http://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; +} + +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(''', ''', htmlspecialchars((string)$v, ENT_QUOTES)); + $xml .= "$k=\"$xv\" "; + } + if (count($children) > 0) { + $xml .= ">\n"; + foreach ($children as $child) { + $xml .= xml_tag($child); + } + $xml .= "\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\.]+)([gmk])?b?$/i', (string)$limit, $m)) { + $value = $m[1]; + if (isset($m[2])) { + switch (strtolower($m[2])) { + /** @noinspection PhpMissingBreakStatementInspection */ + 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, 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 ? "" : $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; +} diff --git a/core/send_event.php b/core/send_event.php new file mode 100644 index 00000000..98a48462 --- /dev/null +++ b/core/send_event.php @@ -0,0 +1,128 @@ +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: http://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; +} diff --git a/core/sys_config.inc.php b/core/sys_config.inc.php deleted file mode 100644 index cb8d2b4a..00000000 --- a/core/sys_config.inc.php +++ /dev/null @@ -1,53 +0,0 @@ -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 + } +} diff --git a/core/tests/block.test.php b/core/tests/block.test.php new file mode 100644 index 00000000..bdaa1e97 --- /dev/null +++ b/core/tests/block.test.php @@ -0,0 +1,17 @@ +assertEquals( + "

head

body
\n", + $b->get_html() + ); + } +} diff --git a/core/tests/polyfills.test.php b/core/tests/polyfills.test.php new file mode 100644 index 00000000..84c3eea0 --- /dev/null +++ b/core/tests/polyfills.test.php @@ -0,0 +1,223 @@ +assertEquals( + html_escape("Foo & "), + "Foo & <waffles>" + ); + + $this->assertEquals( + html_unescape("Foo & <waffles>"), + "Foo & " + ); + + $x = "Foo & <waffles>"; + $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( + "\n\n\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( + "", + 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"]]) + ); + } +} diff --git a/core/tests/tag.test.php b/core/tests/tag.test.php new file mode 100644 index 00000000..ef8e947f --- /dev/null +++ b/core/tests/tag.test.php @@ -0,0 +1,22 @@ +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")); + } +} diff --git a/core/tests/urls.test.php b/core/tests/urls.test.php new file mode 100644 index 00000000..b6ff90f0 --- /dev/null +++ b/core/tests/urls.test.php @@ -0,0 +1,42 @@ +assertEquals( + "/test/foo", + make_link("foo") + ); + + $this->assertEquals( + "/test/foo", + make_link("/foo") + ); + } + + public function test_make_http() + { + // relative to shimmie install + $this->assertEquals( + "http:///test/foo", + make_http("foo") + ); + + // relative to web server + $this->assertEquals( + "http:///foo", + make_http("/foo") + ); + + // absolute + $this->assertEquals( + "http://foo.com", + make_http("http://foo.com") + ); + } +} diff --git a/core/tests/util.test.php b/core/tests/util.test.php new file mode 100644 index 00000000..94f2273b --- /dev/null +++ b/core/tests/util.test.php @@ -0,0 +1,86 @@ +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) + ); + } +} diff --git a/core/urls.php b/core/urls.php new file mode 100644 index 00000000..72dc1b79 --- /dev/null +++ b/core/urls.php @@ -0,0 +1,118 @@ +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 +{ + global $config; + + if (is_null($page)) { + $page = $config->get_string(SetupConfig::MAIN_PAGE); + } + + $install_dir = get_base_href(); + if (SPEED_HAX || $config->get_bool('nice_urls', false)) { + $base = $install_dir; + } else { + $base = "$install_dir/index.php?q="; + } + + if (is_null($query)) { + return str_replace("//", "/", $base.'/'.$page); + } else { + if (strpos($base, "?")) { + return $base .'/'. $page .'&'. $query; + } elseif (strpos($query, "#") === 0) { + return $base .'/'. $page . $query; + } else { + return $base .'/'. $page .'?'. $query; + } + } +} + + +/** + * Take the current URL and modify some parameters + */ +function modify_current_url(array $changes): string +{ + return modify_url($_SERVER['QUERY_STRING'], $changes); +} + +function modify_url(string $url, array $changes): string +{ + // SHIT: PHP is officially the worst web API ever because it does not + // have a built-in function to do this. + + // SHIT: parse_str is magically retarded; not only is it a useless name, it also + // didn't return the parsed array, preferring to overwrite global variables with + // whatever data the user supplied. Thankfully, 4.0.3 added an extra option to + // give it an array to use... + $params = []; + parse_str($url, $params); + + if (isset($changes['q'])) { + $base = $changes['q']; + unset($changes['q']); + } else { + $base = _get_query(); + } + + if (isset($params['q'])) { + unset($params['q']); + } + + foreach ($changes as $k => $v) { + if (is_null($v) and isset($params[$k])) { + unset($params[$k]); + } + $params[$k] = $v; + } + + return make_link($base, http_build_query($params)); +} + + +/** + * 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; +} diff --git a/core/user.class.php b/core/user.class.php deleted file mode 100644 index 662300cd..00000000 --- a/core/user.class.php +++ /dev/null @@ -1,300 +0,0 @@ -id = int_escape($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"]}'"); - } - } - - /** - * Construct a User by session. - * - * @param string $name - * @param string $session - * @return null|User - */ - public static function by_session(/*string*/ $name, /*string*/ $session) { - global $config, $database; - $row = $database->cache->get("user-session:$name-$session"); - if(!$row) { - if($database->get_driver_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)); - $database->cache->set("user-session:$name-$session", $row, 600); - } - return is_null($row) ? null : new User($row); - } - - /** - * Construct a User by session. - * @param int $id - * @return null|User - */ - public static function by_id(/*int*/ $id) { - assert('is_numeric($id)', var_export($id, true)); - 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, 600); - return is_null($row) ? null : new User($row); - } - - /** - * Construct a User by name. - * @param string $name - * @return null|User - */ - public static function by_name(/*string*/ $name) { - assert('is_string($name)', var_export($name, true)); - global $database; - $row = $database->get_row($database->scoreql_to_sql("SELECT * FROM users WHERE SCORE_STRNORM(name) = SCORE_STRNORM(:name)"), array("name"=>$name)); - return is_null($row) ? null : new User($row); - } - - /** - * Construct a User by name and password. - * @param string $name - * @param string $pass - * @return null|User - */ - public static function by_name_and_pass(/*string*/ $name, /*string*/ $pass) { - assert('is_string($name)', var_export($name, true)); - assert('is_string($pass)', var_export($pass, true)); - $user = User::by_name($name); - if($user) { - if($user->passhash == md5(strtolower($name) . $pass)) { - $user->set_password($pass); - } - if(password_verify($pass, $user->passhash)) { - return $user; - } - } - } - - - /* useful user object functions start here */ - - - /** - * @param string $ability - * @return bool - */ - public function can($ability) { - return $this->class->can($ability); - } - - - /** - * Test if this user is anonymous (not logged in). - * - * @return bool - */ - public function is_anonymous() { - global $config; - return ($this->id === $config->get_int('anon_id')); - } - - /** - * Test if this user is logged in. - * - * @return bool - */ - public function is_logged_in() { - global $config; - return ($this->id !== $config->get_int('anon_id')); - } - - /** - * Test if this user is an administrator. - * - * @return bool - */ - public function is_admin() { - return ($this->class->name === "admin"); - } - - /** - * @param string $class - */ - public function set_class(/*string*/ $class) { - assert('is_string($class)', var_export($class, true)); - 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); - } - - /** - * @param string $name - * @throws Exception - */ - public function set_name(/*string*/ $name) { - global $database; - if(User::by_name($name)) { - throw new Exception("Desired username is already in use"); - } - $old_name = $this->name; - $this->name = $name; - $database->Execute("UPDATE users SET name=:name WHERE id=:id", array("name"=>$this->name, "id"=>$this->id)); - log_info("core-user", "Changed username for {$old_name} to {$this->name}"); - } - - /** - * @param string $password - */ - public function set_password(/*string*/ $password) { - 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", array("hash"=>$this->passhash, "id"=>$this->id)); - log_info("core-user", 'Set password for '.$this->name); - } - else { - throw new SCoreException("Failed to hash password"); - } - } - - /** - * @param string $address - */ - 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. - * - * @return 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"); - $cb = date("Y-m-d"); - return ""; - } - } - 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 - * - * @return string A 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 ''; - } - - public function check_auth_token() { - return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token()); - } -} - -class MockUser extends User { - public function __construct($name) { - $row = array( - "name" => $name, - "id" => 1, - "email" => "", - "joindate" => "", - "pass" => "", - "class" => "admin", - ); - parent::__construct($row); - } -} - diff --git a/core/user.php b/core/user.php new file mode 100644 index 00000000..75406597 --- /dev/null +++ b/core/user.php @@ -0,0 +1,259 @@ +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 "avatar"; + } + } + 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 ''; + } + + public function check_auth_token(): bool + { + 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"); + } + } +} diff --git a/core/userclass.class.php b/core/userclass.class.php deleted file mode 100644 index 5780b3fe..00000000 --- a/core/userclass.class.php +++ /dev/null @@ -1,200 +0,0 @@ -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. - * - * @param string $ability - * @return bool - * @throws SCoreException - */ - public function can(/*string*/ $ability) { - 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 $_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 '".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_name" => 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, - "bypass_comment_checks" => False, # spam etc - - "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, - - "hellbanned" => False, - "view_hellbanned" => 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_name" => 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, - "bypass_comment_checks" => 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, - "view_hellbanned" => True, - "protected" => True, -)); - -new UserClass("hellbanned", "user", array( - "hellbanned" => True, -)); - -@include_once "data/config/user-classes.conf.php"; - diff --git a/core/userclass.php b/core/userclass.php new file mode 100644 index 00000000..b5409b25 --- /dev/null +++ b/core/userclass.php @@ -0,0 +1,263 @@ +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'?"); + } + } +} + +// action_object_attribute +// action = create / view / edit / delete +// object = image / user / tag / setting +new UserClass("base", null, [ + Permissions::CHANGE_SETTING => false, # modify web-level settings, eg the config table + Permissions::OVERRIDE_CONFIG => false, # modify sys-level settings, eg shimmie.conf.php + Permissions::BIG_SEARCH => false, # search for more than 3 tags at once (speed mode only) + + Permissions::MANAGE_EXTENSION_LIST => false, + Permissions::MANAGE_ALIAS_LIST => false, + Permissions::MANAGE_AUTO_TAG => false, + Permissions::MASS_TAG_EDIT => false, + + Permissions::VIEW_IP => false, # view IP addresses associated with things + Permissions::BAN_IP => false, + + Permissions::CREATE_USER => false, + Permissions::EDIT_USER_NAME => false, + Permissions::EDIT_USER_PASSWORD => false, + Permissions::EDIT_USER_INFO => false, # email address, etc + Permissions::EDIT_USER_CLASS => false, + Permissions::DELETE_USER => false, + + Permissions::CREATE_COMMENT => false, + Permissions::DELETE_COMMENT => false, + Permissions::BYPASS_COMMENT_CHECKS => false, # spam etc + + Permissions::REPLACE_IMAGE => false, + Permissions::CREATE_IMAGE => false, + Permissions::EDIT_IMAGE_TAG => false, + Permissions::EDIT_IMAGE_SOURCE => false, + Permissions::EDIT_IMAGE_OWNER => false, + Permissions::EDIT_IMAGE_LOCK => false, + Permissions::EDIT_IMAGE_TITLE => false, + Permissions::EDIT_IMAGE_RELATIONSHIPS => false, + Permissions::EDIT_IMAGE_ARTIST => false, + Permissions::BULK_EDIT_IMAGE_TAG => false, + Permissions::BULK_EDIT_IMAGE_SOURCE => false, + Permissions::DELETE_IMAGE => false, + + Permissions::BAN_IMAGE => false, + + Permissions::VIEW_EVENTLOG => false, + Permissions::IGNORE_DOWNTIME => false, + + Permissions::CREATE_IMAGE_REPORT => false, + Permissions::VIEW_IMAGE_REPORT => false, # deal with reported images + + Permissions::WIKI_ADMIN => false, + Permissions::EDIT_WIKI_PAGE => false, + Permissions::DELETE_WIKI_PAGE => false, + + Permissions::MANAGE_BLOCKS => false, + + Permissions::MANAGE_ADMINTOOLS => false, + + Permissions::SEND_PM => false, + Permissions::READ_PM => false, + Permissions::VIEW_OTHER_PMS => false, + Permissions::EDIT_FEATURE => false, + Permissions::BULK_EDIT_VOTE => false, + Permissions::EDIT_OTHER_VOTE => false, + Permissions::VIEW_SYSINTO => false, + + Permissions::HELLBANNED => false, + Permissions::VIEW_HELLBANNED => false, + + Permissions::PROTECTED => false, # only admins can modify protected users (stops a moderator changing an admin's password) + + Permissions::EDIT_IMAGE_RATING => false, + Permissions::BULK_EDIT_IMAGE_RATING => false, + + Permissions::VIEW_TRASH => false, + + Permissions::PERFORM_BULK_ACTIONS => false, + + Permissions::BULK_ADD => false, + Permissions::EDIT_FILES => false, + Permissions::EDIT_TAG_CATEGORIES => false, + Permissions::RESCAN_MEDIA => false, + Permissions::SEE_IMAGE_VIEW_COUNTS => false, + + Permissions::EDIT_FAVOURITES => false, + + Permissions::ARTISTS_ADMIN => false, + Permissions::BLOTTER_ADMIN => false, + Permissions::FORUM_ADMIN => false, + Permissions::NOTES_ADMIN => false, + Permissions::POOLS_ADMIN => false, + Permissions::TIPS_ADMIN => false, + Permissions::CRON_ADMIN => false, + + Permissions::APPROVE_IMAGE => false, + Permissions::APPROVE_COMMENT => 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, +]); + +new UserClass("admin", "base", [ + Permissions::CHANGE_SETTING => true, + Permissions::OVERRIDE_CONFIG => true, + Permissions::BIG_SEARCH => true, + Permissions::EDIT_IMAGE_LOCK => true, + Permissions::VIEW_IP => true, + Permissions::BAN_IP => 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_IMAGE => true, + Permissions::DELETE_IMAGE => true, + Permissions::BAN_IMAGE => true, + Permissions::CREATE_COMMENT => true, + Permissions::DELETE_COMMENT => true, + Permissions::BYPASS_COMMENT_CHECKS => true, + Permissions::REPLACE_IMAGE => true, + Permissions::MANAGE_EXTENSION_LIST => true, + Permissions::MANAGE_ALIAS_LIST => true, + Permissions::MANAGE_AUTO_TAG => true, + Permissions::EDIT_IMAGE_TAG => true, + Permissions::EDIT_IMAGE_SOURCE => true, + Permissions::EDIT_IMAGE_OWNER => true, + Permissions::EDIT_IMAGE_TITLE => true, + Permissions::BULK_EDIT_IMAGE_TAG => true, + Permissions::BULK_EDIT_IMAGE_SOURCE => true, + Permissions::MASS_TAG_EDIT => 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::VIEW_EVENTLOG => true, + Permissions::MANAGE_BLOCKS => true, + Permissions::MANAGE_ADMINTOOLS => true, + Permissions::IGNORE_DOWNTIME => true, + Permissions::SEND_PM => true, + Permissions::READ_PM => true, + Permissions::VIEW_OTHER_PMS => true, + Permissions::EDIT_FEATURE => true, + Permissions::BULK_EDIT_VOTE => true, + Permissions::EDIT_OTHER_VOTE => true, + Permissions::VIEW_SYSINTO => true, + 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::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, +]); + +new UserClass("hellbanned", "user", [ + Permissions::HELLBANNED => true, +]); + +@include_once "data/config/user-classes.conf.php"; diff --git a/core/util.inc.php b/core/util.inc.php deleted file mode 100644 index f6a357ec..00000000 --- a/core/util.inc.php +++ /dev/null @@ -1,1826 +0,0 @@ -escape($input); -} - - -/** - * Turn all manner of HTML / INI / JS / DB booleans into a PHP one - * - * @param mixed $input - * @return boolean - */ -function bool_escape($input) { - /* - Sometimes, I don't like PHP -- this, is one of those times... - "a boolean FALSE is not considered a valid boolean value by this function." - Yay for Got'chas! - http://php.net/manual/en/filter.filters.validate.php - */ - if (is_bool($input)) { - return $input; - } else if (is_numeric($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 - * - * @param string $input - * @return string - */ -function no_escape($input) { - return $input; -} - -/** - * @param int $val - * @param int|null $min - * @param int|null $max - * @return int - */ -function clamp($val, $min, $max) { - 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; -} - -/** - * @param string $name - * @param array $attrs - * @param array $children - * @return string - */ -function xml_tag($name, $attrs=array(), $children=array()) { - $xml = "<$name "; - foreach($attrs as $k => $v) { - $xv = str_replace(''', ''', htmlspecialchars($v, ENT_QUOTES)); - $xml .= "$k=\"$xv\" "; - } - if(count($children) > 0) { - $xml .= ">\n"; - foreach($children as $child) { - $xml .= xml_tag($child); - } - $xml .= "\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. - * - * @param string $string input data - * @param int $limit how long the string should be - * @param string $break where to break the string - * @param string $pad what to add to the end of the string after truncating - */ -function truncate($string, $limit, $break=" ", $pad="...") { - // 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 - * - * @param string|integer $limit - * @return int - */ -function parse_shorthand_int($limit) { - if(is_numeric($limit)) { - return (int)$limit; - } - - if(preg_match('/^([\d\.]+)([gmk])?b?$/i', (string)$limit, $m)) { - $value = $m[1]; - if (isset($m[2])) { - switch(strtolower($m[2])) { - /** @noinspection PhpMissingBreakStatementInspection */ - case 'g': $value *= 1024; // fall through - /** @noinspection PhpMissingBreakStatementInspection */ - case 'm': $value *= 1024; // fall through - /** @noinspection PhpMissingBreakStatementInspection */ - 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 - * - * @param integer $int - * @return string - */ -function to_shorthand_int($int) { - if($int >= pow(1024, 3)) { - return sprintf("%.1fGB", $int / pow(1024, 3)); - } - else if($int >= pow(1024, 2)) { - return sprintf("%.1fMB", $int / pow(1024, 2)); - } - else if($int >= 1024) { - return sprintf("%.1fKB", $int / 1024); - } - else { - return (string)$int; - } -} - - -/** - * Turn a date into a time, a date, an "X minutes ago...", etc - * - * @param string $date - * @param bool $html - * @return string - */ -function autodate($date, $html=true) { - $cpu = date('c', strtotime($date)); - $hum = date('F j, Y; H:i', strtotime($date)); - return ($html ? "" : $hum); -} - -/** - * Check if a given string is a valid date-time. ( Format: yyyy-mm-dd hh:mm:ss ) - * - * @param string $dateTime - * @return bool - */ -function isValidDateTime($dateTime) { - 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($matches[2], $matches[3], $matches[1])) { - return true; - } - } - - return false; -} - -/** - * Check if a given string is a valid date. ( Format: yyyy-mm-dd ) - * - * @param string $date - * @return bool - */ -function isValidDate($date) { - if (preg_match("/^(\d{4})-(\d{2})-(\d{2})$/", $date, $matches)) { - // checkdate wants (month, day, year) - if (checkdate($matches[2], $matches[3], $matches[1])) { - return true; - } - } - - return false; -} - -/** - * @param string[] $inputs - */ -function validate_input($inputs) { - $outputs = array(); - - 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; - } - else if(in_array('user_name', $flags)) { - if(strlen($value) < 1) { - throw new InvalidInput("Username must be at least 1 character"); - } - else if(!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; - } - else if(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; - } - else if(in_array('email', $flags)) { - $outputs[$key] = trim($value); - } - else if(in_array('password', $flags)) { - $outputs[$key] = $value; - } - else if(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; - } - else if(in_array('bool', $flags)) { - $outputs[$key] = bool_escape($value); - } - else if(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; -} - -/** - * 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 - * - * @param string $ip - * @param string $ban_reason - * @return string - */ -function show_ip($ip, $ban_reason) { - global $user; - $u_reason = url_escape($ban_reason); - $u_end = url_escape("+1 week"); - $ban = $user->can("ban_ip") ? ", Ban" : ""; - $ip = $user->can("view_ip") ? $ip.$ban : ""; - return $ip; -} - -/** - * Checks if a given string contains another at the beginning. - * - * @param string $haystack String to examine. - * @param string $needle String to look for. - * @return bool - */ -function startsWith(/*string*/ $haystack, /*string*/ $needle) { - $length = strlen($needle); - return (substr($haystack, 0, $length) === $needle); -} - -/** - * Checks if a given string contains another at the end. - * - * @param string $haystack String to examine. - * @param string $needle String to look for. - * @return bool - */ -function endsWith(/*string*/ $haystack, /*string*/ $needle) { - $length = strlen($needle); - $start = $length * -1; //negative - return (substr($haystack, $start) === $needle); -} - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ -* HTML Generation * -\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -/** - * 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" - * - * @param null|string $page - * @param null|string $query - * @return string - */ -function make_link($page=null, $query=null) { - global $config; - - if(is_null($page)) $page = $config->get_string('main_page'); - - if(!is_null(BASE_URL)) { - $base = BASE_URL; - } - elseif(NICE_URLS || $config->get_bool('nice_urls', false)) { - $base = str_replace('/'.basename($_SERVER["SCRIPT_FILENAME"]), "", $_SERVER["PHP_SELF"]); - } - else { - $base = "./".basename($_SERVER["SCRIPT_FILENAME"])."?q="; - } - - if(is_null($query)) { - return str_replace("//", "/", $base.'/'.$page ); - } - else { - if(strpos($base, "?")) { - return $base .'/'. $page .'&'. $query; - } - else if(strpos($query, "#") === 0) { - return $base .'/'. $page . $query; - } - else { - return $base .'/'. $page .'?'. $query; - } - } -} - - -/** - * Take the current URL and modify some parameters - * - * @param $changes - * @return string - */ -function modify_current_url($changes) { - return modify_url($_SERVER['QUERY_STRING'], $changes); -} - -function modify_url($url, $changes) { - // SHIT: PHP is officially the worst web API ever because it does not - // have a built-in function to do this. - - // SHIT: parse_str is magically retarded; not only is it a useless name, it also - // didn't return the parsed array, preferring to overwrite global variables with - // whatever data the user supplied. Thankfully, 4.0.3 added an extra option to - // give it an array to use... - $params = array(); - parse_str($url, $params); - - if(isset($changes['q'])) { - $base = $changes['q']; - unset($changes['q']); - } - else { - $base = _get_query(); - } - - if(isset($params['q'])) { - unset($params['q']); - } - - foreach($changes as $k => $v) { - if(is_null($v) and isset($params[$k])) unset($params[$k]); - $params[$k] = $v; - } - - return make_link($base, http_build_query($params)); -} - - -/** - * Turn a relative link into an absolute one, including hostname - * - * @param string $link - * @return string - */ -function make_http(/*string*/ $link) { - 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; -} - -/** - * Make a form tag with relevant auth token and stuff - * - * @param string $target - * @param string $method - * @param bool $multipart - * @param string $form_id - * @param string $onsubmit - * - * @return string - */ -function make_form($target, $method="POST", $multipart=False, $form_id="", $onsubmit="") { - global $user; - if($method == "GET") { - $link = html_escape($target); - $target = make_link($target); - $extra_inputs = ""; - } - 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 '
'.$extra_inputs; -} - -/** - * @param string $file The filename - * @return string - */ -function mtimefile($file) { - $data_href = get_base_href(); - $mtime = filemtime($file); - return "$data_href/$file?$mtime"; -} - -/** - * Return the current theme as a string - * - * @return string - */ -function get_theme() { - global $config; - $theme = $config->get_string("theme", "default"); - if(!file_exists("themes/$theme")) $theme = "default"; - return $theme; -} - -/** - * Like glob, with support for matching very long patterns with braces. - * - * @param string $pattern - * @return array - */ -function zglob($pattern) { - $results = array(); - 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 array(); - } -} - -/** - * Gets contact link as mailto: or http: - * @return string - */ -function contact_link() { - global $config; - $text = $config->get_string('contact_link'); - 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; -} - - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ -* CAPTCHA abstraction * -\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -/** - * @return string - */ -function captcha_get_html() { - 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 = " -
- "; - } else { - session_start(); - $captcha = Securimage::getCaptchaHtml(['securimage_path' => './vendor/dapphp/securimage/']); - } - } - return $captcha; -} - -/** - * @return bool - */ -function captcha_check() { - 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\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; -} - - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ -* Misc * -\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -/** - * Check if HTTPS is enabled for the server. - * - * @return bool True if HTTPS is enabled - */ -function is_https_enabled() { - return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); -} - -/** - * Get MIME type for file - * - * The contents of this function are taken from the __getMimeType() function - * from the "Amazon S3 PHP class" which is Copyright (c) 2008, Donovan Schönknecht - * and released under the 'Simplified BSD License'. - * - * @param string $file File path - * @param string $ext - * @param bool $list - * @return string - */ -function getMimeType($file, $ext="", $list=false) { - - // Static extension lookup - $ext = strtolower($ext); - static $exts = array( - 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', - 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon', - 'swf' => 'application/x-shockwave-flash', 'video/x-flv' => 'flv', - 'svg' => 'image/svg+xml', 'pdf' => 'application/pdf', - 'zip' => 'application/zip', 'gz' => 'application/x-gzip', - 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip', - 'bz2' => 'application/x-bzip2', 'txt' => 'text/plain', - 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', - 'css' => 'text/css', 'js' => 'text/javascript', - 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml', - 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav', - 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', - 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php', - 'mp4' => 'video/mp4', 'ogv' => 'video/ogg', 'webm' => 'video/webm' - ); - - if ($list === true){ return $exts; } - - if (isset($exts[$ext])) { return $exts[$ext]; } - - $type = false; - // Fileinfo documentation says fileinfo_open() will use the - // MAGIC env var for the magic file - if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && - ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) - { - if (($type = finfo_file($finfo, $file)) !== false) - { - // Remove the charset and grab the last content-type - $type = explode(' ', str_replace('; charset=', ';charset=', $type)); - $type = array_pop($type); - $type = explode(';', $type); - $type = trim(array_shift($type)); - } - finfo_close($finfo); - - // If anyone is still using mime_content_type() - } elseif (function_exists('mime_content_type')) - $type = trim(mime_content_type($file)); - - if ($type !== false && strlen($type) > 0) return $type; - - return 'application/octet-stream'; -} - -/** - * @param string $mime_type - * @return bool|string - */ -function getExtension ($mime_type){ - if(empty($mime_type)){ - return false; - } - - $extensions = getMimeType(null, null, true); - $ext = array_search($mime_type, $extensions); - return ($ext ? $ext : false); -} - -/** - * Compare two Block objects, used to sort them before being displayed - * - * @param Block $a - * @param Block $b - * @return int - */ -function blockcmp(Block $a, Block $b) { - if($a->position == $b->position) { - return 0; - } - else { - return ($a->position > $b->position); - } -} - -/** - * Figure out PHP's internal memory limit - * - * @return int - */ -function get_memory_limit() { - global $config; - - // thumbnail generation requires lots of memory - $default_limit = 8*1024*1024; // 8 MB of memory is PHP's default. - $shimmie_limit = parse_shorthand_int($config->get_int("thumb_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. - - http://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 - } -} - -/** - * 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 - * - * @param Config $config - * @return string - */ -function get_session_ip(Config $config) { - $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; -} - - -/** - * Set (or extend) a flash-message cookie. - * - * This can optionally be done at the same time as saving a log message with log_*() - * - * Generally one should flash a message in onPageRequest and log a message wherever - * the action actually takes place (eg onWhateverElse) - but much of the time, actions - * are taken from within onPageRequest... - * - * @param string $text - * @param string $type - */ -function flash_message(/*string*/ $text, /*string*/ $type="info") { - global $page; - $current = $page->get_cookie("flash_message"); - if($current) { - $text = $current . "\n" . $text; - } - # the message should be viewed pretty much immediately, - # so 60s timeout should be more than enough - $page->add_cookie("flash_message", $text, time()+60, "/"); -} - -/** - * Figure out the path to the shimmie install directory. - * - * eg if shimmie is visible at http://foo.com/gallery, this - * function should return /gallery - * - * PHP really, really sucks. - * - * @return string - */ -function get_base_href() { - if(defined("BASE_HREF")) return BASE_HREF; - $possible_vars = array('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; -} - -/** - * A shorthand way to send a TextFormattingEvent and get the results. - * - * @param string $string - * @return string - */ -function format_text(/*string*/ $string) { - $tfe = new TextFormattingEvent($string); - send_event($tfe); - return $tfe->formatted; -} - -/** - * @param string $base - * @param string $hash - * @param bool $create - * @return string - */ -function warehouse_path(/*string*/ $base, /*string*/ $hash, /*bool*/ $create=true) { - $ab = substr($hash, 0, 2); - $cd = substr($hash, 2, 2); - if(WH_SPLITS == 2) { - $pa = $base.'/'.$ab.'/'.$cd.'/'.$hash; - } - else { - $pa = $base.'/'.$ab.'/'.$hash; - } - if($create && !file_exists(dirname($pa))) mkdir(dirname($pa), 0755, true); - return $pa; -} - -/** - * @param string $filename - * @return string - */ -function data_path($filename) { - $filename = "data/" . $filename; - if(!file_exists(dirname($filename))) mkdir(dirname($filename), 0755, true); - return $filename; -} - -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); - } -} - -/** - * @param string $url - * @param string $mfile - * @return array|bool - */ -function transload($url, $mfile) { - 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); - - $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); - } - - if($config->get_string("transload_engine") === "fopen") { - $fp_in = @fopen($url, "r"); - $fp_out = fopen($mfile, "w"); - if(!$fp_in || !$fp_out) { - return false; - } - $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 false; -} - -if (!function_exists('http_parse_headers')) { #http://www.php.net/manual/en/function.http-parse-headers.php#112917 - - /** - * @param string $raw_headers - * @return string[] - */ - function http_parse_headers ($raw_headers){ - $headers = array(); // $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]); - }else if(is_array($headers[$h[0]])){ - $tmp = array_merge($headers[$h[0]],array(trim($h[1]))); - $headers[$h[0]] = $tmp; - }else{ - $tmp = array_merge(array($headers[$h[0]]),array(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. - * - * @param array $headers - * @param string $name - * @return string|bool - */ -function findHeader ($headers, $name) { - if (!is_array($headers)) { - return false; - } - - $header = false; - - 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; -} - -/** - * Get the active contents of a .php file - * - * @param string $fname - * @return string|null - */ -function manual_include($fname) { - static $included = array(); - - if(!file_exists($fname)) return null; - - if(in_array($fname, $included)) return null; - - $included[] = $fname; - - print "$fname\n"; - - $text = file_get_contents($fname); - - // we want one continuous file - $text = str_replace('<'.'?php', '', $text); - $text = str_replace('?'.'>', '', $text); - - // most requires are built-in, but we want /lib separately - $text = str_replace('require_', '// require_', $text); - $text = str_replace('// require_once "lib', 'require_once "lib', $text); - - // @include_once is used for user-creatable config files - $text = preg_replace('/@include_once "(.*)";/e', "manual_include('$1')", $text); - - return $text; -} - - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ -* 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); - -/** - * 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 - * - * $flash = null (default) - log to server only, no flash message - * $flash = true - show the message to the user as well - * $flash = "some string" - log the message, flash the string - * - * @param string $section - * @param int $priority - * @param string $message - * @param bool|string $flash - * @param array $args - */ -function log_msg(/*string*/ $section, /*int*/ $priority, /*string*/ $message, $flash=false, $args=array()) { - send_event(new LogEvent($section, $priority, $message, $args)); - $threshold = defined("CLI_LOG_LEVEL") ? CLI_LOG_LEVEL : 0; - - if((PHP_SAPI === 'cli') && ($priority >= $threshold)) { - print date("c")." $section: $message\n"; - } - if($flash === true) { - flash_message($message); - } - else if(is_string($flash)) { - flash_message($flash); - } -} - -// More shorthand ways of logging -/** - * @param string $section - * @param string $message - * @param bool|string $flash - * @param array $args - */ -function log_debug( /*string*/ $section, /*string*/ $message, $flash=false, $args=array()) {log_msg($section, SCORE_LOG_DEBUG, $message, $flash, $args);} -/** - * @param string $section - * @param string $message - * @param bool|string $flash - * @param array $args - */ -function log_info( /*string*/ $section, /*string*/ $message, $flash=false, $args=array()) {log_msg($section, SCORE_LOG_INFO, $message, $flash, $args);} -/** - * @param string $section - * @param string $message - * @param bool|string $flash - * @param array $args - */ -function log_warning( /*string*/ $section, /*string*/ $message, $flash=false, $args=array()) {log_msg($section, SCORE_LOG_WARNING, $message, $flash, $args);} -/** - * @param string $section - * @param string $message - * @param bool|string $flash - * @param array $args - */ -function log_error( /*string*/ $section, /*string*/ $message, $flash=false, $args=array()) {log_msg($section, SCORE_LOG_ERROR, $message, $flash, $args);} -/** - * @param string $section - * @param string $message - * @param bool|string $flash - * @param array $args - */ -function log_critical(/*string*/ $section, /*string*/ $message, $flash=false, $args=array()) {log_msg($section, SCORE_LOG_CRITICAL, $message, $flash, $args);} - - -/** - * Get a unique ID for this request, useful for grouping log messages. - * - * @return null|string - */ -function get_request_id() { - 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; -} - - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ -* Things which should be in the core API * -\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -/** - * Remove an item from an array - * - * @param array $array - * @param mixed $to_remove - * @return array - */ -function array_remove($array, $to_remove) { - $array = array_unique($array); - $a2 = array(); - foreach($array as $existing) { - if($existing != $to_remove) { - $a2[] = $existing; - } - } - return $a2; -} - -/** - * Adds an item to an array. - * - * Also removes duplicate values from the array. - * - * @param array $array - * @param mixed $element - * @return array - */ -function array_add($array, $element) { - // Could we just use array_push() ? - // http://www.php.net/manual/en/function.array-push.php - $array[] = $element; - $array = array_unique($array); - return $array; -} - -/** - * Return the unique elements of an array, case insensitively - * - * @param array $array - * @return array - */ -function array_iunique($array) { - $ok = array(); - 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 http://uk.php.net/network - * - * @param string $IP - * @param string $CIDR - * @return bool - */ -function ip_in_range($IP, $CIDR) { - 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 - * - * @param string $f - */ -function deltree($f) { - //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); - } - else if(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 http://uk.php.net/copy - * - * @param string $source - * @param string $target - */ -function full_copy($source, $target) { - 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 - * - * @param string $base - * @param string $_sub_dir - * @return array file list - */ -function list_files(/*string*/ $base, $_sub_dir="") { - assert(is_dir($base)); - - $file_list = array(); - - $files = array(); - $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)) { - // ignore - } - else if(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; -} - -/** - * @param string $path - * @return string - */ -function path_to_tags($path) { - $matches = array(); - if(preg_match("/\d+ - (.*)\.([a-zA-Z]+)/", basename($path), $matches)) { - $tags = $matches[1]; - } - else { - $tags = dirname($path); - $tags = str_replace("/", " ", $tags); - $tags = str_replace("__", " ", $tags); - $tags = trim($tags); - } - return $tags; -} - - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ -* Event API * -\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -/** @private */ -global $_shm_event_listeners; -$_shm_event_listeners = array(); - -function _load_event_listeners() { - global $_shm_event_listeners; - - ctx_log_start("Loading extensions"); - - $cache_path = data_path("cache/shm_event_listeners.php"); - if(COMPILE_ELS && file_exists($cache_path)) { - require_once($cache_path); - } - else { - _set_event_listeners(); - - if(COMPILE_ELS) { - _dump_event_listeners($_shm_event_listeners, $cache_path); - } - } - - ctx_log_endok(); -} - -function _set_event_listeners() { - global $_shm_event_listeners; - $_shm_event_listeners = array(); - - foreach(get_declared_classes() as $class) { - $rclass = new ReflectionClass($class); - if($rclass->isAbstract()) { - // don't do anything - } - elseif(is_subclass_of($class, "Extension")) { - /** @var Extension $extension */ - $extension = new $class(); - - // skip extensions which don't support our current database - if(!$extension->is_live()) 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; - } - } - } - } -} - -/** - * @param array $event_listeners - * @param string $path - */ -function _dump_event_listeners($event_listeners, $path) { - $p = "<"."?php\n"; - - foreach(get_declared_classes() as $class) { - $rclass = new ReflectionClass($class); - if($rclass->isAbstract()) {} - elseif(is_subclass_of($class, "Extension")) { - $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"; - - $p .= "?".">"; - file_put_contents($path, $p); -} - -/** - * @param string $ext_name Main class name (eg ImageIO as opposed to ImageIOTheme or ImageIOTest) - * @return bool - */ -function ext_is_live($ext_name) { - if (class_exists($ext_name)) { - /** @var Extension $ext */ - $ext = new $ext_name(); - return $ext->is_live(); - } - return false; -} - - -/** @private */ -global $_shm_event_count; -$_shm_event_count = 0; - -/** - * Send an event to all registered Extensions. - * - * @param Event $event - */ -function send_event(Event $event) { - global $_shm_event_listeners, $_shm_event_count; - if(!isset($_shm_event_listeners[get_class($event)])) return; - $method_name = "on".str_replace("Event", "", get_class($event)); - - // send_event() is performance sensitive, and with the number - // of times context gets called the time starts to add up - $ctx = constant('CONTEXT'); - - if($ctx) ctx_log_start(get_class($event)); - // SHIT: http://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($ctx) ctx_log_start(get_class($listener)); - if(method_exists($listener, $method_name)) { - $listener->$method_name($event); - } - if($ctx) ctx_log_endok(); - } - $_shm_event_count++; - if($ctx) ctx_log_endok(); -} - - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ -* 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. - * - * @return string debug info to add to the page. - */ -function get_debug_info() { - global $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 = $database->cache->get_hits(); - $miss = $database->cache->get_misses(); - - $debug = "
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; // .", SCore Version ". SCORE_VERSION; - - return $debug; -} - -function score_assert_handler($file, $line, $code, $desc = null) { - $file = basename($file); - print("Assertion failed at $file:$line: $code ($desc)"); - /* - print("
");
-	debug_print_backtrace();
-	print("
"); - */ -} - - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ -* Request initialisation stuff * -\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -/** @privatesection */ - -function _version_check() { - if(MIN_PHP_VERSION) - { - if(version_compare(phpversion(), MIN_PHP_VERSION, ">=") === FALSE) { - print " -Shimmie (SCore Engine) does not support versions of PHP lower than ".MIN_PHP_VERSION." -(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; - } - } -} - -function _sanitise_environment() { - if(TIMEZONE) { - date_default_timezone_set(TIMEZONE); - } - - if(DEBUG) { - error_reporting(E_ALL); - assert_options(ASSERT_ACTIVE, 1); - assert_options(ASSERT_BAIL, 1); - assert_options(ASSERT_WARNING, 0); - assert_options(ASSERT_QUIET_EVAL, 1); - assert_options(ASSERT_CALLBACK, 'score_assert_handler'); - } - - if(CONTEXT) { - ctx_set_log(CONTEXT); - } - - if(COVERAGE) { - _start_coverage(); - register_shutdown_function("_end_coverage"); - } - - ob_start(); - - if(PHP_SAPI === 'cli') { - 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'] = ""; - } -} - - -/** - * @param string $_theme - * @return array - */ -function _get_themelet_files($_theme) { - $base_themelets = array(); - if(file_exists('themes/'.$_theme.'/custompage.class.php')) $base_themelets[] = 'themes/'.$_theme.'/custompage.class.php'; - $base_themelets[] = 'themes/'.$_theme.'/layout.class.php'; - $base_themelets[] = 'themes/'.$_theme.'/themelet.class.php'; - - $ext_themelets = zglob("ext/{".ENABLED_EXTS."}/theme.php"); - $custom_themelets = zglob('themes/'.$_theme.'/{'.ENABLED_EXTS.'}.theme.php'); - - return array_merge($base_themelets, $ext_themelets, $custom_themelets); -} - - -/** - * Used to display fatal errors to the web user. - * @param Exception $e - */ -function _fatal_error(Exception $e) { - $version = VERSION; - $message = $e->getMessage(); - - //$trace = var_dump($e->getTrace()); - - //$hash = exec("git rev-parse HEAD"); - //$h_hash = $hash ? "

Hash: $hash" : ""; - //'.$h_hash.' - - header("HTTP/1.0 500 Internal Error"); - echo ' - - - Internal error - SCore-'.$version.' - - -

Internal Error

-

Message: '.$message.' -

Version: '.$version.' (on '.phpversion().') - - -'; -} - -/** - * Turn ^^ into ^ and ^s into / - * - * Necessary because various servers and various clients - * think that / is special... - * - * @param string $str - * @return string - */ -function _decaret($str) { - $out = ""; - $length = strlen($str); - for($i=0; $i<$length; $i++) { - if($str[$i] == "^") { - $i++; - if($str[$i] == "^") $out .= "^"; - if($str[$i] == "s") $out .= "/"; - if($str[$i] == "b") $out .= "\\"; - } - else { - $out .= $str[$i]; - } - } - return $out; -} - -/** - * @return User - */ -function _get_user() { - global $config, $page; - $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)) { - $user = $tmp_user; - } - } - if(is_null($user)) { - $user = User::by_id($config->get_int("anon_id", 0)); - } - assert(!is_null($user)); - - return $user; -} - -/** - * @return string - */ -function _get_query() { - return @$_POST["q"]?:@$_GET["q"]; -} - - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ -* Code coverage * -\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -function _start_coverage() { - if(function_exists("xdebug_start_code_coverage")) { - #xdebug_start_code_coverage(XDEBUG_CC_UNUSED|XDEBUG_CC_DEAD_CODE); - xdebug_start_code_coverage(XDEBUG_CC_UNUSED); - } -} - -function _end_coverage() { - if(function_exists("xdebug_get_code_coverage")) { - // Absolute path is necessary because working directory - // inside register_shutdown_function is unpredictable. - $absolute_path = dirname(dirname(__FILE__)) . "/data/coverage"; - if(!file_exists($absolute_path)) mkdir($absolute_path); - $n = 0; - $t = time(); - while(file_exists("$absolute_path/$t.$n.log")) $n++; - file_put_contents("$absolute_path/$t.$n.log", gzdeflate(serialize(xdebug_get_code_coverage()))); - } -} - diff --git a/core/util.php b/core/util.php new file mode 100644 index 00000000..c2913047 --- /dev/null +++ b/core/util.php @@ -0,0 +1,745 @@ +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. + + http://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 + ), + ['..', '.'] + ); +} + +/** + * 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 = "
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 _sanitise_environment(): void +{ + global $tracer_enabled; + + $min_php = "7.3"; + if (version_compare(phpversion(), $min_php, ">=") === false) { + print " +Shimmie does not support versions of PHP lower than $min_php +(PHP reports that it is version ".phpversion()."). +If your web host is running an older version, they are dangerously out of +date and you should plan on moving elsewhere. +"; + exit; + } + + if (file_exists("images") && !file_exists("data/images")) { + die("As of Shimmie 2.7 images and thumbs should be moved to data/images and data/thumbs"); + } + + if (TIMEZONE) { + date_default_timezone_set(TIMEZONE); + } + + # ini_set('zend.assertions', '1'); // generate assertions + ini_set('assert.exception', '1'); // throw exceptions when failed + if (DEBUG) { + error_reporting(E_ALL); + } + + // 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; + + 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'] = ""; + } +} + + +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 ? "

Hash: $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 ? "" : "

Query: " . html_escape($query); + header("HTTP/1.0 500 Internal Error"); + echo ' + + + + Internal error - SCore-'.$version.' + + +

Internal Error

+

Message: '.html_escape($message).' + '.$q.' +

Version: '.$version.' (on '.$phpver.') + + +'; + } +} + +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) ? ", Ban" : ""; + $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 = ""; + } 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 ''.$extra_inputs; +} + +function SHM_FORM(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit="") +{ + global $user; + + $attrs = [ + "action"=>$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]; +} diff --git a/ext/admin/info.php b/ext/admin/info.php new file mode 100644 index 00000000..4a396aa9 --- /dev/null +++ b/ext/admin/info.php @@ -0,0 +1,23 @@ +Lowercase all tags: +
Set all tags to lowercase for consistency +

Recount tag use: +
If the counts of images per tag get messed up somehow, this will reset them, and remove any unused tags +

Database dump: +
Download the contents of the database in plain text format, useful for backups. +

Image dump: +
Download all the images as a .zip file (Requires ZipArchive)"; +} diff --git a/ext/admin/main.php b/ext/admin/main.php index 446a984c..6e41e952 100644 --- a/ext/admin/main.php +++ b/ext/admin/main.php @@ -1,271 +1,194 @@ - - * 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 - *

Lowercase all tags: - *
Set all tags to lowercase for consistency - *

Recount tag use: - *
If the counts of images per tag get messed up somehow, this will - * reset them, and remove any unused tags - *

Database dump: - *
Download the contents of the database in plain text format, useful - * for backups. - *

Image dump: - *
Download all the images as a .zip file (Requires ZipArchive) - */ +page = $page; - } + public function __construct(Page $page) + { + parent::__construct(); + $this->page = $page; + } } -class AdminActionEvent extends Event { - /** @var string */ - public $action; - /** @var bool */ - public $redirect = true; +class AdminActionEvent extends Event +{ + /** @var string */ + public $action; + /** @var bool */ + public $redirect = true; - /** - * @param string $action - */ - public function __construct(/*string*/ $action) { - $this->action = $action; - } + public function __construct(string $action) + { + parent::__construct(); + $this->action = $action; + } } -class AdminPage extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; +class AdminPage extends Extension +{ + /** @var AdminPageTheme */ + protected $theme; - if($event->page_matches("admin")) { - if(!$user->can("manage_admintools")) { - $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); + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; - if($user->check_auth_token()) { - log_info("admin", "Util: $action"); - set_time_limit(0); - send_event($aae); - } + if ($event->page_matches("admin")) { + if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) { + $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($aae->redirect) { - $page->set_mode("redirect"); - $page->set_redirect(make_link("admin")); - } - } - } - } - } + if ($user->check_auth_token()) { + log_info("admin", "Util: $action"); + set_time_limit(0); + send_event($aae); + } - public function onCommand(CommandEvent $event) { - if($event->cmd == "help") { - print " get-page [query string]\n"; - print " eg 'get-page post/list'\n\n"; - } - if($event->cmd == "get-page") { - global $page; - send_event(new PageRequestEvent($event->args[0])); - $page->display(); - } - } + if ($aae->redirect) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("admin")); + } + } + } + } + } - public function onAdminBuilding(AdminBuildingEvent $event) { - $this->theme->display_page(); - $this->theme->display_form(); - } + public function onCommand(CommandEvent $event) + { + if ($event->cmd == "help") { + print "\tget-page \n"; + print "\t\teg 'get-page post/list'\n\n"; + print "\tpost-page \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 \n"; + print "\t\tregenerate a thumbnail\n\n"; + print "\tcache [get|set|del] [key] \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 onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("manage_admintools")) { - $event->add_link("Board Admin", make_link("admin")); - } - } + public function onAdminBuilding(AdminBuildingEvent $event) + { + $this->theme->display_page(); + $this->theme->display_form(); + } - public function onAdminAction(AdminActionEvent $event) { - $action = $event->action; - if(method_exists($this, $action)) { - $event->redirect = $this->$action(); - } - } + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::MANAGE_ADMINTOOLS)) { + $event->add_nav_link("admin", new Link('admin'), "Board Admin"); + } + } + } - public function onPostListBuilding(PostListBuildingEvent $event) { - global $user; - if($user->can("manage_admintools") && !empty($event->search_terms)) { - $event->add_control($this->theme->dbq_html(implode(" ", $event->search_terms))); - } - } + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::MANAGE_ADMINTOOLS)) { + $event->add_link("Board Admin", make_link("admin")); + } + } - private function delete_by_query() { - global $page; - $query = $_POST['query']; - $reason = @$_POST['reason']; - assert(strlen($query) > 1); + public function onAdminAction(AdminActionEvent $event) + { + $action = $event->action; + if (method_exists($this, $action)) { + $event->redirect = $this->$action(); + } + } - log_warning("admin", "Mass deleting: $query"); - $count = 0; - foreach(Image::find_images(0, 1000000, Tag::explode($query)) as $image) { - if($reason && class_exists("ImageBan")) { - send_event(new AddImageHashBanEvent($image->hash, $reason)); - } - send_event(new ImageDeletionEvent($image)); - $count++; - } - log_debug("admin", "Deleted $count images", true); + private function set_tag_case() + { + global $database; + $database->execute( + "UPDATE tags SET tag=:tag1 WHERE LOWER(tag) = LOWER(:tag2)", + ["tag1" => $_POST['tag'], "tag2" => $_POST['tag']] + ); + log_info("admin", "Fixed the case of {$_POST['tag']}", "Fixed case"); + return true; + } - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/list")); - return false; - } + private function lowercase_all_tags() + { + global $database; + $database->execute("UPDATE tags SET tag=lower(tag)"); + log_warning("admin", "Set all tags to lowercase", "Set all tags to lowercase"); + return true; + } - private function set_tag_case() { - global $database; - $database->execute($database->scoreql_to_sql( - "UPDATE tags SET tag=:tag1 WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag2)" - ), array("tag1" => $_POST['tag'], "tag2" => $_POST['tag'])); - log_info("admin", "Fixed the case of ".html_escape($_POST['tag']), true); - return true; - } - - private function lowercase_all_tags() { - global $database; - $database->execute("UPDATE tags SET tag=lower(tag)"); - log_warning("admin", "Set all tags to lowercase", true); - return true; - } - - private function recount_tag_use() { - global $database; - $database->Execute(" + private function recount_tag_use() + { + global $database; + $database->Execute(" UPDATE tags SET count = COALESCE( (SELECT COUNT(image_id) FROM image_tags WHERE tag_id=tags.id GROUP BY tag_id), 0 ) "); - $database->Execute("DELETE FROM tags WHERE count=0"); - log_warning("admin", "Re-counted tags", true); - return true; - } - - - private function database_dump() { - global $page; - - $matches = array(); - preg_match("#^(?P\w+)\:(?:user=(?P\w+)(?:;|$)|password=(?P\w*)(?:;|$)|host=(?P[\w\.\-]+)(?:;|$)|dbname=(?P[\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; - default: - $cmd = false; - } - - //FIXME: .SQL dump is empty if cmd doesn't exist - - if($cmd) { - $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; - - $images = $database->get_all("SELECT hash, ext FROM images"); - $filename = data_path('imgdump-'.date('Ymd').'.zip'); - - $zip = new ZipArchive; - if($zip->open($filename, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE) === TRUE){ - foreach($images as $img){ - $img_loc = warehouse_path("images", $img["hash"], FALSE); - $zip->addFile($img_loc, $img["hash"].".".$img["ext"]); - } - $zip->close(); - } - - $page->set_mode("redirect"); - $page->set_redirect(make_link($filename)); //TODO: Delete file after downloaded? - - return false; // we do want a redirect, but a manual one - } - - private function reset_image_ids() { - global $database; - - //TODO: Make work with PostgreSQL + SQLite - //TODO: Update score_log (Having an optional ID column for score_log would be nice..) - preg_match("#^(?P\w+)\:(?:user=(?P\w+)(?:;|$)|password=(?P\w*)(?:;|$)|host=(?P[\w\.\-]+)(?:;|$)|dbname=(?P[\w_]+)(?:;|$))+#", DATABASE_DSN, $matches); - - if($matches['proto'] == "mysql"){ - $tables = $database->get_col("SELECT TABLE_NAME - FROM information_schema.KEY_COLUMN_USAGE - WHERE TABLE_SCHEMA = :db - AND REFERENCED_COLUMN_NAME = 'id' - AND REFERENCED_TABLE_NAME = 'images'", array("db" => $matches['dbname'])); - - $i = 1; - $ids = $database->get_col("SELECT id FROM images ORDER BY images.id ASC"); - foreach($ids as $id){ - $sql = "SET FOREIGN_KEY_CHECKS=0; - UPDATE images SET id={$i} WHERE image_id={$id};"; - - foreach($tables as $table){ - $sql .= "UPDATE {$table} SET image_id={$i} WHERE image_id={$id};"; - } - - $sql .= " SET FOREIGN_KEY_CHECKS=1;"; - $database->execute($sql); - - $i++; - } - $database->execute("ALTER TABLE images AUTO_INCREMENT=".(count($ids) + 1)); - }elseif($matches['proto'] == "pgsql"){ - //TODO: Make this work with PostgreSQL - }elseif($matches['proto'] == "sqlite"){ - //TODO: Make this work with SQLite - } + $database->Execute("DELETE FROM tags WHERE count=0"); + log_warning("admin", "Re-counted tags", "Re-counted tags"); return true; } } - diff --git a/ext/admin/test.php b/ext/admin/test.php index 3f893899..0e4852bf 100644 --- a/ext/admin/test.php +++ b/ext/admin/test.php @@ -1,84 +1,89 @@ -get_page('admin'); - $this->assert_response(403); - $this->assert_title("Permission Denied"); +get_page('admin'); + $this->assertEquals(403, $page->code); + $this->assertEquals("Permission Denied", $page->title); - $this->log_in_as_user(); - $this->get_page('admin'); - $this->assert_response(403); - $this->assert_title("Permission Denied"); + send_event(new UserLoginEvent(User::by_name(self::$user_name))); + $page = $this->get_page('admin'); + $this->assertEquals(403, $page->code); + $this->assertEquals("Permission Denied", $page->title); - $this->log_in_as_admin(); - $this->get_page('admin'); - $this->assert_response(200); - $this->assert_title("Admin Tools"); - } + send_event(new UserLoginEvent(User::by_name(self::$admin_name))); + $page = $this->get_page('admin'); + $this->assertEquals(200, $page->code); + $this->assertEquals("Admin Tools", $page->title); + } - public function testLowercase() { - $ts = time(); // we need a tag that hasn't been used before + public function testLowercaseAndSetCase() + { + // Create a problem + $ts = time(); // we need a tag that hasn't been used before + send_event(new UserLoginEvent(User::by_name(self::$admin_name))); + $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "TeStCase$ts"); - $this->log_in_as_admin(); - $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "TeStCase$ts"); + // Validate problem + $page = $this->get_page("post/view/$image_id_1"); + $this->assertEquals("Image $image_id_1: TeStCase$ts", $page->title); - $this->get_page("post/view/$image_id_1"); - $this->assert_title("Image $image_id_1: TeStCase$ts"); + // Fix + send_event(new AdminActionEvent('lowercase_all_tags')); - $this->get_page('admin'); - $this->assert_title("Admin Tools"); - //$this->click("All tags to lowercase"); - send_event(new AdminActionEvent('lowercase_all_tags')); + // Validate fix + $this->get_page("post/view/$image_id_1"); + $this->assert_title("Image $image_id_1: testcase$ts"); - $this->get_page("post/view/$image_id_1"); - $this->assert_title("Image $image_id_1: testcase$ts"); + // Change + $_POST["tag"] = "TestCase$ts"; + send_event(new AdminActionEvent('set_tag_case')); - $this->delete_image($image_id_1); - } + // Validate change + $this->get_page("post/view/$image_id_1"); + $this->assert_title("Image $image_id_1: TestCase$ts"); + } - # FIXME: make sure the admin tools actually work - public function testRecount() { - $this->log_in_as_admin(); - $this->get_page('admin'); - $this->assert_title("Admin Tools"); + # FIXME: make sure the admin tools actually work + public function testRecount() + { + global $database; - //$this->click("Recount tag use"); - send_event(new AdminActionEvent('recount_tag_use')); - } + // Create a problem + $ts = time(); // we need a tag that hasn't been used before + send_event(new UserLoginEvent(User::by_name(self::$admin_name))); + $database->execute( + "INSERT INTO tags(tag, count) VALUES(:tag, :count)", + ["tag"=>"tes$ts", "count"=>42] + ); - public function testDump() { - $this->log_in_as_admin(); - $this->get_page('admin'); - $this->assert_title("Admin Tools"); + // Fix + send_event(new AdminActionEvent('recount_tag_use')); - // this calls mysqldump which jams up travis prompting for a password - //$this->click("Download database contents"); - //send_event(new AdminActionEvent('database_dump')); - //$this->assert_response(200); - } + // Validate fix + $this->assertEquals( + 0, + $database->get_one( + "SELECT count FROM tags WHERE tag = :tag", + ["tag"=>"tes$ts"] + ) + ); + } - public function testDBQ() { - $this->log_in_as_user(); - $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test"); - $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "test2"); - $image_id_3 = $this->post_image("tests/favicon.png", "test"); + public function testCommands() + { + send_event(new UserLoginEvent(User::by_name(self::$admin_name))); + ob_start(); + send_event(new CommandEvent(["index.php", "help"])); + send_event(new CommandEvent(["index.php", "get-page", "post/list"])); + 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(); - $this->get_page("post/list/test/1"); - //$this->click("Delete All These Images"); - $_POST['query'] = 'test'; - //$_POST['reason'] = 'reason'; // non-null-reason = add a hash ban - send_event(new AdminActionEvent('delete_by_query')); - - $this->get_page("post/view/$image_id_1"); - $this->assert_response(404); - $this->get_page("post/view/$image_id_2"); - $this->assert_response(200); - $this->get_page("post/view/$image_id_3"); - $this->assert_response(404); - - $this->delete_image($image_id_1); - $this->delete_image($image_id_2); - $this->delete_image($image_id_3); - } + // don't crash + $this->assertTrue(true); + } } - diff --git a/ext/admin/theme.php b/ext/admin/theme.php index 64ee1a92..6979aa59 100644 --- a/ext/admin/theme.php +++ b/ext/admin/theme.php @@ -1,77 +1,54 @@ -set_title("Admin Tools"); - $page->set_heading("Admin Tools"); - $page->add_block(new NavBlock()); - } + $page->set_title("Admin Tools"); + $page->set_heading("Admin Tools"); + $page->add_block(new NavBlock()); + } - /** - * @param string $name - * @param string $action - * @param bool $protected - * @return string - */ - protected function button(/*string*/ $name, /*string*/ $action, /*boolean*/ $protected=false) { - $c_protected = $protected ? " protected" : ""; - $html = make_form(make_link("admin/$action"), "POST", false, "admin$c_protected"); - if($protected) { - $html .= ""; - $html .= ""; - } - else { - $html .= ""; - } - $html .= "\n"; - return $html; - } + protected function button(string $name, string $action, bool $protected=false): string + { + $c_protected = $protected ? " protected" : ""; + $html = make_form(make_link("admin/$action"), "POST", false, "admin$c_protected"); + if ($protected) { + $html .= ""; + $html .= ""; + } else { + $html .= ""; + } + $html .= "\n"; + return $html; + } - /* - * Show a form which links to admin_utils with POST[action] set to one of: - * 'lowercase all tags' - * 'recount tag use' - * etc - */ - public function display_form() { - global $page, $database; + /* + * Show a form which links to admin_utils with POST[action] set to one of: + * 'lowercase all tags' + * 'recount tag use' + * etc + */ + public function display_form() + { + global $page; - $html = ""; - $html .= $this->button("All tags to lowercase", "lowercase_all_tags", true); - $html .= $this->button("Recount tag use", "recount_tag_use", false); - if(class_exists('ZipArchive')) - $html .= $this->button("Download all images", "download_all_images", false); - $html .= $this->button("Download database contents", "database_dump", false); - if($database->get_driver_name() == "mysql") - $html .= $this->button("Reset image IDs", "reset_image_ids", true); - $page->add_block(new Block("Misc Admin Tools", $html)); + $html = ""; + $html .= $this->button("All tags to lowercase", "lowercase_all_tags", true); + $html .= $this->button("Recount tag use", "recount_tag_use", false); + $page->add_block(new Block("Misc Admin Tools", $html)); - $html = make_form(make_link("admin/set_tag_case"), "POST"); - $html .= ""; - $html .= ""; - $html .= "\n"; - $page->add_block(new Block("Set Tag Case", $html)); - } - - public function dbq_html($terms) { - $h_terms = html_escape($terms); - $h_reason = ""; - if(class_exists("ImageBan")) { - $h_reason = ""; - } - $html = make_form(make_link("admin/delete_by_query"), "POST") . " - - - $h_reason - - - "; - return $html; - } + $html = (string)SHM_SIMPLE_FORM( + "admin/set_tag_case", + INPUT(["type"=>'text', "name"=>'tag', "placeholder"=>'Enter tag with correct case', "class"=>'autocomplete_tags', "autocomplete"=>'off']), + SHM_SUBMIT('Set Tag Case'), + ); + $page->add_block(new Block("Set Tag Case", $html)); + } } - diff --git a/ext/alias_editor/info.php b/ext/alias_editor/info.php new file mode 100644 index 00000000..1df8f477 --- /dev/null +++ b/ext/alias_editor/info.php @@ -0,0 +1,15 @@ +/alias/list; only site admins can edit it, other people can view and download it'; + public $core = true; +} diff --git a/ext/alias_editor/main.php b/ext/alias_editor/main.php index d6923693..e8a3a678 100644 --- a/ext/alias_editor/main.php +++ b/ext/alias_editor/main.php @@ -1,178 +1,209 @@ - - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Edit the alias list - * Documentation: - * The list is visible at /alias/list; only - * site admins can edit it, other people can view and download it - */ +oldtag = trim($oldtag); - $this->newtag = trim($newtag); - } +class AliasTable extends Table +{ + public function __construct(\FFSPHP\PDO $db) + { + 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 AddAliasException extends SCoreException {} +class AddAliasEvent extends Event +{ + /** @var string */ + public $oldtag; + /** @var string */ + public $newtag; -class AliasEditor extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $config, $database, $page, $user; - - if($event->page_matches("alias")) { - if($event->get_arg(0) == "add") { - if($user->can("manage_alias_list")) { - if(isset($_POST['oldtag']) && isset($_POST['newtag'])) { - try { - $aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']); - send_event($aae); - $page->set_mode("redirect"); - $page->set_redirect(make_link("alias/list")); - } - catch(AddAliasException $ex) { - $this->theme->display_error(500, "Error adding alias", $ex->getMessage()); - } - } - } - } - else if($event->get_arg(0) == "remove") { - if($user->can("manage_alias_list")) { - if(isset($_POST['oldtag'])) { - $database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", array("oldtag" => $_POST['oldtag'])); - log_info("alias_editor", "Deleted alias for ".$_POST['oldtag'], true); - - $page->set_mode("redirect"); - $page->set_redirect(make_link("alias/list")); - } - } - } - else if($event->get_arg(0) == "list") { - $page_number = $event->get_arg(1); - if(is_null($page_number) || !is_numeric($page_number)) { - $page_number = 0; - } - else if ($page_number <= 0) { - $page_number = 0; - } - else { - $page_number--; - } - - $alias_per_page = $config->get_int('alias_items_per_page', 30); - - $query = "SELECT oldtag, newtag FROM aliases ORDER BY newtag ASC LIMIT :limit OFFSET :offset"; - $alias = $database->get_pairs($query, - array("limit"=>$alias_per_page, "offset"=>$page_number * $alias_per_page) - ); - - $total_pages = ceil($database->get_one("SELECT COUNT(*) FROM aliases") / $alias_per_page); - - $this->theme->display_aliases($alias, $page_number + 1, $total_pages); - } - else if($event->get_arg(0) == "export") { - $page->set_mode("data"); - $page->set_type("text/csv"); - $page->set_filename("aliases.csv"); - $page->set_data($this->get_alias_csv($database)); - } - else if($event->get_arg(0) == "import") { - if($user->can("manage_alias_list")) { - if(count($_FILES) > 0) { - $tmp = $_FILES['alias_file']['tmp_name']; - $contents = file_get_contents($tmp); - $this->add_alias_csv($database, $contents); - log_info("alias_editor", "Imported aliases from file", true); # FIXME: how many? - $page->set_mode("redirect"); - $page->set_redirect(make_link("alias/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 alias list"); - } - } - } - } - - public function onAddAlias(AddAliasEvent $event) { - global $database; - $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 if($database->get_row("SELECT * FROM aliases WHERE oldtag=:newtag", array("newtag" => $event->newtag))) { - throw new AddAliasException("{$event->newtag} is itself an alias"); - } - else { - $database->execute("INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)", $pair); - log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}", true); - } - } - - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("manage_alias_list")) { - $event->add_link("Alias Editor", make_link("alias/list")); - } - } - - /** - * @param Database $database - * @return string - */ - private function get_alias_csv(Database $database) { - $csv = ""; - $aliases = $database->get_pairs("SELECT oldtag, newtag FROM aliases ORDER BY newtag"); - foreach($aliases as $old => $new) { - $csv .= "\"$old\",\"$new\"\n"; - } - return $csv; - } - - /** - * @param Database $database - * @param string $csv - */ - private function add_alias_csv(Database $database, /*string*/ $csv) { - $csv = str_replace("\r", "\n", $csv); - foreach(explode("\n", $csv) as $line) { - $parts = str_getcsv($line); - if(count($parts) == 2) { - try { - $aae = new AddAliasEvent($parts[0], $parts[1]); - send_event($aae); - } - catch(AddAliasException $ex) { - $this->theme->display_error(500, "Error adding alias", $ex->getMessage()); - } - } - } - } - - /** - * 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. - * - * @return int - */ - public function get_priority() {return 60;} + public function __construct(string $oldtag, string $newtag) + { + parent::__construct(); + $this->oldtag = trim($oldtag); + $this->newtag = trim($newtag); + } } +class DeleteAliasEvent extends Event +{ + public $oldtag; + + public function __construct(string $oldtag) + { + parent::__construct(); + $this->oldtag = $oldtag; + } +} + +class AddAliasException extends SCoreException +{ +} + +class AliasEditor extends Extension +{ + /** @var AliasEditorTheme */ + protected $theme; + + public function onPageRequest(PageRequestEvent $event) + { + global $config, $database, $page, $user; + + if ($event->page_matches("alias")) { + if ($event->get_arg(0) == "add") { + if ($user->can(Permissions::MANAGE_ALIAS_LIST)) { + $user->ensure_authed(); + $input = validate_input(["c_oldtag"=>"string", "c_newtag"=>"string"]); + try { + send_event(new AddAliasEvent($input['c_oldtag'], $input['c_newtag'])); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("alias/list")); + } catch (AddAliasException $ex) { + $this->theme->display_error(500, "Error adding alias", $ex->getMessage()); + } + } + } elseif ($event->get_arg(0) == "remove") { + if ($user->can(Permissions::MANAGE_ALIAS_LIST)) { + $user->ensure_authed(); + $input = validate_input(["d_oldtag"=>"string"]); + send_event(new DeleteAliasEvent($input['d_oldtag'])); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("alias/list")); + } + } elseif ($event->get_arg(0) == "list") { + $t = new AliasTable($database->raw_db()); + $t->token = $user->get_auth_token(); + $t->inputs = $_GET; + $t->size = $config->get_int('alias_items_per_page', 30); + if ($user->can(Permissions::MANAGE_ALIAS_LIST)) { + $t->create_url = make_link("alias/add"); + $t->delete_url = make_link("alias/remove"); + } + $this->theme->display_aliases($t->table($t->query()), $t->paginator()); + } elseif ($event->get_arg(0) == "export") { + $page->set_mode(PageMode::DATA); + $page->set_type("text/csv"); + $page->set_filename("aliases.csv"); + $page->set_data($this->get_alias_csv($database)); + } elseif ($event->get_arg(0) == "import") { + if ($user->can(Permissions::MANAGE_ALIAS_LIST)) { + if (count($_FILES) > 0) { + $tmp = $_FILES['alias_file']['tmp_name']; + $contents = file_get_contents($tmp); + $this->add_alias_csv($database, $contents); + 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"); + } + } else { + $this->theme->display_error(401, "Admins Only", "Only admins can edit the alias list"); + } + } + } + } + + public function onAddAlias(AddAliasEvent $event) + { + global $database; + + $row = $database->get_row( + "SELECT * FROM aliases WHERE lower(oldtag)=lower(:oldtag)", + ["oldtag"=>$event->oldtag] + ); + if ($row) { + throw new AddAliasException("{$row['oldtag']} is already an alias for {$row['newtag']}"); + } + + $row = $database->get_row( + "SELECT * FROM aliases WHERE lower(oldtag)=lower(:newtag)", + ["newtag" => $event->newtag] + ); + if ($row) { + throw new AddAliasException("{$row['oldtag']} is itself an alias for {$row['newtag']}"); + } + + $database->execute( + "INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)", + ["oldtag" => $event->oldtag, "newtag" => $event->newtag] + ); + log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}", "Added alias"); + } + + public function onDeleteAlias(DeleteAliasEvent $event) + { + global $database; + $database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", ["oldtag" => $event->oldtag]); + log_info("alias_editor", "Deleted alias for {$event->oldtag}", "Deleted alias"); + } + + 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; + } +} diff --git a/ext/alias_editor/test.php b/ext/alias_editor/test.php index 0b8e4512..9829cec3 100644 --- a/ext/alias_editor/test.php +++ b/ext/alias_editor/test.php @@ -1,104 +1,85 @@ -get_page('alias/list'); - $this->assert_response(200); - $this->assert_title("Alias List"); - } +get_page('alias/list'); + $this->assert_response(200); + $this->assert_title("Alias List"); + } - public function testAliasListReadOnly() { - // Check that normal users can't add aliases. - $this->log_in_as_user(); - $this->get_page('alias/list'); - $this->assert_title("Alias List"); - $this->assert_no_text("Add"); - } + public function testAliasListReadOnly() + { + $this->log_in_as_user(); + $this->get_page('alias/list'); + $this->assert_title("Alias List"); + $this->assert_no_text("Add"); - public function testAliasEditor() { - /* - ********************************************************************** - * FIXME: TODO: - * For some reason the alias tests always fail when they are running - * inside the TravisCI VM environment. I have tried to determine - * the exact cause of this, but have been unable to pin it down. - * - * For now, I am commenting them out until I have more time to - * dig into this and determine exactly what is happening. - * - ********************************************************************* - */ - $this->markTestIncomplete(); + $this->log_out(); + $this->get_page('alias/list'); + $this->assert_title("Alias List"); + $this->assert_no_text("Add"); + } - $this->log_in_as_admin(); + public function testAliasOneToOne() + { + $this->log_in_as_admin(); - # test one to one - $this->get_page('alias/list'); - $this->assert_title("Alias List"); - $this->set_field('oldtag', "test1"); - $this->set_field('newtag', "test2"); - $this->clickSubmit('Add'); - $this->assert_no_text("Error adding alias"); + $this->get_page("alias/export/aliases.csv"); + $this->assert_no_text("test1"); - $this->get_page('alias/list'); - $this->assert_text("test1"); + send_event(new AddAliasEvent("test1", "test2")); + $this->get_page('alias/list'); + $this->assert_text("test1"); + $this->get_page("alias/export/aliases.csv"); + $this->assert_text('"test1","test2"'); - $this->get_page("alias/export/aliases.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: test2"); + $this->get_page("post/list/test1/1"); # searching for an alias should find the master tag + $this->assert_response(302); + $this->get_page("post/list/test2/1"); # check that searching for the main tag still works + $this->assert_response(302); + $this->delete_image($image_id); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1"); - $this->get_page("post/view/$image_id"); # check that the tag has been replaced - $this->assert_title("Image $image_id: test2"); - $this->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 DeleteAliasEvent("test1")); + $this->get_page('alias/list'); + $this->assert_title("Alias List"); + $this->assert_no_text("test1"); + } - $this->get_page('alias/list'); - $this->click("Remove"); - $this->get_page('alias/list'); - $this->assert_title("Alias List"); - $this->assert_no_text("test1"); + public function testAliasOneToMany() + { + $this->log_in_as_admin(); - # test one to many - $this->get_page('alias/list'); - $this->assert_title("Alias List"); - $this->set_field('oldtag', "onetag"); - $this->set_field('newtag', "multi tag"); - $this->click("Add"); - $this->get_page('alias/list'); - $this->assert_text("multi"); - $this->assert_text("tag"); + $this->get_page("alias/export/aliases.csv"); + $this->assert_no_text("multi"); - $this->get_page("alias/export/aliases.csv"); - $this->assert_text("onetag,multi tag"); + send_event(new AddAliasEvent("onetag", "multi tag")); + $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"); - // 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); + $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); - $this->get_page('alias/list'); - $this->click("Remove"); - $this->get_page('alias/list'); - $this->assert_title("Alias List"); - $this->assert_no_text("test1"); - - $this->log_out(); - - $this->get_page('alias/list'); - $this->assert_title("Alias List"); - $this->assert_no_text("Add"); - } + send_event(new DeleteAliasEvent("onetag")); + $this->get_page('alias/list'); + $this->assert_title("Alias List"); + $this->assert_no_text("test1"); + } } - diff --git a/ext/alias_editor/theme.php b/ext/alias_editor/theme.php index 02b7a3a1..61568d29 100644 --- a/ext/alias_editor/theme.php +++ b/ext/alias_editor/theme.php @@ -1,79 +1,36 @@ - $new_tag) - * @param int $pageNumber - * @param int $totalPages - */ - public function display_aliases($aliases, $pageNumber, $totalPages) { - global $page, $user; +class AliasEditorTheme extends Themelet +{ + /** + * Show a page of aliases. + * + * Note: $can_manage = whether things like "add new alias" should be shown + */ + public function display_aliases($table, $paginator): void + { + global $page, $user; - $can_manage = $user->can("manage_alias_list"); - if($can_manage) { - $h_action = "Action"; - $h_add = " - - ".make_form(make_link("alias/add"))." - - - - - - "; - } - else { - $h_action = ""; - $h_add = ""; - } - - $h_aliases = ""; - foreach($aliases as $old => $new) { - $h_old = html_escape($old); - $h_new = "".html_escape($new).""; - - $h_aliases .= "$h_old$h_new"; - if($can_manage) { - $h_aliases .= " - - ".make_form(make_link("alias/remove"))." - - - - - "; - } - $h_aliases .= ""; - } - $html = " - - $h_action - $h_aliases - $h_add -
FromTo
+ $can_manage = $user->can(Permissions::MANAGE_ALIAS_LIST); + $html = " + $table + $paginator

Download as CSV

"; - $bulk_html = " + $bulk_html = " ".make_form(make_link("alias/import"), 'post', true)." "; - $page->set_title("Alias List"); - $page->set_heading("Alias List"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Aliases", $html)); - if($can_manage) { - $page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51)); - } - - $this->display_paginator($page, "alias/list", null, $pageNumber, $totalPages); - } + $page->set_title("Alias List"); + $page->set_heading("Alias List"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Aliases", $html)); + if ($can_manage) { + $page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51)); + } + } } - diff --git a/ext/amazon_s3/lib/S3.php b/ext/amazon_s3/lib/S3.php deleted file mode 100644 index 660844c4..00000000 --- a/ext/amazon_s3/lib/S3.php +++ /dev/null @@ -1,2389 +0,0 @@ - $host, 'type' => $type, 'user' => $user, 'pass' => $pass); - } - - - /** - * Set the error mode to exceptions - * - * @param boolean $enabled Enable exceptions - * @return void - */ - public static function setExceptions($enabled = true) - { - self::$useExceptions = $enabled; - } - - - /** - * Set AWS time correction offset (use carefully) - * - * This can be used when an inaccurate system time is generating - * invalid request signatures. It should only be used as a last - * resort when the system time cannot be changed. - * - * @param string $offset Time offset (set to zero to use AWS server time) - * @return void - */ - public static function setTimeCorrectionOffset($offset = 0) - { - if ($offset == 0) - { - $rest = new S3Request('HEAD'); - $rest = $rest->getResponse(); - $awstime = $rest->headers['date']; - $systime = time(); - $offset = $systime > $awstime ? -($systime - $awstime) : ($awstime - $systime); - } - self::$__timeOffset = $offset; - } - - - /** - * Set signing key - * - * @param string $keyPairId AWS Key Pair ID - * @param string $signingKey Private Key - * @param boolean $isFile Load private key from file, set to false to load string - * @return boolean - */ - public static function setSigningKey($keyPairId, $signingKey, $isFile = true) - { - self::$__signingKeyPairId = $keyPairId; - if ((self::$__signingKeyResource = openssl_pkey_get_private($isFile ? - file_get_contents($signingKey) : $signingKey)) !== false) return true; - self::__triggerError('S3::setSigningKey(): Unable to open load private key: '.$signingKey, __FILE__, __LINE__); - return false; - } - - - /** - * Free signing key from memory, MUST be called if you are using setSigningKey() - * - * @return void - */ - public static function freeSigningKey() - { - if (self::$__signingKeyResource !== false) - openssl_free_key(self::$__signingKeyResource); - } - - - /** - * Internal error handler - * - * @internal Internal error handler - * @param string $message Error message - * @param string $file Filename - * @param integer $line Line number - * @param integer $code Error code - * @return void - */ - private static function __triggerError($message, $file, $line, $code = 0) - { - if (self::$useExceptions) - throw new S3Exception($message, $file, $line, $code); - else - trigger_error($message, E_USER_WARNING); - } - - - /** - * Get a list of buckets - * - * @param boolean $detailed Returns detailed bucket list when true - * @return array | false - */ - public static function listBuckets($detailed = false) - { - $rest = new S3Request('GET', '', '', self::$endpoint); - $rest = $rest->getResponse(); - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::listBuckets(): [%s] %s", $rest->error['code'], - $rest->error['message']), __FILE__, __LINE__); - return false; - } - $results = array(); - if (!isset($rest->body->Buckets)) return $results; - - if ($detailed) - { - if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) - $results['owner'] = array( - 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName - ); - $results['buckets'] = array(); - foreach ($rest->body->Buckets->Bucket as $b) - $results['buckets'][] = array( - 'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate) - ); - } else - foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name; - - return $results; - } - - - /** - * Get contents for a bucket - * - * If maxKeys is null this method will loop through truncated result sets - * - * @param string $bucket Bucket name - * @param string $prefix Prefix - * @param string $marker Marker (last file listed) - * @param string $maxKeys Max keys (maximum number of keys to return) - * @param string $delimiter Delimiter - * @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes - * @return array | false - */ - public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false) - { - $rest = new S3Request('GET', $bucket, '', self::$endpoint); - if ($maxKeys == 0) $maxKeys = null; - if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); - if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker); - if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys); - if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); - else if (!empty(self::$defDelimiter)) $rest->setParameter('delimiter', self::$defDelimiter); - $response = $rest->getResponse(); - if ($response->error === false && $response->code !== 200) - $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status'); - if ($response->error !== false) - { - self::__triggerError(sprintf("S3::getBucket(): [%s] %s", - $response->error['code'], $response->error['message']), __FILE__, __LINE__); - return false; - } - - $results = array(); - - $nextMarker = null; - if (isset($response->body, $response->body->Contents)) - foreach ($response->body->Contents as $c) - { - $results[(string)$c->Key] = array( - 'name' => (string)$c->Key, - 'time' => strtotime((string)$c->LastModified), - 'size' => (int)$c->Size, - 'hash' => substr((string)$c->ETag, 1, -1) - ); - $nextMarker = (string)$c->Key; - } - - if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) - foreach ($response->body->CommonPrefixes as $c) - $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); - - if (isset($response->body, $response->body->IsTruncated) && - (string)$response->body->IsTruncated == 'false') return $results; - - if (isset($response->body, $response->body->NextMarker)) - $nextMarker = (string)$response->body->NextMarker; - - // Loop through truncated results if maxKeys isn't specified - if ($maxKeys == null && $nextMarker !== null && (string)$response->body->IsTruncated == 'true') - do - { - $rest = new S3Request('GET', $bucket, '', self::$endpoint); - if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); - $rest->setParameter('marker', $nextMarker); - if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); - - if (($response = $rest->getResponse()) == false || $response->code !== 200) break; - - if (isset($response->body, $response->body->Contents)) - foreach ($response->body->Contents as $c) - { - $results[(string)$c->Key] = array( - 'name' => (string)$c->Key, - 'time' => strtotime((string)$c->LastModified), - 'size' => (int)$c->Size, - 'hash' => substr((string)$c->ETag, 1, -1) - ); - $nextMarker = (string)$c->Key; - } - - if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) - foreach ($response->body->CommonPrefixes as $c) - $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); - - if (isset($response->body, $response->body->NextMarker)) - $nextMarker = (string)$response->body->NextMarker; - - } while ($response !== false && (string)$response->body->IsTruncated == 'true'); - - return $results; - } - - - /** - * Put a bucket - * - * @param string $bucket Bucket name - * @param constant $acl ACL flag - * @param string $location Set as "EU" to create buckets hosted in Europe - * @return boolean - */ - public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false) - { - $rest = new S3Request('PUT', $bucket, '', self::$endpoint); - $rest->setAmzHeader('x-amz-acl', $acl); - - if ($location !== false) - { - $dom = new DOMDocument; - $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration'); - $locationConstraint = $dom->createElement('LocationConstraint', $location); - $createBucketConfiguration->appendChild($locationConstraint); - $dom->appendChild($createBucketConfiguration); - $rest->data = $dom->saveXML(); - $rest->size = strlen($rest->data); - $rest->setHeader('Content-Type', 'application/xml'); - } - $rest = $rest->getResponse(); - - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::putBucket({$bucket}, {$acl}, {$location}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - return true; - } - - - /** - * Delete an empty bucket - * - * @param string $bucket Bucket name - * @return boolean - */ - public static function deleteBucket($bucket) - { - $rest = new S3Request('DELETE', $bucket, '', self::$endpoint); - $rest = $rest->getResponse(); - if ($rest->error === false && $rest->code !== 204) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::deleteBucket({$bucket}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - return true; - } - - - /** - * Create input info array for putObject() - * - * @param string $file Input file - * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own) - * @return array | false - */ - public static function inputFile($file, $md5sum = true) - { - if (!file_exists($file) || !is_file($file) || !is_readable($file)) - { - self::__triggerError('S3::inputFile(): Unable to open input file: '.$file, __FILE__, __LINE__); - return false; - } - clearstatcache(false, $file); - return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ? - (is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : ''); - } - - - /** - * Create input array info for putObject() with a resource - * - * @param string $resource Input resource to read from - * @param integer $bufferSize Input byte size - * @param string $md5sum MD5 hash to send (optional) - * @return array | false - */ - public static function inputResource(&$resource, $bufferSize = false, $md5sum = '') - { - if (!is_resource($resource) || (int)$bufferSize < 0) - { - self::__triggerError('S3::inputResource(): Invalid resource or buffer size', __FILE__, __LINE__); - return false; - } - - // Try to figure out the bytesize - if ($bufferSize === false) - { - if (fseek($resource, 0, SEEK_END) < 0 || ($bufferSize = ftell($resource)) === false) - { - self::__triggerError('S3::inputResource(): Unable to obtain resource size', __FILE__, __LINE__); - return false; - } - fseek($resource, 0); - } - - $input = array('size' => $bufferSize, 'md5sum' => $md5sum); - $input['fp'] =& $resource; - return $input; - } - - - /** - * Put an object - * - * @param mixed $input Input data - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param constant $acl ACL constant - * @param array $metaHeaders Array of x-amz-meta-* headers - * @param array $requestHeaders Array of request headers or content type as a string - * @param constant $storageClass Storage class constant - * @param constant $serverSideEncryption Server-side encryption - * @return boolean - */ - public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD, $serverSideEncryption = self::SSE_NONE) - { - if ($input === false) return false; - $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); - - if (!is_array($input)) $input = array( - 'data' => $input, 'size' => strlen($input), - 'md5sum' => base64_encode(md5($input, true)) - ); - - // Data - if (isset($input['fp'])) - $rest->fp =& $input['fp']; - elseif (isset($input['file'])) - $rest->fp = @fopen($input['file'], 'rb'); - elseif (isset($input['data'])) - $rest->data = $input['data']; - - // Content-Length (required) - if (isset($input['size']) && $input['size'] >= 0) - $rest->size = $input['size']; - else { - if (isset($input['file'])) { - clearstatcache(false, $input['file']); - $rest->size = filesize($input['file']); - } - elseif (isset($input['data'])) - $rest->size = strlen($input['data']); - } - - // Custom request headers (Content-Type, Content-Disposition, Content-Encoding) - if (is_array($requestHeaders)) - foreach ($requestHeaders as $h => $v) - strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v); - elseif (is_string($requestHeaders)) // Support for legacy contentType parameter - $input['type'] = $requestHeaders; - - // Content-Type - if (!isset($input['type'])) - { - if (isset($requestHeaders['Content-Type'])) - $input['type'] =& $requestHeaders['Content-Type']; - elseif (isset($input['file'])) - $input['type'] = self::__getMIMEType($input['file']); - else - $input['type'] = 'application/octet-stream'; - } - - if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class - $rest->setAmzHeader('x-amz-storage-class', $storageClass); - - if ($serverSideEncryption !== self::SSE_NONE) // Server-side encryption - $rest->setAmzHeader('x-amz-server-side-encryption', $serverSideEncryption); - - // We need to post with Content-Length and Content-Type, MD5 is optional - if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false)) - { - $rest->setHeader('Content-Type', $input['type']); - if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']); - - $rest->setAmzHeader('x-amz-acl', $acl); - foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); - $rest->getResponse(); - } else - $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters'); - - if ($rest->response->error === false && $rest->response->code !== 200) - $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); - if ($rest->response->error !== false) - { - self::__triggerError(sprintf("S3::putObject(): [%s] %s", - $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); - return false; - } - return true; - } - - - /** - * Put an object from a file (legacy function) - * - * @param string $file Input file path - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param constant $acl ACL constant - * @param array $metaHeaders Array of x-amz-meta-* headers - * @param string $contentType Content type - * @return boolean - */ - public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null) - { - return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType); - } - - - /** - * Put an object from a string (legacy function) - * - * @param string $string Input data - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param constant $acl ACL constant - * @param array $metaHeaders Array of x-amz-meta-* headers - * @param string $contentType Content type - * @return boolean - */ - public static function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain') - { - return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType); - } - - - /** - * Get an object - * - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param mixed $saveTo Filename or resource to write to - * @return mixed - */ - public static function getObject($bucket, $uri, $saveTo = false) - { - $rest = new S3Request('GET', $bucket, $uri, self::$endpoint); - if ($saveTo !== false) - { - if (is_resource($saveTo)) - $rest->fp =& $saveTo; - else - if (($rest->fp = @fopen($saveTo, 'wb')) !== false) - $rest->file = realpath($saveTo); - else - $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo); - } - if ($rest->response->error === false) $rest->getResponse(); - - if ($rest->response->error === false && $rest->response->code !== 200) - $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); - if ($rest->response->error !== false) - { - self::__triggerError(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s", - $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); - return false; - } - return $rest->response; - } - - - /** - * Get object information - * - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param boolean $returnInfo Return response information - * @return mixed | false - */ - public static function getObjectInfo($bucket, $uri, $returnInfo = true) - { - $rest = new S3Request('HEAD', $bucket, $uri, self::$endpoint); - $rest = $rest->getResponse(); - if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404)) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false; - } - - - /** - * Copy an object - * - * @param string $srcBucket Source bucket name - * @param string $srcUri Source object URI - * @param string $bucket Destination bucket name - * @param string $uri Destination object URI - * @param constant $acl ACL constant - * @param array $metaHeaders Optional array of x-amz-meta-* headers - * @param array $requestHeaders Optional array of request headers (content type, disposition, etc.) - * @param constant $storageClass Storage class constant - * @return mixed | false - */ - public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) - { - $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); - $rest->setHeader('Content-Length', 0); - foreach ($requestHeaders as $h => $v) - strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v); - foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); - if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class - $rest->setAmzHeader('x-amz-storage-class', $storageClass); - $rest->setAmzHeader('x-amz-acl', $acl); - $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, rawurlencode($srcUri))); - if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0) - $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE'); - - $rest = $rest->getResponse(); - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - return isset($rest->body->LastModified, $rest->body->ETag) ? array( - 'time' => strtotime((string)$rest->body->LastModified), - 'hash' => substr((string)$rest->body->ETag, 1, -1) - ) : false; - } - - - /** - * Set up a bucket redirection - * - * @param string $bucket Bucket name - * @param string $location Target host name - * @return boolean - */ - public static function setBucketRedirect($bucket = NULL, $location = NULL) - { - $rest = new S3Request('PUT', $bucket, '', self::$endpoint); - - if( empty($bucket) || empty($location) ) { - self::__triggerError("S3::setBucketRedirect({$bucket}, {$location}): Empty parameter.", __FILE__, __LINE__); - return false; - } - - $dom = new DOMDocument; - $websiteConfiguration = $dom->createElement('WebsiteConfiguration'); - $redirectAllRequestsTo = $dom->createElement('RedirectAllRequestsTo'); - $hostName = $dom->createElement('HostName', $location); - $redirectAllRequestsTo->appendChild($hostName); - $websiteConfiguration->appendChild($redirectAllRequestsTo); - $dom->appendChild($websiteConfiguration); - $rest->setParameter('website', null); - $rest->data = $dom->saveXML(); - $rest->size = strlen($rest->data); - $rest->setHeader('Content-Type', 'application/xml'); - $rest = $rest->getResponse(); - - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::setBucketRedirect({$bucket}, {$location}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - return true; - } - - - /** - * Set logging for a bucket - * - * @param string $bucket Bucket name - * @param string $targetBucket Target bucket (where logs are stored) - * @param string $targetPrefix Log prefix (e,g; domain.com-) - * @return boolean - */ - public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = null) - { - // The S3 log delivery group has to be added to the target bucket's ACP - if ($targetBucket !== null && ($acp = self::getAccessControlPolicy($targetBucket, '')) !== false) - { - // Only add permissions to the target bucket when they do not exist - $aclWriteSet = false; - $aclReadSet = false; - foreach ($acp['acl'] as $acl) - if ($acl['type'] == 'Group' && $acl['uri'] == 'http://acs.amazonaws.com/groups/s3/LogDelivery') - { - if ($acl['permission'] == 'WRITE') $aclWriteSet = true; - elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true; - } - if (!$aclWriteSet) $acp['acl'][] = array( - 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE' - ); - if (!$aclReadSet) $acp['acl'][] = array( - 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP' - ); - if (!$aclReadSet || !$aclWriteSet) self::setAccessControlPolicy($targetBucket, '', $acp); - } - - $dom = new DOMDocument; - $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus'); - $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/'); - if ($targetBucket !== null) - { - if ($targetPrefix == null) $targetPrefix = $bucket . '-'; - $loggingEnabled = $dom->createElement('LoggingEnabled'); - $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket)); - $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix)); - // TODO: Add TargetGrants? - $bucketLoggingStatus->appendChild($loggingEnabled); - } - $dom->appendChild($bucketLoggingStatus); - - $rest = new S3Request('PUT', $bucket, '', self::$endpoint); - $rest->setParameter('logging', null); - $rest->data = $dom->saveXML(); - $rest->size = strlen($rest->data); - $rest->setHeader('Content-Type', 'application/xml'); - $rest = $rest->getResponse(); - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::setBucketLogging({$bucket}, {$targetBucket}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - return true; - } - - - /** - * Get logging status for a bucket - * - * This will return false if logging is not enabled. - * Note: To enable logging, you also need to grant write access to the log group - * - * @param string $bucket Bucket name - * @return array | false - */ - public static function getBucketLogging($bucket) - { - $rest = new S3Request('GET', $bucket, '', self::$endpoint); - $rest->setParameter('logging', null); - $rest = $rest->getResponse(); - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::getBucketLogging({$bucket}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - if (!isset($rest->body->LoggingEnabled)) return false; // No logging - return array( - 'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket, - 'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix, - ); - } - - - /** - * Disable bucket logging - * - * @param string $bucket Bucket name - * @return boolean - */ - public static function disableBucketLogging($bucket) - { - return self::setBucketLogging($bucket, null); - } - - - /** - * Get a bucket's location - * - * @param string $bucket Bucket name - * @return string | false - */ - public static function getBucketLocation($bucket) - { - $rest = new S3Request('GET', $bucket, '', self::$endpoint); - $rest->setParameter('location', null); - $rest = $rest->getResponse(); - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::getBucketLocation({$bucket}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US'; - } - - - /** - * Set object or bucket Access Control Policy - * - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy) - * @return boolean - */ - public static function setAccessControlPolicy($bucket, $uri = '', $acp = array()) - { - $dom = new DOMDocument; - $dom->formatOutput = true; - $accessControlPolicy = $dom->createElement('AccessControlPolicy'); - $accessControlList = $dom->createElement('AccessControlList'); - - // It seems the owner has to be passed along too - $owner = $dom->createElement('Owner'); - $owner->appendChild($dom->createElement('ID', $acp['owner']['id'])); - $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name'])); - $accessControlPolicy->appendChild($owner); - - foreach ($acp['acl'] as $g) - { - $grant = $dom->createElement('Grant'); - $grantee = $dom->createElement('Grantee'); - $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); - if (isset($g['id'])) - { // CanonicalUser (DisplayName is omitted) - $grantee->setAttribute('xsi:type', 'CanonicalUser'); - $grantee->appendChild($dom->createElement('ID', $g['id'])); - } - elseif (isset($g['email'])) - { // AmazonCustomerByEmail - $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail'); - $grantee->appendChild($dom->createElement('EmailAddress', $g['email'])); - } - elseif ($g['type'] == 'Group') - { // Group - $grantee->setAttribute('xsi:type', 'Group'); - $grantee->appendChild($dom->createElement('URI', $g['uri'])); - } - $grant->appendChild($grantee); - $grant->appendChild($dom->createElement('Permission', $g['permission'])); - $accessControlList->appendChild($grant); - } - - $accessControlPolicy->appendChild($accessControlList); - $dom->appendChild($accessControlPolicy); - - $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); - $rest->setParameter('acl', null); - $rest->data = $dom->saveXML(); - $rest->size = strlen($rest->data); - $rest->setHeader('Content-Type', 'application/xml'); - $rest = $rest->getResponse(); - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - return true; - } - - - /** - * Get object or bucket Access Control Policy - * - * @param string $bucket Bucket name - * @param string $uri Object URI - * @return mixed | false - */ - public static function getAccessControlPolicy($bucket, $uri = '') - { - $rest = new S3Request('GET', $bucket, $uri, self::$endpoint); - $rest->setParameter('acl', null); - $rest = $rest->getResponse(); - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - - $acp = array(); - if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) - $acp['owner'] = array( - 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName - ); - - if (isset($rest->body->AccessControlList)) - { - $acp['acl'] = array(); - foreach ($rest->body->AccessControlList->Grant as $grant) - { - foreach ($grant->Grantee as $grantee) - { - if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser - $acp['acl'][] = array( - 'type' => 'CanonicalUser', - 'id' => (string)$grantee->ID, - 'name' => (string)$grantee->DisplayName, - 'permission' => (string)$grant->Permission - ); - elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail - $acp['acl'][] = array( - 'type' => 'AmazonCustomerByEmail', - 'email' => (string)$grantee->EmailAddress, - 'permission' => (string)$grant->Permission - ); - elseif (isset($grantee->URI)) // Group - $acp['acl'][] = array( - 'type' => 'Group', - 'uri' => (string)$grantee->URI, - 'permission' => (string)$grant->Permission - ); - else continue; - } - } - } - return $acp; - } - - - /** - * Delete an object - * - * @param string $bucket Bucket name - * @param string $uri Object URI - * @return boolean - */ - public static function deleteObject($bucket, $uri) - { - $rest = new S3Request('DELETE', $bucket, $uri, self::$endpoint); - $rest = $rest->getResponse(); - if ($rest->error === false && $rest->code !== 204) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::deleteObject(): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - return true; - } - - - /** - * Get a query string authenticated URL - * - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param integer $lifetime Lifetime in seconds - * @param boolean $hostBucket Use the bucket name as the hostname - * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification) - * @return string - */ - public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) - { - $expires = self::__getTime() + $lifetime; - $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri)); - return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s', - // $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires, - $hostBucket ? $bucket : self::$endpoint.'/'.$bucket, $uri, self::$__accessKey, $expires, - urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}"))); - } - - - /** - * Get a CloudFront signed policy URL - * - * @param array $policy Policy - * @return string - */ - public static function getSignedPolicyURL($policy) - { - $data = json_encode($policy); - $signature = ''; - if (!openssl_sign($data, $signature, self::$__signingKeyResource)) return false; - - $encoded = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($data)); - $signature = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($signature)); - - $url = $policy['Statement'][0]['Resource'] . '?'; - foreach (array('Policy' => $encoded, 'Signature' => $signature, 'Key-Pair-Id' => self::$__signingKeyPairId) as $k => $v) - $url .= $k.'='.str_replace('%2F', '/', rawurlencode($v)).'&'; - return substr($url, 0, -1); - } - - - /** - * Get a CloudFront canned policy URL - * - * @param string $url URL to sign - * @param integer $lifetime URL lifetime - * @return string - */ - public static function getSignedCannedURL($url, $lifetime) - { - return self::getSignedPolicyURL(array( - 'Statement' => array( - array('Resource' => $url, 'Condition' => array( - 'DateLessThan' => array('AWS:EpochTime' => self::__getTime() + $lifetime) - )) - ) - )); - } - - - /** - * Get upload POST parameters for form uploads - * - * @param string $bucket Bucket name - * @param string $uriPrefix Object URI prefix - * @param constant $acl ACL constant - * @param integer $lifetime Lifetime in seconds - * @param integer $maxFileSize Maximum filesize in bytes (default 5MB) - * @param string $successRedirect Redirect URL or 200 / 201 status code - * @param array $amzHeaders Array of x-amz-meta-* headers - * @param array $headers Array of request headers or content type as a string - * @param boolean $flashVars Includes additional "Filename" variable posted by Flash - * @return object - */ - public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600, - $maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false) - { - // Create policy object - $policy = new stdClass; - $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (self::__getTime() + $lifetime)); - $policy->conditions = array(); - $obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj); - $obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj); - - $obj = new stdClass; // 200 for non-redirect uploads - if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) - $obj->success_action_status = (string)$successRedirect; - else // URL - $obj->success_action_redirect = $successRedirect; - array_push($policy->conditions, $obj); - - if ($acl !== self::ACL_PUBLIC_READ) - array_push($policy->conditions, array('eq', '$acl', $acl)); - - array_push($policy->conditions, array('starts-with', '$key', $uriPrefix)); - if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', '')); - foreach (array_keys($headers) as $headerKey) - array_push($policy->conditions, array('starts-with', '$'.$headerKey, '')); - foreach ($amzHeaders as $headerKey => $headerVal) - { - $obj = new stdClass; - $obj->{$headerKey} = (string)$headerVal; - array_push($policy->conditions, $obj); - } - array_push($policy->conditions, array('content-length-range', 0, $maxFileSize)); - $policy = base64_encode(str_replace('\/', '/', json_encode($policy))); - - // Create parameters - $params = new stdClass; - $params->AWSAccessKeyId = self::$__accessKey; - $params->key = $uriPrefix.'${filename}'; - $params->acl = $acl; - $params->policy = $policy; unset($policy); - $params->signature = self::__getHash($params->policy); - if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) - $params->success_action_status = (string)$successRedirect; - else - $params->success_action_redirect = $successRedirect; - foreach ($headers as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; - foreach ($amzHeaders as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; - return $params; - } - - - /** - * Create a CloudFront distribution - * - * @param string $bucket Bucket name - * @param boolean $enabled Enabled (true/false) - * @param array $cnames Array containing CNAME aliases - * @param string $comment Use the bucket name as the hostname - * @param string $defaultRootObject Default root object - * @param string $originAccessIdentity Origin access identity - * @param array $trustedSigners Array of trusted signers - * @return array | false - */ - public static function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = null, $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array()) - { - if (!extension_loaded('openssl')) - { - self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); - return false; - } - $useSSL = self::$useSSL; - - self::$useSSL = true; // CloudFront requires SSL - $rest = new S3Request('POST', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com'); - $rest->data = self::__getCloudFrontDistributionConfigXML( - $bucket.'.s3.amazonaws.com', - $enabled, - (string)$comment, - (string)microtime(true), - $cnames, - $defaultRootObject, - $originAccessIdentity, - $trustedSigners - ); - - $rest->size = strlen($rest->data); - $rest->setHeader('Content-Type', 'application/xml'); - $rest = self::__getCloudFrontResponse($rest); - - self::$useSSL = $useSSL; - - if ($rest->error === false && $rest->code !== 201) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } elseif ($rest->body instanceof SimpleXMLElement) - return self::__parseCloudFrontDistributionConfig($rest->body); - return false; - } - - - /** - * Get CloudFront distribution info - * - * @param string $distributionId Distribution ID from listDistributions() - * @return array | false - */ - public static function getDistribution($distributionId) - { - if (!extension_loaded('openssl')) - { - self::__triggerError(sprintf("S3::getDistribution($distributionId): %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); - return false; - } - $useSSL = self::$useSSL; - - self::$useSSL = true; // CloudFront requires SSL - $rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId, 'cloudfront.amazonaws.com'); - $rest = self::__getCloudFrontResponse($rest); - - self::$useSSL = $useSSL; - - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::getDistribution($distributionId): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - elseif ($rest->body instanceof SimpleXMLElement) - { - $dist = self::__parseCloudFrontDistributionConfig($rest->body); - $dist['hash'] = $rest->headers['hash']; - $dist['id'] = $distributionId; - return $dist; - } - return false; - } - - - /** - * Update a CloudFront distribution - * - * @param array $dist Distribution array info identical to output of getDistribution() - * @return array | false - */ - public static function updateDistribution($dist) - { - if (!extension_loaded('openssl')) - { - self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); - return false; - } - - $useSSL = self::$useSSL; - - self::$useSSL = true; // CloudFront requires SSL - $rest = new S3Request('PUT', '', '2010-11-01/distribution/'.$dist['id'].'/config', 'cloudfront.amazonaws.com'); - $rest->data = self::__getCloudFrontDistributionConfigXML( - $dist['origin'], - $dist['enabled'], - $dist['comment'], - $dist['callerReference'], - $dist['cnames'], - $dist['defaultRootObject'], - $dist['originAccessIdentity'], - $dist['trustedSigners'] - ); - - $rest->size = strlen($rest->data); - $rest->setHeader('If-Match', $dist['hash']); - $rest = self::__getCloudFrontResponse($rest); - - self::$useSSL = $useSSL; - - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } else { - $dist = self::__parseCloudFrontDistributionConfig($rest->body); - $dist['hash'] = $rest->headers['hash']; - return $dist; - } - return false; - } - - - /** - * Delete a CloudFront distribution - * - * @param array $dist Distribution array info identical to output of getDistribution() - * @return boolean - */ - public static function deleteDistribution($dist) - { - if (!extension_loaded('openssl')) - { - self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); - return false; - } - - $useSSL = self::$useSSL; - - self::$useSSL = true; // CloudFront requires SSL - $rest = new S3Request('DELETE', '', '2008-06-30/distribution/'.$dist['id'], 'cloudfront.amazonaws.com'); - $rest->setHeader('If-Match', $dist['hash']); - $rest = self::__getCloudFrontResponse($rest); - - self::$useSSL = $useSSL; - - if ($rest->error === false && $rest->code !== 204) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - return true; - } - - - /** - * Get a list of CloudFront distributions - * - * @return array - */ - public static function listDistributions() - { - if (!extension_loaded('openssl')) - { - self::__triggerError(sprintf("S3::listDistributions(): [%s] %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); - return false; - } - - $useSSL = self::$useSSL; - self::$useSSL = true; // CloudFront requires SSL - $rest = new S3Request('GET', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com'); - $rest = self::__getCloudFrontResponse($rest); - self::$useSSL = $useSSL; - - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - self::__triggerError(sprintf("S3::listDistributions(): [%s] %s", - $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); - return false; - } - elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary)) - { - $list = array(); - if (isset($rest->body->Marker, $rest->body->MaxItems, $rest->body->IsTruncated)) - { - //$info['marker'] = (string)$rest->body->Marker; - //$info['maxItems'] = (int)$rest->body->MaxItems; - //$info['isTruncated'] = (string)$rest->body->IsTruncated == 'true' ? true : false; - } - foreach ($rest->body->DistributionSummary as $summary) - $list[(string)$summary->Id] = self::__parseCloudFrontDistributionConfig($summary); - - return $list; - } - return array(); - } - - /** - * List CloudFront Origin Access Identities - * - * @return array - */ - public static function listOriginAccessIdentities() - { - if (!extension_loaded('openssl')) - { - self::__triggerError(sprintf("S3::listOriginAccessIdentities(): [%s] %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); - return false; - } - - self::$useSSL = true; // CloudFront requires SSL - $rest = new S3Request('GET', '', '2010-11-01/origin-access-identity/cloudfront', 'cloudfront.amazonaws.com'); - $rest = self::__getCloudFrontResponse($rest); - $useSSL = self::$useSSL; - - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - trigger_error(sprintf("S3::listOriginAccessIdentities(): [%s] %s", - $rest->error['code'], $rest->error['message']), E_USER_WARNING); - return false; - } - - if (isset($rest->body->CloudFrontOriginAccessIdentitySummary)) - { - $identities = array(); - foreach ($rest->body->CloudFrontOriginAccessIdentitySummary as $identity) - if (isset($identity->S3CanonicalUserId)) - $identities[(string)$identity->Id] = array('id' => (string)$identity->Id, 's3CanonicalUserId' => (string)$identity->S3CanonicalUserId); - return $identities; - } - return false; - } - - - /** - * Invalidate objects in a CloudFront distribution - * - * Thanks to Martin Lindkvist for S3::invalidateDistribution() - * - * @param string $distributionId Distribution ID from listDistributions() - * @param array $paths Array of object paths to invalidate - * @return boolean - */ - public static function invalidateDistribution($distributionId, $paths) - { - if (!extension_loaded('openssl')) - { - self::__triggerError(sprintf("S3::invalidateDistribution(): [%s] %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); - return false; - } - - $useSSL = self::$useSSL; - self::$useSSL = true; // CloudFront requires SSL - $rest = new S3Request('POST', '', '2010-08-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com'); - $rest->data = self::__getCloudFrontInvalidationBatchXML($paths, (string)microtime(true)); - $rest->size = strlen($rest->data); - $rest = self::__getCloudFrontResponse($rest); - self::$useSSL = $useSSL; - - if ($rest->error === false && $rest->code !== 201) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - trigger_error(sprintf("S3::invalidate('{$distributionId}',{$paths}): [%s] %s", - $rest->error['code'], $rest->error['message']), E_USER_WARNING); - return false; - } - return true; - } - - - /** - * Get a InvalidationBatch DOMDocument - * - * @internal Used to create XML in invalidateDistribution() - * @param array $paths Paths to objects to invalidateDistribution - * @param int $callerReference - * @return string - */ - private static function __getCloudFrontInvalidationBatchXML($paths, $callerReference = '0') - { - $dom = new DOMDocument('1.0', 'UTF-8'); - $dom->formatOutput = true; - $invalidationBatch = $dom->createElement('InvalidationBatch'); - foreach ($paths as $path) - $invalidationBatch->appendChild($dom->createElement('Path', $path)); - - $invalidationBatch->appendChild($dom->createElement('CallerReference', $callerReference)); - $dom->appendChild($invalidationBatch); - return $dom->saveXML(); - } - - - /** - * List your invalidation batches for invalidateDistribution() in a CloudFront distribution - * - * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/ListInvalidation.html - * returned array looks like this: - * Array - * ( - * [I31TWB0CN9V6XD] => InProgress - * [IT3TFE31M0IHZ] => Completed - * [I12HK7MPO1UQDA] => Completed - * [I1IA7R6JKTC3L2] => Completed - * ) - * - * @param string $distributionId Distribution ID from listDistributions() - * @return array - */ - public static function getDistributionInvalidationList($distributionId) - { - if (!extension_loaded('openssl')) - { - self::__triggerError(sprintf("S3::getDistributionInvalidationList(): [%s] %s", - "CloudFront functionality requires SSL"), __FILE__, __LINE__); - return false; - } - - $useSSL = self::$useSSL; - self::$useSSL = true; // CloudFront requires SSL - $rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com'); - $rest = self::__getCloudFrontResponse($rest); - self::$useSSL = $useSSL; - - if ($rest->error === false && $rest->code !== 200) - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - if ($rest->error !== false) - { - trigger_error(sprintf("S3::getDistributionInvalidationList('{$distributionId}'): [%s]", - $rest->error['code'], $rest->error['message']), E_USER_WARNING); - return false; - } - elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->InvalidationSummary)) - { - $list = array(); - foreach ($rest->body->InvalidationSummary as $summary) - $list[(string)$summary->Id] = (string)$summary->Status; - - return $list; - } - return array(); - } - - - /** - * Get a DistributionConfig DOMDocument - * - * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?PutConfig.html - * - * @internal Used to create XML in createDistribution() and updateDistribution() - * @param string $bucket S3 Origin bucket - * @param boolean $enabled Enabled (true/false) - * @param string $comment Comment to append - * @param string $callerReference Caller reference - * @param array $cnames Array of CNAME aliases - * @param string $defaultRootObject Default root object - * @param string $originAccessIdentity Origin access identity - * @param array $trustedSigners Array of trusted signers - * @return string - */ - private static function __getCloudFrontDistributionConfigXML($bucket, $enabled, $comment, $callerReference = '0', $cnames = array(), $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array()) - { - $dom = new DOMDocument('1.0', 'UTF-8'); - $dom->formatOutput = true; - $distributionConfig = $dom->createElement('DistributionConfig'); - $distributionConfig->setAttribute('xmlns', 'http://cloudfront.amazonaws.com/doc/2010-11-01/'); - - $origin = $dom->createElement('S3Origin'); - $origin->appendChild($dom->createElement('DNSName', $bucket)); - if ($originAccessIdentity !== null) $origin->appendChild($dom->createElement('OriginAccessIdentity', $originAccessIdentity)); - $distributionConfig->appendChild($origin); - - if ($defaultRootObject !== null) $distributionConfig->appendChild($dom->createElement('DefaultRootObject', $defaultRootObject)); - - $distributionConfig->appendChild($dom->createElement('CallerReference', $callerReference)); - foreach ($cnames as $cname) - $distributionConfig->appendChild($dom->createElement('CNAME', $cname)); - if ($comment !== '') $distributionConfig->appendChild($dom->createElement('Comment', $comment)); - $distributionConfig->appendChild($dom->createElement('Enabled', $enabled ? 'true' : 'false')); - - $trusted = $dom->createElement('TrustedSigners'); - foreach ($trustedSigners as $id => $type) - $trusted->appendChild($id !== '' ? $dom->createElement($type, $id) : $dom->createElement($type)); - $distributionConfig->appendChild($trusted); - - $dom->appendChild($distributionConfig); - //var_dump($dom->saveXML()); - return $dom->saveXML(); - } - - - /** - * Parse a CloudFront distribution config - * - * See http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?GetDistribution.html - * - * @internal Used to parse the CloudFront DistributionConfig node to an array - * @param object &$node DOMNode - * @return array - */ - private static function __parseCloudFrontDistributionConfig(&$node) - { - if (isset($node->DistributionConfig)) - return self::__parseCloudFrontDistributionConfig($node->DistributionConfig); - - $dist = array(); - if (isset($node->Id, $node->Status, $node->LastModifiedTime, $node->DomainName)) - { - $dist['id'] = (string)$node->Id; - $dist['status'] = (string)$node->Status; - $dist['time'] = strtotime((string)$node->LastModifiedTime); - $dist['domain'] = (string)$node->DomainName; - } - - if (isset($node->CallerReference)) - $dist['callerReference'] = (string)$node->CallerReference; - - if (isset($node->Enabled)) - $dist['enabled'] = (string)$node->Enabled == 'true' ? true : false; - - if (isset($node->S3Origin)) - { - if (isset($node->S3Origin->DNSName)) - $dist['origin'] = (string)$node->S3Origin->DNSName; - - $dist['originAccessIdentity'] = isset($node->S3Origin->OriginAccessIdentity) ? - (string)$node->S3Origin->OriginAccessIdentity : null; - } - - $dist['defaultRootObject'] = isset($node->DefaultRootObject) ? (string)$node->DefaultRootObject : null; - - $dist['cnames'] = array(); - if (isset($node->CNAME)) - foreach ($node->CNAME as $cname) - $dist['cnames'][(string)$cname] = (string)$cname; - - $dist['trustedSigners'] = array(); - if (isset($node->TrustedSigners)) - foreach ($node->TrustedSigners as $signer) - { - if (isset($signer->Self)) - $dist['trustedSigners'][''] = 'Self'; - elseif (isset($signer->KeyPairId)) - $dist['trustedSigners'][(string)$signer->KeyPairId] = 'KeyPairId'; - elseif (isset($signer->AwsAccountNumber)) - $dist['trustedSigners'][(string)$signer->AwsAccountNumber] = 'AwsAccountNumber'; - } - - $dist['comment'] = isset($node->Comment) ? (string)$node->Comment : null; - return $dist; - } - - - /** - * Grab CloudFront response - * - * @internal Used to parse the CloudFront S3Request::getResponse() output - * @param object &$rest S3Request instance - * @return object - */ - private static function __getCloudFrontResponse(&$rest) - { - $rest->getResponse(); - if ($rest->response->error === false && isset($rest->response->body) && - is_string($rest->response->body) && substr($rest->response->body, 0, 5) == 'response->body = simplexml_load_string($rest->response->body); - // Grab CloudFront errors - if (isset($rest->response->body->Error, $rest->response->body->Error->Code, - $rest->response->body->Error->Message)) - { - $rest->response->error = array( - 'code' => (string)$rest->response->body->Error->Code, - 'message' => (string)$rest->response->body->Error->Message - ); - unset($rest->response->body); - } - } - return $rest->response; - } - - - /** - * Get MIME type for file - * - * To override the putObject() Content-Type, add it to $requestHeaders - * - * To use fileinfo, ensure the MAGIC environment variable is set - * - * @internal Used to get mime types - * @param string &$file File path - * @return string - */ - private static function __getMIMEType(&$file) - { - static $exts = array( - 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', - 'png' => 'image/png', 'ico' => 'image/x-icon', 'pdf' => 'application/pdf', - 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'svg' => 'image/svg+xml', - 'svgz' => 'image/svg+xml', 'swf' => 'application/x-shockwave-flash', - 'zip' => 'application/zip', 'gz' => 'application/x-gzip', - 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip', - 'bz2' => 'application/x-bzip2', 'rar' => 'application/x-rar-compressed', - 'exe' => 'application/x-msdownload', 'msi' => 'application/x-msdownload', - 'cab' => 'application/vnd.ms-cab-compressed', 'txt' => 'text/plain', - 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', - 'css' => 'text/css', 'js' => 'text/javascript', - 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml', - 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav', - 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', - 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php' - ); - - $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); - if (isset($exts[$ext])) return $exts[$ext]; - - // Use fileinfo if available - if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && - ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) - { - if (($type = finfo_file($finfo, $file)) !== false) - { - // Remove the charset and grab the last content-type - $type = explode(' ', str_replace('; charset=', ';charset=', $type)); - $type = array_pop($type); - $type = explode(';', $type); - $type = trim(array_shift($type)); - } - finfo_close($finfo); - if ($type !== false && strlen($type) > 0) return $type; - } - - return 'application/octet-stream'; - } - - - /** - * Get the current time - * - * @internal Used to apply offsets to sytem time - * @return integer - */ - public static function __getTime() - { - return time() + self::$__timeOffset; - } - - - /** - * Generate the auth string: "AWS AccessKey:Signature" - * - * @internal Used by S3Request::getResponse() - * @param string $string String to sign - * @return string - */ - public static function __getSignature($string) - { - return 'AWS '.self::$__accessKey.':'.self::__getHash($string); - } - - - /** - * Creates a HMAC-SHA1 hash - * - * This uses the hash extension if loaded - * - * @internal Used by __getSignature() - * @param string $string String to sign - * @return string - */ - private static function __getHash($string) - { - return base64_encode(extension_loaded('hash') ? - hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1( - (str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . - pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^ - (str_repeat(chr(0x36), 64))) . $string))))); - } - -} - -/** - * S3 Request class - * - * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class - * @version 0.5.0-dev - */ -final class S3Request -{ - /** - * AWS URI - * - * @var string - * @access pricate - */ - private $endpoint; - - /** - * Verb - * - * @var string - * @access private - */ - private $verb; - - /** - * S3 bucket name - * - * @var string - * @access private - */ - private $bucket; - - /** - * Object URI - * - * @var string - * @access private - */ - private $uri; - - /** - * Final object URI - * - * @var string - * @access private - */ - private $resource = ''; - - /** - * Additional request parameters - * - * @var array - * @access private - */ - private $parameters = array(); - - /** - * Amazon specific request headers - * - * @var array - * @access private - */ - private $amzHeaders = array(); - - /** - * HTTP request headers - * - * @var array - * @access private - */ - private $headers = array( - 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => '' - ); - - /** - * Use HTTP PUT? - * - * @var bool - * @access public - */ - public $fp = false; - - /** - * PUT file size - * - * @var int - * @access public - */ - public $size = 0; - - /** - * PUT post fields - * - * @var array - * @access public - */ - public $data = false; - - /** - * S3 request respone - * - * @var object - * @access public - */ - public $response; - - - /** - * Constructor - * - * @param string $verb Verb - * @param string $bucket Bucket name - * @param string $uri Object URI - * @param string $endpoint AWS endpoint URI - * @return mixed - */ - function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com') - { - - $this->endpoint = $endpoint; - $this->verb = $verb; - $this->bucket = $bucket; - $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/'; - - //if ($this->bucket !== '') - // $this->resource = '/'.$this->bucket.$this->uri; - //else - // $this->resource = $this->uri; - - if ($this->bucket !== '') - { - if ($this->__dnsBucketName($this->bucket)) - { - $this->headers['Host'] = $this->bucket.'.'.$this->endpoint; - $this->resource = '/'.$this->bucket.$this->uri; - } - else - { - $this->headers['Host'] = $this->endpoint; - $this->uri = $this->uri; - if ($this->bucket !== '') $this->uri = '/'.$this->bucket.$this->uri; - $this->bucket = ''; - $this->resource = $this->uri; - } - } - else - { - $this->headers['Host'] = $this->endpoint; - $this->resource = $this->uri; - } - - - $this->headers['Date'] = gmdate('D, d M Y H:i:s T'); - $this->response = new STDClass; - $this->response->error = false; - $this->response->body = null; - $this->response->headers = array(); - } - - - /** - * Set request parameter - * - * @param string $key Key - * @param string $value Value - * @return void - */ - public function setParameter($key, $value) - { - $this->parameters[$key] = $value; - } - - - /** - * Set request header - * - * @param string $key Key - * @param string $value Value - * @return void - */ - public function setHeader($key, $value) - { - $this->headers[$key] = $value; - } - - - /** - * Set x-amz-meta-* header - * - * @param string $key Key - * @param string $value Value - * @return void - */ - public function setAmzHeader($key, $value) - { - $this->amzHeaders[$key] = $value; - } - - - /** - * Get the S3 response - * - * @return object | false - */ - public function getResponse() - { - $query = ''; - if (sizeof($this->parameters) > 0) - { - $query = substr($this->uri, -1) !== '?' ? '?' : '&'; - foreach ($this->parameters as $var => $value) - if ($value == null || $value == '') $query .= $var.'&'; - else $query .= $var.'='.rawurlencode($value).'&'; - $query = substr($query, 0, -1); - $this->uri .= $query; - - if (array_key_exists('acl', $this->parameters) || - array_key_exists('location', $this->parameters) || - array_key_exists('torrent', $this->parameters) || - array_key_exists('website', $this->parameters) || - array_key_exists('logging', $this->parameters)) - $this->resource .= $query; - } - $url = (S3::$useSSL ? 'https://' : 'http://') . ($this->headers['Host'] !== '' ? $this->headers['Host'] : $this->endpoint) . $this->uri; - - //var_dump('bucket: ' . $this->bucket, 'uri: ' . $this->uri, 'resource: ' . $this->resource, 'url: ' . $url); - - // Basic setup - $curl = curl_init(); - curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php'); - - if (S3::$useSSL) - { - // Set protocol version - curl_setopt($curl, CURLOPT_SSLVERSION, S3::$useSSLVersion); - - // SSL Validation can now be optional for those with broken OpenSSL installations - curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, S3::$useSSLValidation ? 2 : 0); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, S3::$useSSLValidation ? 1 : 0); - - if (S3::$sslKey !== null) curl_setopt($curl, CURLOPT_SSLKEY, S3::$sslKey); - if (S3::$sslCert !== null) curl_setopt($curl, CURLOPT_SSLCERT, S3::$sslCert); - if (S3::$sslCACert !== null) curl_setopt($curl, CURLOPT_CAINFO, S3::$sslCACert); - } - - curl_setopt($curl, CURLOPT_URL, $url); - - if (S3::$proxy != null && isset(S3::$proxy['host'])) - { - curl_setopt($curl, CURLOPT_PROXY, S3::$proxy['host']); - curl_setopt($curl, CURLOPT_PROXYTYPE, S3::$proxy['type']); - if (isset(S3::$proxy['user'], S3::$proxy['pass']) && S3::$proxy['user'] != null && S3::$proxy['pass'] != null) - curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', S3::$proxy['user'], S3::$proxy['pass'])); - } - - // Headers - $headers = array(); $amz = array(); - foreach ($this->amzHeaders as $header => $value) - if (strlen($value) > 0) $headers[] = $header.': '.$value; - foreach ($this->headers as $header => $value) - if (strlen($value) > 0) $headers[] = $header.': '.$value; - - // Collect AMZ headers for signature - foreach ($this->amzHeaders as $header => $value) - if (strlen($value) > 0) $amz[] = strtolower($header).':'.$value; - - // AMZ headers must be sorted - if (sizeof($amz) > 0) - { - //sort($amz); - usort($amz, array(&$this, '__sortMetaHeadersCmp')); - $amz = "\n".implode("\n", $amz); - } else $amz = ''; - - if (S3::hasAuth()) - { - // Authorization string (CloudFront stringToSign should only contain a date) - if ($this->headers['Host'] == 'cloudfront.amazonaws.com') - $headers[] = 'Authorization: ' . S3::__getSignature($this->headers['Date']); - else - { - $headers[] = 'Authorization: ' . S3::__getSignature( - $this->verb."\n". - $this->headers['Content-MD5']."\n". - $this->headers['Content-Type']."\n". - $this->headers['Date'].$amz."\n". - $this->resource - ); - } - } - - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - curl_setopt($curl, CURLOPT_HEADER, false); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, false); - curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback')); - curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback')); - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); - - // Request types - switch ($this->verb) - { - case 'GET': break; - case 'PUT': case 'POST': // POST only used for CloudFront - if ($this->fp !== false) - { - curl_setopt($curl, CURLOPT_PUT, true); - curl_setopt($curl, CURLOPT_INFILE, $this->fp); - if ($this->size >= 0) - curl_setopt($curl, CURLOPT_INFILESIZE, $this->size); - } - elseif ($this->data !== false) - { - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); - curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data); - } - else - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); - break; - case 'HEAD': - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD'); - curl_setopt($curl, CURLOPT_NOBODY, true); - break; - case 'DELETE': - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); - break; - default: break; - } - - // Execute, grab errors - if (curl_exec($curl)) - $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); - else - $this->response->error = array( - 'code' => curl_errno($curl), - 'message' => curl_error($curl), - 'resource' => $this->resource - ); - - @curl_close($curl); - - // Parse body into XML - if ($this->response->error === false && isset($this->response->headers['type']) && - $this->response->headers['type'] == 'application/xml' && isset($this->response->body)) - { - $this->response->body = simplexml_load_string($this->response->body); - - // Grab S3 errors - if (!in_array($this->response->code, array(200, 204, 206)) && - isset($this->response->body->Code, $this->response->body->Message)) - { - $this->response->error = array( - 'code' => (string)$this->response->body->Code, - 'message' => (string)$this->response->body->Message - ); - if (isset($this->response->body->Resource)) - $this->response->error['resource'] = (string)$this->response->body->Resource; - unset($this->response->body); - } - } - - // Clean up file resources - if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp); - - return $this->response; - } - - /** - * Sort compare for meta headers - * - * @internal Used to sort x-amz meta headers - * @param string $a String A - * @param string $b String B - * @return integer - */ - private function __sortMetaHeadersCmp($a, $b) - { - $lenA = strpos($a, ':'); - $lenB = strpos($b, ':'); - $minLen = min($lenA, $lenB); - $ncmp = strncmp($a, $b, $minLen); - if ($lenA == $lenB) return $ncmp; - if (0 == $ncmp) return $lenA < $lenB ? -1 : 1; - return $ncmp; - } - - /** - * CURL write callback - * - * @param resource &$curl CURL resource - * @param string &$data Data - * @return integer - */ - private function __responseWriteCallback(&$curl, &$data) - { - if (in_array($this->response->code, array(200, 206)) && $this->fp !== false) - return fwrite($this->fp, $data); - else - $this->response->body .= $data; - return strlen($data); - } - - - /** - * Check DNS conformity - * - * @param string $bucket Bucket name - * @return boolean - */ - private function __dnsBucketName($bucket) - { - if (strlen($bucket) > 63 || preg_match("/[^a-z0-9\.-]/", $bucket) > 0) return false; - if (S3::$useSSL && strstr($bucket, '.') !== false) return false; - if (strstr($bucket, '-.') !== false) return false; - if (strstr($bucket, '..') !== false) return false; - if (!preg_match("/^[0-9a-z]/", $bucket)) return false; - if (!preg_match("/[0-9a-z]$/", $bucket)) return false; - return true; - } - - - /** - * CURL header callback - * - * @param resource $curl CURL resource - * @param string $data Data - * @return integer - */ - private function __responseHeaderCallback($curl, $data) - { - if (($strlen = strlen($data)) <= 2) return $strlen; - if (substr($data, 0, 4) == 'HTTP') - $this->response->code = (int)substr($data, 9, 3); - else - { - $data = trim($data); - if (strpos($data, ': ') === false) return $strlen; - list($header, $value) = explode(': ', $data, 2); - if ($header == 'Last-Modified') - $this->response->headers['time'] = strtotime($value); - elseif ($header == 'Date') - $this->response->headers['date'] = strtotime($value); - elseif ($header == 'Content-Length') - $this->response->headers['size'] = (int)$value; - elseif ($header == 'Content-Type') - $this->response->headers['type'] = $value; - elseif ($header == 'ETag') - $this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value; - elseif (preg_match('/^x-amz-meta-.*$/', $header)) - $this->response->headers[$header] = $value; - } - return $strlen; - } - -} - -/** - * S3 exception class - * - * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class - * @version 0.5.0-dev - */ - -class S3Exception extends Exception { - /** - * Class constructor - * - * @param string $message Exception message - * @param string $file File in which exception was created - * @param string $line Line number on which exception was created - * @param int $code Exception code - */ - function __construct($message, $file, $line, $code = 0) - { - parent::__construct($message, $code); - $this->file = $file; - $this->line = $line; - } -} diff --git a/ext/amazon_s3/main.php b/ext/amazon_s3/main.php deleted file mode 100644 index a0fda3a4..00000000 --- a/ext/amazon_s3/main.php +++ /dev/null @@ -1,75 +0,0 @@ - - * License: GPLv2 - * Description: Copy uploaded files to S3 - * Documentation: - */ - -require_once "ext/amazon_s3/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", "
Secret key: "); - $sb->add_text_option("amazon_s3_bucket", "
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); - } - } -} - diff --git a/ext/approval/info.php b/ext/approval/info.php new file mode 100644 index 00000000..03ad7631 --- /dev/null +++ b/ext/approval/info.php @@ -0,0 +1,12 @@ +"matthew@darkholme.net"]; + public $license = self::LICENSE_WTFPL; + public $description = "Adds an approval step to the upload/import process."; +} diff --git a/ext/approval/main.php b/ext/approval/main.php new file mode 100644 index 00000000..f2a97bcf --- /dev/null +++ b/ext/approval/main.php @@ -0,0 +1,255 @@ +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); + } + } +} diff --git a/ext/approval/theme.php b/ext/approval/theme.php new file mode 100644 index 00000000..bde28240 --- /dev/null +++ b/ext/approval/theme.php @@ -0,0 +1,61 @@ +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 '

Search for images that are approved/not approved.

+
+
approved:yes
+

Returns images that have been approved.

+
+
+
approved:no
+

Returns images that have not been approved.

+
+ '; + } + + 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)); + } +} diff --git a/ext/arrowkey_navigation/main.php b/ext/arrowkey_navigation/main.php deleted file mode 100644 index 023ca87b..00000000 --- a/ext/arrowkey_navigation/main.php +++ /dev/null @@ -1,101 +0,0 @@ - - * Link: http://www.drudexsoftware.com/ - * License: GPLv2 - * Description: Allows viewers no navigate between images using the left & right arrow keys. - * Documentation: - * Simply enable this extention in the extention manager to enable arrow key navigation. - */ -class ArrowkeyNavigation extends Extension { - /** - * Adds functionality for post/view on images. - * - * @param DisplayingImageEvent $event - */ - public function onDisplayingImage(DisplayingImageEvent $event) { - $prev_url = make_http(make_link("post/prev/".$event->image->id)); - $next_url = make_http(make_link("post/next/".$event->image->id)); - $this->add_arrowkeys_code($prev_url, $next_url); - } - - /** - * Adds functionality for post/list. - * - * @param PageRequestEvent $event - */ - public function onPageRequest(PageRequestEvent $event) { - if($event->page_matches("post/list")) { - $pageinfo = $this->get_list_pageinfo($event); - $prev_url = make_http(make_link("post/list/".$pageinfo["prev"])); - $next_url = make_http(make_link("post/list/".$pageinfo["next"])); - $this->add_arrowkeys_code($prev_url, $next_url); - } - } - - /** - * Adds the javascript to the page with the given urls. - * - * @param string $prev_url - * @param string $next_url - */ - private function add_arrowkeys_code($prev_url, $next_url) { - global $page; - - $page->add_html_header("", 60); - } - - /** - * Returns info about the current page number. - * - * @param PageRequestEvent $event - * @return array - */ - private function get_list_pageinfo(PageRequestEvent $event) { - global $config, $database; - - // get the amount of images per page - $images_per_page = $config->get_int('index_images'); - - // if there are no tags, use default - if (is_null($event->get_arg(1))){ - $prefix = ""; - $page_number = int_escape($event->get_arg(0)); - $total_pages = ceil($database->get_one( - "SELECT COUNT(*) FROM images") / $images_per_page); - } - else { // if there are tags, use pages with tags - $prefix = url_escape($event->get_arg(0)) . "/"; - $page_number = int_escape($event->get_arg(1)); - $total_pages = ceil($database->get_one( - "SELECT count FROM tags WHERE tag=:tag", - array("tag"=>$event->get_arg(0))) / $images_per_page); - } - - // creates previous & next values - // When previous first page, go to last page - if ($page_number <= 1) $prev = $total_pages; - else $prev = $page_number-1; - if ($page_number >= $total_pages) $next = 1; - else $next = $page_number+1; - - // Create return array - $pageinfo = array( - "prev" => $prefix.$prev, - "next" => $prefix.$next, - ); - - return $pageinfo; - } -} - diff --git a/ext/artists/info.php b/ext/artists/info.php new file mode 100644 index 00000000..7b10ee95 --- /dev/null +++ b/ext/artists/info.php @@ -0,0 +1,14 @@ +"mail@seinkraft.info","Alpha"=>"alpha@furries.com.ar"]; + public $license = self::LICENSE_GPLV2; + public $description = "Simple artists extension"; + public $beta = true; +} diff --git a/ext/artists/main.php b/ext/artists/main.php index 5276881f..df49771a 100644 --- a/ext/artists/main.php +++ b/ext/artists/main.php @@ -1,78 +1,90 @@ - - * Alpha - * License: GPLv2 - * Description: Simple artists extension - * Documentation: - * - */ -class AuthorSetEvent extends Event { - /** @var \Image */ - public $image; - /** @var \User */ - public $user; - /** @var string */ - public $author; +image = $image; $this->user = $user; $this->author = $author; } } -class Artists extends Extension { - public function onImageInfoSet(ImageInfoSetEvent $event) { - global $user; - if (isset($_POST["tag_edit__author"])) { - send_event(new AuthorSetEvent($event->image, $user, $_POST["tag_edit__author"])); - } - } +class Artists extends Extension +{ + /** @var ArtistsTheme */ + protected $theme; - public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) { + public function onImageInfoSet(ImageInfoSetEvent $event) + { + global $user; + if ($user->can(Permissions::EDIT_IMAGE_ARTIST) && isset($_POST["tag_edit__author"])) { + send_event(new AuthorSetEvent($event->image, $user, $_POST["tag_edit__author"])); + } + } + + public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) + { global $user; $artistName = $this->get_artistName_by_imageID($event->image->id); - if(!$user->is_anonymous()) { + if (!$user->is_anonymous()) { $event->add_part($this->theme->get_author_editor_html($artistName), 42); } - } + } - public function onSearchTermParse(SearchTermParseEvent $event) { - $matches = array(); - if(preg_match("/^author[=|:](.*)$/i", $event->term, $matches)) { - $char = $matches[1]; - $event->add_querylet(new Querylet("Author = :author_char", array("author_char"=>$char))); - } - } + public function onSearchTermParse(SearchTermParseEvent $event) + { + if (is_null($event->term)) { + return; + } - public function onInitExt(InitExtEvent $event) { - global $config, $database; - - if ($config->get_int("ext_artists_version") < 1) { + $matches = []; + if (preg_match("/^(author|artist)[=|:](.*)$/i", $event->term, $matches)) { + $char = $matches[2]; + $event->add_querylet(new Querylet("author = :author_char", ["author_char"=>$char])); + } + } + + public function onHelpPageBuilding(HelpPageBuildingEvent $event) + { + if ($event->key===HelpPages::SEARCH) { + $block = new Block(); + $block->header = "Artist"; + $block->body = $this->theme->get_help_html(); + $event->add_block($block); + } + } + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $config, $database; + + if ($this->get_version("ext_artists_version") < 1) { $database->create_table("artists", " id SCORE_AIPK, user_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, - created SCORE_DATETIME NOT NULL, - updated SCORE_DATETIME NOT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, notes TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE "); - + $database->create_table("artist_members", " id SCORE_AIPK, artist_id INTEGER NOT NULL, user_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, - created SCORE_DATETIME NOT NULL, - updated SCORE_DATETIME NOT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, FOREIGN KEY (artist_id) REFERENCES artists (id) ON UPDATE CASCADE ON DELETE CASCADE "); @@ -80,8 +92,8 @@ class Artists extends Extension { id SCORE_AIPK, artist_id INTEGER NOT NULL, user_id INTEGER NOT NULL, - created SCORE_DATETIME, - updated SCORE_DATETIME, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, alias VARCHAR(255), FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, FOREIGN KEY (artist_id) REFERENCES artists (id) ON UPDATE CASCADE ON DELETE CASCADE @@ -90,8 +102,8 @@ class Artists extends Extension { id SCORE_AIPK, artist_id INTEGER NOT NULL, user_id INTEGER NOT NULL, - created SCORE_DATETIME NOT NULL, - updated SCORE_DATETIME NOT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, url VARCHAR(1000) NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, FOREIGN KEY (artist_id) REFERENCES artists (id) ON UPDATE CASCADE ON DELETE CASCADE @@ -99,53 +111,57 @@ class Artists extends Extension { $database->execute("ALTER TABLE images ADD COLUMN author VARCHAR(255) NULL"); $config->set_int("artistsPerPage", 20); - $config->set_int("ext_artists_version", 1); - - log_info("artists", "extension installed"); + $this->set_version("ext_artists_version", 1); } } - public function onAuthorSet(AuthorSetEvent $event) { + public function onAuthorSet(AuthorSetEvent $event) + { global $database; $author = strtolower($event->author); - if (strlen($author) === 0 || strpos($author, " ")) - return; + if (strlen($author) === 0 || strpos($author, " ")) { + return; + } $paddedAuthor = str_replace(" ", "_", $author); - $artistID = NULL; - if ($this->artist_exists($author)) + $artistID = null; + if ($this->artist_exists($author)) { $artistID = $this->get_artist_id($author); + } - if (is_null($artistID) && $this->alias_exists_by_name($paddedAuthor)) + if (is_null($artistID) && $this->alias_exists_by_name($paddedAuthor)) { $artistID = $this->get_artistID_by_aliasName($paddedAuthor); + } - if (is_null($artistID) && $this->member_exists_by_name($paddedAuthor)) + if (is_null($artistID) && $this->member_exists_by_name($paddedAuthor)) { $artistID = $this->get_artistID_by_memberName($paddedAuthor); + } - if (is_null($artistID) && $this->url_exists_by_url($author)) + if (is_null($artistID) && $this->url_exists_by_url($author)) { $artistID = $this->get_artistID_by_url($author); + } if (!is_null($artistID)) { $artistName = $this->get_artistName_by_artistID($artistID); - } - else { + } else { $this->save_new_artist($author, ""); $artistName = $author; } $database->execute( - "UPDATE images SET author = ? WHERE id = ?", - array($artistName, $event->image->id) + "UPDATE images SET author = :author WHERE id = :id", + ['author'=>$artistName, 'id'=>$event->image->id] ); } - public function onPageRequest(PageRequestEvent $event) { + public function onPageRequest(PageRequestEvent $event) + { global $page, $user; - if($event->page_matches("artist")) { - switch($event->get_arg(0)) { + if ($event->page_matches("artist")) { + switch ($event->get_arg(0)) { //*************ARTIST SECTION************** case "list": { @@ -155,33 +171,30 @@ class Artists extends Extension { } case "new": { - if(!$user->is_anonymous()) { - $this->theme->new_artist_composer(); - } - else { + if (!$user->is_anonymous()) { + $this->theme->new_artist_composer(); + } else { $this->theme->display_error(401, "Error", "You must be registered and logged in to create a new artist."); } break; } case "new_artist": { - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/new")); break; } case "create": { - if(!$user->is_anonymous()) { + if (!$user->is_anonymous()) { $newArtistID = $this->add_artist(); if ($newArtistID == -1) { $this->theme->display_error(400, "Error", "Error when entering artist data."); - } - else { - $page->set_mode("redirect"); + } else { + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/view/".$newArtistID)); } - } - else { + } else { $this->theme->display_error(401, "Error", "You must be registered and logged in to create a new artist."); } break; @@ -189,44 +202,45 @@ class Artists extends Extension { case "view": { - $artistID = $event->get_arg(1); + $artistID = int_escape($event->get_arg(1)); $artist = $this->get_artist($artistID); $aliases = $this->get_alias($artist['id']); $members = $this->get_members($artist['id']); $urls = $this->get_urls($artist['id']); $userIsLogged = !$user->is_anonymous(); - $userIsAdmin = $user->is_admin(); - + $userIsAdmin = $user->can(Permissions::ARTISTS_ADMIN); + $images = Image::find_images(0, 4, Tag::explode($artist['name'])); $this->theme->show_artist($artist, $aliases, $members, $urls, $images, $userIsLogged, $userIsAdmin); + /* if ($userIsLogged) { - //$this->theme->show_new_alias_composer($artistID); - //$this->theme->show_new_member_composer($artistID); - //$this->theme->show_new_url_composer($artistID); + $this->theme->show_new_alias_composer($artistID); + $this->theme->show_new_member_composer($artistID); + $this->theme->show_new_url_composer($artistID); } - + */ + $this->theme->sidebar_options("editor", $artistID, $userIsAdmin); - + break; } case "edit": { - $artistID = $event->get_arg(1); + $artistID = int_escape($event->get_arg(1)); $artist = $this->get_artist($artistID); $aliases = $this->get_alias($artistID); $members = $this->get_members($artistID); $urls = $this->get_urls($artistID); - - if(!$user->is_anonymous()) { - $this->theme->show_artist_editor($artist, $aliases, $members, $urls); - - $userIsAdmin = $user->is_admin(); + + if (!$user->is_anonymous()) { + $this->theme->show_artist_editor($artist, $aliases, $members, $urls); + + $userIsAdmin = $user->can(Permissions::ARTISTS_ADMIN); $this->theme->sidebar_options("editor", $artistID, $userIsAdmin); - } - else { + } else { $this->theme->display_error(401, "Error", "You must be registered and logged in to edit an artist."); } break; @@ -234,7 +248,7 @@ class Artists extends Extension { case "edit_artist": { $artistID = $_POST['artist_id']; - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/edit/".$artistID)); break; } @@ -242,22 +256,22 @@ class Artists extends Extension { { $artistID = int_escape($_POST['id']); $this->update_artist(); - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/view/".$artistID)); break; } case "nuke_artist": { $artistID = $_POST['artist_id']; - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/nuke/".$artistID)); break; } case "nuke": { - $artistID = $event->get_arg(1); + $artistID = int_escape($event->get_arg(1)); $this->delete_artist($artistID); // this will delete the artist, its alias, its urls and its members - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/list")); break; } @@ -282,22 +296,21 @@ class Artists extends Extension { //***********ALIAS SECTION *********************** case "alias": { - switch ($event->get_arg(1)) - { + switch ($event->get_arg(1)) { case "add": { $artistID = $_POST['artistID']; $this->add_alias(); - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/view/".$artistID)); break; } case "delete": { - $aliasID = $event->get_arg(2); + $aliasID = int_escape($event->get_arg(2)); $artistID = $this->get_artistID_by_aliasID($aliasID); $this->delete_alias($aliasID); - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/view/".$artistID)); break; } @@ -313,7 +326,7 @@ class Artists extends Extension { $this->update_alias(); $aliasID = int_escape($_POST['aliasID']); $artistID = $this->get_artistID_by_aliasID($aliasID); - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/view/".$artistID)); break; } @@ -324,22 +337,21 @@ class Artists extends Extension { //**************** URLS SECTION ********************** case "url": { - switch ($event->get_arg(1)) - { + switch ($event->get_arg(1)) { case "add": { $artistID = $_POST['artistID']; $this->add_urls(); - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/view/".$artistID)); break; } case "delete": { - $urlID = $event->get_arg(2); + $urlID = int_escape($event->get_arg(2)); $artistID = $this->get_artistID_by_urlID($urlID); $this->delete_url($urlID); - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/view/".$artistID)); break; } @@ -355,7 +367,7 @@ class Artists extends Extension { $this->update_url(); $urlID = int_escape($_POST['urlID']); $artistID = $this->get_artistID_by_urlID($urlID); - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/view/".$artistID)); break; } @@ -365,13 +377,12 @@ class Artists extends Extension { //******************* MEMBERS SECTION ********************* case "member": { - switch ($event->get_arg(1)) - { + switch ($event->get_arg(1)) { case "add": { $artistID = $_POST['artistID']; $this->add_members(); - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/view/".$artistID)); break; } @@ -380,7 +391,7 @@ class Artists extends Extension { $memberID = int_escape($event->get_arg(2)); $artistID = $this->get_artistID_by_memberID($memberID); $this->delete_member($memberID); - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/view/".$artistID)); break; } @@ -396,7 +407,7 @@ class Artists extends Extension { $this->update_member(); $memberID = int_escape($_POST['memberID']); $artistID = $this->get_artistID_by_memberID($memberID); - $page->set_mode("redirect"); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("artist/view/".$artistID)); break; } @@ -407,206 +418,127 @@ class Artists extends Extension { } } - /** - * @param int $imageID - * @return string - */ - private function get_artistName_by_imageID($imageID) { - assert(is_numeric($imageID)); - + private function get_artistName_by_imageID(int $imageID): string + { global $database; - $result = $database->get_row("SELECT author FROM images WHERE id = ?", array($imageID)); - return stripslashes($result['author']); + $result = $database->get_row("SELECT author FROM images WHERE id = :id", ['id'=>$imageID]); + return $result['author'] ?? ""; } - /** - * @param string $url - * @return bool - */ - private function url_exists_by_url($url) { + private function url_exists_by_url(string $url): bool + { global $database; - $result = $database->get_one("SELECT COUNT(1) FROM artist_urls WHERE url = ?", array($url)); + $result = $database->get_one("SELECT COUNT(1) FROM artist_urls WHERE url = :url", ['url'=>$url]); return ($result != 0); } - /** - * @param string $member - * @return bool - */ - private function member_exists_by_name($member) { + private function member_exists_by_name(string $member): bool + { global $database; - $result = $database->get_one("SELECT COUNT(1) FROM artist_members WHERE name = ?", array($member)); + $result = $database->get_one("SELECT COUNT(1) FROM artist_members WHERE name = :name", ['name'=>$member]); return ($result != 0); } - /** - * @param string $alias - * @return bool - */ - private function alias_exists_by_name($alias) { + private function alias_exists_by_name(string $alias): bool + { global $database; - - $result = $database->get_one("SELECT COUNT(1) FROM artist_alias WHERE alias = ?", array($alias)); + $result = $database->get_one("SELECT COUNT(1) FROM artist_alias WHERE alias = :alias", ['alias'=>$alias]); return ($result != 0); } - /** - * @param int $artistID - * @param string $alias - * @return bool - */ - private function alias_exists($artistID, $alias) { - assert(is_numeric($artistID)); - + private function alias_exists(int $artistID, string $alias): bool + { global $database; $result = $database->get_one( - "SELECT COUNT(1) FROM artist_alias WHERE artist_id = ? AND alias = ?", - array($artistID, $alias) + "SELECT COUNT(1) FROM artist_alias WHERE artist_id = :artist_id AND alias = :alias", + ['artist_id'=>$artistID, 'alias'=>$alias] ); return ($result != 0); } - /** - * @param string $url - * @return int - */ - private function get_artistID_by_url($url) { + private function get_artistID_by_url(string $url): int + { global $database; - return $database->get_one("SELECT artist_id FROM artist_urls WHERE url = ?", array($url)); + return (int)$database->get_one("SELECT artist_id FROM artist_urls WHERE url = :url", ['url'=>$url]); } - /** - * @param string $member - * @return int - */ - private function get_artistID_by_memberName($member) { + private function get_artistID_by_memberName(string $member): int + { global $database; - return $database->get_one("SELECT artist_id FROM artist_members WHERE name = ?", array($member)); + return (int)$database->get_one("SELECT artist_id FROM artist_members WHERE name = :name", ['name'=>$member]); } - /** - * @param int $artistID - * @return string - */ - private function get_artistName_by_artistID($artistID) { - assert(is_numeric($artistID)); - + private function get_artistName_by_artistID(int $artistID): string + { global $database; - return $database->get_one("SELECT name FROM artists WHERE id = ?", array($artistID)); + return (string)$database->get_one("SELECT name FROM artists WHERE id = :id", ['id'=>$artistID]); } - /** - * @param int $aliasID - * @return int - */ - private function get_artistID_by_aliasID($aliasID) { - assert(is_numeric($aliasID)); - + private function get_artistID_by_aliasID(int $aliasID): int + { global $database; - return $database->get_one("SELECT artist_id FROM artist_alias WHERE id = ?", array($aliasID)); + return (int)$database->get_one("SELECT artist_id FROM artist_alias WHERE id = :id", ['id'=>$aliasID]); } - /** - * @param int $memberID - * @return int - */ - private function get_artistID_by_memberID($memberID) { - assert(is_numeric($memberID)); - + private function get_artistID_by_memberID(int $memberID): int + { global $database; - return $database->get_one("SELECT artist_id FROM artist_members WHERE id = ?", array($memberID)); + return (int)$database->get_one("SELECT artist_id FROM artist_members WHERE id = :id", ['id'=>$memberID]); } - /** - * @param int $urlID - * @return int - */ - private function get_artistID_by_urlID($urlID) { - assert(is_numeric($urlID)); - + private function get_artistID_by_urlID(int $urlID): int + { global $database; - return $database->get_one("SELECT artist_id FROM artist_urls WHERE id = ?", array($urlID)); + return (int)$database->get_one("SELECT artist_id FROM artist_urls WHERE id = :id", ['id'=>$urlID]); } - /** - * @param int $aliasID - */ - private function delete_alias($aliasID) { - assert(is_numeric($aliasID)); - + private function delete_alias(int $aliasID) + { global $database; - $database->execute("DELETE FROM artist_alias WHERE id = ?", array($aliasID)); + $database->execute("DELETE FROM artist_alias WHERE id = :id", ['id'=>$aliasID]); } - /** - * @param int $urlID - */ - private function delete_url($urlID) { - assert(is_numeric($urlID)); - + private function delete_url(int $urlID) + { global $database; - $database->execute("DELETE FROM artist_urls WHERE id = ?", array($urlID)); + $database->execute("DELETE FROM artist_urls WHERE id = :id", ['id'=>$urlID]); } - /** - * @param int $memberID - */ - private function delete_member($memberID) { - assert(is_numeric($memberID)); - + private function delete_member(int $memberID) + { global $database; - $database->execute("DELETE FROM artist_members WHERE id = ?", array($memberID)); + $database->execute("DELETE FROM artist_members WHERE id = :id", ['id'=>$memberID]); } - /** - * @param int $aliasID - * @return array - */ - private function get_alias_by_id($aliasID) { - assert(is_numeric($aliasID)); - + private function get_alias_by_id(int $aliasID): array + { global $database; - $result = $database->get_row("SELECT * FROM artist_alias WHERE id = ?", array($aliasID)); - $result["alias"] = stripslashes($result["alias"]); - return $result; + return $database->get_row("SELECT * FROM artist_alias WHERE id = :id", ['id'=>$aliasID]); } - /** - * @param int $urlID - * @return array - */ - private function get_url_by_id($urlID) { - assert(is_numeric($urlID)); - + private function get_url_by_id(int $urlID): array + { global $database; - $result = $database->get_row("SELECT * FROM artist_urls WHERE id = ?", array($urlID)); - $result["url"] = stripslashes($result["url"]); - return $result; + return $database->get_row("SELECT * FROM artist_urls WHERE id = :id", ['id'=>$urlID]); } - /** - * @param int $memberID - * @return array - */ - private function get_member_by_id($memberID) { - assert(is_numeric($memberID)); - + private function get_member_by_id(int $memberID): array + { global $database; - $result = $database->get_row("SELECT * FROM artist_members WHERE id = ?", array($memberID)); - $result["name"] = stripslashes($result["name"]); - return $result; + return $database->get_row("SELECT * FROM artist_members WHERE id = :id", ['id'=>$memberID]); } - private function update_artist() { + private function update_artist() + { global $user; - $inputs = validate_input(array( + $inputs = validate_input([ 'id' => 'int', 'name' => 'string,lower', 'notes' => 'string,trim,nullify', 'aliases' => 'string,trim,nullify', 'aliasesIDs' => 'string,trim,nullify', 'members' => 'string,trim,nullify', - )); + ]); $artistID = $inputs['id']; $name = $inputs['name']; $notes = $inputs['notes']; @@ -621,165 +553,151 @@ class Artists extends Extension { $urlsAsString = $inputs["urls"]; $urlsIDsAsString = $inputs["urlsIDs"]; - if(strpos($name, " ")) + if (strpos($name, " ")) { return; + } global $database; $database->execute( - "UPDATE artists SET name = ?, notes = ?, updated = now(), user_id = ? WHERE id = ? ", - array($name, $notes, $userID, $artistID) + "UPDATE artists SET name = :name, notes = :notes, updated = now(), user_id = :user_id WHERE id = :id", + ['name'=>$name, 'notes'=>$notes, 'user_id'=>$userID, 'id'=>$artistID] ); // ALIAS MATCHING SECTION $i = 0; - $aliasesAsArray = is_null($aliasesAsString) ? array() : explode(" ", $aliasesAsString); - $aliasesIDsAsArray = is_null($aliasesIDsAsString) ? array() : explode(" ", $aliasesIDsAsString); - while ($i < count($aliasesAsArray)) - { + $aliasesAsArray = is_null($aliasesAsString) ? [] : explode(" ", $aliasesAsString); + $aliasesIDsAsArray = is_null($aliasesIDsAsString) ? [] : explode(" ", $aliasesIDsAsString); + while ($i < count($aliasesAsArray)) { // if an alias was updated - if ($i < count($aliasesIDsAsArray)) + if ($i < count($aliasesIDsAsArray)) { $this->save_existing_alias($aliasesIDsAsArray[$i], $aliasesAsArray[$i], $userID); - else + } else { // if we already updated all, save new ones $this->save_new_alias($artistID, $aliasesAsArray[$i], $userID); + } $i++; } // if we have more ids than alias, then some alias have been deleted -- delete them from db - while ($i < count($aliasesIDsAsArray)) + while ($i < count($aliasesIDsAsArray)) { $this->delete_alias($aliasesIDsAsArray[$i++]); + } // MEMBERS MATCHING SECTION $i = 0; - $membersAsArray = is_null($membersAsString) ? array() : explode(" ", $membersAsString); - $membersIDsAsArray = is_null($membersIDsAsString) ? array() : explode(" ", $membersIDsAsString); - while ($i < count($membersAsArray)) - { + $membersAsArray = is_null($membersAsString) ? [] : explode(" ", $membersAsString); + $membersIDsAsArray = is_null($membersIDsAsString) ? [] : explode(" ", $membersIDsAsString); + while ($i < count($membersAsArray)) { // if a member was updated - if ($i < count($membersIDsAsArray)) + if ($i < count($membersIDsAsArray)) { $this->save_existing_member($membersIDsAsArray[$i], $membersAsArray[$i], $userID); - else + } else { // if we already updated all, save new ones $this->save_new_member($artistID, $membersAsArray[$i], $userID); + } $i++; } // if we have more ids than members, then some members have been deleted -- delete them from db - while ($i < count($membersIDsAsArray)) + while ($i < count($membersIDsAsArray)) { $this->delete_member($membersIDsAsArray[$i++]); + } // URLS MATCHING SECTION $i = 0; $urlsAsString = str_replace("\r\n", "\n", $urlsAsString); $urlsAsString = str_replace("\n\r", "\n", $urlsAsString); - $urlsAsArray = is_null($urlsAsString) ? array() : explode("\n", $urlsAsString); - $urlsIDsAsArray = is_null($urlsIDsAsString) ? array() : explode(" ", $urlsIDsAsString); - while ($i < count($urlsAsArray)) - { + $urlsAsArray = is_null($urlsAsString) ? [] : explode("\n", $urlsAsString); + $urlsIDsAsArray = is_null($urlsIDsAsString) ? [] : explode(" ", $urlsIDsAsString); + while ($i < count($urlsAsArray)) { // if an URL was updated if ($i < count($urlsIDsAsArray)) { $this->save_existing_url($urlsIDsAsArray[$i], $urlsAsArray[$i], $userID); - } - else { + } else { $this->save_new_url($artistID, $urlsAsArray[$i], $userID); } $i++; } - + // if we have more ids than urls, then some urls have been deleted -- delete them from db - while ($i < count($urlsIDsAsArray)) + while ($i < count($urlsIDsAsArray)) { $this->delete_url($urlsIDsAsArray[$i++]); + } } - private function update_alias() { + private function update_alias() + { global $user; - $inputs = validate_input(array( + $inputs = validate_input([ "aliasID" => "int", "alias" => "string,lower", - )); + ]); $this->save_existing_alias($inputs['aliasID'], $inputs['alias'], $user->id); } - /** - * @param int $aliasID - * @param string $alias - * @param int $userID - */ - private function save_existing_alias($aliasID, $alias, $userID) { - assert(is_numeric($userID)); - assert(is_numeric($aliasID)); - + private function save_existing_alias(int $aliasID, string $alias, int $userID) + { global $database; $database->execute( - "UPDATE artist_alias SET alias = ?, updated = now(), user_id = ? WHERE id = ? ", - array($alias, $userID, $aliasID) + "UPDATE artist_alias SET alias = :alias, updated = now(), user_id = :user_id WHERE id = :id", + ['alias'=>$alias, 'user_id'=>$userID, 'id'=>$aliasID] ); } - private function update_url() { + private function update_url() + { global $user; - $inputs = validate_input(array( + $inputs = validate_input([ "urlID" => "int", "url" => "string", - )); + ]); $this->save_existing_url($inputs['urlID'], $inputs['url'], $user->id); } - /** - * @param int $urlID - * @param string $url - * @param int $userID - */ - private function save_existing_url($urlID, $url, $userID) { - assert(is_numeric($userID)); - assert(is_numeric($urlID)); - + private function save_existing_url(int $urlID, string $url, int $userID) + { global $database; $database->execute( - "UPDATE artist_urls SET url = ?, updated = now(), user_id = ? WHERE id = ?", - array($url, $userID, $urlID) + "UPDATE artist_urls SET url = :url, updated = now(), user_id = :user_id WHERE id = :id", + ['url'=>$url, 'user_id'=>$userID, 'id'=>$urlID] ); } - private function update_member() { + private function update_member() + { global $user; - $inputs = validate_input(array( + $inputs = validate_input([ "memberID" => "int", "name" => "string,lower", - )); + ]); $this->save_existing_member($inputs['memberID'], $inputs['name'], $user->id); } - /** - * @param int $memberID - * @param string $memberName - * @param int $userID - */ - private function save_existing_member($memberID, $memberName, $userID) { - assert(is_numeric($memberID)); - assert(is_numeric($userID)); - + private function save_existing_member(int $memberID, string $memberName, int $userID) + { global $database; $database->execute( - "UPDATE artist_members SET name = ?, updated = now(), user_id = ? WHERE id = ?", - array($memberName, $userID, $memberID) + "UPDATE artist_members SET name = :name, updated = now(), user_id = :user_id WHERE id = :id", + ['name'=>$memberName, 'user_id'=>$userID, 'id'=>$memberID] ); } - private function add_artist(){ + private function add_artist() + { global $user; - $inputs = validate_input(array( + $inputs = validate_input([ "name" => "string,lower", "notes" => "string,optional", "aliases" => "string,lower,optional", "members" => "string,lower,optional", "urls" => "string,optional" - )); + ]); $name = $inputs["name"]; - if(strpos($name, " ")) + if (strpos($name, " ")) { return -1; + } $notes = $inputs["notes"]; @@ -791,79 +709,72 @@ class Artists extends Extension { //$artistID = ""; //// WE CHECK IF THE ARTIST ALREADY EXISTS ON DATABASE; IF NOT WE CREATE - if(!$this->artist_exists($name)) { + if (!$this->artist_exists($name)) { $artistID = $this->save_new_artist($name, $notes); log_info("artists", "Artist {$artistID} created by {$user->name}"); - } - else { + } else { $artistID = $this->get_artist_id($name); } if (!is_null($aliases)) { $aliasArray = explode(" ", $aliases); - foreach($aliasArray as $alias) - if (!$this->alias_exists($artistID, $alias)) + foreach ($aliasArray as $alias) { + if (!$this->alias_exists($artistID, $alias)) { $this->save_new_alias($artistID, $alias, $userID); + } + } } if (!is_null($members)) { $membersArray = explode(" ", $members); - foreach ($membersArray as $member) - if (!$this->member_exists($artistID, $member)) + foreach ($membersArray as $member) { + if (!$this->member_exists($artistID, $member)) { $this->save_new_member($artistID, $member, $userID); + } + } } if (!is_null($urls)) { //delete double "separators" $urls = str_replace("\r\n", "\n", $urls); $urls = str_replace("\n\r", "\n", $urls); - + $urlsArray = explode("\n", $urls); - foreach ($urlsArray as $url) - if (!$this->url_exists($artistID, $url)) + foreach ($urlsArray as $url) { + if (!$this->url_exists($artistID, $url)) { $this->save_new_url($artistID, $url, $userID); + } + } } return $artistID; } - /** - * @param string $name - * @param string $notes - * @return int - */ - private function save_new_artist($name, $notes) { + private function save_new_artist(string $name, string $notes): int + { global $database, $user; $database->execute(" INSERT INTO artists (user_id, name, notes, created, updated) - VALUES (?, ?, ?, now(), now()) - ", array($user->id, $name, $notes)); + VALUES (:user_id, :name, :notes, now(), now()) + ", ['user_id'=>$user->id, 'name'=>$name, 'notes'=>$notes]); return $database->get_last_insert_id('artists_id_seq'); } - /** - * @param string $name - * @return bool - */ - private function artist_exists($name) { + private function artist_exists(string $name): bool + { global $database; $result = $database->get_one( - "SELECT COUNT(1) FROM artists WHERE name = ?", - array($name) + "SELECT COUNT(1) FROM artists WHERE name = :name", + ['name'=>$name] ); return ($result != 0); } - /** - * @param int $artistID - * @return array - */ - private function get_artist($artistID){ - assert(is_numeric($artistID)); - + private function get_artist(int $artistID): array + { global $database; $result = $database->get_row( - "SELECT * FROM artists WHERE id = ?", - array($artistID) + "SELECT * FROM artists WHERE id = :id", + ['id'=>$artistID] ); $result["name"] = stripslashes($result["name"]); @@ -872,20 +783,15 @@ class Artists extends Extension { return $result; } - /** - * @param int $artistID - * @return array - */ - private function get_members($artistID) { - assert(is_numeric($artistID)); - + private function get_members(int $artistID): array + { global $database; $result = $database->get_all( - "SELECT * FROM artist_members WHERE artist_id = ?", - array($artistID) + "SELECT * FROM artist_members WHERE artist_id = :artist_id", + ['artist_id'=>$artistID] ); - - $num = count($result); + + $num = count($result); for ($i = 0 ; $i < $num ; $i++) { $result[$i]["name"] = stripslashes($result[$i]["name"]); } @@ -893,20 +799,15 @@ class Artists extends Extension { return $result; } - /** - * @param int $artistID - * @return array - */ - private function get_urls($artistID) { - assert(is_numeric($artistID)); - + private function get_urls(int $artistID): array + { global $database; $result = $database->get_all( - "SELECT id, url FROM artist_urls WHERE artist_id = ?", - array($artistID) + "SELECT id, url FROM artist_urls WHERE artist_id = :artist_id", + ['artist_id'=>$artistID] ); - - $num = count($result); + + $num = count($result); for ($i = 0 ; $i < $num ; $i++) { $result[$i]["url"] = stripslashes($result[$i]["url"]); } @@ -914,57 +815,46 @@ class Artists extends Extension { return $result; } - /** - * @param string $name - * @return int - */ - private function get_artist_id($name) { - global $database; - return (int)$database->get_one( - "SELECT id FROM artists WHERE name = ?", - array($name) - ); - } - - /** - * @param string $alias - * @return int - */ - private function get_artistID_by_aliasName($alias) { + private function get_artist_id(string $name): int + { global $database; - return (int)$database->get_one( - "SELECT artist_id FROM artist_alias WHERE alias = ?", - array($alias) + "SELECT id FROM artists WHERE name = :name", + ['name'=>$name] ); } + private function get_artistID_by_aliasName(string $alias): int + { + global $database; - /** - * @param int $artistID - */ - private function delete_artist($artistID) { - assert(is_numeric($artistID)); + return (int)$database->get_one( + "SELECT artist_id FROM artist_alias WHERE alias = :alias", + ['alias'=>$alias] + ); + } + private function delete_artist(int $artistID) + { global $database; $database->execute( - "DELETE FROM artists WHERE id = ? ", - array($artistID) + "DELETE FROM artists WHERE id = :id", + ['id'=>$artistID] ); - } - - /* - * HERE WE GET THE LIST OF ALL ARTIST WITH PAGINATION - */ - private function get_listing(Page $page, PageRequestEvent $event) - { - global $config, $database; + } - $pageNumber = clamp($event->get_arg(1), 1, null) - 1; - $artistsPerPage = $config->get_int("artistsPerPage"); + /* + * HERE WE GET THE LIST OF ALL ARTIST WITH PAGINATION + */ + private function get_listing(Page $page, PageRequestEvent $event) + { + global $config, $database; - $listing = $database->get_all( - " + $pageNumber = clamp(int_escape($event->get_arg(1)), 1, null) - 1; + $artistsPerPage = $config->get_int("artistsPerPage"); + + $listing = $database->get_all( + " ( SELECT a.id, a.user_id, a.name, u.name AS user_name, COALESCE(t.count, 0) AS posts , 'artist' as type, a.id AS artist_id, a.name AS artist_name, a.updated @@ -1009,22 +899,23 @@ class Artists extends Extension { ORDER BY m.updated DESC ) ORDER BY updated DESC - LIMIT ?, ? - ", array( - $pageNumber * $artistsPerPage - , $artistsPerPage - )); - - $number_of_listings = count($listing); + LIMIT :offset, :limit + ", + [ + "offset"=>$pageNumber * $artistsPerPage, + "limit"=>$artistsPerPage + ] + ); - for ($i = 0 ; $i < $number_of_listings ; $i++) - { - $listing[$i]["name"] = stripslashes($listing[$i]["name"]); - $listing[$i]["user_name"] = stripslashes($listing[$i]["user_name"]); - $listing[$i]["artist_name"] = stripslashes($listing[$i]["artist_name"]); - } + $number_of_listings = count($listing); - $count = $database->get_one(" + for ($i = 0 ; $i < $number_of_listings ; $i++) { + $listing[$i]["name"] = stripslashes($listing[$i]["name"]); + $listing[$i]["user_name"] = stripslashes($listing[$i]["user_name"]); + $listing[$i]["artist_name"] = stripslashes($listing[$i]["artist_name"]); + } + + $count = $database->get_one(" SELECT COUNT(1) FROM artists AS a LEFT OUTER JOIN artist_members AS am @@ -1033,162 +924,134 @@ class Artists extends Extension { ON a.id = aa.artist_id "); - $totalPages = ceil ($count / $artistsPerPage); + $totalPages = ceil($count / $artistsPerPage); - $this->theme->list_artists($listing, $pageNumber + 1, $totalPages); - } - - /* - * HERE WE ADD AN ALIAS - */ - private function add_urls() { + $this->theme->list_artists($listing, $pageNumber + 1, $totalPages); + } + + /* + * HERE WE ADD AN ALIAS + */ + private function add_urls() + { global $user; - $inputs = validate_input(array( + $inputs = validate_input([ "artistID" => "int", "urls" => "string", - )); + ]); $artistID = $inputs["artistID"]; $urls = explode("\n", $inputs["urls"]); - foreach ($urls as $url) - if (!$this->url_exists($artistID, $url)) + foreach ($urls as $url) { + if (!$this->url_exists($artistID, $url)) { $this->save_new_url($artistID, $url, $user->id); + } + } } - /** - * @param int $artistID - * @param string $url - * @param int $userID - */ - private function save_new_url($artistID, $url, $userID) { + private function save_new_url(int $artistID, string $url, int $userID) + { global $database; - assert(is_numeric($artistID)); - assert(is_numeric($userID)); - $database->execute( - "INSERT INTO artist_urls (artist_id, created, updated, url, user_id) VALUES (?, now(), now(), ?, ?)", - array($artistID, $url, $userID) + "INSERT INTO artist_urls (artist_id, created, updated, url, user_id) VALUES (:artist_id, now(), now(), :url, :user_id)", + ['artist'=>$artistID, 'url'=>$url, 'user_id'=>$userID] ); } - private function add_alias() { + private function add_alias() + { global $user; - $inputs = validate_input(array( + $inputs = validate_input([ "artistID" => "int", "aliases" => "string,lower", - )); + ]); $artistID = $inputs["artistID"]; $aliases = explode(" ", $inputs["aliases"]); - foreach ($aliases as $alias) - if (!$this->alias_exists($artistID, $alias)) + foreach ($aliases as $alias) { + if (!$this->alias_exists($artistID, $alias)) { $this->save_new_alias($artistID, $alias, $user->id); + } + } } - /** - * @param int $artistID - * @param string $alias - * @param int $userID - */ - private function save_new_alias($artistID, $alias, $userID) { + private function save_new_alias(int $artistID, string $alias, int $userID) + { global $database; - assert(is_numeric($artistID)); - assert(is_numeric($userID)); - $database->execute( - "INSERT INTO artist_alias (artist_id, created, updated, alias, user_id) VALUES (?, now(), now(), ?, ?)", - array($artistID, $alias, $userID) + "INSERT INTO artist_alias (artist_id, created, updated, alias, user_id) VALUES (:artist_id, now(), now(), :alias, :user_id)", + ['artist_id'=>$artistID, 'alias'=>$alias, 'user_id'=>$userID] ); } - private function add_members() { + private function add_members() + { global $user; - $inputs = validate_input(array( + $inputs = validate_input([ "artistID" => "int", "members" => "string,lower", - )); + ]); $artistID = $inputs["artistID"]; $members = explode(" ", $inputs["members"]); - foreach ($members as $member) - if (!$this->member_exists($artistID, $member)) + foreach ($members as $member) { + if (!$this->member_exists($artistID, $member)) { $this->save_new_member($artistID, $member, $user->id); + } + } } - /** - * @param int $artistID - * @param string $member - * @param int $userID - */ - private function save_new_member($artistID, $member, $userID) { + private function save_new_member(int $artistID, string $member, int $userID) + { global $database; - assert(is_numeric($artistID)); - assert(is_numeric($userID)); - $database->execute( - "INSERT INTO artist_members (artist_id, name, created, updated, user_id) VALUES (?, ?, now(), now(), ?)", - array($artistID, $member, $userID) + "INSERT INTO artist_members (artist_id, name, created, updated, user_id) VALUES (:artist_id, :name, now(), now(), :user_id)", + ['artist'=>$artistID, 'name'=>$member, 'user_id'=>$userID] ); } - /** - * @param int $artistID - * @param string $member - * @return bool - */ - private function member_exists($artistID, $member) { + private function member_exists(int $artistID, string $member): bool + { global $database; - assert(is_numeric($artistID)); + $result = $database->get_one( + "SELECT COUNT(1) FROM artist_members WHERE artist_id = :artist_id AND name = :name", + ['artist_id'=>$artistID, 'name'=>$member] + ); + return ($result != 0); + } + + private function url_exists(int $artistID, string $url): bool + { + global $database; $result = $database->get_one( - "SELECT COUNT(1) FROM artist_members WHERE artist_id = ? AND name = ?", - array($artistID, $member) + "SELECT COUNT(1) FROM artist_urls WHERE artist_id = :artist_id AND url = :url", + ['artist_id'=>$artistID, 'url'=>$url] ); return ($result != 0); } /** - * @param int $artistID - * @param string $url - * @return bool + * HERE WE GET THE INFO OF THE ALIAS */ - private function url_exists($artistID, $url) { + private function get_alias(int $artistID): array + { global $database; - assert(is_numeric($artistID)); - - $result = $database->get_one( - "SELECT COUNT(1) FROM artist_urls WHERE artist_id = ? AND url = ?", - array($artistID, $url) - ); - return ($result != 0); - } - - /** - * HERE WE GET THE INFO OF THE ALIAS - * - * @param int $artistID - * @return array - */ - private function get_alias($artistID) { - global $database; - - assert(is_numeric($artistID)); - $result = $database->get_all(" SELECT id AS alias_id, alias AS alias_name FROM artist_alias - WHERE artist_id = ? + WHERE artist_id = :artist_id ORDER BY alias ASC - ", array($artistID)); + ", ['artist_id'=>$artistID]); for ($i = 0 ; $i < count($result) ; $i++) { $result[$i]["alias_name"] = stripslashes($result[$i]["alias_name"]); } return $result; - } + } } diff --git a/ext/artists/test.php b/ext/artists/test.php index 9cbfdf5e..cedde48e 100644 --- a/ext/artists/test.php +++ b/ext/artists/test.php @@ -1,9 +1,15 @@ -get_page("post/list/author=bob/1"); - #$this->assert_response(200); - } -} +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]); + } +} diff --git a/ext/artists/theme.php b/ext/artists/theme.php index cc30e6bd..0041d495 100644 --- a/ext/artists/theme.php +++ b/ext/artists/theme.php @@ -1,13 +1,10 @@ - Author @@ -16,105 +13,104 @@ class ArtistsTheme extends Themelet { "; - } + } - /** - * @param string $mode - * @param null|int $artistID - * @param bool $is_admin - */ - public function sidebar_options(/*string*/ $mode, $artistID=NULL, $is_admin=FALSE) { - global $page, $user; + public function sidebar_options(string $mode, ?int $artistID=null, $is_admin=false): void + { + global $page, $user; - $html = ""; + $html = ""; - if($mode == "neutral"){ - $html = "
+ if ($mode == "neutral") { + $html = " ".$user->get_auth_html()."
"; - } - - if($mode == "editor"){ - $html = "
+ } + + if ($mode == "editor") { + $html = " ".$user->get_auth_html()." - +
".$user->get_auth_html()." - +
"; - - if($is_admin){ - $html .= "
+ + if ($is_admin) { + $html .= " ".$user->get_auth_html()." - +
"; - } - - $html .= "
+ } + + $html .= " ".$user->get_auth_html()." - +
".$user->get_auth_html()." - +
".$user->get_auth_html()." - +
"; - } + } - if($html) $page->add_block(new Block("Manage Artists", $html, "left", 10)); - } + if ($html) { + $page->add_block(new Block("Manage Artists", $html, "left", 10)); + } + } - public function show_artist_editor($artist, $aliases, $members, $urls) { - global $user; + public function show_artist_editor($artist, $aliases, $members, $urls) + { + global $user; - $artistName = $artist['name']; - $artistNotes = $artist['notes']; - $artistID = $artist['id']; + $artistName = $artist['name']; + $artistNotes = $artist['notes']; + $artistID = $artist['id']; - // aliases - $aliasesString = ""; - $aliasesIDsString = ""; - foreach ($aliases as $alias) { - $aliasesString .= $alias["alias_name"]." "; - $aliasesIDsString .= $alias["alias_id"]." "; - } - $aliasesString = rtrim($aliasesString); - $aliasesIDsString = rtrim($aliasesIDsString); + // aliases + $aliasesString = ""; + $aliasesIDsString = ""; + foreach ($aliases as $alias) { + $aliasesString .= $alias["alias_name"]." "; + $aliasesIDsString .= $alias["alias_id"]." "; + } + $aliasesString = rtrim($aliasesString); + $aliasesIDsString = rtrim($aliasesIDsString); - // members - $membersString = ""; - $membersIDsString = ""; - foreach ($members as $member) { - $membersString .= $member["name"]." "; - $membersIDsString .= $member["id"]." "; - } - $membersString = rtrim($membersString); - $membersIDsString = rtrim($membersIDsString); + // members + $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); + // 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 = ' + $html = '
'.$user->get_auth_html().' @@ -132,14 +128,15 @@ class ArtistsTheme extends Themelet { '; - global $page; - $page->add_block(new Block("Edit artist", $html, "main", 10)); - } - - public function new_artist_composer() { - global $page, $user; + global $page; + $page->add_block(new Block("Edit artist", $html, "main", 10)); + } + + public function new_artist_composer() + { + global $page, $user; - $html = " + $html = " ".$user->get_auth_html()."
@@ -151,86 +148,95 @@ class ArtistsTheme extends Themelet {
Name:
"; - $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; + $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 = "". - "". - "". - "". - "". - ""; + $html = "
NameTypeLast updaterPosts
". + "". + "". + "". + "". + ""; - if(!$user->is_anonymous()) $html .= ""; // space for edit link - - $html .= ""; + if (!$user->is_anonymous()) { + $html .= ""; + } // space for edit link + + $html .= ""; - $deletionLinkActionArray = array( - 'artist' => 'artist/nuke/', - 'alias' => 'artist/alias/delete/', - 'member' => 'artist/member/delete/', - ); + $deletionLinkActionArray = [ + 'artist' => 'artist/nuke/', + 'alias' => 'artist/alias/delete/', + 'member' => 'artist/member/delete/', + ]; - $editionLinkActionArray = array( - 'artist' => 'artist/edit/', - 'alias' => 'artist/alias/edit/', - 'member' => 'artist/member/edit/', - ); + $editionLinkActionArray = [ + 'artist' => 'artist/edit/', + 'alias' => 'artist/alias/edit/', + 'member' => 'artist/member/edit/', + ]; - $typeTextArray = array( - 'artist' => 'Artist', - 'alias' => 'Alias', - 'member' => 'Member', - ); + $typeTextArray = [ + 'artist' => 'Artist', + 'alias' => 'Alias', + 'member' => 'Member', + ]; - foreach ($artists as $artist) { - if ($artist['type'] != 'artist') - $artist['name'] = str_replace("_", " ", $artist['name']); + foreach ($artists as $artist) { + if ($artist['type'] != 'artist') { + $artist['name'] = str_replace("_", " ", $artist['name']); + } - $elementLink = "".str_replace("_", " ", $artist['name']).""; - //$artist_link = "".str_replace("_", " ", $artist['artist_name']).""; - $user_link = "".$artist['user_name'].""; - $edit_link = "Edit"; - $del_link = "Delete"; + $elementLink = "".str_replace("_", " ", $artist['name']).""; + //$artist_link = "".str_replace("_", " ", $artist['artist_name']).""; + $user_link = "".$artist['user_name'].""; + $edit_link = "Edit"; + $del_link = "Delete"; - $html .= "". - "". + "". - "". - "". - ""; + $html .= "". + "". + "". + ""; - if(!$user->is_anonymous()) $html .= ""; - if($user->is_admin()) $html .= ""; + if (!$user->is_anonymous()) { + $html .= ""; + } + if ($user->can(Permissions::ARTISTS_ADMIN)) { + $html .= ""; + } - $html .= ""; - } + $html .= ""; + } - $html .= "
NameTypeLast updaterPostsAction
Action
".$elementLink; + $html .= "
".$elementLink; - //if ($artist['type'] == 'member') - // $html .= " (member of ".$artist_link.")"; + //if ($artist['type'] == 'member') + // $html .= " (member of ".$artist_link.")"; - //if ($artist['type'] == 'alias') - // $html .= " (alias for ".$artist_link.")"; + //if ($artist['type'] == 'alias') + // $html .= " (alias for ".$artist_link.")"; - $html .= "".$typeTextArray[$artist['type']]."".$user_link."".$artist['posts']."".$typeTextArray[$artist['type']]."".$user_link."".$artist['posts']."".$edit_link."".$del_link."".$edit_link."".$del_link."
"; + $html .= ""; - $page->set_title("Artists"); - $page->set_heading("Artists"); - $page->add_block(new Block("Artists", $html, "main", 10)); + $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); - } + $this->display_paginator($page, "artist/list", null, $pageNumber, $totalPages); + } - public function show_new_alias_composer($artistID) { - global $user; + public function show_new_alias_composer($artistID) + { + global $user; - $html = ' + $html = ' '.$user->get_auth_html().' @@ -241,14 +247,15 @@ class ArtistsTheme extends Themelet { '; - global $page; - $page->add_block(new Block("Artist Aliases", $html, "main", 20)); - } + global $page; + $page->add_block(new Block("Artist Aliases", $html, "main", 20)); + } - public function show_new_member_composer($artistID) { - global $user; + public function show_new_member_composer($artistID) + { + global $user; - $html = ' + $html = ' '.$user->get_auth_html().'
@@ -259,14 +266,15 @@ class ArtistsTheme extends Themelet { '; - global $page; - $page->add_block(new Block("Artist members", $html, "main", 30)); - } + global $page; + $page->add_block(new Block("Artist members", $html, "main", 30)); + } - public function show_new_url_composer($artistID) { - global $user; + public function show_new_url_composer($artistID) + { + global $user; - $html = ' + $html = ' '.$user->get_auth_html().'
@@ -277,253 +285,274 @@ class ArtistsTheme extends Themelet { '; - global $page; - $page->add_block(new Block("Artist URLs", $html, "main", 40)); - } + global $page; + $page->add_block(new Block("Artist URLs", $html, "main", 40)); + } - public function show_alias_editor($alias) { - global $user; + public function show_alias_editor($alias) + { + global $user; - $html = ' + $html = ' '.$user->get_auth_html().' - + '; - global $page; - $page->add_block(new Block("Edit Alias", $html, "main", 10)); - } + global $page; + $page->add_block(new Block("Edit Alias", $html, "main", 10)); + } - public function show_url_editor($url) { - global $user; + public function show_url_editor($url) + { + global $user; - $html = ' + $html = ' '.$user->get_auth_html().' - + '; - global $page; - $page->add_block(new Block("Edit URL", $html, "main", 10)); - } + global $page; + $page->add_block(new Block("Edit URL", $html, "main", 10)); + } - public function show_member_editor($member) { - global $user; + public function show_member_editor($member) + { + global $user; - $html = ' + $html = ' '.$user->get_auth_html().' - - + + '; - global $page; - $page->add_block(new Block("Edit Member", $html, "main", 10)); - } + 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; + public function show_artist($artist, $aliases, $members, $urls, $images, $userIsLogged, $userIsAdmin) + { + global $page; - $artist_link = "".str_replace("_", " ", $artist['name']).""; + $artist_link = "".str_replace("_", " ", $artist['name']).""; - $html = "
+ $html = "
"; - - if ($userIsLogged) $html .= ""; - if ($userIsAdmin) $html .= ""; + + if ($userIsLogged) { + $html .= ""; + } + if ($userIsAdmin) { + $html .= ""; + } - $html .= " + $html .= " "; - if ($userIsLogged) $html .= ""; - if ($userIsAdmin) $html .= ""; - $html .= ""; + if ($userIsLogged) { + $html .= ""; + } + if ($userIsAdmin) { + $html .= ""; + } + $html .= ""; - $html .= $this->render_aliases($aliases, $userIsLogged, $userIsAdmin); - $html .= $this->render_members($members, $userIsLogged, $userIsAdmin); - $html .= $this->render_urls($urls, $userIsLogged, $userIsAdmin); + $html .= $this->render_aliases($aliases, $userIsLogged, $userIsAdmin); + $html .= $this->render_members($members, $userIsLogged, $userIsAdmin); + $html .= $this->render_urls($urls, $userIsLogged, $userIsAdmin); - $html .= " + $html .= ""; - if ($userIsLogged) $html .= ""; - if ($userIsAdmin) $html .= ""; - //TODO how will notes be edited? On edit artist? (should there be an editartist?) or on a editnotes? - //same question for deletion - $html .= " + if ($userIsLogged) { + $html .= ""; + } + if ($userIsAdmin) { + $html .= ""; + } + //TODO how will notes be edited? On edit artist? (should there be an editartist?) or on a editnotes? + //same question for deletion + $html .= "
Name: ".$artist_link."
Notes: ".$artist["notes"]."
"; - $page->set_title("Artist"); - $page->set_heading("Artist"); - $page->add_block(new Block("Artist", $html, "main", 10)); + $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 .= ''. - ''.$thumb_html.''. - ''; - } - - $page->add_block(new Block("Artist Images", $artist_images, "main", 20)); - } + //we show the images for the artist + $artist_images = ""; + foreach ($images as $image) { + $thumb_html = $this->build_thumb_html($image); + + $artist_images .= ''. + ''.$thumb_html.''. + ''; + } + + $page->add_block(new Block("Artist Images", $artist_images, "main", 20)); + } - /** - * @param $aliases - * @param $userIsLogged - * @param $userIsAdmin - * @return string - */ - private function render_aliases($aliases, $userIsLogged, $userIsAdmin) { - $html = ""; - if(count($aliases) > 0) { - $aliasViewLink = str_replace("_", " ", $aliases[0]['alias_name']); // no link anymore - $aliasEditLink = "Edit"; - $aliasDeleteLink = "Delete"; + private function render_aliases(array $aliases, bool $userIsLogged, bool $userIsAdmin): string + { + $html = ""; + if (count($aliases) > 0) { + $aliasViewLink = str_replace("_", " ", $aliases[0]['alias_name']); // no link anymore + $aliasEditLink = "Edit"; + $aliasDeleteLink = "Delete"; - $html .= " + $html .= " Aliases: " . $aliasViewLink . ""; - if ($userIsLogged) - $html .= "" . $aliasEditLink . ""; + if ($userIsLogged) { + $html .= "" . $aliasEditLink . ""; + } - if ($userIsAdmin) - $html .= "" . $aliasDeleteLink . ""; + if ($userIsAdmin) { + $html .= "" . $aliasDeleteLink . ""; + } - $html .= ""; + $html .= ""; - if (count($aliases) > 1) { - for ($i = 1; $i < count($aliases); $i++) { - $aliasViewLink = str_replace("_", " ", $aliases[$i]['alias_name']); // no link anymore - $aliasEditLink = "Edit"; - $aliasDeleteLink = "Delete"; + if (count($aliases) > 1) { + for ($i = 1; $i < count($aliases); $i++) { + $aliasViewLink = str_replace("_", " ", $aliases[$i]['alias_name']); // no link anymore + $aliasEditLink = "Edit"; + $aliasDeleteLink = "Delete"; - $html .= " + $html .= "   " . $aliasViewLink . ""; - if ($userIsLogged) - $html .= "" . $aliasEditLink . ""; - if ($userIsAdmin) - $html .= "" . $aliasDeleteLink . ""; + if ($userIsLogged) { + $html .= "" . $aliasEditLink . ""; + } + if ($userIsAdmin) { + $html .= "" . $aliasDeleteLink . ""; + } - $html .= ""; - } - } - } - return $html; - } + $html .= ""; + } + } + } + return $html; + } - /** - * @param $members - * @param $userIsLogged - * @param $userIsAdmin - * @return string - */ - private function render_members($members, $userIsLogged, $userIsAdmin) { - $html = ""; - if(count($members) > 0) { - $memberViewLink = str_replace("_", " ", $members[0]['name']); // no link anymore - $memberEditLink = "Edit"; - $memberDeleteLink = "Delete"; + private function render_members(array $members, bool $userIsLogged, bool $userIsAdmin): string + { + $html = ""; + if (count($members) > 0) { + $memberViewLink = str_replace("_", " ", $members[0]['name']); // no link anymore + $memberEditLink = "Edit"; + $memberDeleteLink = "Delete"; - $html .= " + $html .= " Members: " . $memberViewLink . ""; - if ($userIsLogged) - $html .= "" . $memberEditLink . ""; - if ($userIsAdmin) - $html .= "" . $memberDeleteLink . ""; + if ($userIsLogged) { + $html .= "" . $memberEditLink . ""; + } + if ($userIsAdmin) { + $html .= "" . $memberDeleteLink . ""; + } - $html .= ""; + $html .= ""; - if (count($members) > 1) { - for ($i = 1; $i < count($members); $i++) { - $memberViewLink = str_replace("_", " ", $members[$i]['name']); // no link anymore - $memberEditLink = "Edit"; - $memberDeleteLink = "Delete"; + if (count($members) > 1) { + for ($i = 1; $i < count($members); $i++) { + $memberViewLink = str_replace("_", " ", $members[$i]['name']); // no link anymore + $memberEditLink = "Edit"; + $memberDeleteLink = "Delete"; - $html .= " + $html .= "   " . $memberViewLink . ""; - if ($userIsLogged) - $html .= "" . $memberEditLink . ""; - if ($userIsAdmin) - $html .= "" . $memberDeleteLink . ""; + if ($userIsLogged) { + $html .= "" . $memberEditLink . ""; + } + if ($userIsAdmin) { + $html .= "" . $memberDeleteLink . ""; + } - $html .= ""; - } - } - } - return $html; - } + $html .= ""; + } + } + } + return $html; + } - /** - * @param $urls - * @param $userIsLogged - * @param $userIsAdmin - * @return string - */ - private function render_urls($urls, $userIsLogged, $userIsAdmin) { - $html = ""; - if(count($urls) > 0) { - $urlViewLink = "" . str_replace("_", " ", $urls[0]['url']) . ""; - $urlEditLink = "Edit"; - $urlDeleteLink = "Delete"; + private function render_urls(array $urls, bool $userIsLogged, bool $userIsAdmin): string + { + $html = ""; + if (count($urls) > 0) { + $urlViewLink = "" . str_replace("_", " ", $urls[0]['url']) . ""; + $urlEditLink = "Edit"; + $urlDeleteLink = "Delete"; - $html .= " + $html .= " URLs: " . $urlViewLink . ""; - if ($userIsLogged) - $html .= "" . $urlEditLink . ""; + if ($userIsLogged) { + $html .= "" . $urlEditLink . ""; + } - if ($userIsAdmin) - $html .= "" . $urlDeleteLink . ""; + if ($userIsAdmin) { + $html .= "" . $urlDeleteLink . ""; + } - $html .= ""; + $html .= ""; - if (count($urls) > 1) { - for ($i = 1; $i < count($urls); $i++) { - $urlViewLink = "" . str_replace("_", " ", $urls[$i]['url']) . ""; - $urlEditLink = "Edit"; - $urlDeleteLink = "Delete"; + if (count($urls) > 1) { + for ($i = 1; $i < count($urls); $i++) { + $urlViewLink = "" . str_replace("_", " ", $urls[$i]['url']) . ""; + $urlEditLink = "Edit"; + $urlDeleteLink = "Delete"; - $html .= " + $html .= "   " . $urlViewLink . ""; - if ($userIsLogged) - $html .= "" . $urlEditLink . ""; + if ($userIsLogged) { + $html .= "" . $urlEditLink . ""; + } - if ($userIsAdmin) - $html .= "" . $urlDeleteLink . ""; + if ($userIsAdmin) { + $html .= "" . $urlDeleteLink . ""; + } - $html .= ""; - } - return $html; - } - } - return $html; - } + $html .= ""; + } + return $html; + } + } + return $html; + } + public function get_help_html() + { + return '

Search for images with a particular artist.

+
+
artist=leonardo
+

Returns images with the artist "leonardo".

+
+ '; + } } - diff --git a/ext/auto_tagger/config.php b/ext/auto_tagger/config.php new file mode 100644 index 00000000..f7bb0902 --- /dev/null +++ b/ext/auto_tagger/config.php @@ -0,0 +1,7 @@ +"matthew@darkholme.net"]; + public $license = self::LICENSE_WTFPL; + public $description = "Provides several automatic tagging functions"; +} diff --git a/ext/auto_tagger/main.php b/ext/auto_tagger/main.php new file mode 100644 index 00000000..da32789a --- /dev/null +++ b/ext/auto_tagger/main.php @@ -0,0 +1,331 @@ +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 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("text/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; + } +} diff --git a/ext/auto_tagger/test.php b/ext/auto_tagger/test.php new file mode 100644 index 00000000..9d1e096d --- /dev/null +++ b/ext/auto_tagger/test.php @@ -0,0 +1,58 @@ +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"); + } +} diff --git a/ext/auto_tagger/theme.php b/ext/auto_tagger/theme.php new file mode 100644 index 00000000..e77510f7 --- /dev/null +++ b/ext/auto_tagger/theme.php @@ -0,0 +1,36 @@ +can(Permissions::MANAGE_AUTO_TAG); + $html = " + $table + $paginator +

Download as CSV

+ "; + + $bulk_html = " + ".make_form(make_link("auto_tag/import"), 'post', true)." + + + + "; + + $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)); + } + } +} diff --git a/ext/autocomplete/info.php b/ext/autocomplete/info.php new file mode 100644 index 00000000..0c6c912b --- /dev/null +++ b/ext/autocomplete/info.php @@ -0,0 +1,11 @@ +"admin@codeanimu.net"]; + public $description = "Adds autocomplete to search & tagging."; +} diff --git a/ext/autocomplete/lib/tagit.ui-zendesk.css b/ext/autocomplete/lib/tagit.ui-zendesk.css index b91181bf..18982864 100644 --- a/ext/autocomplete/lib/tagit.ui-zendesk.css +++ b/ext/autocomplete/lib/tagit.ui-zendesk.css @@ -14,8 +14,7 @@ ul.tagit li.tagit-choice { -webkit-border-radius: 6px; border: 1px solid #CAD8F3; - background: none; - background-color: #DEE7F8; + background: #DEE7F8 none; font-weight: normal; } diff --git a/ext/autocomplete/main.php b/ext/autocomplete/main.php index 24e1e87f..843c8536 100644 --- a/ext/autocomplete/main.php +++ b/ext/autocomplete/main.php @@ -1,47 +1,69 @@ - - * Description: Adds autocomplete to search & tagging. - */ +page_matches("api/internal/autocomplete")) { - if(!isset($_GET["s"])) return; + public function onPageRequest(PageRequestEvent $event) + { + global $cache, $page, $database; - //$limit = 0; - $cache_key = "autocomplete-" . strtolower($_GET["s"]); - $limitSQL = ""; - $SQLarr = array("search"=>$_GET["s"]."%"); - if(isset($_GET["limit"]) && $_GET["limit"] !== 0){ - $limitSQL = "LIMIT :limit"; - $SQLarr['limit'] = $_GET["limit"]; - $cache_key .= "-" . $_GET["limit"]; - } + if ($event->page_matches("api/internal/autocomplete")) { + if (!isset($_GET["s"])) { + return; + } - $res = $database->cache->get($cache_key); - if(!$res) { - $res = $database->get_pairs($database->scoreql_to_sql(" + $page->set_mode(PageMode::DATA); + $page->set_type("application/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 SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:search) + WHERE LOWER(tag) LIKE LOWER(:search) + -- OR LOWER(tag) LIKE LOWER(:cat_search) AND count > 0 ORDER BY count DESC - $limitSQL"), $SQLarr - ); - $database->cache->set($cache_key, $res, 600); - } + $limitSQL", + $SQLarr + ); + $cache->set($cache_key, $res, 600); + } - $page->set_mode("data"); - $page->set_type("application/json"); - $page->set_data(json_encode($res)); - } + $page->set_data(json_encode($res)); + } - $this->theme->build_autocomplete($page); - } + $this->theme->build_autocomplete($page); + } } diff --git a/ext/autocomplete/script.js b/ext/autocomplete/script.js index 8fda1cf9..98c7296e 100644 --- a/ext/autocomplete/script.js +++ b/ext/autocomplete/script.js @@ -1,7 +1,7 @@ -$(function(){ +document.addEventListener('DOMContentLoaded', () => { var metatags = ['order:id', 'order:width', 'order:height', 'order:filesize', 'order:filename']; - $('[name=search]').tagit({ + $('[name="search"]').tagit({ singleFieldDelimiter: ' ', beforeTagAdded: function(event, ui) { if(metatags.indexOf(ui.tagLabel) !== -1) { @@ -51,7 +51,7 @@ $(function(){ ); }, error : function (request, status, error) { - alert(error); + console.log(error); } }); }, @@ -66,7 +66,7 @@ $(function(){ if(keyCode == 32) { e.preventDefault(); - $('[name=search]').tagit('createTag', $(this).val()); + $('.autocomplete_tags').tagit('createTag', $(this).val()); $(this).autocomplete('close'); } else if (keyCode == 9) { e.preventDefault(); diff --git a/ext/autocomplete/test.php b/ext/autocomplete/test.php new file mode 100644 index 00000000..40c6b4c9 --- /dev/null +++ b/ext/autocomplete/test.php @@ -0,0 +1,14 @@ +get_page('api/internal/autocomplete', ["s"=>"not-a-tag"]); + $this->assertEquals(200, $page->code); + $this->assertEquals(PageMode::DATA, $page->mode); + $this->assertEquals("[]", $page->data); + } +} diff --git a/ext/autocomplete/theme.php b/ext/autocomplete/theme.php index 462d2bfd..4e5c4a1f 100644 --- a/ext/autocomplete/theme.php +++ b/ext/autocomplete/theme.php @@ -1,13 +1,15 @@ -add_html_header(""); - $page->add_html_header(""); - $page->add_html_header(''); - $page->add_html_header(""); - } + $page->add_html_header(""); + $page->add_html_header(""); + $page->add_html_header(''); + $page->add_html_header(""); + } } diff --git a/ext/ban_words/info.php b/ext/ban_words/info.php new file mode 100644 index 00000000..664ba0e4 --- /dev/null +++ b/ext/ban_words/info.php @@ -0,0 +1,26 @@ +Regex bans are also supported, allowing more complicated +bans like /http:.*\.cn\// to block links to +chinese websites, or /.*?http.*?http.*?http.*?http.*?/ +to block comments with four (or more) links in. +

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\""; +} diff --git a/ext/ban_words/main.php b/ext/ban_words/main.php index 9d9493d4..44a9f48e 100644 --- a/ext/ban_words/main.php +++ b/ext/ban_words/main.php @@ -1,29 +1,11 @@ - - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: For stopping spam and other comment abuse - * 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. - *

Regex bans are also supported, allowing more complicated - * bans like /http:.*\.cn\// to block links to - * chinese websites, or /.*?http.*?http.*?http.*?http.*?/ - * to block comments with four (or more) links in. - *

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" - */ +set_default_string('banned_words', " +class BanWords extends Extension +{ + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_string('banned_words', " a href= anal blowjob @@ -51,86 +33,87 @@ very nice site viagra xanax "); - } + } - public function onCommentPosting(CommentPostingEvent $event) { - global $user; - if(!$user->can("bypass_comment_checks")) { - $this->test_text($event->comment, new CommentPostingException("Comment contains banned terms")); - } - } + public function onCommentPosting(CommentPostingEvent $event) + { + global $user; + if (!$user->can(Permissions::BYPASS_COMMENT_CHECKS)) { + $this->test_text($event->comment, new CommentPostingException("Comment contains banned terms")); + } + } - public function onSourceSet(SourceSetEvent $event) { - $this->test_text($event->source, new SCoreException("Source contains banned terms")); - } + public function onSourceSet(SourceSetEvent $event) + { + $this->test_text($event->source, new SCoreException("Source contains banned terms")); + } - public function onTagSet(TagSetEvent $event) { - $this->test_text(Tag::implode($event->tags), new SCoreException("Tags contain banned terms")); - } + public function onTagSet(TagSetEvent $event) + { + $this->test_text(Tag::implode($event->tags), new SCoreException("Tags contain banned terms")); + } - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Banned Phrases"); - $sb->add_label("One per line, lines that start with slashes are treated as regex
"); - $sb->add_longtext_option("banned_words"); - $failed = array(); - foreach($this->get_words() as $word) { - if($word[0] == '/') { - if(preg_match($word, "") === false) { - $failed[] = $word; - } - } - } - if($failed) { - $sb->add_label("Failed regexes: ".join(", ", $failed)); - } - $event->panel->add_block($sb); - } + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Banned Phrases"); + $sb->add_label("One per line, lines that start with slashes are treated as regex
"); + $sb->add_longtext_option("banned_words"); + $failed = []; + foreach ($this->get_words() as $word) { + if ($word[0] == '/') { + if (preg_match($word, "") === false) { + $failed[] = $word; + } + } + } + if ($failed) { + $sb->add_label("Failed regexes: ".join(", ", $failed)); + } + $event->panel->add_block($sb); + } - /** - * Throws if the comment contains banned words. - * @param string $comment - * @param CommentPostingException|SCoreException $ex - * @throws CommentPostingException|SCoreException if the comment contains banned words. - */ - private function test_text($comment, $ex) { - $comment = strtolower($comment); + /** + * Throws if the comment contains banned words. + */ + private function test_text(string $comment, SCoreException $ex): void + { + $comment = strtolower($comment); - foreach($this->get_words() as $word) { - if($word[0] == '/') { - // lines that start with slash are regex - if(preg_match($word, $comment) === 1) { - throw $ex; - } - } - else { - // other words are literal - if(strpos($comment, $word) !== false) { - throw $ex; - } - } - } - } + foreach ($this->get_words() as $word) { + if ($word[0] == '/') { + // lines that start with slash are regex + if (preg_match($word, $comment) === 1) { + throw $ex; + } + } else { + // other words are literal + if (strpos($comment, $word) !== false) { + throw $ex; + } + } + } + } - /** - * @return string[] - */ - private function get_words() { - global $config; - $words = array(); + private function get_words(): array + { + global $config; + $words = []; - $banned = $config->get_string("banned_words"); - foreach(explode("\n", $banned) as $word) { - $word = trim(strtolower($word)); - if(strlen($word) == 0) { - // line is blank - continue; - } - $words[] = $word; - } + $banned = $config->get_string("banned_words"); + foreach (explode("\n", $banned) as $word) { + $word = trim(strtolower($word)); + if (strlen($word) == 0) { + // line is blank + continue; + } + $words[] = $word; + } - return $words; - } + return $words; + } - public function get_priority() {return 30;} + public function get_priority(): int + { + return 30; + } } - diff --git a/ext/ban_words/test.php b/ext/ban_words/test.php index 886aee18..448694dc 100644 --- a/ext/ban_words/test.php +++ b/ext/ban_words/test.php @@ -1,33 +1,34 @@ -fail("Exception not thrown"); - } - catch(CommentPostingException $e) { - $this->assertEquals($e->getMessage(), "Comment contains banned terms"); - } - } +fail("Exception not thrown"); + } catch (CommentPostingException $e) { + $this->assertEquals($e->getMessage(), "Comment contains banned terms"); + } + } - public function testWordBan() { - global $config; - $config->set_string("banned_words", "viagra\nporn\n\n/http:.*\.cn\//"); + public function testWordBan() + { + global $config; + $config->set_string("banned_words", "viagra\nporn\n\n/http:.*\.cn\//"); - $this->log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - $this->check_blocked($image_id, "kittens and viagra"); - $this->check_blocked($image_id, "kittens and ViagrA"); - $this->check_blocked($image_id, "kittens and viagra!"); - $this->check_blocked($image_id, "some link to http://something.cn/"); + $this->check_blocked($image_id, "kittens and viagra"); + $this->check_blocked($image_id, "kittens and ViagrA"); + $this->check_blocked($image_id, "kittens and viagra!"); + $this->check_blocked($image_id, "some link to http://something.cn/"); - $this->get_page('comment/list'); - $this->assert_title('Comments'); - $this->assert_no_text('viagra'); - $this->assert_no_text('ViagrA'); - $this->assert_no_text('http://something.cn/'); - } + $this->get_page('comment/list'); + $this->assert_title('Comments'); + $this->assert_no_text('viagra'); + $this->assert_no_text('ViagrA'); + $this->assert_no_text('http://something.cn/'); + } } - diff --git a/ext/bbcode/info.php b/ext/bbcode/info.php new file mode 100644 index 00000000..c0291a84 --- /dev/null +++ b/ext/bbcode/info.php @@ -0,0 +1,32 @@ + +

  • [img]url[/img] +
  • [url]http://code.shishnet.org/[/url] +
  • [email]webmaster@shishnet.org[/email] +
  • [b]bold[/b] +
  • [i]italic[/i] +
  • [u]underline[/u] +
  • [s]strikethrough[/s] +
  • [sup]superscript[/sup] +
  • [sub]subscript[/sub] +
  • [[wiki article]] +
  • [[wiki article|with some text]] +
  • [quote]text[/quote] +
  • [quote=Username]text[/quote] +
  • >>123 (link to image #123) + "; +} diff --git a/ext/bbcode/main.php b/ext/bbcode/main.php index ee20fa7c..52ddcc4b 100644 --- a/ext/bbcode/main.php +++ b/ext/bbcode/main.php @@ -1,184 +1,162 @@ - - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Turns BBCode into HTML - * Documentation: - * Supported tags: - *
      - *
    • [img]url[/img] - *
    • [url]http://code.shishnet.org/[/url] - *
    • [email]webmaster@shishnet.org[/email] - *
    • [b]bold[/b] - *
    • [i]italic[/i] - *
    • [u]underline[/u] - *
    • [s]strikethrough[/s] - *
    • [sup]superscript[/sup] - *
    • [sub]subscript[/sub] - *
    • [[wiki article]] - *
    • [[wiki article|with some text]] - *
    • [quote]text[/quote] - *
    • [quote=Username]text[/quote] - *
    • >>123 (link to image #123) - *
    - */ +extract_code($text); - foreach(array( - "b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4", - ) as $el) { - $text = preg_replace("!\[$el\](.*?)\[/$el\]!s", "<$el>$1", $text); - } - $text = preg_replace('!^>>([^\d].+)!', '
    $1
    ', $text); - $text = preg_replace('!>>(\d+)(#c?\d+)?!s', '>>$1$2', $text); - $text = preg_replace('!\[anchor=(.*?)\](.*?)\[/anchor\]!s', '$2 ', $text); // add "bb-" to avoid clashing with eg #top - $text = preg_replace('!\[url=site://(.*?)(#c\d+)?\](.*?)\[/url\]!s', '$3', $text); - $text = preg_replace('!\[url\]site://(.*?)(#c\d+)?\[/url\]!s', '$1$2', $text); - $text = preg_replace('!\[url=((?:https?|ftp|irc|mailto)://.*?)\](.*?)\[/url\]!s', '$2', $text); - $text = preg_replace('!\[url\]((?:https?|ftp|irc|mailto)://.*?)\[/url\]!s', '$1', $text); - $text = preg_replace('!\[email\](.*?)\[/email\]!s', '$1', $text); - $text = preg_replace('!\[img\](https?:\/\/.*?)\[/img\]!s', '', $text); - $text = preg_replace('!\[\[([^\|\]]+)\|([^\]]+)\]\]!s', '$2', $text); - $text = preg_replace('!\[\[([^\]]+)\]\]!s', '$1', $text); - $text = preg_replace("!\n\s*\n!", "\n\n", $text); - $text = str_replace("\n", "\n
    ", $text); - $text = preg_replace("/\[quote\](.*?)\[\/quote\]/s", "
    \\1
    ", $text); - $text = preg_replace("/\[quote=(.*?)\](.*?)\[\/quote\]/s", "
    \\1 said:
    \\2
    ", $text); - while(preg_match("/\[list\](.*?)\[\/list\]/s", $text)) - $text = preg_replace("/\[list\](.*?)\[\/list\]/s", "
      \\1
    ", $text); - while(preg_match("/\[ul\](.*?)\[\/ul\]/s", $text)) - $text = preg_replace("/\[ul\](.*?)\[\/ul\]/s", "
      \\1
    ", $text); - while(preg_match("/\[ol\](.*?)\[\/ol\]/s", $text)) - $text = preg_replace("/\[ol\](.*?)\[\/ol\]/s", "
      \\1
    ", $text); - $text = preg_replace("/\[li\](.*?)\[\/li\]/s", "
  • \\1
  • ", $text); - $text = preg_replace("#\[\*\]#s", "
  • ", $text); - $text = preg_replace("#
    <(li|ul|ol|/ul|/ol)>#s", "<\\1>", $text); - $text = preg_replace("#\[align=(left|center|right)\](.*?)\[\/align\]#s", "
    \\2
    ", $text); - $text = $this->filter_spoiler($text); - $text = $this->insert_code($text); - return $text; - } - /** - * @param string $text - * @return string - */ - public function strip(/*string*/ $text) { - foreach(array( - "b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4", - "code", "url", "email", "li", - ) as $el) { - $text = preg_replace("!\[$el\](.*?)\[/$el\]!s", '$1', $text); - } - $text = preg_replace("!\[anchor=(.*?)\](.*?)\[/anchor\]!s", '$2', $text); - $text = preg_replace("!\[url=(.*?)\](.*?)\[/url\]!s", '$2', $text); - $text = preg_replace("!\[img\](.*?)\[/img\]!s", "", $text); - $text = preg_replace("!\[\[([^\|\]]+)\|([^\]]+)\]\]!s", '$2', $text); - $text = preg_replace("!\[\[([^\]]+)\]\]!s", '$1', $text); - $text = preg_replace("!\[quote\](.*?)\[/quote\]!s", "", $text); - $text = preg_replace("!\[quote=(.*?)\](.*?)\[/quote\]!s", "", $text); - $text = preg_replace("!\[/?(list|ul|ol)\]!", "", $text); - $text = preg_replace("!\[\*\](.*?)!s", '$1', $text); - $text = $this->strip_spoiler($text); - return $text; - } +class BBCode extends FormatterExtension +{ + public function format(string $text): string + { + $text = $this->extract_code($text); + foreach ([ + "b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4", + ] as $el) { + $text = preg_replace("!\[$el\](.*?)\[/$el\]!s", "<$el>$1", $text); + } + $text = preg_replace('!^>>([^\d].+)!', '
    $1
    ', $text); + $text = preg_replace('!>>(\d+)(#c?\d+)?!s', '>>$1$2', $text); + $text = preg_replace('!\[anchor=(.*?)\](.*?)\[/anchor\]!s', '$2 ', $text); // add "bb-" to avoid clashing with eg #top + $text = preg_replace('!\[url=site://(.*?)(#c\d+)?\](.*?)\[/url\]!s', '$3', $text); + $text = preg_replace('!\[url\]site://(.*?)(#c\d+)?\[/url\]!s', '$1$2', $text); + $text = preg_replace('!\[url=((?:https?|ftp|irc|mailto)://.*?)\](.*?)\[/url\]!s', '$2', $text); + $text = preg_replace('!\[url\]((?:https?|ftp|irc|mailto)://.*?)\[/url\]!s', '$1', $text); + $text = preg_replace('!\[email\](.*?)\[/email\]!s', '$1', $text); + $text = preg_replace('!\[img\](https?:\/\/.*?)\[/img\]!s', 'user image', $text); + $text = preg_replace('!\[\[([^\|\]]+)\|([^\]]+)\]\]!s', '$2', $text); + $text = preg_replace('!\[\[([^\]]+)\]\]!s', '$1', $text); + $text = preg_replace("!\n\s*\n!", "\n\n", $text); + $text = str_replace("\n", "\n
    ", $text); + $text = preg_replace("/\[quote\](.*?)\[\/quote\]/s", "
    \\1
    ", $text); + $text = preg_replace("/\[quote=(.*?)\](.*?)\[\/quote\]/s", "
    \\1 said:
    \\2
    ", $text); + while (preg_match("/\[list\](.*?)\[\/list\]/s", $text)) { + $text = preg_replace("/\[list\](.*?)\[\/list\]/s", "
      \\1
    ", $text); + } + while (preg_match("/\[ul\](.*?)\[\/ul\]/s", $text)) { + $text = preg_replace("/\[ul\](.*?)\[\/ul\]/s", "
      \\1
    ", $text); + } + while (preg_match("/\[ol\](.*?)\[\/ol\]/s", $text)) { + $text = preg_replace("/\[ol\](.*?)\[\/ol\]/s", "
      \\1
    ", $text); + } + $text = preg_replace("/\[li\](.*?)\[\/li\]/s", "
  • \\1
  • ", $text); + $text = preg_replace("#\[\*\]#s", "
  • ", $text); + $text = preg_replace("#
    <(li|ul|ol|/ul|/ol)>#s", "<\\1>", $text); + $text = preg_replace("#\[align=(left|center|right)\](.*?)\[\/align\]#s", "
    \\2
    ", $text); + $text = $this->filter_spoiler($text); + $text = $this->insert_code($text); + return $text; + } - /** - * @param string $text - * @return string - */ - private function filter_spoiler(/*string*/ $text) { - return str_replace( - array("[spoiler]","[/spoiler]"), - array("",""), - $text); - } + public function strip(string $text): string + { + foreach ([ + "b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4", + "code", "url", "email", "li", + ] as $el) { + $text = preg_replace("!\[$el\](.*?)\[/$el\]!s", '$1', $text); + } + $text = preg_replace("!\[anchor=(.*?)\](.*?)\[/anchor\]!s", '$2', $text); + $text = preg_replace("!\[url=(.*?)\](.*?)\[/url\]!s", '$2', $text); + $text = preg_replace("!\[img\](.*?)\[/img\]!s", "", $text); + $text = preg_replace("!\[\[([^\|\]]+)\|([^\]]+)\]\]!s", '$2', $text); + $text = preg_replace("!\[\[([^\]]+)\]\]!s", '$1', $text); + $text = preg_replace("!\[quote\](.*?)\[/quote\]!s", "", $text); + $text = preg_replace("!\[quote=(.*?)\](.*?)\[/quote\]!s", "", $text); + $text = preg_replace("!\[/?(list|ul|ol)\]!", "", $text); + $text = preg_replace("!\[\*\](.*?)!s", '$1', $text); + $text = $this->strip_spoiler($text); + return $text; + } - /** - * @param string $text - * @return string - */ - private function strip_spoiler(/*string*/ $text) { - $l1 = strlen("[spoiler]"); - $l2 = strlen("[/spoiler]"); - while(true) { - $start = strpos($text, "[spoiler]"); - if($start === false) break; + private function filter_spoiler(string $text): string + { + return str_replace( + ["[spoiler]","[/spoiler]"], + ["",""], + $text + ); + } - $end = strpos($text, "[/spoiler]"); - if($end === false) break; + private function strip_spoiler(string $text): string + { + $l1 = strlen("[spoiler]"); + $l2 = strlen("[/spoiler]"); + while (true) { + $start = strpos($text, "[spoiler]"); + if ($start === false) { + break; + } - if($end < $start) break; + $end = strpos($text, "[/spoiler]"); + if ($end === false) { + break; + } - $beginning = substr($text, 0, $start); - $middle = str_rot13(substr($text, $start+$l1, ($end-$start-$l1))); - $ending = substr($text, $end + $l2, (strlen($text)-$end+$l2)); + if ($end < $start) { + break; + } - $text = $beginning . $middle . $ending; - } - return $text; - } + $beginning = substr($text, 0, $start); + $middle = str_rot13(substr($text, $start+$l1, ($end-$start-$l1))); + $ending = substr($text, $end + $l2, (strlen($text)-$end+$l2)); - /** - * @param string $text - * @return string - */ - private function extract_code(/*string*/ $text) { - # at the end of this function, the only code! blocks should be - # the ones we've added -- others may contain malicious content, - # which would only appear after decoding - $text = str_replace("[code!]", "[code]", $text); - $text = str_replace("[/code!]", "[/code]", $text); + $text = $beginning . $middle . $ending; + } + return $text; + } - $l1 = strlen("[code]"); - $l2 = strlen("[/code]"); - while(true) { - $start = strpos($text, "[code]"); - if($start === false) break; + private function extract_code(string $text): string + { + # at the end of this function, the only code! blocks should be + # the ones we've added -- others may contain malicious content, + # which would only appear after decoding + $text = str_replace("[code!]", "[code]", $text); + $text = str_replace("[/code!]", "[/code]", $text); - $end = strpos($text, "[/code]", $start); - if($end === false) break; + $l1 = strlen("[code]"); + $l2 = strlen("[/code]"); + while (true) { + $start = strpos($text, "[code]"); + if ($start === false) { + break; + } - if($end < $start) break; + $end = strpos($text, "[/code]", $start); + if ($end === false) { + break; + } - $beginning = substr($text, 0, $start); - $middle = base64_encode(substr($text, $start+$l1, ($end-$start-$l1))); - $ending = substr($text, $end + $l2, (strlen($text)-$end+$l2)); + if ($end < $start) { + break; + } - $text = $beginning . "[code!]" . $middle . "[/code!]" . $ending; - } - return $text; - } + $beginning = substr($text, 0, $start); + $middle = base64_encode(substr($text, $start+$l1, ($end-$start-$l1))); + $ending = substr($text, $end + $l2, (strlen($text)-$end+$l2)); - /** - * @param string $text - * @return string - */ - private function insert_code(/*string*/ $text) { - $l1 = strlen("[code!]"); - $l2 = strlen("[/code!]"); - while(true) { - $start = strpos($text, "[code!]"); - if($start === false) break; + $text = $beginning . "[code!]" . $middle . "[/code!]" . $ending; + } + return $text; + } - $end = strpos($text, "[/code!]"); - if($end === false) break; + private function insert_code(string $text): string + { + $l1 = strlen("[code!]"); + $l2 = strlen("[/code!]"); + while (true) { + $start = strpos($text, "[code!]"); + if ($start === false) { + break; + } - $beginning = substr($text, 0, $start); - $middle = base64_decode(substr($text, $start+$l1, ($end-$start-$l1))); - $ending = substr($text, $end + $l2, (strlen($text)-$end+$l2)); + $end = strpos($text, "[/code!]"); + if ($end === false) { + break; + } - $text = $beginning . "
    " . $middle . "
    " . $ending; - } - return $text; - } + $beginning = substr($text, 0, $start); + $middle = base64_decode(substr($text, $start+$l1, ($end-$start-$l1))); + $ending = substr($text, $end + $l2, (strlen($text)-$end+$l2)); + + $text = $beginning . "
    " . $middle . "
    " . $ending; + } + return $text; + } } - diff --git a/ext/bbcode/script.js b/ext/bbcode/script.js new file mode 100644 index 00000000..96c6ea7d --- /dev/null +++ b/ext/bbcode/script.js @@ -0,0 +1,18 @@ +document.addEventListener('DOMContentLoaded', () => { + $(".shm-clink").each(function(idx, elm) { + var target_id = $(elm).data("clink-sel"); + if(target_id && $(target_id).length > 0) { + // if the target comment is already on this page, don't bother + // switching pages + $(elm).attr("href", target_id); + // highlight it when clicked + $(elm).click(function(e) { + // This needs jQuery UI + $(target_id).highlight(); + }); + // vanilla target name should already be in the URL tag, but this + // will include the anon ID as displayed on screen + $(elm).html("@"+$(target_id+" .username").html()); + } + }); +}); diff --git a/ext/bbcode/test.php b/ext/bbcode/test.php index 9df81c0f..60f2b65b 100644 --- a/ext/bbcode/test.php +++ b/ext/bbcode/test.php @@ -1,85 +1,110 @@ -assertEquals( - $this->filter("[b]bold[/b][i]italic[/i]"), - "bolditalic"); - } +assertEquals( + $this->filter("[b]bold[/b][i]italic[/i]"), + "bolditalic" + ); + } - public function testStacking() { - $this->assertEquals( - $this->filter("[b]B[/b][i]I[/i][b]B[/b]"), - "BIB"); - $this->assertEquals( - $this->filter("[b]bold[i]bolditalic[/i]bold[/b]"), - "boldbolditalicbold"); - } + public function testStacking() + { + $this->assertEquals( + $this->filter("[b]B[/b][i]I[/i][b]B[/b]"), + "BIB" + ); + $this->assertEquals( + $this->filter("[b]bold[i]bolditalic[/i]bold[/b]"), + "boldbolditalicbold" + ); + } - public function testFailure() { - $this->assertEquals( - $this->filter("[b]bold[i]italic"), - "[b]bold[i]italic"); - } + public function testFailure() + { + $this->assertEquals( + $this->filter("[b]bold[i]italic"), + "[b]bold[i]italic" + ); + } - public function testCode() { - $this->assertEquals( - $this->filter("[code][b]bold[/b][/code]"), - "
    [b]bold[/b]
    "); - } + public function testCode() + { + $this->assertEquals( + $this->filter("[code][b]bold[/b][/code]"), + "
    [b]bold[/b]
    " + ); + } - public function testNestedList() { - $this->assertEquals( - $this->filter("[list][*]a[list][*]a[*]b[/list][*]b[/list]"), - "
    • a
      • a
      • b
    • b
    "); - $this->assertEquals( - $this->filter("[ul][*]a[ol][*]a[*]b[/ol][*]b[/ul]"), - "
    • a
      1. a
      2. b
    • b
    "); - } + public function testNestedList() + { + $this->assertEquals( + $this->filter("[list][*]a[list][*]a[*]b[/list][*]b[/list]"), + "
    • a
      • a
      • b
    • b
    " + ); + $this->assertEquals( + $this->filter("[ul][*]a[ol][*]a[*]b[/ol][*]b[/ul]"), + "
    • a
      1. a
      2. b
    • b
    " + ); + } - public function testSpoiler() { - $this->assertEquals( - $this->filter("[spoiler]ShishNet[/spoiler]"), - "ShishNet"); - $this->assertEquals( - $this->strip("[spoiler]ShishNet[/spoiler]"), - "FuvfuArg"); - #$this->assertEquals( - # $this->filter("[spoiler]ShishNet"), - # "[spoiler]ShishNet"); - } + public function testSpoiler() + { + $this->assertEquals( + $this->filter("[spoiler]ShishNet[/spoiler]"), + "ShishNet" + ); + $this->assertEquals( + $this->strip("[spoiler]ShishNet[/spoiler]"), + "FuvfuArg" + ); + #$this->assertEquals( + # $this->filter("[spoiler]ShishNet"), + # "[spoiler]ShishNet"); + } - public function testURL() { - $this->assertEquals( - $this->filter("[url]http://shishnet.org[/url]"), - "http://shishnet.org"); - $this->assertEquals( - $this->filter("[url=http://shishnet.org]ShishNet[/url]"), - "ShishNet"); - $this->assertEquals( - $this->filter("[url=javascript:alert(\"owned\")]click to fail[/url]"), - "[url=javascript:alert(\"owned\")]click to fail[/url]"); - } + public function testURL() + { + $this->assertEquals( + $this->filter("[url]http://shishnet.org[/url]"), + "http://shishnet.org" + ); + $this->assertEquals( + $this->filter("[url=http://shishnet.org]ShishNet[/url]"), + "ShishNet" + ); + $this->assertEquals( + $this->filter("[url=javascript:alert(\"owned\")]click to fail[/url]"), + "[url=javascript:alert(\"owned\")]click to fail[/url]" + ); + } - public function testEmailURL() { - $this->assertEquals( - $this->filter("[email]spam@shishnet.org[/email]"), - "spam@shishnet.org"); - } + public function testEmailURL() + { + $this->assertEquals( + $this->filter("[email]spam@shishnet.org[/email]"), + "spam@shishnet.org" + ); + } - public function testAnchor() { - $this->assertEquals( - $this->filter("[anchor=rules]Rules[/anchor]"), - 'Rules '); - } + public function testAnchor() + { + $this->assertEquals( + $this->filter("[anchor=rules]Rules[/anchor]"), + 'Rules ' + ); + } - private function filter($in) { - $bb = new BBCode(); - return $bb->format($in); - } + private function filter($in) + { + $bb = new BBCode(); + return $bb->format($in); + } - private function strip($in) { - $bb = new BBCode(); - return $bb->strip($in); - } + private function strip($in) + { + $bb = new BBCode(); + return $bb->strip($in); + } } - diff --git a/ext/blocks/info.php b/ext/blocks/info.php new file mode 100644 index 00000000..d5c34b9c --- /dev/null +++ b/ext/blocks/info.php @@ -0,0 +1,13 @@ + - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Add HTML to some space (News, Ads, etc) - */ +get_int("ext_blocks_version") < 1) { - $database->create_table("blocks", " +class Blocks extends Extension +{ + /** @var BlocksTheme */ + protected $theme; + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $database; + if ($this->get_version("ext_blocks_version") < 1) { + $database->create_table("blocks", " id SCORE_AIPK, pages VARCHAR(128) NOT NULL, title VARCHAR(128) NOT NULL, @@ -19,73 +17,82 @@ class Blocks extends Extension { priority INTEGER NOT NULL, content TEXT NOT NULL "); - $database->execute("CREATE INDEX blocks_pages_idx ON blocks(pages)", array()); - $config->set_int("ext_blocks_version", 1); - } - } + $database->execute("CREATE INDEX blocks_pages_idx ON blocks(pages)", []); + $this->set_version("ext_blocks_version", 1); + } + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("manage_blocks")) { - $event->add_link("Blocks Editor", make_link("blocks/list")); - } - } + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::MANAGE_BLOCKS)) { + $event->add_nav_link("blocks", new Link('blocks/list'), "Blocks Editor"); + } + } + } - public function onPageRequest(PageRequestEvent $event) { - global $database, $page, $user; + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::MANAGE_BLOCKS)) { + $event->add_link("Blocks Editor", make_link("blocks/list")); + } + } - $blocks = $database->cache->get("blocks"); - if($blocks === false) { - $blocks = $database->get_all("SELECT * FROM blocks"); - $database->cache->set("blocks", $blocks, 600); - } - foreach($blocks as $block) { - $path = implode("/", $event->args); - if(strlen($path) < 4000 && fnmatch($block['pages'], $path)) { - $b = new Block($block['title'], $block['content'], $block['area'], $block['priority']); - $b->is_content = false; - $page->add_block($b); - } - } + public function onPageRequest(PageRequestEvent $event) + { + global $cache, $database, $page, $user; - if($event->page_matches("blocks") && $user->can("manage_blocks")) { - if($event->get_arg(0) == "add") { - if($user->check_auth_token()) { - $database->execute(" + $blocks = $cache->get("blocks"); + if ($blocks === false) { + $blocks = $database->get_all("SELECT * FROM blocks"); + $cache->set("blocks", $blocks, 600); + } + foreach ($blocks as $block) { + $path = implode("/", $event->args); + if (strlen($path) < 4000 && fnmatch($block['pages'], $path)) { + $b = new Block($block['title'], $block['content'], $block['area'], (int)$block['priority']); + $b->is_content = false; + $page->add_block($b); + } + } + + if ($event->page_matches("blocks") && $user->can(Permissions::MANAGE_BLOCKS)) { + if ($event->get_arg(0) == "add") { + if ($user->check_auth_token()) { + $database->execute(" INSERT INTO blocks (pages, title, area, priority, content) - VALUES (?, ?, ?, ?, ?) - ", array($_POST['pages'], $_POST['title'], $_POST['area'], (int)$_POST['priority'], $_POST['content'])); - log_info("blocks", "Added Block #".($database->get_last_insert_id('blocks_id_seq'))." (".$_POST['title'].")"); - $database->cache->delete("blocks"); - $page->set_mode("redirect"); - $page->set_redirect(make_link("blocks/list")); - } - } - if($event->get_arg(0) == "update") { - if($user->check_auth_token()) { - if(!empty($_POST['delete'])) { - $database->execute(" + VALUES (:pages, :title, :area, :priority, :content) + ", ['pages'=>$_POST['pages'], 'title'=>$_POST['title'], 'area'=>$_POST['area'], 'priority'=>(int)$_POST['priority'], 'content'=>$_POST['content']]); + log_info("blocks", "Added Block #".($database->get_last_insert_id('blocks_id_seq'))." (".$_POST['title'].")"); + $cache->delete("blocks"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("blocks/list")); + } + } + if ($event->get_arg(0) == "update") { + if ($user->check_auth_token()) { + if (!empty($_POST['delete'])) { + $database->execute(" DELETE FROM blocks - WHERE id=? - ", array($_POST['id'])); - log_info("blocks", "Deleted Block #".$_POST['id']); - } - else { - $database->execute(" - UPDATE blocks SET pages=?, title=?, area=?, priority=?, content=? - WHERE id=? - ", array($_POST['pages'], $_POST['title'], $_POST['area'], (int)$_POST['priority'], $_POST['content'], $_POST['id'])); - log_info("blocks", "Updated Block #".$_POST['id']." (".$_POST['title'].")"); - } - $database->cache->delete("blocks"); - $page->set_mode("redirect"); - $page->set_redirect(make_link("blocks/list")); - } - } - else if($event->get_arg(0) == "list") { - $this->theme->display_blocks($database->get_all("SELECT * FROM blocks ORDER BY area, priority")); - } - } - } + WHERE id=:id + ", ['id'=>$_POST['id']]); + log_info("blocks", "Deleted Block #".$_POST['id']); + } else { + $database->execute(" + UPDATE blocks SET pages=:pages, title=:title, area=:area, priority=:priority, content=:content + WHERE id=:id + ", ['pages'=>$_POST['pages'], 'title'=>$_POST['title'], 'area'=>$_POST['area'], 'priority'=>(int)$_POST['priority'], 'content'=>$_POST['content'], 'id'=>$_POST['id']]); + log_info("blocks", "Updated Block #".$_POST['id']." (".$_POST['title'].")"); + } + $cache->delete("blocks"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("blocks/list")); + } + } elseif ($event->get_arg(0) == "list") { + $this->theme->display_blocks($database->get_all("SELECT * FROM blocks ORDER BY area, priority")); + } + } + } } - diff --git a/ext/blocks/test.php b/ext/blocks/test.php index e5681c4e..73c8e07b 100644 --- a/ext/blocks/test.php +++ b/ext/blocks/test.php @@ -1,10 +1,11 @@ -log_in_as_admin(); - $this->get_page("blocks/list"); - $this->assert_response(200); - $this->assert_title("Blocks"); - } +log_in_as_admin(); + $this->get_page("blocks/list"); + $this->assert_response(200); + $this->assert_title("Blocks"); + } } - diff --git a/ext/blocks/theme.php b/ext/blocks/theme.php index 8a490977..01661393 100644 --- a/ext/blocks/theme.php +++ b/ext/blocks/theme.php @@ -1,46 +1,68 @@ -"; - foreach($blocks as $block) { - $html .= make_form(make_link("blocks/update")); - $html .= ""; - $html .= ""; - $html .= "Title"; - $html .= "Area"; - $html .= "Priority"; - $html .= "Pages"; - $html .= "Delete"; - $html .= ""; - $html .= ""; - $html .= ""; - $html .= ""; - $html .= "\n"; - $html .= ""; - $html .= " "; - $html .= "\n"; - $html .= "\n"; - } - $html .= make_form(make_link("blocks/add")); - $html .= ""; - $html .= "Title"; - $html .= "Area"; - $html .= "Priority"; - $html .= "Pages"; - $html .= ""; - $html .= ""; - $html .= ""; - $html .= ""; - $html .= "\n"; - $html .= ""; - $html .= ""; +class BlocksTheme extends Themelet +{ + public function display_blocks($blocks) + { + global $page; - $page->set_title("Blocks"); - $page->set_heading("Blocks"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Block Editor", $html)); - } + $html = TABLE(["class"=>"form", "style"=>"width: 100%;"]); + foreach ($blocks as $block) { + $html->appendChild(SHM_SIMPLE_FORM( + "blocks/update", + TR( + INPUT(["type"=>"hidden", "name"=>"id", "value"=>$block['id']]), + TH("Title"), + TD(INPUT(["type"=>"text", "name"=>"title", "value"=>$block['title']])), + TH("Area"), + TD(INPUT(["type"=>"text", "name"=>"area", "value"=>$block['area']])), + TH("Priority"), + TD(INPUT(["type"=>"text", "name"=>"priority", "value"=>$block['priority']])), + TH("Pages"), + TD(INPUT(["type"=>"text", "name"=>"pages", "value"=>$block['pages']])), + TH("Delete"), + TD(INPUT(["type"=>"checkbox", "name"=>"delete"])), + TD(INPUT(["type"=>"submit", "value"=>"Save"])) + ), + TR( + TD(["colspan"=>"11"], TEXTAREA(["rows"=>"5", "name"=>"content"], $block['content'])) + ), + TR( + TD(["colspan"=>"11"], rawHTML(" ")) + ), + )); + } + + $html->appendChild(SHM_SIMPLE_FORM( + "blocks/add", + TR( + TH("Title"), + TD(INPUT(["type"=>"text", "name"=>"title", "value"=>""])), + TH("Area"), + TD(SELECT(["name"=>"area"], OPTION("left"), OPTION("main"))), + TH("Priority"), + TD(INPUT(["type"=>"text", "name"=>"priority", "value"=>'50'])), + TH("Pages"), + TD(INPUT(["type"=>"text", "name"=>"pages", "value"=>'post/list*'])), + TD(["colspan"=>'3'], INPUT(["type"=>"submit", "value"=>"Add"])) + ), + TR( + TD(["colspan"=>"11"], TEXTAREA(["rows"=>"5", "name"=>"content"])) + ), + )); + + $page->set_title("Blocks"); + $page->set_heading("Blocks"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Block Editor", (string)$html)); + } } - diff --git a/ext/blotter/info.php b/ext/blotter/info.php new file mode 100644 index 00000000..f5126e42 --- /dev/null +++ b/ext/blotter/info.php @@ -0,0 +1,16 @@ +"zach@sosguy.net"]; + public $license = self::LICENSE_GPLV2; + public $description = "Displays brief updates about whatever you want on every page. +Colors and positioning can be configured to match your site's design. + +Development TODO at http://github.com/zshall/shimmie2/issues"; +} diff --git a/ext/blotter/main.php b/ext/blotter/main.php index 645154ec..22c6dfb7 100644 --- a/ext/blotter/main.php +++ b/ext/blotter/main.php @@ -1,132 +1,156 @@ - [http://seemslegit.com/] - * License: GPLv2 - * Description: Displays brief updates about whatever you want on every page. - * Colors and positioning can be configured to match your site's design. - * - * Development TODO at http://github.com/zshall/shimmie2/issues - */ -class Blotter extends Extension { - public function onInitExt(InitExtEvent $event) { - /** - * I love re-using this installer don't I... - */ - global $config; - $version = $config->get_int("blotter_version", 0); - /** - * If this version is less than "1", it's time to install. - * - * REMINDER: If I change the database tables, I must change up version by 1. - */ - if($version < 1) { - /** - * Installer - */ - global $database, $config; - $database->create_table("blotter", " +set_default_int("blotter_recent", 5); + $config->set_default_string("blotter_color", "FF0000"); + $config->set_default_string("blotter_position", "subheading"); + } + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $config; + $version = $config->get_int("blotter_version", 0); + /** + * If this version is less than "1", it's time to install. + * + * REMINDER: If I change the database tables, I must change up version by 1. + */ + if ($version < 1) { + /** + * Installer + */ + global $database, $config; + $database->create_table("blotter", " id SCORE_AIPK, - entry_date SCORE_DATETIME DEFAULT SCORE_NOW, + entry_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, entry_text TEXT NOT NULL, important SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N "); - // Insert sample data: - $database->execute("INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), ?, ?)", - array("Installed the blotter extension!", "Y")); - log_info("blotter", "Installed tables for blotter extension."); - $config->set_int("blotter_version", 1); - } - // Set default config: - $config->set_default_int("blotter_recent", 5); - $config->set_default_string("blotter_color", "FF0000"); - $config->set_default_string("blotter_position", "subheading"); - } + // Insert sample data: + $database->execute( + "INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)", + ["text"=>"Installed the blotter extension!", "important"=>"Y"] + ); + log_info("blotter", "Installed tables for blotter extension."); + $config->set_int("blotter_version", 1); + } + } - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Blotter"); - $sb->add_int_option("blotter_recent", "
    Number of recent entries to display: "); - $sb->add_text_option("blotter_color", "
    Color of important updates: (ABCDEF format) "); - $sb->add_choice_option("blotter_position", array("Top of page" => "subheading", "In navigation bar" => "left"), "
    Position: "); - $event->panel->add_block($sb); - } + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Blotter"); + $sb->add_int_option("blotter_recent", "
    Number of recent entries to display: "); + $sb->add_text_option("blotter_color", "
    Color of important updates: (ABCDEF format) "); + $sb->add_choice_option("blotter_position", ["Top of page" => "subheading", "In navigation bar" => "left"], "
    Position: "); + $event->panel->add_block($sb); + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->is_admin()) { - $event->add_link("Blotter Editor", make_link("blotter/editor")); - } - } + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::BLOTTER_ADMIN)) { + $event->add_nav_link("blotter", new Link('blotter/editor'), "Blotter Editor"); + } + } + } - public function onPageRequest(PageRequestEvent $event) { - global $page, $database, $user; - if($event->page_matches("blotter")) { - switch($event->get_arg(0)) { - case "editor": - /** - * Displays the blotter editor. - */ - if(!$user->is_admin()) { - $this->theme->display_permission_denied(); - } else { - $entries = $database->get_all("SELECT * FROM blotter ORDER BY id DESC"); - $this->theme->display_editor($entries); - } - break; - case "add": - /** - * Adds an entry - */ - if(!$user->is_admin() || !$user->check_auth_token()) { - $this->theme->display_permission_denied(); - } else { - $entry_text = $_POST['entry_text']; - if($entry_text == "") { die("No entry message!"); } - if(isset($_POST['important'])) { $important = 'Y'; } else { $important = 'N'; } - // Now insert into db: - $database->execute("INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), ?, ?)", - array($entry_text, $important)); - log_info("blotter", "Added Message: $entry_text"); - $page->set_mode("redirect"); - $page->set_redirect(make_link("blotter/editor")); - } - break; - case "remove": - /** - * Removes an entry - */ - if(!$user->is_admin() || !$user->check_auth_token()) { - $this->theme->display_permission_denied(); - } else { - $id = int_escape($_POST['id']); - if(!isset($id)) { die("No ID!"); } - $database->Execute("DELETE FROM blotter WHERE id=:id", array("id"=>$id)); - log_info("blotter", "Removed Entry #$id"); - $page->set_mode("redirect"); - $page->set_redirect(make_link("blotter/editor")); - } - break; - case "list": - /** - * Displays all blotter entries - */ - $entries = $database->get_all("SELECT * FROM blotter ORDER BY id DESC"); - $this->theme->display_blotter_page($entries); - break; - } - } - /** - * Finally, display the blotter on whatever page we're viewing. - */ - $this->display_blotter(); - } - private function display_blotter() { - global $database, $config; - $limit = $config->get_int("blotter_recent", 5); - $sql = 'SELECT * FROM blotter ORDER BY id DESC LIMIT '.intval($limit); - $entries = $database->get_all($sql); - $this->theme->display_blotter($entries); - } + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::BLOTTER_ADMIN)) { + $event->add_link("Blotter Editor", make_link("blotter/editor")); + } + } + + public function onPageRequest(PageRequestEvent $event) + { + global $page, $database, $user; + if ($event->page_matches("blotter") && $event->count_args() > 0) { + switch ($event->get_arg(0)) { + case "editor": + /** + * Displays the blotter editor. + */ + if (!$user->can(Permissions::BLOTTER_ADMIN)) { + $this->theme->display_permission_denied(); + } else { + $entries = $database->get_all("SELECT * FROM blotter ORDER BY id DESC"); + $this->theme->display_editor($entries); + } + break; + case "add": + /** + * Adds an entry + */ + if (!$user->can(Permissions::BLOTTER_ADMIN) || !$user->check_auth_token()) { + $this->theme->display_permission_denied(); + } else { + $entry_text = $_POST['entry_text']; + if ($entry_text == "") { + die("No entry message!"); + } + if (isset($_POST['important'])) { + $important = 'Y'; + } else { + $important = 'N'; + } + // Now insert into db: + $database->execute( + "INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)", + ["text"=>$entry_text, "important"=>$important] + ); + log_info("blotter", "Added Message: $entry_text"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("blotter/editor")); + } + break; + case "remove": + /** + * Removes an entry + */ + if (!$user->can(Permissions::BLOTTER_ADMIN) || !$user->check_auth_token()) { + $this->theme->display_permission_denied(); + } else { + $id = int_escape($_POST['id']); + if (!isset($id)) { + die("No ID!"); + } + $database->Execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]); + log_info("blotter", "Removed Entry #$id"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("blotter/editor")); + } + break; + case "list": + /** + * Displays all blotter entries + */ + $entries = $database->get_all("SELECT * FROM blotter ORDER BY id DESC"); + $this->theme->display_blotter_page($entries); + break; + } + } + /** + * Finally, display the blotter on whatever page we're viewing. + */ + $this->display_blotter(); + } + + private function display_blotter() + { + global $database, $config; + $limit = $config->get_int("blotter_recent", 5); + $sql = 'SELECT * FROM blotter ORDER BY id DESC LIMIT '.intval($limit); + $entries = $database->get_all($sql); + $this->theme->display_blotter($entries); + } } - diff --git a/ext/blotter/script.js b/ext/blotter/script.js index dfffcf35..2623f3e4 100644 --- a/ext/blotter/script.js +++ b/ext/blotter/script.js @@ -1,6 +1,6 @@ /*jshint bitwise:true, curly:true, forin:false, noarg:true, noempty:true, nonew:true, undef:true, strict:false, browser:true, jquery:true */ -$(document).ready(function() { +document.addEventListener('DOMContentLoaded', () => { $(".shm-blotter2-toggle").click(function() { $(".shm-blotter2").slideToggle("slow", function() { if($(".shm-blotter2").is(":hidden")) { diff --git a/ext/blotter/test.php b/ext/blotter/test.php index eafec499..b512f2b0 100644 --- a/ext/blotter/test.php +++ b/ext/blotter/test.php @@ -1,34 +1,31 @@ -log_in_as_admin(); - //$this->assert_text("Blotter Editor"); - //$this->click("Blotter Editor"); - //$this->log_out(); - } +get_page("blotter/editor"); + $this->assert_response(403); + $this->get_page("blotter/add"); + $this->assert_response(403); + $this->get_page("blotter/remove"); + $this->assert_response(403); + } - public function testDenial() { - $this->get_page("blotter/editor"); - $this->assert_response(403); - $this->get_page("blotter/add"); - $this->assert_response(403); - $this->get_page("blotter/remove"); - $this->assert_response(403); - } + public function testAddViewRemove() + { + $this->log_in_as_admin(); - public function testAddViewRemove() { - $this->log_in_as_admin(); + $page = $this->get_page("blotter/editor"); + $this->assertEquals(200, $page->code); + //$this->set_field("entry_text", "blotter testing"); + //$this->click("Add"); + //$this->assert_text("blotter testing"); - $this->get_page("blotter/editor"); - //$this->set_field("entry_text", "blotter testing"); - //$this->click("Add"); - //$this->assert_text("blotter testing"); + $this->get_page("blotter"); + //$this->assert_text("blotter testing"); - $this->get_page("blotter"); - //$this->assert_text("blotter testing"); - - $this->get_page("blotter/editor"); - //$this->click("Remove"); - //$this->assert_no_text("blotter testing"); - } + $this->get_page("blotter/editor"); + //$this->click("Remove"); + //$this->assert_no_text("blotter testing"); + } } diff --git a/ext/blotter/theme.php b/ext/blotter/theme.php index ba274cf8..c1d38d37 100644 --- a/ext/blotter/theme.php +++ b/ext/blotter/theme.php @@ -1,45 +1,50 @@ -get_html_for_blotter_editor($entries); - $page->set_title("Blotter Editor"); - $page->set_heading("Blotter Editor"); - $page->add_block(new Block("Welcome to the Blotter Editor!", $html, "main", 10)); - $page->add_block(new Block("Navigation", "Index", "left", 0)); - } +get_html_for_blotter_editor($entries); + $page->set_title("Blotter Editor"); + $page->set_heading("Blotter Editor"); + $page->add_block(new Block("Welcome to the Blotter Editor!", $html, "main", 10)); + $page->add_block(new Block("Navigation", "Index", "left", 0)); + } - public function display_blotter_page($entries) { - global $page; - $html = $this->get_html_for_blotter_page($entries); - $page->set_title("Blotter"); - $page->set_heading("Blotter"); - $page->add_block(new Block("Blotter Entries", $html, "main", 10)); - } + public function display_blotter_page($entries) + { + global $page; + $html = $this->get_html_for_blotter_page($entries); + $page->set_title("Blotter"); + $page->set_heading("Blotter"); + $page->add_block(new Block("Blotter Entries", $html, "main", 10)); + } - public function display_blotter($entries) { - global $page, $config; - $html = $this->get_html_for_blotter($entries); - $position = $config->get_string("blotter_position", "subheading"); - $page->add_block(new Block(null, $html, $position, 20)); - } + public function display_blotter($entries) + { + global $page, $config; + $html = $this->get_html_for_blotter($entries); + $position = $config->get_string("blotter_position", "subheading"); + $page->add_block(new Block(null, $html, $position, 20)); + } - private function get_html_for_blotter_editor($entries) { - global $user; + private function get_html_for_blotter_editor($entries) + { + global $user; - /** - * Long function name, but at least I won't confuse it with something else ^_^ - */ + /** + * Long function name, but at least I won't confuse it with something else ^_^ + */ - // Add_new stuff goes here. - $table_header = " + // Add_new stuff goes here. + $table_header = " Date Message Important? Action "; - $add_new = " + $add_new = " ".make_form(make_link("blotter/add"))." @@ -49,21 +54,25 @@ class BlotterTheme extends Themelet { "; - // Now, time for entries list. - $table_rows = ""; - $num_entries = count($entries); - for ($i = 0 ; $i < $num_entries ; $i++) { - /** - * Add table rows - */ - $id = $entries[$i]['id']; - $entry_date = $entries[$i]['entry_date']; - $entry_text = $entries[$i]['entry_text']; - if($entries[$i]['important'] == 'Y') { $important = 'Y'; } else { $important = 'N'; } + // Now, time for entries list. + $table_rows = ""; + $num_entries = count($entries); + for ($i = 0 ; $i < $num_entries ; $i++) { + /** + * Add table rows + */ + $id = $entries[$i]['id']; + $entry_date = $entries[$i]['entry_date']; + $entry_text = $entries[$i]['entry_text']; + if ($entries[$i]['important'] == 'Y') { + $important = 'Y'; + } else { + $important = 'N'; + } - // Add the new table row(s) - $table_rows .= - " + // Add the new table row(s) + $table_rows .= + " $entry_date $entry_text $important @@ -74,9 +83,9 @@ class BlotterTheme extends Themelet { "; - } + } - $html = " + $html = " $table_header$add_new @@ -87,82 +96,83 @@ class BlotterTheme extends Themelet { Help:
    Add entries to the blotter, and they will be displayed.
    "; - return $html; - } + return $html; + } - private function get_html_for_blotter_page($entries) { - /** - * This one displays a list of all blotter entries. - */ - global $config; - $i_color = $config->get_string("blotter_color", "#FF0000"); - $html = "
    ";
    +    private function get_html_for_blotter_page($entries)
    +    {
    +        /**
    +         * This one displays a list of all blotter entries.
    +         */
    +        global $config;
    +        $i_color = $config->get_string("blotter_color", "#FF0000");
    +        $html = "
    ";
     
    -		$num_entries = count($entries);
    -		for ($i = 0 ; $i < $num_entries ; $i++) {
    -			/**
    -			 * Blotter entries
    -			 */
    -			// Reset variables:
    -			$i_open = "";
    -			$i_close = "";
    -			//$id = $entries[$i]['id'];
    -			$messy_date = $entries[$i]['entry_date'];
    -			$clean_date = date("y/m/d", strtotime($messy_date));
    -			$entry_text = $entries[$i]['entry_text'];
    -			if($entries[$i]['important'] == 'Y') {
    -				$i_open = "";
    -				$i_close="";
    -			}
    -			$html .= "{$i_open}{$clean_date} - {$entry_text}{$i_close}

    "; - } - $html .= "
    "; - return $html; - } + $num_entries = count($entries); + for ($i = 0 ; $i < $num_entries ; $i++) { + /** + * Blotter entries + */ + // Reset variables: + $i_open = ""; + $i_close = ""; + //$id = $entries[$i]['id']; + $messy_date = $entries[$i]['entry_date']; + $clean_date = date("y/m/d", strtotime($messy_date)); + $entry_text = $entries[$i]['entry_text']; + if ($entries[$i]['important'] == 'Y') { + $i_open = ""; + $i_close=""; + } + $html .= "{$i_open}{$clean_date} - {$entry_text}{$i_close}

    "; + } + $html .= "
    "; + return $html; + } - private function get_html_for_blotter($entries) { - global $config; - $i_color = $config->get_string("blotter_color", "#FF0000"); - $position = $config->get_string("blotter_position", "subheading"); - $entries_list = ""; - $num_entries = count($entries); - for ($i = 0 ; $i < $num_entries ; $i++) { - /** - * Blotter entries - */ - // Reset variables: - $i_open = ""; - $i_close = ""; - //$id = $entries[$i]['id']; - $messy_date = $entries[$i]['entry_date']; - $clean_date = date("m/d/y", strtotime($messy_date)); - $entry_text = $entries[$i]['entry_text']; - if($entries[$i]['important'] == 'Y') { - $i_open = ""; - $i_close=""; - } - $entries_list .= "
  • {$i_open}{$clean_date} - {$entry_text}{$i_close}
  • "; - } + private function get_html_for_blotter($entries) + { + global $config; + $i_color = $config->get_string("blotter_color", "#FF0000"); + $position = $config->get_string("blotter_position", "subheading"); + $entries_list = ""; + $num_entries = count($entries); + for ($i = 0 ; $i < $num_entries ; $i++) { + /** + * Blotter entries + */ + // Reset variables: + $i_open = ""; + $i_close = ""; + //$id = $entries[$i]['id']; + $messy_date = $entries[$i]['entry_date']; + $clean_date = date("m/d/y", strtotime($messy_date)); + $entry_text = $entries[$i]['entry_text']; + if ($entries[$i]['important'] == 'Y') { + $i_open = ""; + $i_close=""; + } + $entries_list .= "
  • {$i_open}{$clean_date} - {$entry_text}{$i_close}
  • "; + } - $pos_break = ""; - $pos_align = "text-align: right; position: absolute; right: 0px;"; + $pos_break = ""; + $pos_align = "text-align: right; position: absolute; right: 0px;"; - if($position === "left") { - $pos_break = "
    "; - $pos_align = ""; - } + if ($position === "left") { + $pos_break = "
    "; + $pos_align = ""; + } - if(count($entries) === 0) { - $out_text = "No blotter entries yet."; - $in_text = "Empty."; - } - else { - $clean_date = date("m/d/y", strtotime($entries[0]['entry_date'])); - $out_text = "Blotter updated: {$clean_date}"; - $in_text = "
      $entries_list
    "; - } + if (count($entries) === 0) { + $out_text = "No blotter entries yet."; + $in_text = "Empty."; + } else { + $clean_date = date("m/d/y", strtotime($entries[0]['entry_date'])); + $out_text = "Blotter updated: {$clean_date}"; + $in_text = "
      $entries_list
    "; + } - $html = " + $html = "
    $out_text {$pos_break} @@ -173,6 +183,6 @@ class BlotterTheme extends Themelet {
    $in_text
    "; - return $html; - } + return $html; + } } diff --git a/ext/browser_search/info.php b/ext/browser_search/info.php new file mode 100644 index 00000000..35154b48 --- /dev/null +++ b/ext/browser_search/info.php @@ -0,0 +1,18 @@ +"atg@atravelinggeek.com"]; + public $license = self::LICENSE_GPLV2; + public $version = "0.1c, October 26, 2007"; + public $description = "Allows the user to add a browser 'plugin' to search the site with real-time suggestions"; + public $documentation = +"Once installed, users with an opensearch compatible browser should see their search box light up with whatever \"click here to add a search engine\" notification they have + +Some code (and lots of help) by Artanis (Erik Youngren ) from the 'tagger' extension - Used with permission"; +} diff --git a/ext/browser_search/main.php b/ext/browser_search/main.php index 719dddfc..d29326e0 100644 --- a/ext/browser_search/main.php +++ b/ext/browser_search/main.php @@ -1,43 +1,33 @@ - - * Some code (and lots of help) by Artanis (Erik Youngren ) from the 'tagger' extention - Used with permission - * Link: http://atravelinggeek.com/ - * License: GPLv2 - * Description: Allows the user to add a browser 'plugin' to search the site with real-time suggestions - * Version: 0.1c, October 26, 2007 - * Documentation: - * Once installed, users with an opensearch compatible browser should see - * their search box light up with whatever "click here to add a search - * engine" notification they have - */ +set_default_string("search_suggestions_results_order", 'a'); - } +class BrowserSearch extends Extension +{ + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_string("search_suggestions_results_order", 'a'); + } - public function onPageRequest(PageRequestEvent $event) { - global $config, $database, $page; + public function onPageRequest(PageRequestEvent $event) + { + global $config, $database, $page; - // Add in header code to let the browser know that the search plugin exists - // We need to build the data for the header - $search_title = $config->get_string('title'); - $search_file_url = make_link('browser_search/please_dont_use_this_tag_as_it_would_break_stuff__search.xml'); - $page->add_html_header(""); + // Add in header code to let the browser know that the search plugin exists + // We need to build the data for the header + $search_title = $config->get_string(SetupConfig::TITLE); + $search_file_url = make_link('browser_search.xml'); + $page->add_html_header(""); - // The search.xml file that is generated on the fly - if($event->page_matches("browser_search/please_dont_use_this_tag_as_it_would_break_stuff__search.xml")) { - // First, we need to build all the variables we'll need - $search_title = $config->get_string('title'); - $search_form_url = make_link('post/list/{searchTerms}'); - $suggenton_url = make_link('browser_search/')."{searchTerms}"; - $icon_b64 = base64_encode(file_get_contents("lib/static/favicon.ico")); + // The search.xml file that is generated on the fly + if ($event->page_matches("browser_search.xml")) { + // First, we need to build all the variables we'll need + $search_title = $config->get_string(SetupConfig::TITLE); + $search_form_url = make_link('post/list/{searchTerms}'); + $suggenton_url = make_link('browser_search/')."{searchTerms}"; + $icon_b64 = base64_encode(file_get_contents("ext/static_files/static/favicon.ico")); - // Now for the XML - $xml = " + // Now for the XML + $xml = " $search_title UTF-8 @@ -50,55 +40,46 @@ class BrowserSearch extends Extension { "; - // And now to send it to the browser - $page->set_mode("data"); - $page->set_type("text/xml"); - $page->set_data($xml); - } + // And now to send it to the browser + $page->set_mode(PageMode::DATA); + $page->set_type("text/xml"); + $page->set_data($xml); + } elseif ($event->page_matches("browser_search")) { + $suggestions = $config->get_string("search_suggestions_results_order"); + if ($suggestions == "n") { + return; + } - else if( - $event->page_matches("browser_search") && - !$config->get_bool("disable_search_suggestions") - ) { - // We have to build some json stuff - $tag_search = $event->get_arg(0); + // We have to build some json stuff + $tag_search = $event->get_arg(0); - // Now to get DB results - if($config->get_string("search_suggestions_results_order") == "a") { - $tags = $database->execute("SELECT tag FROM tags WHERE tag LIKE ? AND count > 0 ORDER BY tag ASC LIMIT 30",array($tag_search."%")); - } else { - $tags = $database->execute("SELECT tag FROM tags WHERE tag LIKE ? AND count > 0 ORDER BY count DESC LIMIT 30",array($tag_search."%")); - } + // Now to get DB results + if ($suggestions == "a") { + $order = "tag ASC"; + } else { + $order = "count DESC"; + } + $tags = $database->get_col( + "SELECT tag FROM tags WHERE tag LIKE :tag AND count > 0 ORDER BY $order LIMIT 30", + ['tag'=>$tag_search."%"] + ); + // And to do stuff with it. We want our output to look like: + // ["shimmie",["shimmies","shimmy","shimmie","21 shimmies","hip shimmies","skea shimmies"],[],[]] + $page->set_mode(PageMode::DATA); + $page->set_data(json_encode([$tag_search, $tags, [], []])); + } + } - // And to do stuff with it. We want our output to look like: - // ["shimmie",["shimmies","shimmy","shimmie","21 shimmies","hip shimmies","skea shimmies"],[],[]] - $json_tag_list = ""; + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sort_by = []; + $sort_by['Alphabetical'] = 'a'; + $sort_by['Tag Count'] = 't'; + $sort_by['Disabled'] = 'n'; - $tags_array = array(); - foreach($tags as $tag) { - array_push($tags_array,$tag['tag']); - } - - $json_tag_list .= implode("\",\"", $tags_array); - - // And now for the final output - $json_string = "[\"$tag_search\",[\"$json_tag_list\"],[],[]]"; - $page->set_mode("data"); - $page->set_data($json_string); - } - } - - public function onSetupBuilding(SetupBuildingEvent $event) { - $sort_by = array(); - $sort_by['Alphabetical'] = 'a'; - $sort_by['Tag Count'] = 't'; - - $sb = new SetupBlock("Browser Search"); - $sb->add_bool_option("disable_search_suggestions", "Disable search suggestions: "); - $sb->add_label("
    "); - $sb->add_choice_option("search_suggestions_results_order", $sort_by, "Sort the suggestions by:"); - $event->panel->add_block($sb); - } + $sb = new SetupBlock("Browser Search"); + $sb->add_choice_option("search_suggestions_results_order", $sort_by, "Sort the suggestions by:"); + $event->panel->add_block($sb); + } } - diff --git a/ext/browser_search/test.php b/ext/browser_search/test.php index 3d77f423..5fae365f 100644 --- a/ext/browser_search/test.php +++ b/ext/browser_search/test.php @@ -1,8 +1,12 @@ -get_page("browser_search/please_dont_use_this_tag_as_it_would_break_stuff__search.xml"); - $this->get_page("browser_search/test"); - } -} +get_page("browser_search.xml"); + $this->assertEquals(200, $page->code); + $page = $this->get_page("browser_search/test"); + $this->assertEquals(200, $page->code); + } +} diff --git a/ext/bulk_actions/info.php b/ext/bulk_actions/info.php new file mode 100644 index 00000000..fc25998d --- /dev/null +++ b/ext/bulk_actions/info.php @@ -0,0 +1,13 @@ +"matthew@darkholme.net"]; + public $license = self::LICENSE_WTFPL; + public $description = "Provides query and selection-based bulk action support"; + public $documentation = "Provides bulk action section in list view. Allows performing actions against a set of images based on query or manual selection. Based on Mass Tagger by Christian Walde , contributions by Shish and Agasa."; +} diff --git a/ext/bulk_actions/main.php b/ext/bulk_actions/main.php new file mode 100644 index 00000000..3e217c7e --- /dev/null +++ b/ext/bulk_actions/main.php @@ -0,0 +1,290 @@ +actions as $existing) { + if ($existing["access_key"]==$access_key) { + throw new SCoreException("Access key $access_key is already in use"); + } + } + } + + $this->actions[] =[ + "block" => $block, + "access_key" => $access_key, + "confirmation_message" => $confirmation_message, + "action" => $action, + "button_text" => $button_text, + "position" => $position + ]; + } +} + +class BulkActionEvent extends Event +{ + /** @var string */ + public $action; + /** @var array */ + public $items; + + public function __construct(String $action, Generator $items) + { + parent::__construct(); + $this->action = $action; + $this->items = $items; + } +} + +class BulkActions extends Extension +{ + /** @var BulkActionsTheme */ + protected $theme; + + public function onPostListBuilding(PostListBuildingEvent $event) + { + global $page, $user; + + if ($user->is_logged_in()) { + $babbe = new BulkActionBlockBuildingEvent(); + $babbe->search_terms = $event->search_terms; + + send_event($babbe); + + if (sizeof($babbe->actions) == 0) { + return; + } + + usort($babbe->actions, [$this, "sort_blocks"]); + + $this->theme->display_selector($page, $babbe->actions, Tag::implode($event->search_terms)); + } + } + + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) + { + global $user; + + if ($user->can(Permissions::DELETE_IMAGE)) { + $event->add_action("bulk_delete", "(D)elete", "d", "Delete selected images?", $this->theme->render_ban_reason_input(), 10); + } + + if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) { + $event->add_action( + "bulk_tag", + "Tag", + "t", + "", + $this->theme->render_tag_input(), + 10 + ); + } + + if ($user->can(Permissions::BULK_EDIT_IMAGE_SOURCE)) { + $event->add_action("bulk_source", "Set (S)ource", "s", "", $this->theme->render_source_input(), 10); + } + } + + public function onCommand(CommandEvent $event) + { + if ($event->cmd == "help") { + print "\tbulk-action \n"; + print "\t\tperform an action on all query results\n\n"; + } + if ($event->cmd == "bulk-action") { + if (count($event->args) < 2) { + return; + } + $action = $event->args[0]; + $query = $event->args[1]; + $items = $this->yield_search_results($query); + log_info("bulk_actions", "Performing $action on {$event->args[1]}"); + send_event(new BulkActionEvent($event->args[0], $items)); + } + } + + public function onBulkAction(BulkActionEvent $event) + { + global $page, $user; + + switch ($event->action) { + case "bulk_delete": + if ($user->can(Permissions::DELETE_IMAGE)) { + $i = $this->delete_items($event->items); + $page->flash("Deleted $i items"); + } + break; + case "bulk_tag": + if (!isset($_POST['bulk_tags'])) { + return; + } + if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) { + $tags = $_POST['bulk_tags']; + $replace = false; + if (isset($_POST['bulk_tags_replace']) && $_POST['bulk_tags_replace'] == "true") { + $replace = true; + } + + $i= $this->tag_items($event->items, $tags, $replace); + $page->flash("Tagged $i items"); + } + break; + case "bulk_source": + if (!isset($_POST['bulk_source'])) { + return; + } + if ($user->can(Permissions::BULK_EDIT_IMAGE_SOURCE)) { + $source = $_POST['bulk_source']; + $i = $this->set_source($event->items, $source); + $page->flash("Set source for $i items"); + } + break; + } + } + + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; + if ($event->page_matches("bulk_action") && $user->can(Permissions::PERFORM_BULK_ACTIONS)) { + if (!isset($_POST['bulk_action'])) { + return; + } + + $action = $_POST['bulk_action']; + + $items = null; + if (isset($_POST['bulk_selected_ids']) && $_POST['bulk_selected_ids'] != "") { + $data = json_decode($_POST['bulk_selected_ids']); + if (is_array($data)&&!empty($data)) { + $items = $this->yield_items($data); + } + } elseif (isset($_POST['bulk_query']) && $_POST['bulk_query'] != "") { + $query = $_POST['bulk_query']; + if ($query != null && $query != "") { + $items = $this->yield_search_results($query); + } + } + + if (is_iterable($items)) { + send_event(new BulkActionEvent($action, $items)); + } + + $page->set_mode(PageMode::REDIRECT); + if (!isset($_SERVER['HTTP_REFERER'])) { + $_SERVER['HTTP_REFERER'] = make_link(); + } + $page->set_redirect($_SERVER['HTTP_REFERER']); + } + } + + private function yield_items(array $data): Generator + { + foreach ($data as $id) { + if (is_numeric($id)) { + $image = Image::by_id($id); + if ($image!=null) { + yield $image; + } + } + } + } + + private function yield_search_results(string $query): Generator + { + $tags = Tag::explode($query); + return Image::find_images_iterable(0, null, $tags); + } + + private function sort_blocks($a, $b) + { + return $a["position"] - $b["position"]; + } + + private function delete_items(iterable $items): int + { + global $page; + $total = 0; + foreach ($items as $image) { + try { + if (class_exists("ImageBan") && isset($_POST['bulk_ban_reason'])) { + $reason = $_POST['bulk_ban_reason']; + if ($reason) { + send_event(new AddImageHashBanEvent($image->hash, $reason)); + } + } + send_event(new ImageDeletionEvent($image)); + $total++; + } catch (Exception $e) { + $page->flash("Error while removing {$image->id}: " . $e->getMessage()); + } + } + return $total; + } + + private function tag_items(iterable $items, string $tags, bool $replace): int + { + $tags = Tag::explode($tags); + + $pos_tag_array = []; + $neg_tag_array = []; + foreach ($tags as $new_tag) { + if (strpos($new_tag, '-') === 0) { + $neg_tag_array[] = substr($new_tag, 1); + } else { + $pos_tag_array[] = $new_tag; + } + } + + $total = 0; + if ($replace) { + foreach ($items as $image) { + send_event(new TagSetEvent($image, $tags)); + $total++; + } + } else { + foreach ($items as $image) { + $img_tags = array_map("strtolower", $image->get_tag_array()); + + if (!empty($neg_tag_array)) { + $neg_tag_array = array_map("strtolower", $neg_tag_array); + + $img_tags = array_merge($pos_tag_array, $img_tags); + $img_tags = array_diff($img_tags, $neg_tag_array); + } else { + $img_tags = array_merge($tags, $img_tags); + } + send_event(new TagSetEvent($image, $img_tags)); + $total++; + } + } + + return $total; + } + + private function set_source(iterable $items, String $source): int + { + global $page; + $total = 0; + foreach ($items as $image) { + try { + send_event(new SourceSetEvent($image, $source)); + $total++; + } catch (Exception $e) { + $page->flash("Error while setting source for {$image->id}: " . $e->getMessage()); + } + } + return $total; + } +} diff --git a/ext/bulk_actions/script.js b/ext/bulk_actions/script.js new file mode 100644 index 00000000..5e0b9423 --- /dev/null +++ b/ext/bulk_actions/script.js @@ -0,0 +1,195 @@ +/*jshint bitwise:true, curly:true, forin:false, noarg:true, noempty:true, nonew:true, undef:true, strict:false, browser:true, jquery:true */ + +var bulk_selector_active = false; +var bulk_selector_initialized = false; +var bulk_selector_valid = false; + +function validate_selections(form, confirmationMessage) { + var queryOnly = false; + if(bulk_selector_active) { + var data = get_selected_items(); + if(data.length==0) { + return false; + } + } else { + var query = $(form).find('input[name="bulk_query"]').val(); + + if (query == null || query == "") { + return false; + } else { + queryOnly = true; + } + } + + + if(confirmationMessage!=null&&confirmationMessage!="") { + return confirm(confirmationMessage); + } else if(queryOnly) { + var action = $(form).find('input[name="submit_button"]').val(); + + return confirm("Perform bulk action \"" + action + "\" on all images matching the current search?"); + } + + return true; +} + + +function activate_bulk_selector () { + set_selected_items([]); + if(!bulk_selector_initialized) { + $(".shm-thumb").each( + function (index, block) { + add_selector_button($(block)); + } + ); + } + $('#bulk_selector_controls').show(); + $('#bulk_selector_activate').hide(); + bulk_selector_active = true; + bulk_selector_initialized = true; +} + +function deactivate_bulk_selector() { + set_selected_items([]); + $('#bulk_selector_controls').hide(); + $('#bulk_selector_activate').show(); + bulk_selector_active = false; +} + +function get_selected_items() { + var data = $('#bulk_selected_ids').val(); + if(data==""||data==null) { + data = []; + } else { + data = JSON.parse(data); + } + return data; +} + +function set_selected_items(items) { + $(".shm-thumb").removeClass('selected'); + + $(items).each( + function(index,item) { + $('.shm-thumb[data-post-id="' + item + '"]').addClass('selected'); + } + ); + + $('input[name="bulk_selected_ids"]').val(JSON.stringify(items)); +} + +function select_item(id) { + var data = get_selected_items(); + if(!data.includes(id)) + data.push(id); + set_selected_items(data); +} + +function deselect_item(id) { + var data = get_selected_items(); + if(data.includes(id)) + data.splice(data.indexOf(id, 1)); + set_selected_items(data); +} + +function toggle_selection( id ) { + var data = get_selected_items(); + console.log(id); + if(data.includes(id)) { + data.splice(data.indexOf(id),1); + set_selected_items(data); + return false; + } else { + data.push(id); + set_selected_items(data); + return true; + } +} + + +function select_all() { + var items = []; + $(".shm-thumb").each( + function ( index, block ) { + block = $(block); + var id = block.data("post-id"); + items.push(id); + } + ); + set_selected_items(items); +} + +function select_invert() { + var currentItems = get_selected_items(); + var items = []; + $(".shm-thumb").each( + function ( index, block ) { + block = $(block); + var id = block.data("post-id"); + if(!currentItems.includes(id)) { + items.push(id); + } + } + ); + set_selected_items(items); +} + +function select_none() { + set_selected_items([]); +} + +function select_range(start, end) { + var data = get_selected_items(); + var selecting = false; + $(".shm-thumb").each( + function ( index, block ) { + block = $(block); + var id = block.data("post-id"); + if(id==start) + selecting = true; + + if(selecting) { + if(!data.includes(id)) + data.push(id); + } + + if(id==end) { + selecting = false; + } + } + ); + set_selected_items(data); +} + +var last_clicked_item; + +function add_selector_button($block) { + var c = function(e) { + if(!bulk_selector_active) + return true; + + e.preventDefault(); + e.stopPropagation(); + + var id = $block.data("post-id"); + if(e.shiftKey) { + if(last_clicked_item { + // Clear the selection, in case it was autocompleted by the browser. + $('#bulk_selected_ids').val(""); +}); diff --git a/ext/bulk_actions/style.css b/ext/bulk_actions/style.css new file mode 100644 index 00000000..4e7449fc --- /dev/null +++ b/ext/bulk_actions/style.css @@ -0,0 +1,10 @@ +.selected { + outline: 3px solid blue; +} + +.bulk_action { + margin-top: 8pt; +} +.bulk_selector_controls table td { + width: 33%; +} \ No newline at end of file diff --git a/ext/bulk_actions/theme.php b/ext/bulk_actions/theme.php new file mode 100644 index 00000000..52b3f24c --- /dev/null +++ b/ext/bulk_actions/theme.php @@ -0,0 +1,67 @@ + + +
    + + + + + +
    + "; + + $hasQuery = ($query != null && $query != ""); + + if ($hasQuery) { + $body .= ""; + } + + foreach ($actions as $action) { + $body .= "
    " . make_form(make_link("bulk_action"), "POST", false, "", "return validate_selections(this,'" . html_escape($action["confirmation_message"]) . "');") . + "" . + "" . + "" . + $action["block"] . + "" . + "
    "; + } + + if (!$hasQuery) { + $body .= ""; + } + $block = new Block("Bulk Actions", $body, "left", 30); + $page->add_block($block); + } + + public function render_ban_reason_input() + { + if (class_exists("ImageBan")) { + return ""; + } else { + return ""; + } + } + + public function render_tag_input() + { + return "" . + ""; + } + + public function render_source_input() + { + return ""; + } +} diff --git a/ext/bulk_add/info.php b/ext/bulk_add/info.php new file mode 100644 index 00000000..568b333b --- /dev/null +++ b/ext/bulk_add/info.php @@ -0,0 +1,22 @@ +/home/bob/uploads/holiday/2008/ and point + shimmie at /home/bob/uploads, then images will be + tagged \"holiday 2008\") +

    Note: requires the \"admin\" extension to be enabled +"; +} diff --git a/ext/bulk_add/main.php b/ext/bulk_add/main.php index fa532526..022589b6 100644 --- a/ext/bulk_add/main.php +++ b/ext/bulk_add/main.php @@ -1,74 +1,64 @@ - - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Bulk add server-side images - * Documentation: - * Upload the images into a new directory via ftp or similar, go to - * shimmie's admin page and put that directory in the bulk add box. - * If there are subdirectories, they get used as tags (eg if you - * upload into /home/bob/uploads/holiday/2008/ and point - * shimmie at /home/bob/uploads, then images will be - * tagged "holiday 2008") - *

    Note: requires the "admin" extension to be enabled - */ +dir = $dir; - $this->results = array(); - } + public function __construct(string $dir) + { + parent::__construct(); + $this->dir = $dir; + $this->results = []; + } } -class BulkAdd extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; - if($event->page_matches("bulk_add")) { - if($user->is_admin() && $user->check_auth_token() && isset($_POST['dir'])) { - set_time_limit(0); - $bae = new BulkAddEvent($_POST['dir']); - send_event($bae); - if(is_array($bae->results)) { - foreach($bae->results as $result) { - $this->theme->add_status("Adding files", $result); - } - } else if(strlen($bae->results) > 0) { - $this->theme->add_status("Adding files", $bae->results); - } - $this->theme->display_upload_results($page); - } - } - } +class BulkAdd extends Extension +{ + /** @var BulkAddTheme */ + protected $theme; - public function onCommand(CommandEvent $event) { - if($event->cmd == "help") { - print "\tbulk-add [directory]\n"; - print "\t\tImport this directory\n\n"; - } - if($event->cmd == "bulk-add") { - if(count($event->args) == 1) { - $bae = new BulkAddEvent($event->args[0]); - send_event($bae); - print(implode("\n", $bae->results)); - } - } - } + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; + if ($event->page_matches("bulk_add")) { + if ($user->can(Permissions::BULK_ADD) && $user->check_auth_token() && isset($_POST['dir'])) { + set_time_limit(0); + $bae = send_event(new BulkAddEvent($_POST['dir'])); + foreach ($bae->results as $result) { + $this->theme->add_status("Adding files", $result); + } + $this->theme->display_upload_results($page); + } + } + } - public function onAdminBuilding(AdminBuildingEvent $event) { - $this->theme->display_admin_block(); - } + public function onCommand(CommandEvent $event) + { + if ($event->cmd == "help") { + print "\tbulk-add [directory]\n"; + print "\t\tImport this directory\n\n"; + } + if ($event->cmd == "bulk-add") { + if (count($event->args) == 1) { + $bae = send_event(new BulkAddEvent($event->args[0])); + print(implode("\n", $bae->results)); + } + } + } - public function onBulkAdd(BulkAddEvent $event) { - if(is_dir($event->dir) && is_readable($event->dir)) { - $event->results = add_dir($event->dir); - } - else { - $h_dir = html_escape($event->dir); - $event->results[] = "Error, $h_dir is not a readable directory"; - } - } + public function onAdminBuilding(AdminBuildingEvent $event) + { + $this->theme->display_admin_block(); + } + + public function onBulkAdd(BulkAddEvent $event) + { + if (is_dir($event->dir) && is_readable($event->dir)) { + $event->results = add_dir($event->dir); + } else { + $h_dir = html_escape($event->dir); + $event->results[] = "Error, $h_dir is not a readable directory"; + } + } } diff --git a/ext/bulk_add/test.php b/ext/bulk_add/test.php index 5ecb7de1..48dad449 100644 --- a/ext/bulk_add/test.php +++ b/ext/bulk_add/test.php @@ -1,37 +1,23 @@ -log_in_as_admin(); +get_page('admin'); - $this->assert_title("Admin Tools"); +class BulkAddTest extends ShimmiePHPUnitTestCase +{ + public function testInvalidDir() + { + send_event(new UserLoginEvent(User::by_name(self::$admin_name))); + $bae = send_event(new BulkAddEvent('asdf')); + $this->assertContains( + "Error, asdf is not a readable directory", + $bae->results, + implode("\n", $bae->results) + ); + } - $bae = new BulkAddEvent('asdf'); - send_event($bae); - $this->assertContains("Error, asdf is not a readable directory", - $bae->results, implode("\n", $bae->results)); - - // FIXME: have BAE return a list of successes as well as errors? - $this->markTestIncomplete(); - - $this->get_page('admin'); - $this->assert_title("Admin Tools"); - send_event(new BulkAddEvent('tests')); - - # FIXME: test that the output here makes sense, no "adding foo.php ... ok" - - $this->get_page("post/list/hash=17fc89f372ed3636e28bd25cc7f3bac1/1"); - $this->assert_title(new PatternExpectation("/^Image \d+: data/")); - $this->click("Delete"); - - $this->get_page("post/list/hash=feb01bab5698a11dd87416724c7a89e3/1"); - $this->assert_title(new PatternExpectation("/^Image \d+: data/")); - $this->click("Delete"); - - $this->get_page("post/list/hash=e106ea2983e1b77f11e00c0c54e53805/1"); - $this->assert_title(new PatternExpectation("/^Image \d+: data/")); - $this->click("Delete"); - - $this->log_out(); - } + public function testValidDir() + { + send_event(new UserLoginEvent(User::by_name(self::$admin_name))); + send_event(new BulkAddEvent('tests')); + $page = $this->get_page("post/list/hash=17fc89f372ed3636e28bd25cc7f3bac1/1"); + $this->assertEquals(302, $page->code); + } } diff --git a/ext/bulk_add/theme.php b/ext/bulk_add/theme.php index 98cd9b7f..bfdcf5c4 100644 --- a/ext/bulk_add/theme.php +++ b/ext/bulk_add/theme.php @@ -1,30 +1,33 @@ -set_title("Adding folder"); - $page->set_heading("Adding folder"); - $page->add_block(new NavBlock()); - $html = ""; - foreach($this->messages as $block) { - $html .= "
    " . $block->body; - } - $page->add_block(new Block("Results", $html)); - } + /* + * Show a standard page for results to be put into + */ + public function display_upload_results(Page $page) + { + $page->set_title("Adding folder"); + $page->set_heading("Adding folder"); + $page->add_block(new NavBlock()); + $html = ""; + foreach ($this->messages as $block) { + $html .= "
    " . $block->body; + } + $page->add_block(new Block("Results", $html)); + } - /* - * Add a section to the admin page. This should contain a form which - * links to bulk_add with POST[dir] set to the name of a server-side - * directory full of images - */ - public function display_admin_block() { - global $page; - $html = " + /* + * Add a section to the admin page. This should contain a form which + * links to bulk_add with POST[dir] set to the name of a server-side + * directory full of images + */ + public function display_admin_block() + { + global $page; + $html = " Add a folder full of images; any subfolders will have their names used as tags for the images within.
    Note: this is the folder as seen by the server -- you need to @@ -37,10 +40,11 @@ class BulkAddTheme extends Themelet { "; - $page->add_block(new Block("Bulk Add", $html)); - } + $page->add_block(new Block("Bulk Add", $html)); + } - public function add_status($title, $body) { - $this->messages[] = new Block($title, $body); - } + public function add_status($title, $body) + { + $this->messages[] = new Block($title, $body); + } } diff --git a/ext/bulk_add_csv/info.php b/ext/bulk_add_csv/info.php new file mode 100644 index 00000000..35f72e1f --- /dev/null +++ b/ext/bulk_add_csv/info.php @@ -0,0 +1,24 @@ +"velocity37@gmail.com"]; + public $license = self::LICENSE_GPLV2; + public $description = "Bulk add server-side images with metadata from CSV file"; + public $documentation = +"Modification of \"Bulk Add\" by Shish.

    +Adds images from a CSV with the five following values:
    +\"/path/to/image.jpg\",\"spaced tags\",\"source\",\"rating s/q/e\",\"/path/thumbnail.jpg\"
    +e.g. \"/tmp/cat.png\",\"shish oekaki\",\"shimmie.shishnet.org\",\"s\",\"tmp/custom.jpg\"

    +Any value but the first may be omitted, but there must be five values per line.
    +e.g. \"/why/not/try/bulk_add.jpg\",\"\",\"\",\"\",\"\"

    +Image thumbnails will be displayed at the AR of the full image. Thumbnails that are +normally static (e.g. SWF) will be displayed at the board's max thumbnail size

    +Useful for importing tagged images without having to do database manipulation.
    +

    Note: requires \"Admin Controls\" and optionally \"Image Ratings\" to be enabled

    "; +} diff --git a/ext/bulk_add_csv/main.php b/ext/bulk_add_csv/main.php index a5b999e7..5816dded 100644 --- a/ext/bulk_add_csv/main.php +++ b/ext/bulk_add_csv/main.php @@ -1,147 +1,125 @@ - - * License: GPLv2 - * Description: Bulk add server-side images with metadata from CSV file - * Documentation: - * Modification of "Bulk Add" by Shish.

    - * Adds images from a CSV with the five following values:
    - * "/path/to/image.jpg","spaced tags","source","rating s/q/e","/path/thumbnail.jpg"
    - * e.g. "/tmp/cat.png","shish oekaki","shimmie.shishnet.org","s","tmp/custom.jpg"

    - * Any value but the first may be omitted, but there must be five values per line.
    - * e.g. "/why/not/try/bulk_add.jpg","","","",""

    - * Image thumbnails will be displayed at the AR of the full image. Thumbnails that are - * normally static (e.g. SWF) will be displayed at the board's max thumbnail size

    - * Useful for importing tagged images without having to do database manipulation.
    - *

    Note: requires "Admin Controls" and optionally "Image Ratings" to be enabled

    - * - */ +page_matches("bulk_add_csv")) { - if($user->is_admin() && $user->check_auth_token() && isset($_POST['csv'])) { - set_time_limit(0); - $this->add_csv($_POST['csv']); - $this->theme->display_upload_results($page); - } - } - } +class BulkAddCSV extends Extension +{ + /** @var BulkAddCSVTheme */ + protected $theme; - public function onCommand(CommandEvent $event) { - if($event->cmd == "help") { - print " bulk-add-csv [/path/to.csv]\n"; - print " Import this .csv file (refer to documentation)\n\n"; - } - if($event->cmd == "bulk-add-csv") { - global $user; - - //Nag until CLI is admin by default - if (!$user->is_admin()) { - print "Not running as an admin, which can cause problems.\n"; - print "Please add the parameter: -u admin_username"; - } elseif(count($event->args) == 1) { - $this->add_csv($event->args[0]); - } - } - } + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; + if ($event->page_matches("bulk_add_csv")) { + if ($user->can(Permissions::BULK_ADD) && $user->check_auth_token() && isset($_POST['csv'])) { + set_time_limit(0); + $this->add_csv($_POST['csv']); + $this->theme->display_upload_results($page); + } + } + } - public function onAdminBuilding(AdminBuildingEvent $event) { - $this->theme->display_admin_block(); - } + public function onCommand(CommandEvent $event) + { + if ($event->cmd == "help") { + print " bulk-add-csv [/path/to.csv]\n"; + print " Import this .csv file (refer to documentation)\n\n"; + } + if ($event->cmd == "bulk-add-csv") { + global $user; - /** - * Generate the necessary DataUploadEvent for a given image and tags. - * - * @param string $tmpname - * @param string $filename - * @param string $tags - * @param string $source - * @param string $rating - * @param string $thumbfile - * @throws UploadException - */ - private function add_image($tmpname, $filename, $tags, $source, $rating, $thumbfile) { - assert(file_exists($tmpname)); + //Nag until CLI is admin by default + if (!$user->can(Permissions::BULK_ADD)) { + print "Not running as an admin, which can cause problems.\n"; + print "Please add the parameter: -u admin_username"; + } elseif (count($event->args) == 1) { + $this->add_csv($event->args[0]); + } + } + } - $pathinfo = pathinfo($filename); - if(!array_key_exists('extension', $pathinfo)) { - throw new UploadException("File has no extension"); - } - $metadata = array(); - $metadata['filename'] = $pathinfo['basename']; - $metadata['extension'] = $pathinfo['extension']; - $metadata['tags'] = Tag::explode($tags); - $metadata['source'] = $source; - $event = new DataUploadEvent($tmpname, $metadata); - send_event($event); - if($event->image_id == -1) { - throw new UploadException("File type not recognised"); - } else { - if(class_exists("RatingSetEvent") && in_array($rating, array("s", "q", "e"))) { - $ratingevent = new RatingSetEvent(Image::by_id($event->image_id), $rating); - send_event($ratingevent); - } - if (file_exists($thumbfile)) { - copy($thumbfile, warehouse_path("thumbs", $event->hash)); - } - } - } + public function onAdminBuilding(AdminBuildingEvent $event) + { + $this->theme->display_admin_block(); + } - private function add_csv(/*string*/ $csvfile) { - if(!file_exists($csvfile)) { - $this->theme->add_status("Error", "$csvfile not found"); - return; - } - if (!is_file($csvfile) || strtolower(substr($csvfile, -4)) != ".csv") { - $this->theme->add_status("Error", "$csvfile doesn't appear to be a csv file"); - return; - } - - $linenum = 1; - $list = ""; - $csvhandle = fopen($csvfile, "r"); - - while (($csvdata = fgetcsv($csvhandle, 0, ",")) !== FALSE) { - if(count($csvdata) != 5) { - if(strlen($list) > 0) { - $this->theme->add_status("Error", "Encountered malformed data. Line $linenum $csvfile
    ".$list); - fclose($csvhandle); - return; - } else { - $this->theme->add_status("Error", "Encountered malformed data. Line $linenum $csvfile
    Check here for the expected format"); - fclose($csvhandle); - return; - } - } - $fullpath = $csvdata[0]; - $tags = trim($csvdata[1]); - $source = $csvdata[2]; - $rating = $csvdata[3]; - $thumbfile = $csvdata[4]; - $pathinfo = pathinfo($fullpath); - $shortpath = $pathinfo["basename"]; - $list .= "
    ".html_escape("$shortpath (".str_replace(" ", ", ", $tags).")... "); - if (file_exists($csvdata[0]) && is_file($csvdata[0])) { - try{ - $this->add_image($fullpath, $pathinfo["basename"], $tags, $source, $rating, $thumbfile); - $list .= "ok\n"; - } - catch(Exception $ex) { - $list .= "failed:
    ". $ex->getMessage(); - } - } else { - $list .= "failed:
    File doesn't exist ".html_escape($csvdata[0]); - } - $linenum += 1; - } - - if(strlen($list) > 0) { - $this->theme->add_status("Adding $csvfile", $list); - } - fclose($csvhandle); - } + /** + * Generate the necessary DataUploadEvent for a given image and tags. + */ + private function add_image(string $tmpname, string $filename, string $tags, string $source, string $rating, string $thumbfile) + { + 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'] = $source; + $event = send_event(new DataUploadEvent($tmpname, $metadata)); + if ($event->image_id == -1) { + throw new UploadException("File type not recognised"); + } else { + if (class_exists("RatingSetEvent") && in_array($rating, ["s", "q", "e"])) { + send_event(new RatingSetEvent(Image::by_id($event->image_id), $rating)); + } + if (file_exists($thumbfile)) { + copy($thumbfile, warehouse_path(Image::THUMBNAIL_DIR, $event->hash)); + } + } + } + + private function add_csv(string $csvfile) + { + if (!file_exists($csvfile)) { + $this->theme->add_status("Error", "$csvfile not found"); + return; + } + if (!is_file($csvfile) || strtolower(substr($csvfile, -4)) != ".csv") { + $this->theme->add_status("Error", "$csvfile doesn't appear to be a csv file"); + return; + } + + $linenum = 1; + $list = ""; + $csvhandle = fopen($csvfile, "r"); + + while (($csvdata = fgetcsv($csvhandle, 0, ",")) !== false) { + if (count($csvdata) != 5) { + if (strlen($list) > 0) { + $this->theme->add_status("Error", "Encountered malformed data. Line $linenum $csvfile
    ".$list); + fclose($csvhandle); + return; + } else { + $this->theme->add_status("Error", "Encountered malformed data. Line $linenum $csvfile
    Check here for the expected format"); + fclose($csvhandle); + return; + } + } + $fullpath = $csvdata[0]; + $tags = trim($csvdata[1]); + $source = $csvdata[2]; + $rating = $csvdata[3]; + $thumbfile = $csvdata[4]; + $pathinfo = pathinfo($fullpath); + $shortpath = $pathinfo["basename"]; + $list .= "
    ".html_escape("$shortpath (".str_replace(" ", ", ", $tags).")... "); + if (file_exists($csvdata[0]) && is_file($csvdata[0])) { + try { + $this->add_image($fullpath, $pathinfo["basename"], $tags, $source, $rating, $thumbfile); + $list .= "ok\n"; + } catch (Exception $ex) { + $list .= "failed:
    ". $ex->getMessage(); + } + } else { + $list .= "failed:
    File doesn't exist ".html_escape($csvdata[0]); + } + $linenum += 1; + } + + if (strlen($list) > 0) { + $this->theme->add_status("Adding $csvfile", $list); + } + fclose($csvhandle); + } } - diff --git a/ext/bulk_add_csv/theme.php b/ext/bulk_add_csv/theme.php index 88fcc41d..b552b07d 100644 --- a/ext/bulk_add_csv/theme.php +++ b/ext/bulk_add_csv/theme.php @@ -1,28 +1,31 @@ -set_title("Adding images from csv"); - $page->set_heading("Adding images from csv"); - $page->add_block(new NavBlock()); - foreach($this->messages as $block) { - $page->add_block($block); - } - } + /* + * Show a standard page for results to be put into + */ + public function display_upload_results(Page $page) + { + $page->set_title("Adding images from csv"); + $page->set_heading("Adding images from csv"); + $page->add_block(new NavBlock()); + foreach ($this->messages as $block) { + $page->add_block($block); + } + } - /* - * Add a section to the admin page. This should contain a form which - * links to bulk_add_csv with POST[csv] set to the name of a server-side - * csv file - */ - public function display_admin_block() { - global $page; - $html = " + /* + * Add a section to the admin page. This should contain a form which + * links to bulk_add_csv with POST[csv] set to the name of a server-side + * csv file + */ + public function display_admin_block() + { + global $page; + $html = " Add images from a csv. Images will be tagged and have their source and rating set (if \"Image Ratings\" is enabled)
    Specify the absolute or relative path to a local .csv file. Check here for the expected format. @@ -34,11 +37,11 @@ class BulkAddCSVTheme extends Themelet { "; - $page->add_block(new Block("Bulk Add CSV", $html)); - } + $page->add_block(new Block("Bulk Add CSV", $html)); + } - public function add_status($title, $body) { - $this->messages[] = new Block($title, $body); - } + public function add_status($title, $body) + { + $this->messages[] = new Block($title, $body); + } } - diff --git a/ext/bulk_remove/main.php b/ext/bulk_remove/main.php deleted file mode 100644 index 592d85f6..00000000 --- a/ext/bulk_remove/main.php +++ /dev/null @@ -1,133 +0,0 @@ - - * Link: http://www.drudexsoftware.com/ - * License: GPLv2 - * Description: Allows admin to delete many images at once through Board Admin. - * Documentation: - * - */ -//todo: removal by tag returns 1 less image in test for some reason, actually a combined search doesn't seem to work for shit either - -class BulkRemove extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $user; - if($event->page_matches("bulk_remove") && $user->is_admin() && $user->check_auth_token()) { - if ($event->get_arg(0) == "confirm") $this->do_bulk_remove(); - else $this->show_confirm(); - } - } - - public function onAdminBuilding(AdminBuildingEvent $event) { - global $page; - $html = "Be extremely careful when using this!
    - Once an image is removed there is no way to recover it so it is recommended that - you first take when removing a large amount of images.
    - Note: Entering both an ID range and tags will only remove images between the given ID's that have the given tags. - -

    ".make_form(make_link("bulk_remove"))." - - - - - - - - -
    Remove images by ID
    From
    Until
    Where tags are
    - -
    - - "; - $page->add_block(new Block("Bulk Remove", $html)); - } - - // returns a list of images to be removed - private function determine_images() - { - // set vars - $images_for_removal = array(); - $error = ""; - - $min_id = $_POST['remove_id_min']; - $max_id = $_POST['remove_id_max']; - $tags = $_POST['remove_tags']; - - - // if using id range to remove (comined removal with tags) - if ($min_id != "" && $max_id != "") - { - // error if values are not correctly entered - if (!is_numeric($min_id) || !is_numeric($max_id) || - intval($max_id) < intval($min_id)) - $error = "Values not correctly entered for removal between id."; - - else { // if min & max id are valid - - // Grab the list of images & place it in the removing array - foreach (Image::find_images(intval($min_id), intval($max_id)) as $image) - array_push($images_for_removal, $image); - } - } - - // refine previous results or create results from tags - if ($tags != "") - { - $tags_arr = explode(" ", $_POST['remove_tags']); - - // Search all images with the specified tags & add to list - foreach (Image::find_images(1, 2147483647, $tags_arr) as $image) - array_push($images_for_removal, $image); - } - - - // if no images were found with the given info - if (count($images_for_removal) == 0) - $error = "No images selected for removal"; - - //var_dump($tags_arr); - return array( - "error" => $error, - "images_for_removal" => $images_for_removal); - } - - // displays confirmation to admin before removal - private function show_confirm() - { - global $page; - - // set vars - $determined_imgs = $this->determine_images(); - $error = $determined_imgs["error"]; - $images_for_removal = $determined_imgs["images_for_removal"]; - - // if there was an error in determine_images() - if ($error != "") { - $page->add_block(new Block("Cannot remove images", $error)); - return; - } - // generates the image array & places it in $_POST["bulk_remove_images"] - $_POST["bulk_remove_images"] = $images_for_removal; - - // Display confirmation message - $html = make_form(make_link("bulk_remove")). - "Are you sure you want to PERMANENTLY remove ". - count($images_for_removal) ." images?
    "; - $page->add_block(new Block("Confirm Removal", $html)); - } - - private function do_bulk_remove() - { - global $page; - // display error if user didn't go through admin board - if (!isset($_POST["bulk_remove_images"])) { - $page->add_block(new Block("Bulk Remove Error", - "Please use Board Admin to use bulk remove.")); - } - - // - $image_arr = $_POST["bulk_remove_images"]; - } -} - diff --git a/ext/chatbox/cp/ajax.php b/ext/chatbox/cp/ajax.php deleted file mode 100644 index f682649f..00000000 --- a/ext/chatbox/cp/ajax.php +++ /dev/null @@ -1,457 +0,0 @@ - false, - 'html' => cp() - ); - - echo json_encode($result); - return; - } - - login(md5($_POST['password'])); - $result = array(); - if (loggedIn()) { - $result['error'] = false; - $result['html'] = cp(); - } else - $result['error'] = 'invalid'; - - echo json_encode($result); -} - -function doLogout() { - logout(); - - $result = array( - 'error' => false - ); - - echo json_encode($result); -} - -function doUnban() { - global $kioskMode; - - if ($kioskMode) { - $result = array( - 'error' => false - ); - - echo json_encode($result); - return; - } - - if (!loggedIn()) return; - - $ys = ys(); - $result = array(); - - $ip = $_POST['ip']; - - if ($ys->banned($ip)) { - $ys->unban($ip); - $result['error'] = false; - } else - $result['error'] = 'notbanned'; - - - echo json_encode($result); -} - -function doUnbanAll() { - global $kioskMode; - - if ($kioskMode) { - $result = array( - 'error' => false - ); - - echo json_encode($result); - return; - } - - if (!loggedIn()) return; - - $ys = ys(); - $ys->unbanAll(); - - $result = array( - 'error' => false - ); - - echo json_encode($result); -} - - -function doSetPreference() { - global $prefs, $kioskMode; - - if ($kioskMode) { - $result = array( - 'error' => false - ); - - echo json_encode($result); - return; - } - - if (!loggedIn()) return; - - $pref = $_POST['preference']; - $value = magic($_POST['value']); - - if ($value === 'true') $value = true; - if ($value === 'false') $value = false; - - $prefs[$pref] = $value; - - savePrefs($prefs); - - if ($pref == 'password') login(md5($value)); - - $result = array( - 'error' => false - ); - - echo json_encode($result); -} - - -function doResetPreferences() { - global $prefs, $kioskMode; - - if ($kioskMode) { - $result = array( - 'error' => false - ); - - echo json_encode($result); - return; - } - - if (!loggedIn()) return; - - resetPrefs(); - login(md5($prefs['password'])); - - // $prefs['password'] = 'lol no'; - $result = array( - 'error' => false, - 'prefs' => $prefs - ); - - echo json_encode($result); -} - -/* CP Display */ - -function cp() { - global $kioskMode; - - if (!loggedIn() && !$kioskMode) return 'You\'re not logged in!'; - - return ' - -

    - -
    -

    YShout.Preferences

    - Logout -
    - - - - ' . preferencesForm() . ' -
    - -
    -
    -

    YShout.About

    - Logout -
    - - - - ' . about() . ' -
    - -
    -
    -

    YShout.Bans

    - Logout -
    - - - - ' . bansList() . ' - -
    '; -} - -function bansList() { - global $kioskMode; - - $ys = ys(); - $bans = $ys->bans(); - - $html = '
      '; - - $hasBans = false; - foreach($bans as $ban) { - $hasBans = true; - $html .= ' -
    • - ' . $ban['nickname']. ' - (' . ($kioskMode ? '[No IP in Kiosk Mode]' : $ban['ip']) . ') - Unban -
    • - '; - } - - if (!$hasBans) - $html = '

      No one is banned.

      '; - else - $html .= '
    '; - - return $html; -} - -function preferencesForm() { - global $prefs, $kioskMode; - - return ' -
    -
    -
    -
    Control Panel
    -
      -
    1. - - -
    2. -
    -
    - -
    -
    Flood Control
    -
      -
    1. - - -
    2. -
    3. - - -
    4. -
    5. - - -
    6. -
    7. - - -
    8. -
    9. - - -
    10. -
    -
    - -
    -
    History
    -
      -
    1. - - -
    2. -
    3. - - -
    4. -
    -
    - -
    -
    Miscellaneous
    -
      -
    1. - - -
    2. -
    3. - - -
    4. -
    -
    -
    - -
    -
    -
    Form
    -
      -
    1. - - -
    2. -
    3. - - -
    4. -
    5. - - -
    6. -
    7. - - -
    8. -
    9. - - -
    10. -
    11. - - -
    12. -
    13. - - -
    14. -
    15. - - -
    16. -
    -
    - -
    -
    Shouts
    -
      -
    1. - - -
    2. -
    3. - - -
    4. -
    5. - - -
    6. -
    7. - - -
    8. -
    9. - - -
    10. -
    -
    -
    -
    - '; -} - -function about() { - global $prefs; - - $html = ' -
    -

    About YShout

    -

    YShout was created and developed by Yuri Vishnevsky. Version 5 is the first one with an about page, so you\'ll have to excuse the lack of appropriate information — I\'m not quite sure what it is that goes on "About" pages anyway.

    -

    Other than that obviously important tidbit of information, there\'s really nothing else that I can think of putting here... If anyone knows what a good and proper about page should contain, please contact me! -

    - -
    -

    Contact Yuri

    -

    If you have any questions or comments, you can contact me by email at yurivish@gmail.com, or on AIM at yurivish42.

    -

    I hope you\'ve enjoyed using YShout!

    -
    - '; - - - return $html; -} - diff --git a/ext/chatbox/cp/css/style.css b/ext/chatbox/cp/css/style.css deleted file mode 100644 index b37a885e..00000000 --- a/ext/chatbox/cp/css/style.css +++ /dev/null @@ -1,386 +0,0 @@ -* { - margin: 0; - padding: 0; -} - -html, body {height: 100%;} - -body { - background: #1a1a1a url(../images/bg.gif) center center no-repeat; - color: #a7a7a7; - font: 11px/1 Tahoma, Arial, sans-serif; - text-shadow: 0 0 0 #273541; - overflow: hidden; -} - -a { - outline: none; - color: #fff; - text-decoration: none; -} - -a:hover{ - color: #fff; -} - -input { - font-size: 11px; - background: #e5e5e5; - border: 1px solid #f5f5f5; - padding: 2px; -} - -select { - font-size: 11px; -} - -#cp { - height: 440px; - width: 620px; - position: absolute; - top: 50%; - left: 50%; - margin-top: -220px; - margin-left: -310px; -} - -#nav { - height: 65px; - width: 100%; - background: url(../images/bg-nav.gif) repeat-x; - position: absolute; - bottom: 0; -} - - #nav ul { - display: none; - width: 240px; - height: 65px; - margin: 0 auto; - list-style: none; - } - - #nav li { - width: 80px; - float: left; - text-align: center; - } - - #nav a { - display: block; - height: 65px; - text-indent: -4200px; - outline: none; - } - - #nav a:active { - background-position: 0 -65px; - } - - #n-prefs a { background: 0 0 url("../images/n-prefs.gif") no-repeat; } - #n-bans a { background: 0 0 url("../images/n-bans.gif") no-repeat; } - #n-about a { background: 0 0 url("../images/n-about.gif") no-repeat; } - -.subnav { - height: 25px; - background: url(../images/bg-subnav.gif) repeat-x; - list-style: none; -} - - .subnav input { - float: left; - margin-top: 2px; - margin-right: 10px; - } - - .subnav li { - width: 85px; - float: left; - text-indent: -4200px; - } - - .subnav a { - display: block; - height: 25px; - } - - .subnav a:hover { - background-position: bottom left !important; - } - - #sn-administration a { background: url(../images/sn-administration.gif) no-repeat; } - #sn-display a { background: url(../images/sn-display.gif) no-repeat; } - #sn-form a { background: url(../images/sn-form.gif) no-repeat; } - #sn-resetall a { background: url(../images/sn-resetall.gif) no-repeat; } - #sn-ban a { background: url(../images/sn-ban.gif) no-repeat; } - #sn-unbanall a { background: url(../images/sn-unbanall.gif) no-repeat; } - #sn-deleteall a { background: url(../images/sn-deleteall.gif) no-repeat; } - #sn-about a { background: url(../images/sn-about.gif) no-repeat; } - #sn-contact a { background: url(../images/sn-contact.gif) no-repeat; } - - - - .sn-loading { - display: block; - height: 25px; - width: 25px; - float: right; - text-indent: -4200px; - background: url(../images/sn-spinny.gif) no-repeat; - _position: absolute; - _right: 20px; - _top: 50px; - } - - @media { .sn-loading { - position: absolute; - right: 15px; - top: 41px; - }} - -#content { - position: relative; - height: 375px; - overflow: hidden; -} - - .header { - height: 33px; - padding-bottom: 2px; - border-bottom: 1px solid #444; - } - - #login .header { border-bottom: 1px solid #4c657b; } - - h1 { - float: left; - height: 32px; - width: 185px; - text-indent: -4200px; - } - - #login h1 { background: url(../images/h-login.gif) no-repeat; } - #preferences h1 { background: url(../images/h-preferences.gif) no-repeat; } - #bans h1 { background: url(../images/h-bans.gif) no-repeat; } - #about h1 { background: url(../images/h-about.gif) no-repeat; } - - .logout { - display: block; - height: 32px; - width: 45px; - float: right; - text-indent: -4200px; - background: url(../images/a-logout.gif) no-repeat; - } - - .logout:hover { - background-position: bottom left; - } - - .section { - clear: both; - width: 590px; - height: 355px; - padding: 15px; - padding-top: 5px; - position: absolute; - } - -#login { - left: 0; - background: url(../images/bg-login.gif) repeat-x; - z-index: 5; -} - - #login-form { - height: 45px; - width: 300px; - position: absolute; - top: 50%; - left: 50%; - margin-top: -45px; - margin-left: -150px; - background: url(../images/bg-login-form.gif) no-repeat; - } - - #login-form label { - display: none; - } - - #login-form input { - position: absolute; - left: 127px; - top: 13px; - width: 153px; - z-index: 2; - border: 1px solid #d4e7fa; - background: #e7eef6; - } - - #login-loading { - display: block; - position: absolute; - top: 12px; - right: 8px; - height: 25px; - width: 25px; - text-indent: -4200px; - background: url(../images/login-spinny.gif) no-repeat; - z-index: 1; - } - -#preferences { - left: 0; - background: url(../images/bg-prefs.gif) repeat-x; -} - - #preferences-form { } - - #preferences-form fieldset { - margin-top: 10px; - width: 295px; - border: none; - } - - #preferences-form fieldset.odd { - float: right; - } - - #preferences-form fieldset.even { - float: left; - } - - #preferences-form .legend { - display: block; - width: 265px; - color: #fff; - padding-bottom: 3px; - border-bottom: 1px solid #80a147; - } - - /* IE7 */ - @media {#preferences-form legend { - margin-left: -7px; - }} - - #preferences-form ol { - list-style: none; - margin-top: 15px; - } - - #preferences-form li { - width: 295px; - padding-bottom: 10px; - } - - #preferences-form label { - display: block; - width: 130px; - float: left; - } - - #preferences-form input { - width: 129px; - } - - #preferences-form select { - width: 135px; - } - - .cp-pane { - position: absolute; - width: 590px; - display: none; - } - - #cp-pane-administration { - display: block; - } - -#bans { - left: 0; - background: url(../images/bg-bans.gif) repeat-x; - line-height: 1.3; -} - - #cp #bans-list a { - color: #d9d9d9; - border-bottom: 1px solid transparent; - _border-bottom: none; - } - - #cp #bans-list a:hover { - color: #fff; - border-bottom: 1px solid #de4147; - } - - #bans-list { - padding-top: 10px; - list-style: none; - height: 280px; - overflow: auto; - } - - #bans-list li { - clear: both; - padding: 3px 5px; - - } - - #bans-list .nickname { - color: #fff; - font-size: 12px; - } - - #bans-list .unban-link { - position: absolute; - right: 20px; - - } - - #no-bans { - margin-top: 100px; - text-align: center; - font-size: 22px; - color: #383838; - } - -#about { - left: 0; - background: url(../images/bg-about.gif) repeat-x; - line-height: 1.6; -} - - #about h2 { - color: #fff; - font: Arial, sans-serif; - font-size: 14px; - font-weight: normal; - margin-bottom: 5px; - } - - #about p { - margin-bottom: 5px; - } - - - #cp-pane-about { - margin-top: 10px; - display: block; - } - - #cp-pane-contact { - margin-top: 10px; - } - - #cp-pane-about a, - #cp-pane-contact a { - color: #d9d9d9; - padding-bottom: 2px; - } - - #cp-pane-about a:hover, - #cp-pane-contact a:hover { - color: #fff; - border-bottom: 1px solid #f3982d; - } diff --git a/ext/chatbox/cp/index.php b/ext/chatbox/cp/index.php deleted file mode 100644 index c44d3255..00000000 --- a/ext/chatbox/cp/index.php +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - YShout: Admin CP - - - - - - -
    - - -
    -
    -
    -

    YShout.Preferences

    -
    - -
    - - - Loading... -
    -
    - -
    -
    - - \ No newline at end of file diff --git a/ext/chatbox/cp/js/admincp.js b/ext/chatbox/cp/js/admincp.js deleted file mode 100644 index 3fe93b27..00000000 --- a/ext/chatbox/cp/js/admincp.js +++ /dev/null @@ -1,388 +0,0 @@ -/*jshint bitwise:true, curly:true, devel:true, eqeqeq:true, evil:true, forin:false, noarg:true, noempty:true, nonew:true, undef:true, strict:false, browser:true, jquery:true */ - -Array.prototype.inArray = function (value) { - for (var i = 0; i < this.length; i++) { - if (this[i] === value) { - return true; - } - } - - return false; -}; - -var AdminCP = function() { - var self = this; - var args = arguments; - $(function(){ - self.init.apply(self, args); - }); -}; - -AdminCP.prototype = { - z: 5, - animSpeed: 'normal', - curSection: 'login', - curPrefPane: 'administration', - curAboutPane: 'about', - - init: function(options) { - this.initializing = true; - this.loginForm(); - this.initEvents(); - if (this.loaded()) { - this.afterLogin(); - } else { - $('#login-password')[0].focus(); - } - - this.initializing = false; - }, - - loginForm: function() { - $('#login-loading').fadeTo(1, 0); - }, - - initEvents: function() { - var self = this; - - $('#login-form').submit(function() { self.login(); return false; }); - $('#n-prefs').click(function() { self.show('preferences'); return false; }); - $('#n-bans').click(function() { self.show('bans'); return false; }); - $('#n-about').click(function() { self.show('about'); return false; }); - }, - - afterLogin: function() { - var self = this; - - // Login and logout - $('#login-password')[0].blur(); - $('.logout').click(function() { self.logout(); return false; }); - - // Show the nav - if (this.initializing) { - $('#nav ul').css('display', 'block'); - } else { - $('#nav ul').slideDown(); - } - - // Some css for betterlookingness - $('#preferences-form fieldset:odd').addClass('odd'); - $('#preferences-form fieldset:even').addClass('even'); - - $('#bans-list li:odd').addClass('odd'); - $('#bans-list li:even').addClass('even'); - - // Hide the loading thingie - $('.sn-loading').fadeTo(1, 0); - - // Events after load - this.initEventsAfter(); - - // If they want to go directly to a section - var anchor = this.getAnchor(); - - if (anchor.length > 0 && ['preferences', 'bans', 'about'].inArray(anchor)) { - self.show(anchor); - } else { - self.show('preferences'); - } - }, - - initEventsAfter: function() { - var self = this; - - // Navigation - $('#sn-administration').click(function() { self.showPrefPane('administration'); return false; }); - $('#sn-display').click(function() { self.showPrefPane('display'); return false; }); - $('#sn-about').click(function() { self.showAboutPane('about'); return false; }); - $('#sn-contact').click(function() { self.showAboutPane('contact'); return false; }); - $('#sn-resetall').click(function() { self.resetPrefs(); return false; }); - $('#sn-unbanall').click(function() { self.unbanAll(); return false; }); - - // Bans - $('.unban-link').click(function() { - self.unban($(this).parent().find('.ip').html(), $(this).parent()); - return false; - }); - - // Preferences - $('#preferences-form input').keypress(function(e) { - var key = window.event ? e.keyCode : e.which; - if (key === 13 || key === 3) { - self.changePref.apply(self, [$(this).attr('rel'), this.value]); - return false; - } - }).focus(function() { - this.name = this.value; - }).blur(function() { - if (this.name !== this.value) { - self.changePref.apply(self, [$(this).attr('rel'), this.value]); - } - }); - - $('#preferences-form select').change(function() { - self.changePref.apply(self, [$(this).attr('rel'), $(this).find('option:selected').attr('rel')]); - }); - }, - - changePref: function(pref, value) { - this.loading(); - var pars = { - mode: 'setpreference', - preference: pref, - 'value': value - }; - this.ajax(function(json) { - if (!json.error) { - this.done(); - } else { - alert(json.error); - } - }, pars); - }, - - resetPrefs: function() { - this.loading(); - - var pars = { - mode: 'resetpreferences' - }; - - this.ajax(function(json) { - this.done(); - if (json.prefs) { - for (pref in json.prefs) { - var value = json.prefs[pref]; - var el = $('#preferences-form input[@rel=' + pref + '], select[@rel=' + pref + ']')[0]; - - if (el.type === 'text') { - el.value = value; - } else { - if (value === true) { value = 'true'; } - if (value === false) { value = 'false'; } - - $('#preferences-form select[@rel=' + pref + ']') - .find('option') - .removeAttr('selected') - .end() - .find('option[@rel=' + value + ']') - .attr('selected', 'yeah'); - } - } - } - }, pars); - }, - - invalidPassword: function() { - // Shake the login form - $('#login-form') - .animate({ marginLeft: -145 }, 100) - .animate({ marginLeft: -155 }, 100) - .animate({ marginLeft: -145 }, 100) - .animate({ marginLeft: -155 }, 100) - .animate({ marginLeft: -150 }, 50); - - $('#login-password').val('').focus(); - }, - - login: function() { - if (this.loaded()) { - alert('Something _really_ weird has happened. Refresh and pretend nothing ever happened.'); - return; - } - - var self = this; - var pars = { - mode: 'login', - password: $('#login-password').val() - }; - - this.loginLoading(); - - this.ajax(function() { - this.ajax(function(json) { - self.loginDone(); - if (json.error) { - self.invalidPassword(); - return; - } - - $('#content').append(json.html); - self.afterLogin.apply(self); - }, pars); - }, pars); - - }, - - logout: function() { - var self = this; - var pars = { - mode: 'logout' - }; - - this.loading(); - - this.ajax(function() { - $('#login-password').val(''); - $('#nav ul').slideUp(); - self.show('login', function() { - $('#login-password')[0].focus(); - $('.section').not('#login').remove(); - self.done(); - }); - }, pars); - }, - - show: function(section, callback) { -// var sections = ['login', 'preferences', 'bans', 'about']; -// if (!sections.inArray(section)) section = 'preferences'; - - if ($.browser.msie) { - if (section === 'preferences') { - $('#preferences select').css('display', 'block'); - } else { - $('#preferences select').css('display', 'none'); - } - } - - if (section === this.curSection) { return; } - - this.curSection = section; - - $('#' + section)[0].style.zIndex = ++this.z; - - if (this.initializing) { - $('#' + section).css('display', 'block'); - } else { - $('#' + section).fadeIn(this.animSpeed, callback); - } - }, - - showPrefPane: function(pane) { - var self = this; - - if (pane === this.curPrefPane) { return; } - this.curPrefPane = pane; - $('#preferences .cp-pane').css('display', 'none'); - $('#cp-pane-' + pane).css('display', 'block').fadeIn(this.animSpeed, function() { - if (self.curPrefPane === pane) { - $('#preferences .cp-pane').not('#cp-pane-' + pane).css('display', 'none'); - } else { - $('#cp-pane-' + pane).css('display', 'none'); - } - }); - }, - - showAboutPane: function(pane) { - var self = this; - - if (pane === this.curAboutPane) { return; } - this.curAboutPane = pane; - $('#about .cp-pane').css('display', 'none'); - $('#cp-pane-' + pane).css('display', 'block').fadeIn(this.animSpeed, function() { - if (self.curAboutPane === pane) { - $('#about .cp-pane').not('#cp-pane-' + pane).css('display', 'none'); - } else { - $('#cp-pane-' + pane).css('display', 'none'); - } - }); - }, - - ajax: function(callback, pars, html) { - var self = this; - - $.post('ajax.php', pars, function(parse) { - // alert(parse); - if (parse) { - if (html) { - callback.apply(self, [parse]); - } else { - callback.apply(self, [self.json(parse)]); - } - } else { - callback.apply(self); - } - }); - }, - - json: function(parse) { - var json = eval('(' + parse + ')'); - return json; - }, - - loaded: function() { - return ($('#cp-loaded').length === 1); - }, - - loading: function() { - $('#' + this.curSection + ' .sn-loading').fadeTo(this.animSpeed, 1); - }, - - done: function() { - $('#' + this.curSection + ' .sn-loading').fadeTo(this.animSpeed, 0); - }, - - loginLoading: function() { - $('#login-password').animate({ - width: 134 - }); - - $('#login-loading').fadeTo(this.animSpeed, 1); - - }, - - loginDone: function() { - $('#login-password').animate({ - width: 157 - }); - $('#login-loading').fadeTo(this.animSpeed, 0); - }, - - getAnchor: function() { - var href = window.location.href; - if (href.indexOf('#') > -1 ) { - return href.substr(href.indexOf('#') + 1).toLowerCase(); - } - return ''; - }, - - unban: function(ip, el) { - var self = this; - - this.loading(); - var pars = { - mode: 'unban', - 'ip': ip - }; - - this.ajax(function(json) { - if (!json.error) { - $(el).fadeOut(function() { - $(this).remove(); - $('#bans-list li:odd').removeClass('even').addClass('odd'); - $('#bans-list li:even').removeClass('odd').addClass('even'); - }, this.animSpeed); - } - self.done(); - }, pars); - }, - - unbanAll: function() { - this.loading(); - - var pars = { - mode: 'unbanall' - }; - - this.ajax(function(json) { - this.done(); - $('#bans-list').fadeOut(this.animSpeed, function() { - $('#bans-list').children().remove(); - $('#bans-list').fadeIn(); - }); - }, pars); - } - -}; - -var cp = new AdminCP(); \ No newline at end of file diff --git a/ext/chatbox/css/dark.yshout.css b/ext/chatbox/css/dark.yshout.css deleted file mode 100644 index 41e7899c..00000000 --- a/ext/chatbox/css/dark.yshout.css +++ /dev/null @@ -1,389 +0,0 @@ -/* - -YShout HTML Structure: - -
    -
    -
    - - Yurivish: - Hey! - - - Info | - Delete | - Ban - -
    - -
    - - Hello. - - - Info | - Delete | - Ban - -
    - -
    - - Yup... - - - Info | - Delete | - Ban - -
    -
    -
    - -
    -
    -
    - - - [View History|Admin CP] - -
    -
    -
    - - - -*/ - - -#yshout * { - margin: 0; - padding: 0; -} - -#yshout a { - text-decoration: none; - color: #989898; -} - -#yshout a:hover { - color: #fff; -} - -#yshout a:active { - color: #e5e5e5; -} - -/* Adjust the width here --------------------------- */ - -#yshout { - position: relative; - overflow: hidden; - font: 11px/1.4 Arial, Helvetica, sans-serif; -} - -/* Posts -------------------------------------- */ - -#yshout #ys-posts { - position: relative; - background: #1a1a1a; -} - -#yshout .ys-post { - border-bottom: 1px solid #212121; - margin: 0 5px; - padding: 5px; - position: relative; - overflow: hidden; - text-align: left; -} - - -#yshout .ys-admin-post .ys-post-nickname { - padding-left: 11px; - background: url(../images/star-dark.gif) 0 2px no-repeat; -} - - -#yshout .ys-post-timestamp { - color: #333; -} - -#yshout .ys-post-nickname { - color: #e5e5e5; -} - -#yshout .ys-post-message { - color: #595959; -} - - -/* Banned -------------------------------------- */ - -#yshout .ys-banned-post .ys-post-nickname, -#yshout .ys-banned-post .ys-post-message, -#yshout .ys-banned-post { - color: #b3b3b3 !important; -} - -#yshout #ys-banned { - position: absolute; - z-index: 75; - height: 100%; - _height: 430px; - top: 0; - left: 0; - margin: 0 5px; - background: #1a1a1a; -} - -#yshout #ys-banned span { - position: absolute; - display: block; - height: 20px; - margin-top: -10px; - top: 50%; - padding: 0 20px; - color: #666; - text-align: center; - font-size: 13px; - z-index: 80; -} - -#yshout #ys-banned a { - color: #999; -} - -#yshout #ys-banned a:hover { - color: #666; -} - -/* Hover Controls -------------------------------------- */ - -#yshout .ys-post-actions { - display: none; - position: absolute; - top: 0; - right: 0; - padding: 5px; - font-size: 11px; - z-index: 50; - background: #1a1a1a; - color: #666; -} - -#yshout .ys-post-actions a { - color: #989898; -} - -#yshout .ys-post-actions a:hover { - color: #fff; -} - -#yshout .ys-post:hover .ys-post-actions { - display: block; -} - -#yshout .ys-post-info { - color: #595959; -} - -#yshout .ys-post-info em { - font-style: normal; - color: #1a1a1a; -} - -#yshout .ys-info-overlay { - display: none; - position: absolute; - z-index: 45; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: #1a1a1a; - padding: 5px; -} - -#yshout .ys-info-inline { - display: none; - margin-top: 2px; - padding-top: 3px; - border-top: 1px solid #f2f2f2; -} - -/* Post Form -------------------------------------- */ - -#yshout #ys-post-form { - height: 40px; - line-height: 40px; - background: #262626; - text-align: left; -} - - #yshout #ys-input-nickname, - #yshout #ys-input-message { - font-size: 11px; - padding: 2px; - background: #333; - border: 1px solid #404040; - } - - #yshout #ys-post-form fieldset { - _position: absolute; - border: none; - padding: 0 10px; - _margin-top: 10px; - } - - #yshout #ys-input-nickname { - width: 105px; - margin-left: 5px; - } - - #yshout #ys-input-message { - margin-left: 5px; - width: 400px; - } - - #yshout #ys-input-submit { - font-size: 11px; - width: 64px; - margin-left: 5px; - } - - #yshout #ys-input-submit:hover { - cursor: pointer; - } - - #yshout .ys-before-focus { - color: #4d4d4d; - } - - #yshout .ys-after-focus { - color: #e5e5e5; - } - - #yshout .ys-input-invalid { - - } - - #yshout .ys-post-form-link { - margin-left: 5px; - - } - - -/* Overlays - This should go in all YShout styles -------------------------------------- */ - -#ys-overlay { - position: fixed; - _position: absolute; - z-index: 100; - width: 100%; - height: 100%; - top: 0; - left: 0; - background-color: #000; - filter: alpha(opacity=60); - -moz-opacity: 0.6; - opacity: 0.6; -} - -* html body { - height: 100%; - width: 100%; -} - -#ys-closeoverlay-link, -#ys-switchoverlay-link { - display: block; - font-weight: bold; - height: 13px; - font: 11px/1 Arial, Helvetica, sans-serif; - color: #fff; - text-decoration: none; - margin-bottom: 1px; - outline: none; - float: left; -} - -#ys-switchoverlay-link { - float: right; -} - -.ys-window { - z-index: 102; - position: fixed; - _position: absolute; - top: 50%; - left: 50%; -} - - #ys-cp { - margin-top: -220px; - margin-left: -310px; - width: 620px; - } - - #ys-yshout { - margin-top: -250px; - margin-left: -255px; - width: 500px; - } - - #ys-history { - margin-top: -220px; - margin-left: -270px; - width: 540px; - } - -#yshout .ys-browser { - border: none !important; - outline: none !important; - z-index: 102; - overflow: auto; - background: transparent !important; -} - - #yshout-browser { - height: 580px; - width: 510px; - } - - #cp-browser { - height: 440px; - width: 620px; - _height: 450px; - _width: 440px; - } - - #history-browser { - height: 440px; - width: 540px; - border-top: 1px solid #545454; - border-left: 1px solid #545454; - border-bottom: 1px solid #444; - border-right: 1px solid #444; - } \ No newline at end of file diff --git a/ext/chatbox/css/overlay.css b/ext/chatbox/css/overlay.css deleted file mode 100644 index a2c00179..00000000 --- a/ext/chatbox/css/overlay.css +++ /dev/null @@ -1,93 +0,0 @@ -/* Overlays - Use this stylesheet if you want to only use yLink. -------------------------------------- */ - -#ys-overlay { - position: fixed; - _position: absolute; - z-index: 100; - width: 100%; - height: 100%; - top: 0; - left: 0; - background-color: #000; - filter: alpha(opacity=60); - -moz-opacity: 0.6; - opacity: 0.6; -} - -* html body { - height: 100%; - width: 100%; -} - -#ys-closeoverlay-link, -#ys-switchoverlay-link { - display: block; - font-weight: bold; - height: 13px; - font: 11px/1 Arial, Helvetica, sans-serif; - color: #fff; - text-decoration: none; - margin-bottom: 1px; - outline: none; - float: left; -} - -#ys-switchoverlay-link { - float: right; -} - -.ys-window { - z-index: 102; - position: fixed; - _position: absolute; - top: 50%; - left: 50%; -} - - #ys-cp { - margin-top: -220px; - margin-left: -310px; - width: 620px; - } - - #ys-yshout { - margin-top: -250px; - margin-left: -255px; - width: 500px; - } - - #ys-history { - margin-top: -220px; - margin-left: -270px; - width: 540px; - } - -#yshout .ys-browser { - border: none !important; - outline: none !important; - z-index: 102; - overflow: auto; - background: transparent !important; -} - - #yshout-browser { - height: 580px; - width: 510px; - } - - #cp-browser { - height: 440px; - width: 620px; - _height: 450px; - _width: 440px; - } - - #history-browser { - height: 440px; - width: 540px; - border-top: 1px solid #545454; - border-left: 1px solid #545454; - border-bottom: 1px solid #444; - border-right: 1px solid #444; - } \ No newline at end of file diff --git a/ext/chatbox/css/style.css b/ext/chatbox/css/style.css deleted file mode 100644 index 3ad07b80..00000000 --- a/ext/chatbox/css/style.css +++ /dev/null @@ -1,113 +0,0 @@ -* { - margin: 0; - padding: 0; -} - -body { - background: #182635 url(../images/bg.gif) fixed repeat-x; - font: 11px/1.6 Arial, Helvetica, sans-serif; - color: #92b5ce; -} - -a { - color: #d5edff; - text-decoration: none; -} - -a:hover { - color: #fff !important; - text-decoration: underline; -} - -h2 { - font-weight: normal; - color: #fff; - font-size: 14px; - margin-bottom: 5px; - margin-top:10px; -} - -p { - margin-bottom: 5px; -} - -pre { - padding: 3px; - margin-top: 5px; - margin-bottom: 10px; - background: url(../images/bg-code.png); - _background: none; - color: #b4d4eb; -} - -code { - color: #fff; -} - -pre code { - padding: 0; - color: #b4d4eb; -} - -ul { - list-style: none; -} - -li { - margin-bottom: 5px; -} - -em { - font-weight: normal; - font-style: normal; - color: #fff; -} - -#container { - width: 510px; - margin: 0 auto; -} - - #top { - width: 510px; - margin-top: 25px; - height: 20px; - border-bottom: 1px solid #567083; - font-size: 11px; - overflow: hidden; - - } - - h1 { - text-indent: -4200px; - height: 13px; - width: 120px; - background: url(../images/h-welcome.gif) no-repeat; - float: left; - } - - #nav { - color: #93b3ca; - float: right; - line-height: 1.6; - } - -#footer { - width: 510px; - margin: 20px auto 10px auto; - padding-top: 5px; - border-top: 1px solid #273e56; - color: #384858; -} - -#footer:hover { - color: #92b5ce; -} - -#footer:hover a { - color: #fff; -} - -#footer a { - color: #425d7a; -} \ No newline at end of file diff --git a/ext/chatbox/history/css/style.css b/ext/chatbox/history/css/style.css deleted file mode 100644 index dc76f214..00000000 --- a/ext/chatbox/history/css/style.css +++ /dev/null @@ -1,85 +0,0 @@ -* { - margin: 0; - padding: 0; -} - -body { - background: #202020 url(../images/bg.gif) fixed repeat-x; - color: #5c5c5c; - font: 11px/1.6 Arial, Helvetica, sans-serif; -} - -#top { - height: 25px; - width: 510px; - margin: 0 auto; - margin-top: 20px; - border-bottom: 1px solid #444; - overflow: none; - line-height: 1.0; -} - - h1 { - text-indent: -4200px; - background: url(../images/h-history.gif) no-repeat; - width: 105px; - height: 17px; - margin-top: 5px; - float: left; - overflow: none; - _position: absolute; - } - - #top a, #bottom a { - color: #7d7d7d; - text-decoration: none; - } - - #top a { - line-height: 25px; - } - - #top a:hover, #bottom a:hover { - color: #fff; - border-bottom-color: #5e5e5e; - } - - - #log { - font-size: 11px; - margin-left: 10px; - border: 1px solid #767676; - border-right: none; - width: 60px; - - } - - #controls { - float: right; - } - - -#yshout { - margin: 0 auto; - margin-top: 10px; -} - -#bottom { - width:510px; - margin: 10px auto; -} - - #bottom #to-top { - margin-left: 5px; - } - -/* Inane IE Compatibility PNG fixes -------------------------------------- */ - -#yshout #ys-before-posts { _filter:progid:DXImageTransform.Microsoft.AlphaImageLoader (src='../example/images/ys-bg-posts-top.png',sizingMethod='crop'); } -#yshout #ys-posts { _filter:progid:DXImageTransform.Microsoft.AlphaImageLoader (src='../example/images/bg-posts.png',sizingMethod='scale'); } -#yshout #ys-after-posts { _filter:progid:DXImageTransform.Microsoft.AlphaImageLoader (src='../example/images/ys-bg-posts-bottom.png',sizingMethod='crop'); } -#yshout #ys-banned { _filter:progid:DXImageTransform.Microsoft.AlphaImageLoader (src='../example/images/bg-banned.png',sizingMethod='scale'); } -#yshout #ys-post-form { _filter:progid:DXImageTransform.Microsoft.AlphaImageLoader (src='../example/images/bg-form.png',sizingMethod='crop'); } -#yshout .ys-post { _height: 1%; } - diff --git a/ext/chatbox/history/index.php b/ext/chatbox/history/index.php deleted file mode 100644 index f3755e98..00000000 --- a/ext/chatbox/history/index.php +++ /dev/null @@ -1,143 +0,0 @@ -'; - - $admin = loggedIn(); - - $log = 1; - - if (isset($_GET['log'])) - { - $log = $_GET['log']; - } - - if (isset($_POST['log'])) - { - $log = $_POST['log']; - } - - if (filter_var($log, FILTER_VALIDATE_INT) === false) - { - $log = 1; - } - - $ys = ys($log); - $posts = $ys->posts(); - - if (sizeof($posts) === 0) - $html .= ' -
    - - Yurivish: - Hey, there aren\'t any posts in this log. -
    - '; - - $id = 0; - - foreach($posts as $post) { - $id++; - - $banned = $ys->banned($post['adminInfo']['ip']); - $html .= '
    ' . "\n"; - - $ts = ''; - - switch($prefs['timestamp']) { - case 12: - $ts = date('h:i', $post['timestamp']); - break; - case 24: - $ts = date('H:i', $post['timestamp']); - break; - case 0: - $ts = ''; - break; - } - - $html .= ' ' . "\n"; - $html .= ' ' . $post['nickname'] . '' . $prefs['nicknameSeparator'] . ' ' . "\n"; - $html .= ' ' . $post['message'] . '' . "\n"; - $html .= ' ' . "\n"; - - $html .= ' ' . "\n"; - $html .= ' Info' . ($admin ? ' | Delete | ' . ($banned ? 'Unban' : 'Ban') : '') . "\n"; - $html .= ' ' . "\n"; - - if ($admin) { - $html .= ''; - } - - $html .= '
    ' . "\n"; - } - - $html .= '' . "\n"; - - -if (isset($_POST['p'])) { - echo $html; - exit; -} - -?> - - - - - YShout: History - - - - - - - - - - -
    -

    YShout.History

    -
    - - Clear this log, or - Clear all logs. - - - -
    -
    -
    -
    -
    - -
    -
    -
    - - - - diff --git a/ext/chatbox/history/js/history.js b/ext/chatbox/history/js/history.js deleted file mode 100644 index 438c5c90..00000000 --- a/ext/chatbox/history/js/history.js +++ /dev/null @@ -1,276 +0,0 @@ -/*jshint bitwise:true, curly:true, devel:true, eqeqeq:true, evil:true, forin:false, noarg:true, noempty:true, nonew:true, undef:true, strict:false, browser:true, jquery:true */ - -var History = function() { - var self = this; - var args = arguments; - $(function(){ - self.init.apply(self, args); - }); -}; - -History.prototype = { - animSpeed: 'normal', - noPosts: '
    \n\nYurivish:\nHey, there aren\'t any posts in this log.\n
    ', - - init: function(options) { - this.prefsInfo = options.prefsInfo; - this.log = options.log; - this.initEvents(); - $('body').ScrollToAnchors({ duration: 800 }); - }, - - initEvents: function() { - var self = this; - - this.initLogEvents(); - - // Select log - $('#log').change(function() { - var logIndex = $(this).find('option[@selected]').attr('rel'); - - var pars = { - p: 'yes', - log: logIndex - }; - - self.ajax(function(html) { - $('#ys-posts').html(html); - $('#yshout').fadeIn(); - self.initLogEvents(); - }, pars, true, 'index.php'); - }); - - // Clear the log - $('#clear-log').click(function() { - var el = this; - var pars = { - reqType: 'clearlog' - }; - - self.ajax(function(json) { - if (json.error) { - switch(json.error) { - case 'admin': - self.error('You\'re not an admin. Log in through the admin CP to clear the log.'); - el.innerHTML = 'Clear this log'; - return; - } - } - - $('#ys-posts').html(self.noPosts); - self.initLogEvents(); - el.innerHTML = 'Clear this log'; - }, pars); - - this.innerHTML = 'Clearing...'; - return false; - }); - - // Clear all logs - $('#clear-logs').click(function() { - var el = this; - var pars = { - reqType: 'clearlogs' - }; - - self.ajax(function(json) { - if (json.error) { - switch(json.error) { - case 'admin': - el.innerHTML = 'Clear all logs'; - self.error('You\'re not an admin. Log in through the admin CP to clear logs.'); - return; - } - } - - $('#ys-posts').html(self.noPosts); - self.initLogEvents(); - el.innerHTML = 'Clear all logs'; - }, pars); - - this.innerHTML = 'Clearing...'; - return false; - }); - }, - - initLogEvents: function() { - var self = this; - - $('#yshout .ys-post') - .find('.ys-info-link').toggle( - function() { self.showInfo.apply(self, [$(this).parent().parent()[0].id, this]); return false; }, - function() { self.hideInfo.apply(self, [$(this).parent().parent()[0].id, this]); return false; }) - .end() - .find('.ys-ban-link').click( - function() { self.ban.apply(self, [$(this).parent().parent()[0]]); return false; }) - .end() - .find('.ys-delete-link').click( - function() { self.del.apply(self, [$(this).parent().parent()[0]]); return false; }); - }, - - showInfo: function(id, el) { - var jEl = $('#' + id + ' .ys-post-info'); - - if (jEl.length === 0) { return false; } - - if (this.prefsInfo === 'overlay') { - jEl.css('display', 'block').fadeIn(this.animSpeed); - } else { - jEl.slideDown(this.animSpeed); - } - - el.innerHTML ='Close Info'; - return false; - }, - - hideInfo: function(id, el) { - var jEl = $('#' + id + ' .ys-post-info'); - - if (jEl.length === 0) { return false; } - - if (this.prefsInfo === 'overlay') { - jEl.fadeOut(this.animSpeed); - } else { - jEl.slideUp(this.animSpeed); - } - - el.innerHTML = 'Info'; - return false; - }, - - ban: function(post) { - var self = this; - var link = $('#' + post.id).find('.ys-ban-link')[0]; - - switch(link.innerHTML) - { - case 'Ban': - var pIP = $(post).find('.ys-h-ip').html(); - var pNickname = $(post).find('.ys-h-nickname').html(); - - var pars = { - log: self.log, - reqType: 'ban', - ip: pIP, - nickname: pNickname - }; - - this.ajax(function(json) { - if (json.error) { - switch (json.error) { - case 'admin': - self.error('You\'re not an admin. Log in through the admin CP to ban people.'); - break; - } - return; - } - - $('#yshout .ys-post[@rel="' + pars.ip + '"]') - .addClass('ys-banned-post') - .find('.ys-ban-link') - .html('Unban'); - - }, pars); - - link.innerHTML = 'Banning...'; - return false; - - case 'Banning...': - return false; - - case 'Unban': - var pIP = $(post).find('.ys-h-ip').html(); - var pars = { - reqType: 'unban', - ip: pIP - }; - - this.ajax(function(json) { - if (json.error) { - switch(json.error) { - case 'admin': - self.error('You\'re not an admin. Log in through the admin CP to unban people.'); - return; - } - } - - $('#yshout .ys-post[@rel="' + pars.ip + '"]') - .removeClass('ys-banned-post') - .find('.ys-ban-link') - .html('Ban'); - - }, pars); - - link.innerHTML = 'Unbanning...'; - return false; - - case 'Unbanning...': - return false; - } - }, - - del: function(post) { - var self = this; - - var link = $('#' + post.id).find('.ys-delete-link')[0]; - if (link.innerHTML === 'Deleting...') { return; } - - var pUID = $(post).find('.ys-h-uid').html(); - - var pars = { - reqType: 'delete', - uid: pUID - }; - - self.ajax(function(json) { - if (json.error) { - switch(json.error) { - case 'admin': - self.error('You\'re not an admin. Log in through the admin CP to ban people.'); - return; - } - } - - $(post).slideUp(self.animSpeed); - - }, pars); - - link.innerHTML = 'Deleting...'; - return false; - - }, - - json: function(parse) { - var json = eval('(' + parse + ')'); - return json; - }, - - ajax: function(callback, pars, html, page) { - pars = jQuery.extend({ - reqFor: 'history', - log: this.log - }, pars); - - var self = this; - - if (page === null) { page = '../yshout.php'; } - - $.post(page, pars, function(parse) { - if (parse) { - if (html) { - callback.apply(self, [parse]); - } else { - callback.apply(self, [self.json(parse)]); - } - } else { - callback.apply(self); - } - }); - }, - - error: function(err) { - alert(err); - } - -}; - diff --git a/ext/chatbox/include.php b/ext/chatbox/include.php deleted file mode 100644 index a3d4b7b7..00000000 --- a/ext/chatbox/include.php +++ /dev/null @@ -1,8 +0,0 @@ - 0) && (this.options.yPath.charAt(this.options.yPath.length - 1) !== '/')) { - this.options.yPath += '/'; - } - - if (this.options.yLink) { - if (this.options.yLink.charAt(0) !== '#') { - this.options.yLink = '#' + this.options.yLink; - } - - $(this.options.yLink).click(function() { - self.openYShout.apply(self); - return false; - }); - } - - // Load YShout from a link, in-page - if (this.options.h_loadlink) { - $(this.options.h_loadlink).click(function() { - $('#yshout').css('display', 'block'); - $(this).unbind('click').click(function() { return false; }); - return false; - }); - this.load(true); - } else { - this.load(); - } - }, - - load: function(hidden) { - if ($('#yshout').length === 0) { return; } - - if (hidden) { $('#yshout').css('display', 'none'); } - - this.ajax(this.initialLoad, { - reqType: 'init', - yPath: this.options.yPath, - log: this.options.log - }); - }, - - initialLoad: function(updates) { - - if (updates.yError) { - alert('There appears to be a problem: \n' + updates.yError + '\n\nIf you haven\'t already, try chmodding everything inside the YShout directory to 777.'); - } - - var self = this; - - this.prefs = jQuery.extend(updates.prefs, this.options.prefs); - this.initForm(); - this.initRefresh(); - this.initLinks(); - if (this.prefs.flood) { this.initFlood(); } - - if (updates.nickname) { - $('#ys-input-nickname') - .removeClass('ys-before-focus') - .addClass( 'ys-after-focus') - .val(updates.nickname); - } - - if (updates) { - this.updates(updates); - } - - if (!this.prefs.doTruncate) { - $('#ys-posts').css('height', $('#ys-posts').height + 'px'); - } - - if (!this.prefs.inverse) { - var postsDiv = $('#ys-posts')[0]; - postsDiv.scrollTop = postsDiv.scrollHeight; - } - - this.markEnds(); - - this.initializing = false; - }, - - initForm: function() { - this.d('In initForm'); - - var postForm = - '
    ' + - '' + - '' + - (this.prefs.showSubmit ? '' : '') + - (this.prefs.postFormLink === 'cp' ? 'Admin CP' : '') + - (this.prefs.postFormLink === 'history' ? 'View History' : '') + - '
    '; - - var postsDiv = '
    '; - - if (this.prefs.inverse) { $('#yshout').html(postForm + postsDiv); } - else { $('#yshout').html(postsDiv + postForm); } - - $('#ys-posts') - .before('
    ') - .after('
    '); - - $('#ys-post-form') - .before('
    ') - .after('
    '); - - var self = this; - - var defaults = { - 'ys-input-nickname': self.prefs.defaultNickname, - 'ys-input-message': self.prefs.defaultMessage - }; - - var keypress = function(e) { - var key = window.event ? e.keyCode : e.which; - if (key === 13 || key === 3) { - self.send.apply(self); - return false; - } - }; - - var focus = function() { - if (this.value === defaults[this.id]) { - $(this).removeClass('ys-before-focus').addClass( 'ys-after-focus').val(''); - } - }; - - var blur = function() { - if (this.value === '') { - $(this).removeClass('ys-after-focus').addClass('ys-before-focus').val(defaults[this.id]); - } - }; - - $('#ys-input-message').keypress(keypress).focus(focus).blur(blur); - $('#ys-input-nickname').keypress(keypress).focus(focus).blur(blur); - - $('#ys-input-submit').click(function(){ self.send.apply(self); }); - $('#ys-post-form').submit(function(){ return false; }); - }, - - initRefresh: function() { - var self = this; - if (this.refreshTimer) { clearInterval(this.refreshTimer); } - - this.refreshTimer = setInterval(function() { - self.ajax(self.updates, { reqType: 'refresh' }); - }, this.prefs.refresh); // ! 3000..? - }, - - initFlood: function() { - this.d('in initFlood'); - var self = this; - this.floodCount = 0; - this.floodControl = false; - - this.floodTimer = setInterval(function() { - self.floodCount = 0; - }, this.prefs.floodTimeout); - }, - - initLinks: function() { - if ($.browser.msie) { return; } - - var self = this; - - $('#ys-cp-link').click(function() { - self.openCP.apply(self); - return false; - }); - - $('#ys-history-link').click(function() { - self.openHistory.apply(self); - return false; - }); - - }, - - openCP: function() { - var self = this; - if (this.cpOpen) { return; } - this.cpOpen = true; - - var url = this.options.yPath + 'cp/index.php'; - - $('body').append('
    CloseView HistorySomething went horribly wrong.
    '); - - $('#ys-overlay, #ys-closeoverlay-link').click(function() { - self.reload.apply(self, [true]); - self.closeCP.apply(self); - return false; - }); - - $('#ys-switchoverlay-link').click(function() { - self.closeCP.apply(self); - self.openHistory.apply(self); - return false; - }); - - }, - - closeCP: function() { - this.cpOpen = false; - $('#ys-overlay, #ys-cp').remove(); - }, - - openHistory: function() { - var self = this; - if (this.hOpen) { return; } - this.hOpen = true; - var url = this.options.yPath + 'history/index.php?log='+ this.options.log; - $('body').append('
    CloseView Admin CPSomething went horribly wrong.
    '); - - $('#ys-overlay, #ys-closeoverlay-link').click(function() { - self.reload.apply(self, [true]); - self.closeHistory.apply(self); - return false; - }); - - $('#ys-switchoverlay-link').click(function() { - self.closeHistory.apply(self); - self.openCP.apply(self); - return false; - }); - - }, - - closeHistory: function() { - this.hOpen = false; - $('#ys-overlay, #ys-history').remove(); - }, - - openYShout: function() { - var self = this; - if (this.ysOpen) { return; } - this.ysOpen = true; - var url = this.options.yPath + 'example/yshout.html'; - - $('body').append('
    CloseSomething went horribly wrong.
    '); - - $('#ys-overlay, #ys-closeoverlay-link').click(function() { - self.reload.apply(self, [true]); - self.closeYShout.apply(self); - return false; - }); - }, - - closeYShout: function() { - this.ysOpen = false; - $('#ys-overlay, #ys-yshout').remove(); - }, - - send: function() { - if (!this.validate()) { return; } - if (this.prefs.flood && this.floodControl) { return; } - - var postNickname = $('#ys-input-nickname').val(), postMessage = $('#ys-input-message').val(); - - if (postMessage === '/cp') { - this.openCP(); - } else if (postMessage === '/history') { - this.openHistory(); - } else { - this.ajax(this.updates, { - reqType: 'post', - nickname: postNickname, - message: postMessage - }); - } - - $('#ys-input-message').val(''); - - if (this.prefs.flood) { this.flood(); } - }, - - validate: function() { - var nickname = $('#ys-input-nickname').val(), - message = $('#ys-input-message').val(), - error = false; - - var showInvalid = function(input) { - $(input).removeClass('ys-input-valid').addClass('ys-input-invalid')[0].focus(); - error = true; - }; - - var showValid = function(input) { - $(input).removeClass('ys-input-invalid').addClass('ys-input-valid'); - }; - - if (nickname === '' || nickname === this.prefs.defaultNickname) { - showInvalid('#ys-input-nickname'); - } else { - showValid('#ys-input-nickname'); - } - - if (message === '' || message === this.prefs.defaultMessage) { - showInvalid('#ys-input-message'); - } else { - showValid('#ys-input-message'); - } - - return !error; - }, - - flood: function() { - var self = this; - this.d('in flood'); - if (this.floodCount < this.prefs.floodMessages) { - this.floodCount++; - return; - } - - this.floodAttempt++; - this.disable(); - - if (this.floodAttempt === this.prefs.autobanFlood) { - this.banSelf('You have been banned for flooding the shoutbox!'); - } - - setTimeout(function() { - self.floodCount = 0; - self.enable.apply(self); - }, this.prefs.floodDisable); - }, - - disable: function () { - $('#ys-input-submit')[0].disabled = true; - this.floodControl = true; - }, - - enable: function () { - $('#ys-input-submit')[0].disabled = false; - this.floodControl = false; - }, - - findBySame: function(ip) { - if (!$.browser.safari) {return;} - - var same = []; - - for (var i = 0; i < this.p.length; i++) { - if (this.p[i].adminInfo.ip === ip) { - same.push(this.p[i]); - } - } - - for (var j = 0; j < same.length; j++) { - $('#' + same[j].id).fadeTo(this.animSpeed, 0.8).fadeTo(this.animSpeed, 1); - } - }, - - updates: function(updates) { - if (!updates) {return;} - if (updates.prefs) {this.prefs = updates.prefs;} - if (updates.posts) {this.posts(updates.posts);} - if (updates.banned) {this.banned();} - }, - - banned: function() { - var self = this; - clearInterval(this.refreshTimer); - clearInterval(this.floodTimer); - if (this.initializing) { - $('#ys-post-form').css('display', 'none'); - } else { - $('#ys-post-form').fadeOut(this.animSpeed); - } - - if ($('#ys-banned').length === 0) { - $('#ys-input-message')[0].blur(); - $('#ys-posts').append('
    You\'re banned. Click here to unban yourself if you\'re an admin. If you\'re not, go log in!
    '); - - $('#ys-banned-cp-link').click(function() { - self.openCP.apply(self); - return false; - }); - - $('#ys-unban-self').click(function() { - self.ajax(function(json) { - if (!json.error) { - self.unbanned(); - } else if (json.error === 'admin') { - alert('You can only unban yourself if you\'re an admin.'); - } - }, { reqType: 'unbanself' }); - return false; - }); - } - }, - - unbanned: function() { - var self = this; - $('#ys-banned').fadeOut(function() { $(this).remove(); }); - this.initRefresh(); - $('#ys-post-form').css('display', 'block').fadeIn(this.animSpeed, function(){ - self.reload(); - }); - }, - - posts: function(p) { - for (var i = 0; i < p.length; i++) { - this.post(p[i]); - } - - this.truncate(); - - if (!this.prefs.inverse) { - var postsDiv = $('#ys-posts')[0]; - postsDiv.scrollTop = postsDiv.scrollHeight; - } - }, - - post: function(post) { - var self = this; - - var pad = function(n) { return n > 9 ? n : '0' + n; }; - var date = function(ts) { return new Date(ts * 1000); }; - var time = function(ts) { - var d = date(ts); - var h = d.getHours(), m = d.getMinutes(); - - if (self.prefs.timestamp === 12) { - h = (h > 12 ? h - 12 : h); - if (h === 0) { h = 12; } - } - - return pad(h) + ':' + pad(m); - }; - - var dateStr = function(ts) { - var t = date(ts); - - var Y = t.getFullYear(); - var M = t.getMonth(); - var D = t.getDay(); - var d = t.getDate(); - var day = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][D]; - var mon = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][M]; - - return day + ' ' + mon + '. ' + d + ', ' + Y; - }; - - var self = this; - - this.postNum++; - var id = 'ys-post-' + this.postNum; - post.id = id; - - post.message = this.links(post.message); - post.message = this.smileys(post.message); - post.message = this.bbcode(post.message); - var html = - '
    ' + - (this.prefs.timestamp> 0 ? ' ' : '') + - '' + post.nickname + this.prefs.nicknameSeparator + ' ' + - '' + post.message + ' ' + - '' + - 'Info' + (post.adminInfo ? ' | Delete | ' + (post.banned ? 'Unban' : 'Ban') : '') + '' + - '
    '; - if (this.prefs.inverse) { $('#ys-posts').prepend(html); } - else { $('#ys-posts').append(html); } - - this.p.push(post); - - $('#' + id) - .find('.ys-post-nickname').click(function() { - if (post.adminInfo) { - self.findBySame(post.adminInfo.ip); - } - }).end() - .find('.ys-info-link').toggle( - function() { self.showInfo.apply(self, [id, this]); return false; }, - function() { self.hideInfo.apply(self, [id, this]); return false; }) - .end() - .find('.ys-ban-link').click( - function() { self.ban.apply(self, [post, id]); return false; }) - .end() - .find('.ys-delete-link').click( - function() { self.del.apply(self, [post, id]); return false; }); - - }, - - showInfo: function(id, el) { - var jEl = $('#' + id + ' .ys-post-info'); - if (this.prefs.info === 'overlay') { - jEl.css('display', 'block').fadeIn(this.animSpeed); - } else { - jEl.slideDown(this.animSpeed); - } - - el.innerHTML = 'Close Info'; - return false; - }, - - hideInfo: function(id, el) { - var jEl = $('#' + id + ' .ys-post-info'); - if (this.prefs.info === 'overlay') { - jEl.fadeOut(this.animSpeed); - } else { - jEl.slideUp(this.animSpeed); - } - - el.innerHTML = 'Info'; - return false; - }, - - ban: function(post, id) { - var self = this; - - var link = $('#' + id).find('.ys-ban-link')[0]; - - switch(link.innerHTML) { - case 'Ban': - var pars = { - reqType: 'ban', - ip: post.adminInfo.ip, - nickname: post.nickname - }; - - this.ajax(function(json) { - if (json.error) { - switch (json.error) { - case 'admin': - self.error('You\'re not an admin. Log in through the Admin CP to ban people.'); - break; - } - return; - } - //alert('p: ' + this.p + ' / ' + this.p.length); - if (json.bannedSelf) { - self.banned(); // ? - } else { - $.each(self.p, function(i) { - if (this.adminInfo && this.adminInfo.ip === post.adminInfo.ip) { - $('#' + this.id) - .addClass('ys-banned-post') - .find('.ys-ban-link').html('Unban'); - } - }); - } - }, pars); - - link.innerHTML = 'Banning...'; - return false; - - case 'Banning...': - return false; - - case 'Unban': - var pars = { - reqType: 'unban', - ip: post.adminInfo.ip - }; - - this.ajax(function(json) { - if (json.error) { - switch(json.error) { - case 'admin': - self.error('You\'re not an admin. Log in through the Admin CP to unban people.'); - return; - } - } - - $.each(self.p, function(i) { - if (this.adminInfo && this.adminInfo.ip === post.adminInfo.ip) { - $('#' + this.id) - .removeClass('ys-banned-post') - .find('.ys-ban-link').html('Ban'); - } - }); - - }, pars); - - link.innerHTML = 'Unbanning...'; - return false; - - case 'Unbanning...': - return false; - } - }, - - del: function(post, id) { - var self = this; - var link = $('#' + id).find('.ys-delete-link')[0]; - - if (link.innerHTML === 'Deleting...') { return; } - - var pars = { - reqType: 'delete', - uid: post.uid - }; - - self.ajax(function(json) { - if (json.error) { - switch(json.error) { - case 'admin': - self.error('You\'re not an admin. Log in through the Admin CP to ban people.'); - return; - } - } - self.reload(); - }, pars); - - link.innerHTML = 'Deleting...'; - return false; - }, - - banSelf: function(reason) { - var self = this; - - this.ajax(function(json) { - if (json.error === false) { - self.banned(); - } - }, { - reqType: 'banself', - nickname: $('#ys-input-nickname').val() - }); - }, - - bbcode: function(s) { - s = s.sReplace('[i]', ''); - s = s.sReplace('[/i]', ''); - s = s.sReplace('[I]', ''); - s = s.sReplace('[/I]', ''); - - s = s.sReplace('[b]', ''); - s = s.sReplace('[/b]', ''); - s = s.sReplace('[B]', ''); - s = s.sReplace('[/B]', ''); - - s = s.sReplace('[u]', ''); - s = s.sReplace('[/u]', ''); - s = s.sReplace('[U]', ''); - s = s.sReplace('[/U]', ''); - - return s; - }, - - smileys: function(s) { - var yp = this.options.yPath; - - var smile = function(str, smiley, image) { - return str.sReplace(smiley, ''); - }; - - s = smile(s, ':twisted:', 'twisted.gif'); - s = smile(s, ':cry:', 'cry.gif'); - s = smile(s, ':\'(', 'cry.gif'); - s = smile(s, ':shock:', 'eek.gif'); - s = smile(s, ':evil:', 'evil.gif'); - s = smile(s, ':lol:', 'lol.gif'); - s = smile(s, ':mrgreen:', 'mrgreen.gif'); - s = smile(s, ':oops:', 'redface.gif'); - s = smile(s, ':roll:', 'rolleyes.gif'); - - s = smile(s, ':?', 'confused.gif'); - s = smile(s, ':D', 'biggrin.gif'); - s = smile(s, '8)', 'cool.gif'); - s = smile(s, ':x', 'mad.gif'); - s = smile(s, ':|', 'neutral.gif'); - s = smile(s, ':P', 'razz.gif'); - s = smile(s, ':(', 'sad.gif'); - s = smile(s, ':)', 'smile.gif'); - s = smile(s, ':o', 'surprised.gif'); - s = smile(s, ';)', 'wink.gif'); - - return s; - }, - - links: function(s) { - return s.replace(/((https|http|ftp|ed2k):\/\/[\S]+)/gi, '$1'); - }, - - truncate: function(clearAll) { - var truncateTo = clearAll ? 0 : this.prefs.truncate; - var posts = $('#ys-posts .ys-post').length; - if (posts <= truncateTo) { return; } - //alert(this.initializing); - if (this.prefs.doTruncate || this.initializing) { - var diff = posts - truncateTo; - for (var i = 0; i < diff; i++) { - this.p.shift(); - } - - // $('#ys-posts .ys-post:gt(' + truncateTo + ')').remove(); - - if (this.prefs.inverse) { - $('#ys-posts .ys-post:gt(' + (truncateTo - 1) + ')').remove(); - } else { - $('#ys-posts .ys-post:lt(' + (posts - truncateTo) + ')').remove(); - } - } - - this.markEnds(); - }, - - markEnds: function() { - $('#ys-posts') - .find('.ys-first').removeClass('ys-first').end() - .find('.ys-last').removeClass('ys-last'); - - $('#ys-posts .ys-post:first-child').addClass('ys-first'); - $('#ys-posts .ys-post:last-child').addClass('ys-last'); - }, - - reload: function(everything) { - var self = this; - this.initializing = true; - - if (everything) { - this.ajax(function(json) { - $('#yshout').html(''); - clearInterval(this.refreshTimer); - clearInterval(this.floodTimer); - this.initialLoad(json); - }, { - reqType: 'init', - yPath: this.options.yPath, - log: this.options.log - }); - } else { - this.ajax(function(json) { this.truncate(true); this.updates(json); this.initializing = false; }, { - reqType: 'reload' - }); - } - }, - - error: function(str) { - alert(str); - }, - - json: function(parse) { - this.d('In json: ' + parse); - var json = eval('(' + parse + ')'); - if (!this.checkError(json)) { return json; } - }, - - checkError: function(json) { - if (!json.yError) { return false; } - - this.d('Error: ' + json.yError); - return true; - }, - - ajax: function(callback, pars, html) { - pars = jQuery.extend({ - reqFor: 'shout' - }, pars); - - var self = this; - - $.ajax({ - type: 'POST', - url: this.options.yPath + 'yshout.php', - dataType: html ? 'text' : 'json', - data: pars, - success: function(parse) { - var arr = [parse]; - callback.apply(self, arr); - } - }); - }, - - d: function(message) { - // console.log(message); - $('#debug').css('display', 'block').prepend('

    ' + message + '

    '); - return message; - } -}; diff --git a/ext/chatbox/logs/.htaccess b/ext/chatbox/logs/.htaccess deleted file mode 100644 index fdb803ca..00000000 --- a/ext/chatbox/logs/.htaccess +++ /dev/null @@ -1,4 +0,0 @@ - -order allow,deny -deny from all - \ No newline at end of file diff --git a/ext/chatbox/logs/log.1.txt b/ext/chatbox/logs/log.1.txt deleted file mode 100644 index 7b63d5b6..00000000 --- a/ext/chatbox/logs/log.1.txt +++ /dev/null @@ -1 +0,0 @@ -a:2:{s:4:"info";a:1:{s:15:"latestTimestamp";d:1365655195.8733589649200439453125;}s:5:"posts";a:1:{i:0;a:6:{s:8:"nickname";s:7:"YaoiFox";s:7:"message";s:42:"I hope enjoy this chatbox based on YShout!";s:9:"timestamp";d:1365655195.8733589649200439453125;s:5:"admin";b:0;s:3:"uid";s:32:"ee9e9a7a01909be8065571655dad044d";s:9:"adminInfo";a:1:{s:2:"ip";s:11:"84.193.78.8";}}}} \ No newline at end of file diff --git a/ext/chatbox/logs/yshout.bans.txt b/ext/chatbox/logs/yshout.bans.txt deleted file mode 100644 index c856afcf..00000000 --- a/ext/chatbox/logs/yshout.bans.txt +++ /dev/null @@ -1 +0,0 @@ -a:0:{} \ No newline at end of file diff --git a/ext/chatbox/logs/yshout.prefs.txt b/ext/chatbox/logs/yshout.prefs.txt deleted file mode 100644 index d76446b3..00000000 --- a/ext/chatbox/logs/yshout.prefs.txt +++ /dev/null @@ -1 +0,0 @@ -a:23:{s:8:"password";s:8:"fortytwo";s:7:"refresh";i:6000;s:4:"logs";i:5;s:7:"history";i:200;s:7:"inverse";b:0;s:8:"truncate";i:15;s:10:"doTruncate";b:1;s:9:"timestamp";i:12;s:15:"defaultNickname";s:8:"Nickname";s:14:"defaultMessage";s:12:"Message Text";s:13:"defaultSubmit";s:6:"Shout!";s:10:"showSubmit";b:1;s:14:"nicknameLength";i:25;s:13:"messageLength";i:175;s:17:"nicknameSeparator";s:1:":";s:5:"flood";b:1;s:12:"floodTimeout";i:5000;s:13:"floodMessages";i:4;s:12:"floodDisable";i:8000;s:12:"autobanFlood";i:0;s:11:"censorWords";s:19:"fuck shit bitch ass";s:12:"postFormLink";s:7:"history";s:4:"info";s:6:"inline";} \ No newline at end of file diff --git a/ext/chatbox/main.php b/ext/chatbox/main.php deleted file mode 100644 index 80081d46..00000000 --- a/ext/chatbox/main.php +++ /dev/null @@ -1,36 +0,0 @@ - - * Link: http://www.drudexsoftware.com - * License: GPLv2 - * Description: Places an ajax chatbox at the bottom of each page - * Documentation: - * This chatbox uses YShout 5 as core. - */ -class Chatbox extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; - - // Adds header to enable chatbox - $root = get_base_href(); - $yPath = make_http( $root . "/ext/chatbox/"); - $page->add_html_header(" - - - - - - - ", 500); - - // loads the chatbox at the set location - $html = "
    "; - $chatblock = new Block("Chatbox", $html, "main", 97); - $chatblock->is_content = false; - $page->add_block($chatblock); - } -} diff --git a/ext/chatbox/php/ajaxcall.class.php b/ext/chatbox/php/ajaxcall.class.php deleted file mode 100644 index 78107e09..00000000 --- a/ext/chatbox/php/ajaxcall.class.php +++ /dev/null @@ -1,284 +0,0 @@ -reqType = $_POST['reqType']; - } - - function process() { - switch($this->reqType) { - case 'init': - - $this->initSession(); - $this->sendFirstUpdates(); - break; - - case 'post': - $nickname = $_POST['nickname']; - $message = $_POST['message']; - cookie('yNickname', $nickname); - $ys = ys($_SESSION['yLog']); - - if ($ys->banned(ip())) { $this->sendBanned(); break; } - if ($post = $ys->post($nickname, $message)) { - // To use $post somewheres later - $this->sendUpdates(); - } - break; - - case 'refresh': - $ys = ys($_SESSION['yLog']); - if ($ys->banned(ip())) { $this->sendBanned(); break; } - - $this->sendUpdates(); - break; - - case 'reload': - $this->reload(); - break; - - case 'ban': - $this->doBan(); - break; - - case 'unban': - $this->doUnban(); - break; - - case 'delete': - $this->doDelete(); - break; - - case 'banself': - $this->banSelf(); - break; - - case 'unbanself': - $this->unbanSelf(); - break; - - case 'clearlog': - $this->clearLog(); - break; - - case 'clearlogs': - $this->clearLogs(); - break; - } - } - - function doBan() { - $ip = $_POST['ip']; - $nickname = $_POST['nickname']; - $send = array(); - $ys = ys($_SESSION['yLog']); - - switch(true) { - case !loggedIn(): - $send['error'] = 'admin'; - break; - case $ys->banned($ip): - $send['error'] = 'already'; - break; - default: - $ys->ban($ip, $nickname); - if ($ip == ip()) - $send['bannedSelf'] = true; - $send['error'] = false; - } - - echo json_encode($send); - } - - function doUnban() { - $ip = $_POST['ip']; - $send = array(); - $ys = ys($_SESSION['yLog']); - - switch(true) { - case !loggedIn(): - $send['error'] = 'admin'; - break; - case !$ys->banned($ip): - $send['error'] = 'already'; - break; - default: - $ys->unban($ip); - $send['error'] = false; - } - - echo json_encode($send); - } - - function doDelete() { - $uid = $_POST['uid']; - $send = array(); - $ys = ys($_SESSION['yLog']); - - switch(true) { - case !loggedIn(): - $send['error'] = 'admin'; - break; - default: - $ys->delete($uid); - $send['error'] = false; - } - - echo json_encode($send); - } - - function banSelf() { - $ys = ys($_SESSION['yLog']); - $nickname = $_POST['nickname']; - $ys->ban(ip(), $nickname); - - $send = array(); - $send['error'] = false; - - echo json_encode($send); - } - - function unbanSelf() { - if (loggedIn()) { - $ys = ys($_SESSION['yLog']); - $ys->unban(ip()); - - $send = array(); - $send['error'] = false; - } else { - $send = array(); - $send['error'] = 'admin'; - } - - echo json_encode($send); - } - - function reload() { - global $prefs; - $ys = ys($_SESSION['yLog']); - - $posts = $ys->latestPosts($prefs['truncate']); - $this->setSessTimestamp($posts); - $this->updates['posts'] = $posts; - echo json_encode($this->updates); - } - - function initSession() { - $_SESSION['yLatestTimestamp'] = 0; - $_SESSION['yYPath'] = $_POST['yPath']; - $_SESSION['yLog'] = $_POST['log']; - $loginHash = cookieGet('yLoginHash') ; - if (isset($loginHash) && $loginHash != '') { - login($loginHash); - } - } - - function sendBanned() { - $this->updates = array( - 'banned' => true - ); - - echo json_encode($this->updates); - } - - function sendUpdates() { - global $prefs; - $ys = ys($_SESSION['yLog']); - if (!$ys->hasPostsAfter($_SESSION['yLatestTimestamp'])) return; - - $posts = $ys->postsAfter($_SESSION['yLatestTimestamp']); - $this->setSessTimestamp($posts); - - $this->updates['posts'] = $posts; - - echo json_encode($this->updates); - } - - function setSessTimestamp(&$posts) { - if (!$posts) return; - - $latest = array_slice( $posts, -1, 1); - $_SESSION['yLatestTimestamp'] = $latest[0]['timestamp']; - } - - function sendFirstUpdates() { - global $prefs, $overrideNickname; - - $this->updates = array(); - - $ys = ys($_SESSION['yLog']); - - $posts = $ys->latestPosts($prefs['truncate']); - $this->setSessTimestamp($posts); - - $this->updates['posts'] = $posts; - $this->updates['prefs'] = $this->cleanPrefs($prefs); - - if ($nickname = cookieGet('yNickname')) - $this->updates['nickname'] = $nickname; - - if ($overrideNickname) - $this->updates['nickname'] = $overrideNickname; - - if ($ys->banned(ip())) - $this->updates['banned'] = true; - - echo json_encode($this->updates); - } - - function cleanPrefs($prefs) { - unset($prefs['password']); - return $prefs; - } - - function clearLog() { - //$log = $_POST['log']; - $send = array(); - $ys = ys($_SESSION['yLog']); - - switch(true) { - case !loggedIn(): - $send['error'] = 'admin'; - break; - default: - $ys->clear(); - $send['error'] = false; - } - - echo json_encode($send); - } - - function clearLogs() { - global $prefs; - - //$log = $_POST['log']; - $send = array(); - - //$ys = ys($_SESSION['yLog']); - - switch(true) { - case !loggedIn(): - $send['error'] = 'admin'; - break; - default: - for ($i = 1; $i <= $prefs['logs']; $i++) { - $ys = ys($i); - $ys->clear(); - } - - $send['error'] = false; - } - - echo json_encode($send); - } - } - - diff --git a/ext/chatbox/php/filestorage.class.php b/ext/chatbox/php/filestorage.class.php deleted file mode 100644 index a7ab5ba4..00000000 --- a/ext/chatbox/php/filestorage.class.php +++ /dev/null @@ -1,84 +0,0 @@ -shoutLog = $shoutLog; - $folder = 'logs'; - if (!is_dir($folder)) $folder = '../' . $folder; - if (!is_dir($folder)) $folder = '../' . $folder; - - $this->path = $folder . '/' . $path . '.txt'; - } - - function open($lock = false) { - $this->handle = fopen($this->path, 'a+'); - - if ($lock) { - $this->lock(); - return $this->load(); - } - } - - function close(&$array) { - if (isset($array)) - $this->save($array); - - $this->unlock(); - fclose($this->handle); - unset($this->handle); - } - - function load() { - if (($contents = $this->read($this->path)) == null) - return $this->resetArray(); - - return unserialize($contents); - } - - function save(&$array, $unlock = true) { - $contents = serialize($array); - $this->write($contents); - if ($unlock) $this->unlock(); - } - - function unlock() { - if (isset($this->handle)) - flock($this->handle, LOCK_UN); - } - - function lock() { - if (isset($this->handle)) - flock($this->handle, LOCK_EX); - } - - function read() { - fseek($this->handle, 0); - //return stream_get_contents($this->handle); - return file_get_contents($this->path); - } - - function write($contents) { - ftruncate($this->handle, 0); - fwrite($this->handle, $contents); - } - - function resetArray() { - if ($this->shoutLog) - $default = array( - 'info' => array( - 'latestTimestamp' => -1 - ), - - 'posts' => array() - ); - else - $default = array(); - - $this->save($default, false); - return $default; - } -} - diff --git a/ext/chatbox/php/functions.php b/ext/chatbox/php/functions.php deleted file mode 100644 index 23eca1c1..00000000 --- a/ext/chatbox/php/functions.php +++ /dev/null @@ -1,141 +0,0 @@ -= $len) break; - if ($chr & 0x80) { - $chr <<= 1; - while ($chr & 0x80) { - $i++; - $chr <<= 1; - } - } - } - - return $count; - } - - function error($err) { - echo 'Error: ' . $err; - exit; - } - - function ys($log = 1) { - global $yShout, $prefs; - if ($yShout) return $yShout; - - if (filter_var($log, FILTER_VALIDATE_INT, array("options" => array("min_range" => 0, "max_range" => $prefs['logs']))) === false) - { - $log = 1; - } - - $log = 'log.' . $log; - return new YShout($log, loggedIn()); - } - - function dstart() { - global $ts; - - $ts = ts(); - } - - function dstop() { - global $ts; - echo 'Time elapsed: ' . ((ts() - $ts) * 100000); - exit; - } - - function login($hash) { - // echo 'login: ' . $hash . "\n"; - - $_SESSION['yLoginHash'] = $hash; - cookie('yLoginHash', $hash); - // return loggedIn(); - } - - function logout() { - $_SESSION['yLoginHash'] = ''; - cookie('yLoginHash', ''); -// cookieClear('yLoginHash'); - } - - function loggedIn() { - global $prefs; - - $loginHash = cookieGet('yLoginHash', false); -// echo 'loggedin: ' . $loginHash . "\n"; -// echo 'pw: ' . $prefs['password'] . "\n"; - - if (isset($loginHash)) return $loginHash == md5($prefs['password']); - - if (isset($_SESSION['yLoginHash'])) - return $_SESSION['yLoginHash'] == md5($prefs['password']); - - return false; - } - diff --git a/ext/chatbox/php/yshout.class.php b/ext/chatbox/php/yshout.class.php deleted file mode 100644 index e3b3f02b..00000000 --- a/ext/chatbox/php/yshout.class.php +++ /dev/null @@ -1,251 +0,0 @@ -storage = new $storage($path, true); - $this->admin = $admin; - } - - function posts() { - global $null; - $this->storage->open(); - $s = $this->storage->load(); - $this->storage->close($null); - - if ($s) - return $s['posts']; - } - - function info() { - global $null; - $s = $this->storage->open(true); - - $this->storage->close($null); - - if ($s) - return $s['info']; - } - - function postsAfter($ts) { - $allPosts = $this->posts(); - - $posts = array(); - - /* for ($i = sizeof($allPosts) - 1; $i > -1; $i--) { - $post = $allPosts[$i]; - - if ($post['timestamp'] > $ts) - $posts[] = $post; - } */ - - foreach($allPosts as $post) { - if ($post['timestamp'] > $ts) - $posts[] = $post; - } - - $this->postProcess($posts); - return $posts; - } - - function latestPosts($num) { - $allPosts = $this->posts(); - $posts = array_slice($allPosts, -$num, $num); - - $this->postProcess($posts); - return array_values($posts); - } - - function hasPostsAfter($ts) { - $info = $this->info(); - $timestamp = $info['latestTimestamp']; - return $timestamp > $ts; - } - - function post($nickname, $message) { - global $prefs; - - if ($this->banned(ip()) /* && !$this->admin*/) return false; - - if (!$this->validate($message, $prefs['messageLength'])) return false; - if (!$this->validate($nickname, $prefs['nicknameLength'])) return false; - - $message = trim(clean($message)); - $nickname = trim(clean($nickname)); - - if ($message == '') return false; - if ($nickname == '') return false; - - $timestamp = ts(); - - $message = $this->censor($message); - $nickname = $this->censor($nickname); - - $post = array( - 'nickname' => $nickname, - 'message' => $message, - 'timestamp' => $timestamp, - 'admin' => $this->admin, - 'uid' => md5($timestamp . ' ' . $nickname), - 'adminInfo' => array( - 'ip' => ip() - ) - ); - - $s = $this->storage->open(true); - - $s['posts'][] = $post; - - if (sizeof($s['posts']) > $prefs['history']) - $this->truncate($s['posts']); - - $s['info']['latestTimestamp'] = $post['timestamp']; - - $this->storage->close($s); - $this->postProcess($post); - return $post; - } - - function truncate(&$array) { - global $prefs; - - $array = array_slice($array, -$prefs['history']); - $array = array_values($array); - } - - function clear() { - global $null; - - $this->storage->open(true); - $this->storage->resetArray(); - // ? Scared to touch it... Misspelled though. Update: Touched! Used to be $nulls... - $this->storage->close($null); - } - - function bans() { - global $storage, $null; - - $s = new $storage('yshout.bans'); - $s->open(); - $bans = $s->load(); - $s->close($null); - - return $bans; - } - - function ban($ip, $nickname = '', $info = '') { - global $storage; - - $s = new $storage('yshout.bans'); - $bans = $s->open(true); - - $bans[] = array( - 'ip' => $ip, - 'nickname' => $nickname, - 'info' => $info, - 'timestamp' => ts() - ); - - $s->close($bans); - } - - function banned($ip) { - global $storage, $null; - - $s = new $storage('yshout.bans'); - $bans = $s->open(true); - $s->close($null); - - foreach($bans as $ban) { - if ($ban['ip'] == $ip) - return true; - } - - return false; - } - - function unban($ip) { - global $storage; - - $s = new $storage('yshout.bans'); - $bans = $s->open(true); - - foreach($bans as $key=>$value) - if ($value['ip'] == $ip) { - unset($bans[$key]); - } - - $bans = array_values($bans); - $s->close($bans); - - } - - function unbanAll() { - global $storage, $null; - - $s = new $storage('yshout.bans'); - $s->open(true); - $s->resetArray(); - $s->close($null); - } - - function delete($uid) { - global $prefs, $storage; - - - $s = $this->storage->open(true); - - $posts = $s['posts']; - - foreach($posts as $key=>$value) { - if (!isset($value['uid'])) - unset($posts['key']); - else - if($value['uid'] == $uid) - unset($posts[$key]); - } - - $s['posts'] = array_values($posts); - $this->storage->close($s); - - return true; - } - - function validate($str, $maxLen) { - return len($str) <= $maxLen; - } - - function censor($str) { - global $prefs; - - $cWords = explode(' ', $prefs['censorWords']); - $words = explode(' ', $str); - $endings = '|ed|es|ing|s|er|ers'; - $arrEndings = explode('|', $endings); - - foreach ($cWords as $cWord) foreach ($words as $i=>$word) { - $pattern = '/^(' . $cWord . ')+(' . $endings . ')\W*$/i'; - $words[$i] = preg_replace($pattern, str_repeat('*', strlen($word)), $word); - } - - return implode(' ', $words); - } - - function postProcess(&$post) { - if (isset($post['message'])) { - if ($this->banned($post['adminInfo']['ip'])) $post['banned'] = true; - if (!$this->admin) unset($post['adminInfo']); - } else { - foreach($post as $key=>$value) { - if ($this->banned($value['adminInfo']['ip'])) $post[$key]['banned'] = true; - if (!$this->admin) unset($post[$key]['adminInfo']); - } - } - } -} - - diff --git a/ext/chatbox/preferences.php b/ext/chatbox/preferences.php deleted file mode 100644 index cc72b33b..00000000 --- a/ext/chatbox/preferences.php +++ /dev/null @@ -1,74 +0,0 @@ -open(); - $prefs = $s->load(); - $s->close($null); - } - - function savePrefs($newPrefs) { - global $prefs, $storage; - - $s = new $storage('yshout.prefs'); - $s->open(true); - $s->close($newPrefs); - $prefs = $newPrefs; - } - - function resetPrefs() { - $defaultPrefs = array( - 'password' => 'fortytwo', // The password for the CP - - 'refresh' => 6000, // Refresh rate - - 'logs' => 5, // Amount of different log files to allow - 'history' => 200, // Shouts to keep in history - - 'inverse' => false, // Inverse shoutbox / form on top - - 'truncate' => 15, // Truncate messages client-side - 'doTruncate' => true, // Truncate messages? - - 'timestamp' => 12, // Timestamp format 12- or 24-hour - - 'defaultNickname' => 'Nickname', - 'defaultMessage' => 'Message Text', - 'defaultSubmit' => 'Shout!', - 'showSubmit' => true, - - 'nicknameLength' => 25, - 'messageLength' => 175, - - 'nicknameSeparator' => ':', - - 'flood' => true, - 'floodTimeout' => 5000, - 'floodMessages' => 4, - 'floodDisable' => 8000, - 'floodDelete' => false, - - 'autobanFlood' => 0, // Autoban people for flooding after X messages - - 'censorWords' => 'fuck shit bitch ass', - - 'postFormLink' => 'history', - - 'info' => 'inline' - ); - - savePrefs($defaultPrefs); - } - - resetPrefs(); - //loadPrefs(); - - diff --git a/ext/chatbox/smileys/biggrin.gif b/ext/chatbox/smileys/biggrin.gif deleted file mode 100644 index d3527723..00000000 Binary files a/ext/chatbox/smileys/biggrin.gif and /dev/null differ diff --git a/ext/chatbox/smileys/confused.gif b/ext/chatbox/smileys/confused.gif deleted file mode 100644 index 0c49e069..00000000 Binary files a/ext/chatbox/smileys/confused.gif and /dev/null differ diff --git a/ext/chatbox/smileys/cool.gif b/ext/chatbox/smileys/cool.gif deleted file mode 100644 index cead0306..00000000 Binary files a/ext/chatbox/smileys/cool.gif and /dev/null differ diff --git a/ext/chatbox/smileys/cry.gif b/ext/chatbox/smileys/cry.gif deleted file mode 100644 index 7d54b1f9..00000000 Binary files a/ext/chatbox/smileys/cry.gif and /dev/null differ diff --git a/ext/chatbox/smileys/eek.gif b/ext/chatbox/smileys/eek.gif deleted file mode 100644 index 5d397810..00000000 Binary files a/ext/chatbox/smileys/eek.gif and /dev/null differ diff --git a/ext/chatbox/smileys/evil.gif b/ext/chatbox/smileys/evil.gif deleted file mode 100644 index ab1aa8e1..00000000 Binary files a/ext/chatbox/smileys/evil.gif and /dev/null differ diff --git a/ext/chatbox/smileys/lol.gif b/ext/chatbox/smileys/lol.gif deleted file mode 100644 index 374ba150..00000000 Binary files a/ext/chatbox/smileys/lol.gif and /dev/null differ diff --git a/ext/chatbox/smileys/mad.gif b/ext/chatbox/smileys/mad.gif deleted file mode 100644 index 1f6c3c2f..00000000 Binary files a/ext/chatbox/smileys/mad.gif and /dev/null differ diff --git a/ext/chatbox/smileys/mrgreen.gif b/ext/chatbox/smileys/mrgreen.gif deleted file mode 100644 index b54cd0f9..00000000 Binary files a/ext/chatbox/smileys/mrgreen.gif and /dev/null differ diff --git a/ext/chatbox/smileys/neutral.gif b/ext/chatbox/smileys/neutral.gif deleted file mode 100644 index 4f311567..00000000 Binary files a/ext/chatbox/smileys/neutral.gif and /dev/null differ diff --git a/ext/chatbox/smileys/razz.gif b/ext/chatbox/smileys/razz.gif deleted file mode 100644 index 29da2a2f..00000000 Binary files a/ext/chatbox/smileys/razz.gif and /dev/null differ diff --git a/ext/chatbox/smileys/redface.gif b/ext/chatbox/smileys/redface.gif deleted file mode 100644 index ad762832..00000000 Binary files a/ext/chatbox/smileys/redface.gif and /dev/null differ diff --git a/ext/chatbox/smileys/rolleyes.gif b/ext/chatbox/smileys/rolleyes.gif deleted file mode 100644 index d7f5f2f4..00000000 Binary files a/ext/chatbox/smileys/rolleyes.gif and /dev/null differ diff --git a/ext/chatbox/smileys/sad.gif b/ext/chatbox/smileys/sad.gif deleted file mode 100644 index d2ac78c0..00000000 Binary files a/ext/chatbox/smileys/sad.gif and /dev/null differ diff --git a/ext/chatbox/smileys/smile.gif b/ext/chatbox/smileys/smile.gif deleted file mode 100644 index 7b1f6d30..00000000 Binary files a/ext/chatbox/smileys/smile.gif and /dev/null differ diff --git a/ext/chatbox/smileys/surprised.gif b/ext/chatbox/smileys/surprised.gif deleted file mode 100644 index cb214243..00000000 Binary files a/ext/chatbox/smileys/surprised.gif and /dev/null differ diff --git a/ext/chatbox/smileys/twisted.gif b/ext/chatbox/smileys/twisted.gif deleted file mode 100644 index 502fe247..00000000 Binary files a/ext/chatbox/smileys/twisted.gif and /dev/null differ diff --git a/ext/chatbox/smileys/wink.gif b/ext/chatbox/smileys/wink.gif deleted file mode 100644 index d1482880..00000000 Binary files a/ext/chatbox/smileys/wink.gif and /dev/null differ diff --git a/ext/chatbox/yshout.php b/ext/chatbox/yshout.php deleted file mode 100644 index 0994309f..00000000 --- a/ext/chatbox/yshout.php +++ /dev/null @@ -1,38 +0,0 @@ -process(); - break; - - case 'history': - - // echo $_POST['log']; - $ajax = new AjaxCall($_POST['log']); - $ajax->process(); - break; - - default: - exit; - } else { - include 'example.html'; - } - -function errorOccurred($num, $str, $file, $line) { - $err = array ( - 'yError' => "$str. \n File: $file \n Line: $line" - ); - - echo json_encode($err); - - exit; -} - diff --git a/ext/comment/info.php b/ext/comment/info.php new file mode 100644 index 00000000..acec497f --- /dev/null +++ b/ext/comment/info.php @@ -0,0 +1,15 @@ + - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Allow users to make comments on images - * Documentation: - * Formatting is done with the standard formatting API (normally BBCode) - */ +image_id = $image_id; - $this->user = $user; - $this->comment = $comment; - } + public function __construct(int $image_id, User $user, string $comment) + { + parent::__construct(); + $this->image_id = $image_id; + $this->user = $user; + $this->comment = $comment; + } } /** @@ -37,380 +25,440 @@ class CommentPostingEvent extends Event { * detectors to get a feel for what should be deleted * and what should be kept? */ -class CommentDeletionEvent extends Event { - /** @var int */ - public $comment_id; +class CommentDeletionEvent extends Event +{ + /** @var int */ + public $comment_id; - /** - * @param int $comment_id - */ - public function __construct($comment_id) { - assert('is_numeric($comment_id)'); - $this->comment_id = $comment_id; - } + public function __construct(int $comment_id) + { + parent::__construct(); + $this->comment_id = $comment_id; + } } -class CommentPostingException extends SCoreException {} +class CommentPostingException extends SCoreException +{ +} -class Comment { - public $owner, $owner_id, $owner_name, $owner_email, $owner_class; - public $comment, $comment_id; - public $image_id, $poster_ip, $posted; +class Comment +{ + /** @var User */ + public $owner; - public function __construct($row) { - $this->owner = null; - $this->owner_id = $row['user_id']; - $this->owner_name = $row['user_name']; - $this->owner_email = $row['user_email']; // deprecated - $this->owner_class = $row['user_class']; - $this->comment = $row['comment']; - $this->comment_id = $row['comment_id']; - $this->image_id = $row['image_id']; - $this->poster_ip = $row['poster_ip']; - $this->posted = $row['posted']; - } + /** @var int */ + public $owner_id; - /** - * @param User $user - * @return mixed - */ - public static function count_comments_by_user($user) { - global $database; - return $database->get_one(" + /** @var string */ + public $owner_name; + + /** @var string */ + public $owner_email; + + /** @var string */ + public $owner_class; + + /** @var string */ + public $comment; + + /** @var int */ + public $comment_id; + + /** @var int */ + public $image_id; + + /** @var string */ + public $poster_ip; + + /** @var string */ + public $posted; + + public function __construct($row) + { + $this->owner = null; + $this->owner_id = $row['user_id']; + $this->owner_name = $row['user_name']; + $this->owner_email = $row['user_email']; // deprecated + $this->owner_class = $row['user_class']; + $this->comment = $row['comment']; + $this->comment_id = $row['comment_id']; + $this->image_id = $row['image_id']; + $this->poster_ip = $row['poster_ip']; + $this->posted = $row['posted']; + } + + public static function count_comments_by_user(User $user): int + { + global $database; + return (int)$database->get_one(" SELECT COUNT(*) AS count FROM comments WHERE owner_id=:owner_id - ", array("owner_id"=>$user->id)); - } + ", ["owner_id"=>$user->id]); + } - /** - * @return null|User - */ - public function get_owner() { - if(empty($this->owner)) $this->owner = User::by_id($this->owner_id); - return $this->owner; - } + public function get_owner(): User + { + if (empty($this->owner)) { + $this->owner = User::by_id($this->owner_id); + } + return $this->owner; + } } -class CommentList extends Extension { - /** @var CommentListTheme $theme */ - public $theme; +class CommentList extends Extension +{ + /** @var CommentListTheme $theme */ + public $theme; - public function onInitExt(InitExtEvent $event) { - global $config, $database; - $config->set_default_int('comment_window', 5); - $config->set_default_int('comment_limit', 10); - $config->set_default_int('comment_list_count', 10); - $config->set_default_int('comment_count', 5); - $config->set_default_bool('comment_captcha', false); + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_int('comment_window', 5); + $config->set_default_int('comment_limit', 10); + $config->set_default_int('comment_list_count', 10); + $config->set_default_int('comment_count', 5); + $config->set_default_bool('comment_captcha', false); + } - if($config->get_int("ext_comments_version") < 3) { - // shortcut to latest - if($config->get_int("ext_comments_version") < 1) { - $database->create_table("comments", " + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $database; + if ($this->get_version("ext_comments_version") < 3) { + // shortcut to latest + if ($this->get_version("ext_comments_version") < 1) { + $database->create_table("comments", " id SCORE_AIPK, image_id INTEGER NOT NULL, owner_id INTEGER NOT NULL, owner_ip SCORE_INET NOT NULL, - posted SCORE_DATETIME DEFAULT NULL, + posted TIMESTAMP DEFAULT CURRENT_TIMESTAMP, comment TEXT NOT NULL, FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT "); - $database->execute("CREATE INDEX comments_image_id_idx ON comments(image_id)", array()); - $database->execute("CREATE INDEX comments_owner_id_idx ON comments(owner_id)", array()); - $database->execute("CREATE INDEX comments_posted_idx ON comments(posted)", array()); - $config->set_int("ext_comments_version", 3); - } + $database->execute("CREATE INDEX comments_image_id_idx ON comments(image_id)", []); + $database->execute("CREATE INDEX comments_owner_id_idx ON comments(owner_id)", []); + $database->execute("CREATE INDEX comments_posted_idx ON comments(posted)", []); + $this->set_version("ext_comments_version", 3); + } - // the whole history - if($config->get_int("ext_comments_version") < 1) { - $database->create_table("comments", " + // the whole history + if ($this->get_version("ext_comments_version") < 1) { + $database->create_table("comments", " id SCORE_AIPK, image_id INTEGER NOT NULL, owner_id INTEGER NOT NULL, owner_ip CHAR(16) NOT NULL, - posted SCORE_DATETIME DEFAULT NULL, + posted TIMESTAMP DEFAULT CURRENT_TIMESTAMP, comment TEXT NOT NULL "); - $database->execute("CREATE INDEX comments_image_id_idx ON comments(image_id)", array()); - $config->set_int("ext_comments_version", 1); - } + $database->execute("CREATE INDEX comments_image_id_idx ON comments(image_id)", []); + $this->set_version("ext_comments_version", 1); + } - if($config->get_int("ext_comments_version") == 1) { - $database->Execute("CREATE INDEX comments_owner_ip ON comments(owner_ip)"); - $database->Execute("CREATE INDEX comments_posted ON comments(posted)"); - $config->set_int("ext_comments_version", 2); - } + if ($this->get_version("ext_comments_version") == 1) { + $database->Execute("CREATE INDEX comments_owner_ip ON comments(owner_ip)"); + $database->Execute("CREATE INDEX comments_posted ON comments(posted)"); + $this->set_version("ext_comments_version", 2); + } - if($config->get_int("ext_comments_version") == 2) { - $config->set_int("ext_comments_version", 3); - $database->Execute("ALTER TABLE comments ADD FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE"); - $database->Execute("ALTER TABLE comments ADD FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT"); - } + if ($this->get_version("ext_comments_version") == 2) { + $this->set_version("ext_comments_version", 3); + $database->Execute("ALTER TABLE comments ADD FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE"); + $database->Execute("ALTER TABLE comments ADD FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT"); + } - // FIXME: add foreign keys, bump to v3 - } - } + // FIXME: add foreign keys, bump to v3 + } + } - public function onPageRequest(PageRequestEvent $event) { - if($event->page_matches("comment")) { - switch($event->get_arg(0)) { - case "add": $this->onPageRequest_add(); break; - case "delete": $this->onPageRequest_delete($event); break; - case "bulk_delete": $this->onPageRequest_bulk_delete(); break; - case "list": $this->onPageRequest_list($event); break; - case "beta-search": $this->onPageRequest_beta_search($event); break; - } - } - } - private function onPageRequest_add() { - global $user, $page; - if (isset($_POST['image_id']) && isset($_POST['comment'])) { - try { - $i_iid = int_escape($_POST['image_id']); - $cpe = new CommentPostingEvent($_POST['image_id'], $user, $_POST['comment']); - send_event($cpe); - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/$i_iid#comment_on_$i_iid")); - } catch (CommentPostingException $ex) { - $this->theme->display_error(403, "Comment Blocked", $ex->getMessage()); - } - } - } + public function onPageNavBuilding(PageNavBuildingEvent $event) + { + $event->add_nav_link("comment", new Link('comment/list'), "Comments"); + } - private function onPageRequest_delete(PageRequestEvent $event) { - global $user, $page; - if ($user->can("delete_comment")) { - // FIXME: post, not args - if ($event->count_args() === 3) { - send_event(new CommentDeletionEvent($event->get_arg(1))); - flash_message("Deleted comment"); - $page->set_mode("redirect"); - if (!empty($_SERVER['HTTP_REFERER'])) { - $page->set_redirect($_SERVER['HTTP_REFERER']); - } else { - $page->set_redirect(make_link("post/view/" . $event->get_arg(2))); - } - } - } else { - $this->theme->display_permission_denied(); - } - } - private function onPageRequest_bulk_delete() { - global $user, $database, $page; - if ($user->can("delete_comment") && !empty($_POST["ip"])) { - $ip = $_POST['ip']; + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + if ($event->parent=="comment") { + $event->add_nav_link("comment_list", new Link('comment/list'), "All"); + $event->add_nav_link("comment_help", new Link('ext_doc/comment'), "Help"); + } + } - $comment_ids = $database->get_col(" + public function onPageRequest(PageRequestEvent $event) + { + if ($event->page_matches("comment")) { + switch ($event->get_arg(0)) { + case "add": $this->onPageRequest_add(); break; + case "delete": $this->onPageRequest_delete($event); break; + case "bulk_delete": $this->onPageRequest_bulk_delete(); break; + case "list": $this->onPageRequest_list($event); break; + case "beta-search": $this->onPageRequest_beta_search($event); break; + } + } + } + + private function onPageRequest_add() + { + global $user, $page; + if (isset($_POST['image_id']) && isset($_POST['comment'])) { + try { + $i_iid = int_escape($_POST['image_id']); + $cpe = new CommentPostingEvent(int_escape($_POST['image_id']), $user, $_POST['comment']); + send_event($cpe); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/$i_iid#comment_on_$i_iid")); + } catch (CommentPostingException $ex) { + $this->theme->display_error(403, "Comment Blocked", $ex->getMessage()); + } + } + } + + private function onPageRequest_delete(PageRequestEvent $event) + { + global $user, $page; + if ($user->can(Permissions::DELETE_COMMENT)) { + // FIXME: post, not args + if ($event->count_args() === 3) { + send_event(new CommentDeletionEvent(int_escape($event->get_arg(1)))); + $page->flash("Deleted comment"); + $page->set_mode(PageMode::REDIRECT); + if (!empty($_SERVER['HTTP_REFERER'])) { + $page->set_redirect($_SERVER['HTTP_REFERER']); + } else { + $page->set_redirect(make_link("post/view/" . $event->get_arg(2))); + } + } + } else { + $this->theme->display_permission_denied(); + } + } + + private function onPageRequest_bulk_delete() + { + global $user, $database, $page; + if ($user->can(Permissions::DELETE_COMMENT) && !empty($_POST["ip"])) { + $ip = $_POST['ip']; + + $comment_ids = $database->get_col(" SELECT id FROM comments WHERE owner_ip=:ip - ", array("ip" => $ip)); - $num = count($comment_ids); - log_warning("comment", "Deleting $num comments from $ip"); - foreach($comment_ids as $cid) { - send_event(new CommentDeletionEvent($cid)); - } - flash_message("Deleted $num comments"); + ", ["ip" => $ip]); + $num = count($comment_ids); + log_warning("comment", "Deleting $num comments from $ip"); + foreach ($comment_ids as $cid) { + send_event(new CommentDeletionEvent($cid)); + } + $page->flash("Deleted $num comments"); - $page->set_mode("redirect"); - $page->set_redirect(make_link("admin")); - } else { - $this->theme->display_permission_denied(); - } - } + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("admin")); + } else { + $this->theme->display_permission_denied(); + } + } - private function onPageRequest_list(PageRequestEvent $event) { - $page_num = int_escape($event->get_arg(1)); - $this->build_page($page_num); - } + private function onPageRequest_list(PageRequestEvent $event) + { + $page_num = $event->try_page_num(1); + $this->build_page($page_num); + } - private function onPageRequest_beta_search(PageRequestEvent $event) { - $search = $event->get_arg(1); - $page_num = int_escape($event->get_arg(2)); - $duser = User::by_name($search); - $i_comment_count = Comment::count_comments_by_user($duser); - $com_per_page = 50; - $total_pages = ceil($i_comment_count / $com_per_page); - $page_num = clamp($page_num, 1, $total_pages); - $comments = $this->get_user_comments($duser->id, $com_per_page, ($page_num - 1) * $com_per_page); - $this->theme->display_all_user_comments($comments, $page_num, $total_pages, $duser); - } + private function onPageRequest_beta_search(PageRequestEvent $event) + { + $search = $event->get_arg(1); + $page_num = $event->try_page_num(2); + $duser = User::by_name($search); + $i_comment_count = Comment::count_comments_by_user($duser); + $com_per_page = 50; + $total_pages = (int)ceil($i_comment_count / $com_per_page); + $page_num = clamp($page_num, 1, $total_pages); + $comments = $this->get_user_comments($duser->id, $com_per_page, ($page_num - 1) * $com_per_page); + $this->theme->display_all_user_comments($comments, $page_num, $total_pages, $duser); + } - public function onAdminBuilding(AdminBuildingEvent $event) { - $this->theme->display_admin_block(); - } + public function onAdminBuilding(AdminBuildingEvent $event) + { + $this->theme->display_admin_block(); + } - public function onPostListBuilding(PostListBuildingEvent $event) { - global $config, $database; - $cc = $config->get_int("comment_count"); - if($cc > 0) { - $recent = $database->cache->get("recent_comments"); - if(empty($recent)) { - $recent = $this->get_recent_comments($cc); - $database->cache->set("recent_comments", $recent, 60); - } - if(count($recent) > 0) { - $this->theme->display_recent_comments($recent); - } - } - } + public function onPostListBuilding(PostListBuildingEvent $event) + { + global $cache, $config; + $cc = $config->get_int("comment_count"); + if ($cc > 0) { + $recent = $cache->get("recent_comments"); + if (empty($recent)) { + $recent = $this->get_recent_comments($cc); + $cache->set("recent_comments", $recent, 60); + } + if (count($recent) > 0) { + $this->theme->display_recent_comments($recent); + } + } + } - public function onUserPageBuilding(UserPageBuildingEvent $event) { - $i_days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1; - $i_comment_count = Comment::count_comments_by_user($event->display_user); - $h_comment_rate = sprintf("%.1f", ($i_comment_count / $i_days_old)); - $event->add_stats("Comments made: $i_comment_count, $h_comment_rate per day"); + public function onUserPageBuilding(UserPageBuildingEvent $event) + { + $i_days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1; + $i_comment_count = Comment::count_comments_by_user($event->display_user); + $h_comment_rate = sprintf("%.1f", ($i_comment_count / $i_days_old)); + $event->add_stats("Comments made: $i_comment_count, $h_comment_rate per day"); - $recent = $this->get_user_comments($event->display_user->id, 10); - $this->theme->display_recent_user_comments($recent, $event->display_user); - } + $recent = $this->get_user_comments($event->display_user->id, 10); + $this->theme->display_recent_user_comments($recent, $event->display_user); + } - public function onDisplayingImage(DisplayingImageEvent $event) { - global $user; - $this->theme->display_image_comments( - $event->image, - $this->get_comments($event->image->id), - $user->can("create_comment") - ); - } + public function onDisplayingImage(DisplayingImageEvent $event) + { + global $user; + $this->theme->display_image_comments( + $event->image, + $this->get_comments($event->image->id), + $user->can(Permissions::CREATE_COMMENT) + ); + } - // TODO: split akismet into a separate class, which can veto the event - public function onCommentPosting(CommentPostingEvent $event) { - $this->add_comment_wrapper($event->image_id, $event->user, $event->comment); - } + // TODO: split akismet into a separate class, which can veto the event + public function onCommentPosting(CommentPostingEvent $event) + { + $this->add_comment_wrapper($event->image_id, $event->user, $event->comment); + } - public function onCommentDeletion(CommentDeletionEvent $event) { - global $database; - $database->Execute(" + public function onCommentDeletion(CommentDeletionEvent $event) + { + global $database; + $database->Execute(" DELETE FROM comments WHERE id=:comment_id - ", array("comment_id"=>$event->comment_id)); - log_info("comment", "Deleting Comment #{$event->comment_id}"); - } + ", ["comment_id"=>$event->comment_id]); + log_info("comment", "Deleting Comment #{$event->comment_id}"); + } - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Comment Options"); - $sb->add_bool_option("comment_captcha", "Require CAPTCHA for anonymous comments: "); - $sb->add_label("
    Limit to "); - $sb->add_int_option("comment_limit"); - $sb->add_label(" comments per "); - $sb->add_int_option("comment_window"); - $sb->add_label(" minutes"); - $sb->add_label("
    Show "); - $sb->add_int_option("comment_count"); - $sb->add_label(" recent comments on the index"); - $sb->add_label("
    Show "); - $sb->add_int_option("comment_list_count"); - $sb->add_label(" comments per image on the list"); - $sb->add_label("
    Make samefags public "); - $sb->add_bool_option("comment_samefags_public"); - $event->panel->add_block($sb); - } + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Comment Options"); + $sb->add_bool_option("comment_captcha", "Require CAPTCHA for anonymous comments: "); + $sb->add_label("
    Limit to "); + $sb->add_int_option("comment_limit"); + $sb->add_label(" comments per "); + $sb->add_int_option("comment_window"); + $sb->add_label(" minutes"); + $sb->add_label("
    Show "); + $sb->add_int_option("comment_count"); + $sb->add_label(" recent comments on the index"); + $sb->add_label("
    Show "); + $sb->add_int_option("comment_list_count"); + $sb->add_label(" comments per image on the list"); + $sb->add_label("
    Make samefags public "); + $sb->add_bool_option("comment_samefags_public"); + $event->panel->add_block($sb); + } - public function onSearchTermParse(SearchTermParseEvent $event) { - $matches = array(); + public function onSearchTermParse(SearchTermParseEvent $event) + { + if (is_null($event->term)) { + return; + } - if(preg_match("/^comments([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { - $cmp = ltrim($matches[1], ":") ?: "="; - $comments = $matches[2]; - $event->add_querylet(new Querylet("images.id IN (SELECT DISTINCT image_id FROM comments GROUP BY image_id HAVING count(image_id) $cmp $comments)")); - } - else if(preg_match("/^commented_by[=|:](.*)$/i", $event->term, $matches)) { - $user = User::by_name($matches[1]); - if(!is_null($user)) { - $user_id = $user->id; - } else { - $user_id = -1; - } + $matches = []; + if (preg_match("/^comments([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { + $cmp = ltrim($matches[1], ":") ?: "="; + $comments = $matches[2]; + $event->add_querylet(new Querylet("images.id IN (SELECT DISTINCT image_id FROM comments GROUP BY image_id HAVING count(image_id) $cmp $comments)")); + } elseif (preg_match("/^commented_by[=|:](.*)$/i", $event->term, $matches)) { + $user_id = User::name_to_id($matches[1]); + $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM comments WHERE owner_id = $user_id)")); + } elseif (preg_match("/^commented_by_userno[=|:]([0-9]+)$/i", $event->term, $matches)) { + $user_id = int_escape($matches[1]); + $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM comments WHERE owner_id = $user_id)")); + } + } - $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM comments WHERE owner_id = $user_id)")); - } - else if(preg_match("/^commented_by_userno[=|:]([0-9]+)$/i", $event->term, $matches)) { - $user_id = int_escape($matches[1]); - $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM comments WHERE owner_id = $user_id)")); - } - } + public function onHelpPageBuilding(HelpPageBuildingEvent $event) + { + if ($event->key===HelpPages::SEARCH) { + $block = new Block(); + $block->header = "Comments"; + $block->body = $this->theme->get_help_html(); + $event->add_block($block); + } + } -// page building {{{ - /** - * @param int $current_page - */ - private function build_page(/*int*/ $current_page) { - global $database, $user; + private function build_page(int $current_page) + { + global $cache, $database, $user; - $where = SPEED_HAX ? "WHERE posted > now() - interval '24 hours'" : ""; - - $total_pages = $database->cache->get("comment_pages"); - if(empty($total_pages)) { - $total_pages = (int)($database->get_one(" + $where = SPEED_HAX ? "WHERE posted > now() - interval '24 hours'" : ""; + + $total_pages = $cache->get("comment_pages"); + if (empty($total_pages)) { + $total_pages = (int)($database->get_one(" SELECT COUNT(c1) FROM (SELECT COUNT(image_id) AS c1 FROM comments $where GROUP BY image_id) AS s1 ") / 10); - $database->cache->set("comment_pages", $total_pages, 600); - } - $total_pages = max($total_pages, 1); + $cache->set("comment_pages", $total_pages, 600); + } + $total_pages = max($total_pages, 1); - $current_page = clamp($current_page, 1, $total_pages); - - $threads_per_page = 10; - $start = $threads_per_page * ($current_page - 1); + $current_page = clamp($current_page, 1, $total_pages); - $result = $database->Execute(" + $threads_per_page = 10; + $start = $threads_per_page * ($current_page - 1); + + $result = $database->Execute(" SELECT image_id,MAX(posted) AS latest FROM comments $where GROUP BY image_id ORDER BY latest DESC LIMIT :limit OFFSET :offset - ", array("limit"=>$threads_per_page, "offset"=>$start)); + ", ["limit"=>$threads_per_page, "offset"=>$start]); - $user_ratings = ext_is_live("Ratings") ? Ratings::get_user_privs($user) : ""; + $user_ratings = Extension::is_enabled(RatingsInfo::KEY) ? Ratings::get_user_class_privs($user) : ""; - $images = array(); - while($row = $result->fetch()) { - $image = Image::by_id($row["image_id"]); - if( - ext_is_live("Ratings") && !is_null($image) && - strpos($user_ratings, $image->rating) === FALSE - ) { - $image = null; // this is "clever", I may live to regret it - } - if(!is_null($image)) { - $comments = $this->get_comments($image->id); - $images[] = array($image, $comments); - } - } + $images = []; + while ($row = $result->fetch()) { + $image = Image::by_id((int)$row["image_id"]); + if ( + Extension::is_enabled(RatingsInfo::KEY) && !is_null($image) && + !in_array($image->rating, $user_ratings) + ) { + $image = null; // this is "clever", I may live to regret it + } + if (!is_null($image)) { + $comments = $this->get_comments($image->id); + $images[] = [$image, $comments]; + } + } - $this->theme->display_comment_list($images, $current_page, $total_pages, $user->can("create_comment")); - } -// }}} + $this->theme->display_comment_list($images, $current_page, $total_pages, $user->can(Permissions::CREATE_COMMENT)); + } -// get comments {{{ - /** - * @param string $query - * @param array $args - * @return Comment[] - */ - private function get_generic_comments($query, $args) { - global $database; - $rows = $database->get_all($query, $args); - $comments = array(); - foreach($rows as $row) { - $comments[] = new Comment($row); - } - return $comments; - } + /** + * #return Comment[] + */ + private function get_generic_comments(string $query, array $args): array + { + global $database; + $rows = $database->get_all($query, $args); + $comments = []; + foreach ($rows as $row) { + $comments[] = new Comment($row); + } + return $comments; + } - /** - * @param int $count - * @return Comment[] - */ - private function get_recent_comments($count) { - return $this->get_generic_comments(" + /** + * #return Comment[] + */ + private function get_recent_comments(int $count): array + { + return $this->get_generic_comments(" SELECT users.id as user_id, users.name as user_name, users.email as user_email, users.class as user_class, comments.comment as comment, comments.id as comment_id, @@ -420,17 +468,15 @@ class CommentList extends Extension { LEFT JOIN users ON comments.owner_id=users.id ORDER BY comments.id DESC LIMIT :limit - ", array("limit"=>$count)); - } + ", ["limit"=>$count]); + } - /** - * @param int $user_id - * @param int $count - * @param int $offset - * @return Comment[] - */ - private function get_user_comments(/*int*/ $user_id, /*int*/ $count, /*int*/ $offset=0) { - return $this->get_generic_comments(" + /** + * #return Comment[] + */ + private function get_user_comments(int $user_id, int $count, int $offset=0): array + { + return $this->get_generic_comments(" SELECT users.id as user_id, users.name as user_name, users.email as user_email, users.class as user_class, comments.comment as comment, comments.id as comment_id, @@ -441,15 +487,15 @@ class CommentList extends Extension { WHERE users.id = :user_id ORDER BY comments.id DESC LIMIT :limit OFFSET :offset - ", array("user_id"=>$user_id, "offset"=>$offset, "limit"=>$count)); - } + ", ["user_id"=>$user_id, "offset"=>$offset, "limit"=>$count]); + } - /** - * @param int $image_id - * @return Comment[] - */ - private function get_comments(/*int*/ $image_id) { - return $this->get_generic_comments(" + /** + * #return Comment[] + */ + private function get_comments(int $image_id): array + { + return $this->get_generic_comments(" SELECT users.id as user_id, users.name as user_name, users.email as user_email, users.class as user_class, comments.comment as comment, comments.id as comment_id, @@ -459,192 +505,167 @@ class CommentList extends Extension { LEFT JOIN users ON comments.owner_id=users.id WHERE comments.image_id=:image_id ORDER BY comments.id ASC - ", array("image_id"=>$image_id)); - } -// }}} + ", ["image_id"=>$image_id]); + } -// add / remove / edit comments {{{ - /** - * @return bool - */ - private function is_comment_limit_hit() { - global $config, $database; + private function is_comment_limit_hit(): bool + { + global $config, $database; - // sqlite fails at intervals - if($database->get_driver_name() === "sqlite") return false; + // sqlite fails at intervals + if ($database->get_driver_name() === DatabaseDriver::SQLITE) { + return false; + } - $window = int_escape($config->get_int('comment_window')); - $max = int_escape($config->get_int('comment_limit')); + $window = $config->get_int('comment_window'); + $max = $config->get_int('comment_limit'); - if($database->get_driver_name() == "mysql") $window_sql = "interval $window minute"; - else $window_sql = "interval '$window minute'"; + if ($database->get_driver_name() == DatabaseDriver::MYSQL) { + $window_sql = "interval $window minute"; + } else { + $window_sql = "interval '$window minute'"; + } - // window doesn't work as an SQL param because it's inside quotes >_< - $result = $database->get_all(" + // window doesn't work as an SQL param because it's inside quotes >_< + $result = $database->get_all(" SELECT * FROM comments WHERE owner_ip = :remote_ip AND posted > now() - $window_sql - ", array("remote_ip"=>$_SERVER['REMOTE_ADDR'])); + ", ["remote_ip"=>$_SERVER['REMOTE_ADDR']]); - return (count($result) >= $max); - } + return (count($result) >= $max); + } - /** - * @return bool - */ - private function hash_match() { - return ($_POST['hash'] == $this->get_hash()); - } + private function hash_match(): bool + { + return ($_POST['hash'] == $this->get_hash()); + } - /** - * get a hash which semi-uniquely identifies a submission form, - * to stop spam bots which download the form once then submit - * many times. - * - * FIXME: assumes comments are posted via HTTP... - * - * @return string - */ - public static function get_hash() { - return md5($_SERVER['REMOTE_ADDR'] . date("%Y%m%d")); - } + /** + * get a hash which semi-uniquely identifies a submission form, + * to stop spam bots which download the form once then submit + * many times. + * + * FIXME: assumes comments are posted via HTTP... + */ + public static function get_hash(): string + { + return md5($_SERVER['REMOTE_ADDR'] . date("%Y%m%d")); + } - /** - * @param string $text - * @return bool - */ - private function is_spam_akismet(/*string*/ $text) { - global $config, $user; - if(strlen($config->get_string('comment_wordpress_key')) > 0) { - $comment = array( - 'author' => $user->name, - 'email' => $user->email, - 'website' => '', - 'body' => $text, - 'permalink' => '', - ); + private function is_spam_akismet(string $text): bool + { + global $config, $user; + if (strlen($config->get_string('comment_wordpress_key')) > 0) { + $comment = [ + 'author' => $user->name, + 'email' => $user->email, + 'website' => '', + 'body' => $text, + 'permalink' => '', + ]; - # akismet breaks if there's no referrer in the environment; so if there - # isn't, supply one manually - if(!isset($_SERVER['HTTP_REFERER'])) { - $comment['referrer'] = 'none'; - log_warning("comment", "User '{$user->name}' commented with no referrer: $text"); - } - if(!isset($_SERVER['HTTP_USER_AGENT'])) { - $comment['user_agent'] = 'none'; - log_warning("comment", "User '{$user->name}' commented with no user-agent: $text"); - } + # akismet breaks if there's no referrer in the environment; so if there + # isn't, supply one manually + if (!isset($_SERVER['HTTP_REFERER'])) { + $comment['referrer'] = 'none'; + log_warning("comment", "User '{$user->name}' commented with no referrer: $text"); + } + if (!isset($_SERVER['HTTP_USER_AGENT'])) { + $comment['user_agent'] = 'none'; + log_warning("comment", "User '{$user->name}' commented with no user-agent: $text"); + } - $akismet = new Akismet( - $_SERVER['SERVER_NAME'], - $config->get_string('comment_wordpress_key'), - $comment); + $akismet = new Akismet( + $_SERVER['SERVER_NAME'], + $config->get_string('comment_wordpress_key'), + $comment + ); - if($akismet->errorsExist()) { - return false; - } - else { - return $akismet->isSpam(); - } - } + if ($akismet->errorsExist()) { + return false; + } else { + return $akismet->isSpam(); + } + } - return false; - } + return false; + } - /** - * @param int $image_id - * @param int $comment - * @return null - */ - private function is_dupe(/*int*/ $image_id, /*string*/ $comment) { - global $database; - return $database->get_row(" + private function is_dupe(int $image_id, string $comment): bool + { + global $database; + return (bool)$database->get_row(" SELECT * FROM comments WHERE image_id=:image_id AND comment=:comment - ", array("image_id"=>$image_id, "comment"=>$comment)); - } -// do some checks + ", ["image_id"=>$image_id, "comment"=>$comment]); + } + // do some checks - /** - * @param int $image_id - * @param User $user - * @param string $comment - * @throws CommentPostingException - */ - private function add_comment_wrapper(/*int*/ $image_id, User $user, /*string*/ $comment) { - global $database, $page; + private function add_comment_wrapper(int $image_id, User $user, string $comment) + { + global $database, $page; - if(!$user->can("bypass_comment_checks")) { - // will raise an exception if anything is wrong - $this->comment_checks($image_id, $user, $comment); - } + if (!$user->can(Permissions::BYPASS_COMMENT_CHECKS)) { + // will raise an exception if anything is wrong + $this->comment_checks($image_id, $user, $comment); + } - // all checks passed - if($user->is_anonymous()) { - $page->add_cookie("nocache", "Anonymous Commenter", time()+60*60*24, "/"); - } - $database->Execute( - "INSERT INTO comments(image_id, owner_id, owner_ip, posted, comment) ". - "VALUES(:image_id, :user_id, :remote_addr, now(), :comment)", - array("image_id"=>$image_id, "user_id"=>$user->id, "remote_addr"=>$_SERVER['REMOTE_ADDR'], "comment"=>$comment)); - $cid = $database->get_last_insert_id('comments_id_seq'); - $snippet = substr($comment, 0, 100); - $snippet = str_replace("\n", " ", $snippet); - $snippet = str_replace("\r", " ", $snippet); - log_info("comment", "Comment #$cid added to Image #$image_id: $snippet", false, array("image_id"=>$image_id, "comment_id"=>$cid)); - } + // all checks passed + if ($user->is_anonymous()) { + $page->add_cookie("nocache", "Anonymous Commenter", time()+60*60*24, "/"); + } + $database->Execute( + "INSERT INTO comments(image_id, owner_id, owner_ip, posted, comment) ". + "VALUES(:image_id, :user_id, :remote_addr, now(), :comment)", + ["image_id"=>$image_id, "user_id"=>$user->id, "remote_addr"=>$_SERVER['REMOTE_ADDR'], "comment"=>$comment] + ); + $cid = $database->get_last_insert_id('comments_id_seq'); + $snippet = substr($comment, 0, 100); + $snippet = str_replace("\n", " ", $snippet); + $snippet = str_replace("\r", " ", $snippet); + log_info("comment", "Comment #$cid added to Image #$image_id: $snippet"); + } - /** - * @param int $image_id - * @param User $user - * @param string $comment - * @throws CommentPostingException - */ - private function comment_checks(/*int*/ $image_id, User $user, /*string*/ $comment) { - global $config, $page; + private function comment_checks(int $image_id, User $user, string $comment) + { + global $config, $page; - // basic sanity checks - if(!$user->can("create_comment")) { - throw new CommentPostingException("Anonymous posting has been disabled"); - } - else if(is_null(Image::by_id($image_id))) { - throw new CommentPostingException("The image does not exist"); - } - else if(trim($comment) == "") { - throw new CommentPostingException("Comments need text..."); - } - else if(strlen($comment) > 9000) { - throw new CommentPostingException("Comment too long~"); - } + // basic sanity checks + if (!$user->can(Permissions::CREATE_COMMENT)) { + throw new CommentPostingException("Anonymous posting has been disabled"); + } elseif (is_null(Image::by_id($image_id))) { + throw new CommentPostingException("The image does not exist"); + } elseif (trim($comment) == "") { + throw new CommentPostingException("Comments need text..."); + } elseif (strlen($comment) > 9000) { + throw new CommentPostingException("Comment too long~"); + } - // advanced sanity checks - else if(strlen($comment)/strlen(gzcompress($comment)) > 10) { - throw new CommentPostingException("Comment too repetitive~"); - } - else if($user->is_anonymous() && !$this->hash_match()) { - $page->add_cookie("nocache", "Anonymous Commenter", time()+60*60*24, "/"); - throw new CommentPostingException( - "Comment submission form is out of date; refresh the ". - "comment form to show you aren't a spammer~"); - } + // advanced sanity checks + elseif (strlen($comment)/strlen(gzcompress($comment)) > 10) { + throw new CommentPostingException("Comment too repetitive~"); + } elseif ($user->is_anonymous() && !$this->hash_match()) { + $page->add_cookie("nocache", "Anonymous Commenter", time()+60*60*24, "/"); + throw new CommentPostingException( + "Comment submission form is out of date; refresh the ". + "comment form to show you aren't a spammer~" + ); + } - // database-querying checks - else if($this->is_comment_limit_hit()) { - throw new CommentPostingException("You've posted several comments recently; wait a minute and try again..."); - } - else if($this->is_dupe($image_id, $comment)) { - throw new CommentPostingException("Someone already made that comment on that image -- try and be more original?"); - } + // database-querying checks + elseif ($this->is_comment_limit_hit()) { + throw new CommentPostingException("You've posted several comments recently; wait a minute and try again..."); + } elseif ($this->is_dupe($image_id, $comment)) { + throw new CommentPostingException("Someone already made that comment on that image -- try and be more original?"); + } - // rate-limited external service checks last - else if($config->get_bool('comment_captcha') && !captcha_check()) { - throw new CommentPostingException("Error in captcha"); - } - else if($user->is_anonymous() && $this->is_spam_akismet($comment)) { - throw new CommentPostingException("Akismet thinks that your comment is spam. Try rewriting the comment, or logging in."); - } - } -// }}} + // rate-limited external service checks last + elseif ($config->get_bool('comment_captcha') && !captcha_check()) { + throw new CommentPostingException("Error in captcha"); + } elseif ($user->is_anonymous() && $this->is_spam_akismet($comment)) { + throw new CommentPostingException("Akismet thinks that your comment is spam. Try rewriting the comment, or logging in."); + } + } } - diff --git a/ext/comment/script.js b/ext/comment/script.js new file mode 100644 index 00000000..47023be7 --- /dev/null +++ b/ext/comment/script.js @@ -0,0 +1,8 @@ +function replyTo(imageId, commentId, userId) { + var box = $("#comment_on_"+imageId); + var text = "[url=site://post/view/"+imageId+"#c"+commentId+"]@"+userId+"[/url]: "; + + box.focus(); + box.val(box.val() + text); + $("#c"+commentId).highlight(); +} diff --git a/ext/comment/style.css b/ext/comment/style.css index 74aa19e6..373b34f4 100644 --- a/ext/comment/style.css +++ b/ext/comment/style.css @@ -14,11 +14,11 @@ background: #DDD; border: 1px solid #CCC; position: absolute; - top: 0px; + top: 0; left: -195px; width: 180px; z-index: 1; - box-shadow: 0px 0px 4px #000; + box-shadow: 0 0 4px #000; border-radius: 4px; } .comment:hover .info { @@ -39,6 +39,6 @@ background: none; border: none; box-shadow: none; - margin: 0px; - padding: 0px; + margin: 0; + padding: 0; } diff --git a/ext/comment/test.php b/ext/comment/test.php index 93230a71..9541d6fa 100644 --- a/ext/comment/test.php +++ b/ext/comment/test.php @@ -1,110 +1,106 @@ -set_int("comment_limit", 100); - $this->log_out(); - } +set_int("comment_limit", 100); + $this->log_out(); + } - public function tearDown() { - global $config; - $config->set_int("comment_limit", 10); - parent::tearDown(); - } + public function tearDown(): void + { + global $config; + $config->set_int("comment_limit", 10); + parent::tearDown(); + } - public function testCommentsPage() { - global $user; + public function testCommentsPage() + { + global $user; - $this->log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - # a good comment - send_event(new CommentPostingEvent($image_id, $user, "Test Comment ASDFASDF")); - $this->get_page("post/view/$image_id"); - $this->assert_text("ASDFASDF"); + # a good comment + send_event(new CommentPostingEvent($image_id, $user, "Test Comment ASDFASDF")); + $this->get_page("post/view/$image_id"); + $this->assert_text("ASDFASDF"); - # dupe - try { - send_event(new CommentPostingEvent($image_id, $user, "Test Comment ASDFASDF")); - } - catch(CommentPostingException $e) { - $this->assertContains("try and be more original", $e->getMessage()); - } + # dupe + try { + send_event(new CommentPostingEvent($image_id, $user, "Test Comment ASDFASDF")); + } catch (CommentPostingException $e) { + $this->assertStringContainsString("try and be more original", $e->getMessage()); + } - # empty comment - try { - send_event(new CommentPostingEvent($image_id, $user, "")); - } - catch(CommentPostingException $e) { - $this->assertContains("Comments need text", $e->getMessage()); - } + # empty comment + try { + send_event(new CommentPostingEvent($image_id, $user, "")); + } catch (CommentPostingException $e) { + $this->assertStringContainsString("Comments need text", $e->getMessage()); + } - # whitespace is still empty... - try { - send_event(new CommentPostingEvent($image_id, $user, " \t\r\n")); - } - catch(CommentPostingException $e) { - $this->assertContains("Comments need text", $e->getMessage()); - } + # whitespace is still empty... + try { + send_event(new CommentPostingEvent($image_id, $user, " \t\r\n")); + } catch (CommentPostingException $e) { + $this->assertStringContainsString("Comments need text", $e->getMessage()); + } - # repetitive (aka. gzip gives >= 10x improvement) - try { - send_event(new CommentPostingEvent($image_id, $user, str_repeat("U", 5000))); - } - catch(CommentPostingException $e) { - $this->assertContains("Comment too repetitive", $e->getMessage()); - } + # repetitive (aka. gzip gives >= 10x improvement) + try { + send_event(new CommentPostingEvent($image_id, $user, str_repeat("U", 5000))); + } catch (CommentPostingException $e) { + $this->assertStringContainsString("Comment too repetitive", $e->getMessage()); + } - # test UTF8 - send_event(new CommentPostingEvent($image_id, $user, "Test Comment むちむち")); - $this->get_page("post/view/$image_id"); - $this->assert_text("むちむち"); + # test UTF8 + send_event(new CommentPostingEvent($image_id, $user, "Test Comment むちむち")); + $this->get_page("post/view/$image_id"); + $this->assert_text("むちむち"); - # test that search by comment metadata works -// $this->get_page("post/list/commented_by=test/1"); -// $this->assert_title("Image $image_id: pbx"); -// $this->get_page("post/list/comments=2/1"); -// $this->assert_title("Image $image_id: pbx"); + # test that search by comment metadata works + // $this->get_page("post/list/commented_by=test/1"); + // $this->assert_title("Image $image_id: pbx"); + // $this->get_page("post/list/comments=2/1"); + // $this->assert_title("Image $image_id: pbx"); - $this->log_out(); + $this->log_out(); - $this->get_page('comment/list'); - $this->assert_title('Comments'); - $this->assert_text('ASDFASDF'); + $this->get_page('comment/list'); + $this->assert_title('Comments'); + $this->assert_text('ASDFASDF'); - $this->get_page('comment/list/2'); - $this->assert_title('Comments'); + $this->get_page('comment/list/2'); + $this->assert_title('Comments'); - $this->log_in_as_admin(); - $this->delete_image($image_id); - $this->log_out(); + $this->log_in_as_admin(); + $this->delete_image($image_id); + $this->log_out(); - $this->get_page('comment/list'); - $this->assert_title('Comments'); - $this->assert_no_text('ASDFASDF'); - } + $this->get_page('comment/list'); + $this->assert_title('Comments'); + $this->assert_no_text('ASDFASDF'); + } - public function testSingleDel() { - $this->markTestIncomplete(); + public function testSingleDel() + { + global $database, $user; - $this->log_in_as_admin(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + $this->log_in_as_admin(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - # make a comment - $this->get_page("post/view/$image_id"); - $this->set_field('comment', "Test Comment ASDFASDF"); - $this->click("Post Comment"); - $this->assert_title("Image $image_id: pbx"); - $this->assert_text("ASDFASDF"); + # make a comment + send_event(new CommentPostingEvent($image_id, $user, "Test Comment ASDFASDF")); + $this->get_page("post/view/$image_id"); + $this->assert_text("ASDFASDF"); - # delete it - $this->click("Del"); - $this->assert_title("Image $image_id: pbx"); - $this->assert_no_text("ASDFASDF"); - - # tidy up - $this->delete_image($image_id); - $this->log_out(); - } + # delete a comment + $comment_id = (int)$database->get_one("SELECT id FROM comments"); + send_event(new CommentDeletionEvent($comment_id)); + $this->get_page("post/view/$image_id"); + $this->assert_no_text("ASDFASDF"); + } } diff --git a/ext/comment/theme.php b/ext/comment/theme.php index f017bdb3..9c969783 100644 --- a/ext/comment/theme.php +++ b/ext/comment/theme.php @@ -1,114 +1,97 @@ -ct)) { - $this->ct = hsl_rainbow(); - } - if(!array_key_exists($ip, $this->anon_map)) { - $this->anon_map[$ip] = $this->ct[$this->anon_cid++ % count($this->ct)]; - } - return $this->anon_map[$ip]; - } + /** + * Display a page with a list of images, and for each image, the image's comments. + */ + public function display_comment_list(array $images, int $page_number, int $total_pages, bool $can_post) + { + global $config, $page, $user; - /** - * Display a page with a list of images, and for each image, the image's comments. - * - * @param array $images - * @param int $page_number - * @param int $total_pages - * @param bool $can_post - */ - public function display_comment_list($images, $page_number, $total_pages, $can_post) { - global $config, $page, $user; + // aaaaaaargh php + assert(is_array($images)); + assert(is_numeric($page_number)); + assert(is_numeric($total_pages)); + assert(is_bool($can_post)); - // aaaaaaargh php - assert(is_array($images)); - assert(is_numeric($page_number)); - assert(is_numeric($total_pages)); - assert(is_bool($can_post)); + // parts for the whole page + $prev = $page_number - 1; + $next = $page_number + 1; - // parts for the whole page - $prev = $page_number - 1; - $next = $page_number + 1; + $h_prev = ($page_number <= 1) ? "Prev" : + 'Prev'; + $h_index = "Index"; + $h_next = ($page_number >= $total_pages) ? "Next" : + 'Next'; - $h_prev = ($page_number <= 1) ? "Prev" : - 'Prev'; - $h_index = "Index"; - $h_next = ($page_number >= $total_pages) ? "Next" : - 'Next'; + $nav = $h_prev.' | '.$h_index.' | '.$h_next; - $nav = $h_prev.' | '.$h_index.' | '.$h_next; + $page->set_title("Comments"); + $page->set_heading("Comments"); + $page->add_block(new Block("Navigation", $nav, "left")); + $this->display_paginator($page, "comment/list", null, $page_number, $total_pages); - $page->set_title("Comments"); - $page->set_heading("Comments"); - $page->add_block(new Block("Navigation", $nav, "left")); - $this->display_paginator($page, "comment/list", null, $page_number, $total_pages); + // parts for each image + $position = 10; - // parts for each image - $position = 10; + $comment_limit = $config->get_int("comment_list_count", 10); + $comment_captcha = $config->get_bool('comment_captcha'); - $comment_limit = $config->get_int("comment_list_count", 10); - $comment_captcha = $config->get_bool('comment_captcha'); - - foreach($images as $pair) { - $image = $pair[0]; - $comments = $pair[1]; + foreach ($images as $pair) { + $image = $pair[0]; + $comments = $pair[1]; - $thumb_html = $this->build_thumb_html($image); - $comment_html = ""; - - $comment_count = count($comments); - if($comment_limit > 0 && $comment_count > $comment_limit) { - $comment_html .= "

    showing $comment_limit of $comment_count comments

    "; - $comments = array_slice($comments, -$comment_limit); - $this->show_anon_id = false; - } - else { - $this->show_anon_id = true; - } - $this->anon_id = 1; - foreach($comments as $comment) { - $comment_html .= $this->comment_to_html($comment); - } - if(!$user->is_anonymous()) { - if($can_post) { - $comment_html .= $this->build_postbox($image->id); - } - } else { - if ($can_post) { - if(!$comment_captcha) { - $comment_html .= $this->build_postbox($image->id); - } - else { - $link = make_link("post/view/".$image->id); - $comment_html .= "Add Comment"; - } - } - } + $thumb_html = $this->build_thumb_html($image); + $comment_html = ""; - $html = ' + $comment_count = count($comments); + if ($comment_limit > 0 && $comment_count > $comment_limit) { + $comment_html .= "

    showing $comment_limit of $comment_count comments

    "; + $comments = array_slice($comments, -$comment_limit); + $this->show_anon_id = false; + } else { + $this->show_anon_id = true; + } + $this->anon_id = 1; + foreach ($comments as $comment) { + $comment_html .= $this->comment_to_html($comment); + } + if (!$user->is_anonymous()) { + if ($can_post) { + $comment_html .= $this->build_postbox($image->id); + } + } else { + if ($can_post) { + if (!$comment_captcha) { + $comment_html .= $this->build_postbox($image->id); + } else { + $link = make_link("post/view/".$image->id); + $comment_html .= "Add Comment"; + } + } + } + + $html = '
    '.$thumb_html.' '.$comment_html.'
    '; - $page->add_block(new Block( $image->id.': '.$image->get_tag_list(), $html, "main", $position++, "comment-list-list")); - } - } + $page->add_block(new Block($image->id.': '.$image->get_tag_list(), $html, "main", $position++, "comment-list-list")); + } + } - public function display_admin_block() { - global $page; + public function display_admin_block() + { + global $page; - $html = ' + $html = ' Delete comments by IP.

    '.make_form(make_link("comment/bulk_delete"), 'POST')." @@ -118,175 +101,160 @@ class CommentListTheme extends Themelet { "; - $page->add_block(new Block("Mass Comment Delete", $html)); - } + $page->add_block(new Block("Mass Comment Delete", $html)); + } - /** - * Add some comments to the page, probably in a sidebar. - * - * @param \Comment[] $comments An array of Comment objects to be shown - */ - public function display_recent_comments($comments) { - global $page; - $this->show_anon_id = false; - $html = ""; - foreach($comments as $comment) { - $html .= $this->comment_to_html($comment, true); - } - $html .= "Full List"; - $page->add_block(new Block("Comments", $html, "left", 50, "comment-list-recent")); - } + /** + * Add some comments to the page, probably in a sidebar. + * + * #param Comment[] $comments An array of Comment objects to be shown + */ + public function display_recent_comments(array $comments) + { + global $page; + $this->show_anon_id = false; + $html = ""; + foreach ($comments as $comment) { + $html .= $this->comment_to_html($comment, true); + } + $html .= "Full List"; + $page->add_block(new Block("Comments", $html, "left", 50, "comment-list-recent")); + } - /** - * Show comments for an image. - * - * @param Image $image - * @param \Comment[] $comments - * @param bool $postbox - */ - public function display_image_comments(Image $image, $comments, $postbox) { - global $page; - $this->show_anon_id = true; - $html = ""; - foreach($comments as $comment) { - $html .= $this->comment_to_html($comment); - } - if($postbox) { - $html .= $this->build_postbox($image->id); - } - $page->add_block(new Block("Comments", $html, "main", 30, "comment-list-image")); - } + /** + * Show comments for an image. + * + * #param Comment[] $comments + */ + public function display_image_comments(Image $image, array $comments, bool $postbox) + { + global $page; + $this->show_anon_id = true; + $html = ""; + foreach ($comments as $comment) { + $html .= $this->comment_to_html($comment); + } + if ($postbox) { + $html .= $this->build_postbox($image->id); + } + $page->add_block(new Block("Comments", $html, "main", 30, "comment-list-image")); + } - /** - * Show comments made by a user. - * - * @param \Comment[] $comments - * @param \User $user - */ - public function display_recent_user_comments($comments, User $user) { - global $page; - $html = ""; - foreach($comments as $comment) { - $html .= $this->comment_to_html($comment, true); - } - if(empty($html)) { - $html = '

    No comments by this user.

    '; - } - else { - $html .= "

    More

    "; - } - $page->add_block(new Block("Comments", $html, "left", 70, "comment-list-user")); - } + /** + * Show comments made by a user. + * + * #param Comment[] $comments + */ + public function display_recent_user_comments(array $comments, User $user) + { + global $page; + $html = ""; + foreach ($comments as $comment) { + $html .= $this->comment_to_html($comment, true); + } + if (empty($html)) { + $html = '

    No comments by this user.

    '; + } else { + $html .= "

    More

    "; + } + $page->add_block(new Block("Comments", $html, "left", 70, "comment-list-user")); + } - /** - * @param \Comment[] $comments - * @param int $page_number - * @param int $total_pages - * @param \User $user - */ - public function display_all_user_comments($comments, $page_number, $total_pages, User $user) { - global $page; - - assert(is_numeric($page_number)); - assert(is_numeric($total_pages)); - - $html = ""; - foreach($comments as $comment) { - $html .= $this->comment_to_html($comment, true); - } - if(empty($html)) { - $html = '

    No comments by this user.

    '; - } - $page->add_block(new Block("Comments", $html, "main", 70, "comment-list-user")); + public function display_all_user_comments(array $comments, int $page_number, int $total_pages, User $user) + { + global $page; + + $html = ""; + foreach ($comments as $comment) { + $html .= $this->comment_to_html($comment, true); + } + if (empty($html)) { + $html = '

    No comments by this user.

    '; + } + $page->add_block(new Block("Comments", $html, "main", 70, "comment-list-user")); - $prev = $page_number - 1; - $next = $page_number + 1; - - //$search_terms = array('I','have','no','idea','what','this','does!'); - //$u_tags = url_escape(implode(" ", $search_terms)); - //$query = empty($u_tags) ? "" : '/'.$u_tags; + $prev = $page_number - 1; + $next = $page_number + 1; - $h_prev = ($page_number <= 1) ? "Prev" : "Prev"; - $h_index = "Index"; - $h_next = ($page_number >= $total_pages) ? "Next" : "Next"; + //$search_terms = array('I','have','no','idea','what','this','does!'); + //$u_tags = url_escape(Tag::implode($search_terms)); + //$query = empty($u_tags) ? "" : '/'.$u_tags; - $page->set_title(html_escape($user->name)."'s comments"); - $page->add_block(new Block("Navigation", $h_prev.' | '.$h_index.' | '.$h_next, "left", 0)); - $this->display_paginator($page, "comment/beta-search/{$user->name}", null, $page_number, $total_pages); - } + $h_prev = ($page_number <= 1) ? "Prev" : "Prev"; + $h_index = "Index"; + $h_next = ($page_number >= $total_pages) ? "Next" : "Next"; - /** - * @param \Comment $comment - * @param bool $trim - * @return string - */ - protected function comment_to_html(Comment $comment, $trim=false) { - global $config, $user; + $page->set_title(html_escape($user->name)."'s comments"); + $page->add_block(new Block("Navigation", $h_prev.' | '.$h_index.' | '.$h_next, "left", 0)); + $this->display_paginator($page, "comment/beta-search/{$user->name}", null, $page_number, $total_pages); + } - $tfe = new TextFormattingEvent($comment->comment); - send_event($tfe); + protected function comment_to_html(Comment $comment, bool $trim=false): string + { + global $config, $user; - $i_uid = int_escape($comment->owner_id); - $h_name = html_escape($comment->owner_name); - $h_timestamp = autodate($comment->posted); - $h_comment = ($trim ? truncate($tfe->stripped, 50) : $tfe->formatted); - $i_comment_id = int_escape($comment->comment_id); - $i_image_id = int_escape($comment->image_id); + $tfe = new TextFormattingEvent($comment->comment); + send_event($tfe); - if($i_uid == $config->get_int("anon_id")) { - $anoncode = ""; - $anoncode2 = ""; - if($this->show_anon_id) { - $anoncode = ''.$this->anon_id.''; - if(!array_key_exists($comment->poster_ip, $this->anon_map)) { - $this->anon_map[$comment->poster_ip] = $this->anon_id; - } - #if($user->can("view_ip")) { - #$style = " style='color: ".$this->get_anon_colour($comment->poster_ip).";'"; - if($user->can("view_ip") || $config->get_bool("comment_samefags_public", false)) { - if($this->anon_map[$comment->poster_ip] != $this->anon_id) { - $anoncode2 = '('.$this->anon_map[$comment->poster_ip].')'; - } - } - } - $h_userlink = "" . $h_name . $anoncode . $anoncode2 . ""; - $this->anon_id++; - } - else { - $h_userlink = ''.$h_name.''; - } + $i_uid = $comment->owner_id; + $h_name = html_escape($comment->owner_name); + $h_timestamp = autodate($comment->posted); + $h_comment = ($trim ? truncate($tfe->stripped, 50) : $tfe->formatted); + $i_comment_id = $comment->comment_id; + $i_image_id = $comment->image_id; - $hb = ($comment->owner_class == "hellbanned" ? "hb" : ""); - if($trim) { - $html = " + if ($i_uid == $config->get_int("anon_id")) { + $anoncode = ""; + $anoncode2 = ""; + if ($this->show_anon_id) { + $anoncode = ''.$this->anon_id.''; + if (!array_key_exists($comment->poster_ip, $this->anon_map)) { + $this->anon_map[$comment->poster_ip] = $this->anon_id; + } + #if($user->can(UserAbilities::VIEW_IP)) { + #$style = " style='color: ".$this->get_anon_colour($comment->poster_ip).";'"; + if ($user->can(Permissions::VIEW_IP) || $config->get_bool("comment_samefags_public", false)) { + if ($this->anon_map[$comment->poster_ip] != $this->anon_id) { + $anoncode2 = '('.$this->anon_map[$comment->poster_ip].')'; + } + } + } + $h_userlink = "" . $h_name . $anoncode . $anoncode2 . ""; + $this->anon_id++; + } else { + $h_userlink = ''.$h_name.''; + } + + $hb = ($comment->owner_class == "hellbanned" ? "hb" : ""); + if ($trim) { + $html = "
    $h_userlink: $h_comment >>>
    "; - } - else { - $h_avatar = ""; - if(!empty($comment->owner_email)) { - $hash = md5(strtolower($comment->owner_email)); - $cb = date("Y-m-d"); - $h_avatar = "
    "; - } - $h_reply = " - Reply"; - $h_ip = $user->can("view_ip") ? "
    ".show_ip($comment->poster_ip, "Comment posted {$comment->posted}") : ""; - $h_del = ""; - if ($user->can("delete_comment")) { - $comment_preview = substr(html_unescape($tfe->stripped), 0, 50); - $j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview"); - $h_delete_script = html_escape("return confirm($j_delete_confirm_message);"); - $h_delete_link = make_link("comment/delete/$i_comment_id/$i_image_id"); - $h_del = " - Del"; - } - $html = " + } else { + $h_avatar = ""; + if (!empty($comment->owner_email)) { + $hash = md5(strtolower($comment->owner_email)); + $cb = date("Y-m-d"); + $h_avatar = "avatar
    "; + } + $h_reply = " - Reply"; + $h_ip = $user->can(Permissions::VIEW_IP) ? "
    ".show_ip($comment->poster_ip, "Comment posted {$comment->posted}") : ""; + $h_del = ""; + if ($user->can(Permissions::DELETE_COMMENT)) { + $comment_preview = substr(html_unescape($tfe->stripped), 0, 50); + $j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview"); + $h_delete_script = html_escape("return confirm($j_delete_confirm_message);"); + $h_delete_link = make_link("comment/delete/$i_comment_id/$i_image_id"); + $h_del = " - Del"; + } + $html = "
    $h_avatar @@ -295,32 +263,50 @@ class CommentListTheme extends Themelet { $h_userlink: $h_comment
    "; - } - return $html; - } + } + return $html; + } - /** - * @param int $image_id - * @return string - */ - protected function build_postbox(/*int*/ $image_id) { - global $config; + protected function build_postbox(int $image_id): string + { + global $config; - $i_image_id = int_escape($image_id); - $hash = CommentList::get_hash(); - $h_captcha = $config->get_bool("comment_captcha") ? captcha_get_html() : ""; + $hash = CommentList::get_hash(); + $h_captcha = $config->get_bool("comment_captcha") ? captcha_get_html() : ""; - return ' + return '
    '.make_form(make_link("comment/add")).' - + - + '.$h_captcha.'
    '; - } -} + } + public function get_help_html() + { + return '

    Search for images containing a certain number of comments, or comments by a particular individual.

    +
    +
    comments=1
    +

    Returns images with exactly 1 comment.

    +
    +
    +
    comments>0
    +

    Returns images with 1 or more comments.

    +
    +

    Can use <, <=, >, >=, or =.

    +
    +
    commented_by:username
    +

    Returns images that have been commented on by "username".

    +
    +
    +
    commented_by_userno:123
    +

    Returns images that have been commented on by user 123.

    +
    + '; + } +} diff --git a/ext/cron_uploader/config.php b/ext/cron_uploader/config.php new file mode 100644 index 00000000..5c1d962c --- /dev/null +++ b/ext/cron_uploader/config.php @@ -0,0 +1,96 @@ +set_default_int(self::COUNT, 1); + $config->set_default_string(self::DIR, data_path(self::DEFAULT_PATH)); + + $upload_key = $config->get_string(self::KEY, ""); + if (empty($upload_key)) { + $upload_key = self::generate_key(); + + $config->set_string(self::KEY, $upload_key); + } + } + + public static function get_user(): int + { + global $config; + return $config->get_int(self::USER); + } + + public static function set_user(int $value): void + { + global $config; + $config->set_int(self::USER, $value); + } + + public static function get_key(): string + { + global $config; + return $config->get_string(self::KEY); + } + + public static function set_key(string $value): void + { + global $config; + $config->set_string(self::KEY, $value); + } + + public static function get_count(): int + { + global $config; + return $config->get_int(self::COUNT); + } + + public static function set_count(int $value): void + { + global $config; + $config->set_int(self::COUNT, $value); + } + + public static function get_dir(): string + { + global $config; + $value = $config->get_string(self::DIR); + if (empty($value)) { + $value = data_path("cron_uploader"); + self::set_dir($value); + } + return $value; + } + + public static function set_dir(string $value): void + { + global $config; + $config->set_string(self::DIR, $value); + } + + + /* + * Generates a unique key for the website to prevent unauthorized access. + */ + private static function generate_key() + { + $length = 20; + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $randomString = ''; + + for ($i = 0; $i < $length; $i++) { + $randomString .= $characters [rand(0, strlen($characters) - 1)]; + } + + return $randomString; + } +} diff --git a/ext/cron_uploader/info.php b/ext/cron_uploader/info.php new file mode 100644 index 00000000..7b920aa3 --- /dev/null +++ b/ext/cron_uploader/info.php @@ -0,0 +1,28 @@ +, Matthew Barbour + * Link: http://www.yaoifox.com/ + * License: GPLv2 + * Description: Uploads images automatically using Cron Jobs + * Documentation: Installation guide: activate this extension and navigate to www.yoursite.com/cron_upload + */ + +class CronUploaderInfo extends ExtensionInfo +{ + public const KEY = "cron_uploader"; + + public $key = self::KEY; + public $name = "Cron Uploader"; + public $url = self::SHIMMIE_URL; + public $authors = ["YaoiFox"=>"admin@yaoifox.com", "Matthew Barbour"=>"matthew@darkholme.net"]; + public $license = self::LICENSE_GPLV2; + public $description = "Uploads images automatically using Cron Jobs"; + + public function __construct() + { + $this->documentation = "Installation guide: activate this extension and navigate to System Config screen."; + parent::__construct(); + } +} diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php index a9b05650..a8d2c585 100644 --- a/ext/cron_uploader/main.php +++ b/ext/cron_uploader/main.php @@ -1,430 +1,505 @@ - - * Link: http://www.yaoifox.com/ - * License: GPLv2 - * Description: Uploads images automatically using Cron Jobs - * Documentation: Installation guide: activate this extension and navigate to www.yoursite.com/cron_upload - */ -class CronUploader extends Extension { - // TODO: Checkbox option to only allow localhost + a list of additional IP adresses that can be set in /cron_upload - // TODO: Change logging to MySQL + display log at /cron_upload - // TODO: Move stuff to theme.php - - /** - * Lists all log events this session - * @var string - */ - private $upload_info = ""; - - /** - * Lists all files & info required to upload. - * @var array - */ - private $image_queue = array(); - - /** - * Cron Uploader root directory - * @var string - */ - private $root_dir = ""; - - /** - * Key used to identify uploader - * @var string - */ - private $upload_key = ""; - - /** - * Checks if the cron upload page has been accessed - * and initializes the upload. - * @param PageRequestEvent $event - */ - public function onPageRequest(PageRequestEvent $event) { - global $config, $user; - - if ($event->page_matches ( "cron_upload" )) { - $this->upload_key = $config->get_string ( "cron_uploader_key", "" ); - - // If the key is in the url, upload - if ($this->upload_key != "" && $event->get_arg ( 0 ) == $this->upload_key) { - // log in as admin - $this->process_upload(); // Start upload - } - else if ($user->is_admin()) { - $this->set_dir(); - $this->display_documentation(); - } - - } - } - - private function display_documentation() { - global $page; - $this->set_dir(); // Determines path to cron_uploader_dir - - - $queue_dir = $this->root_dir . "/queue"; - $uploaded_dir = $this->root_dir . "/uploaded"; - $failed_dir = $this->root_dir . "/failed_to_upload"; - - $queue_dirinfo = $this->scan_dir($queue_dir); - $uploaded_dirinfo = $this->scan_dir($uploaded_dir); - $failed_dirinfo = $this->scan_dir($failed_dir); - - $cron_url = make_http(make_link("/cron_upload/" . $this->upload_key)); - $cron_cmd = "curl --silent $cron_url"; - $log_path = $this->root_dir . "/uploads.log"; - - $info_html = "Information -
    - - - - - - - - - - - - - - - - - - - - - -
    DirectoryFilesSize (MB)Directory Path
    Queue{$queue_dirinfo['total_files']}{$queue_dirinfo['total_mb']}
    Uploaded{$uploaded_dirinfo['total_files']}{$uploaded_dirinfo['total_mb']}
    Failed{$failed_dirinfo['total_files']}{$failed_dirinfo['total_mb']}
    - -
    Cron Command:
    - Create a cron job with the command above.
    - Read the documentation if you're not sure what to do.
    "; - - $install_html = " - This cron uploader is fairly easy to use but has to be configured first. -
    1. Install & activate this plugin. -
    -
    2. Upload your images you want to be uploaded to the queue directory using your FTP client. -
    ($queue_dir) -
    This also supports directory names to be used as tags. -
    -
    3. Go to the Board Config to the Cron Uploader menu and copy the Cron Command. -
    ($cron_cmd) -
    -
    4. Create a cron job or something else that can open a url on specified times. -
    If you're not sure how to do this, you can give the command to your web host and you can ask them to create the cron job for you. -
    When you create the cron job, you choose when to upload new images. -
    -
    5. When the cron command is set up, your image queue will upload x file(s) at the specified times. -
    You can see any uploads or failed uploads in the log file. ($log_path) -
    Your uploaded images will be moved to the 'uploaded' directory, it's recommended that you remove everything out of this directory from time to time. -
    ($uploaded_dir) -
    -
    Whenever the url in that cron job command is opened, a new file will upload from the queue. -
    So when you want to manually upload an image, all you have to do is open the link once. -
    This link can be found under 'Cron Command' in the board config, just remove the 'wget ' part and only the url remains. -
    ($cron_url)"; - - - $block = new Block("Cron Uploader", $info_html, "main", 10); - $block_install = new Block("Installation Guide", $install_html, "main", 20); - $page->add_block($block); - $page->add_block($block_install); - } +get_string("cron_uploader_key", "")) { - $this->upload_key = $this->generate_key (); - - $config->set_default_int ( 'cron_uploader_count', 1 ); - $config->set_default_string ( 'cron_uploader_key', $this->upload_key ); - $this->set_dir(); - } - } - - public function onSetupBuilding(SetupBuildingEvent $event) { - $this->set_dir(); - - $cron_url = make_http(make_link("/cron_upload/" . $this->upload_key)); - $cron_cmd = "curl --silent $cron_url"; - $documentation_link = make_http(make_link("cron_upload")); - - $sb = new SetupBlock ( "Cron Uploader" ); - $sb->add_label ( "Settings
    " ); - $sb->add_int_option ( "cron_uploader_count", "How many to upload each time" ); - $sb->add_text_option ( "cron_uploader_dir", "
    Set Cron Uploader root directory
    "); - - $sb->add_label ("
    Cron Command:
    - Create a cron job with the command above.
    - Read the documentation if you're not sure what to do."); +require_once "config.php"; - $event->panel->add_block ( $sb ); - } - - /* - * Generates a unique key for the website to prevent unauthorized access. - */ - private function generate_key() { - $length = 20; - $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - $randomString = ''; - - for($i = 0; $i < $length; $i ++) { - $randomString .= $characters [rand ( 0, strlen ( $characters ) - 1 )]; - } - - return $randomString; - } - - /* - * Set the directory for the image queue. If no directory was given, set it to the default directory. - */ - private function set_dir() { - global $config; - // Determine directory (none = default) - - $dir = $config->get_string("cron_uploader_dir", ""); - - // Sets new default dir if not in config yet/anymore - if ($dir == "") { - $dir = data_path("cron_uploader"); - $config->set_string ('cron_uploader_dir', $dir); - } - - // Make the directory if it doesn't exist yet - if (!is_dir($dir . "/queue/")) - mkdir ( $dir . "/queue/", 0775, true ); - if (!is_dir($dir . "/uploaded/")) - mkdir ( $dir . "/uploaded/", 0775, true ); - if (!is_dir($dir . "/failed_to_upload/")) - mkdir ( $dir . "/failed_to_upload/", 0775, true ); - - $this->root_dir = $dir; - return $dir; - } - - /** - * Returns amount of files & total size of dir. - * @param string $path directory name to scan - * @return multitype:number - */ - function scan_dir($path){ - $ite=new RecursiveDirectoryIterator($path); - - $bytestotal=0; - $nbfiles=0; - foreach (new RecursiveIteratorIterator($ite) as $filename=>$cur) { - $filesize = $cur->getSize(); - $bytestotal += $filesize; - $nbfiles++; - } - - $size_mb = $bytestotal / 1048576; // to mb - $size_mb = number_format($size_mb, 2, '.', ''); - return array('total_files'=>$nbfiles,'total_mb'=>$size_mb); - } - - /** - * Uploads the image & handles everything - * @param int $upload_count to upload a non-config amount of imgs - * @return boolean returns true if the upload was successful - */ - public function process_upload($upload_count = 0) { - global $config; - set_time_limit(0); - $this->set_dir(); - $this->generate_image_queue(); - - // Gets amount of imgs to upload - if ($upload_count == 0) $upload_count = $config->get_int ("cron_uploader_count", 1); - - // Throw exception if there's nothing in the queue - if (count($this->image_queue) == 0) { - $this->add_upload_info("Your queue is empty so nothing could be uploaded."); - $this->handle_log(); - return false; - } - - // Randomize Images - shuffle($this->image_queue); +class CronUploader extends Extension +{ + /** @var CronUploaderTheme */ + protected $theme; - // Upload the file(s) - for ($i = 0; $i < $upload_count; $i++) { - $img = $this->image_queue[$i]; - - try { - $this->add_image($img[0], $img[1], $img[2]); - $this->move_uploaded($img[0], $img[1], false); - - } - catch (Exception $e) { - $this->move_uploaded($img[0], $img[1], true); - } - - // Remove img from queue array - unset($this->image_queue[$i]); - } - - // Display & save upload log - $this->handle_log(); - - return true; - } - - private function move_uploaded($path, $filename, $corrupt = false) { - global $config; - - // Create - $newDir = $this->root_dir; - - // Determine which dir to move to - if ($corrupt) { - // Move to corrupt dir - $newDir .= "/failed_to_upload/"; - $info = "ERROR: Image was not uploaded."; - } - else { - $newDir .= "/uploaded/"; - $info = "Image successfully uploaded. "; - } - - // move file to correct dir - rename($path, $newDir.$filename); - - $this->add_upload_info($info . "Image \"$filename\" moved from queue to \"$newDir\"."); - } + public const NAME = "cron_uploader"; - /** - * Generate the necessary DataUploadEvent for a given image and tags. - * - * @param string $tmpname - * @param string $filename - * @param string $tags - */ - private function add_image($tmpname, $filename, $tags) { - assert ( file_exists ( $tmpname ) ); - assert('is_string($tags)'); - - $pathinfo = pathinfo ( $filename ); - if (! array_key_exists ( 'extension', $pathinfo )) { - throw new UploadException ( "File has no extension" ); - } - $metadata = array(); - $metadata ['filename'] = $pathinfo ['basename']; - $metadata ['extension'] = $pathinfo ['extension']; - $metadata ['tags'] = array(); // = $tags; doesn't work when not logged in here - $metadata ['source'] = null; - $event = new DataUploadEvent ( $tmpname, $metadata ); - send_event ( $event ); - - // Generate info message - $infomsg = ""; // Will contain info message - if ($event->image_id == -1) - $infomsg = "File type not recognised. Filename: {$filename}"; - else $infomsg = "Image uploaded. ID: {$event->image_id} - Filename: {$filename} - Tags: {$tags}"; - $msgNumber = $this->add_upload_info($infomsg); - - // Set tags - $img = Image::by_id($event->image_id); - $img->set_tags(Tag::explode($tags)); - } - - private function generate_image_queue($base = "", $subdir = "") { - if ($base == "") - $base = $this->root_dir . "/queue"; - - if (! is_dir ( $base )) { - $this->add_upload_info("Image Queue Directory could not be found at \"$base\"."); - return array(); - } - - foreach ( glob ( "$base/$subdir/*" ) as $fullpath ) { - $fullpath = str_replace ( "//", "/", $fullpath ); - //$shortpath = str_replace ( $base, "", $fullpath ); - - if (is_link ( $fullpath )) { - // ignore - } else if (is_dir ( $fullpath )) { - $this->generate_image_queue ( $base, str_replace ( $base, "", $fullpath ) ); - } else { - $pathinfo = pathinfo ( $fullpath ); - $matches = array (); - - if (preg_match ( "/\d+ - (.*)\.([a-zA-Z]+)/", $pathinfo ["basename"], $matches )) { - $tags = $matches [1]; - } else { - $tags = $subdir; - $tags = str_replace ( "/", " ", $tags ); - $tags = str_replace ( "__", " ", $tags ); - if ($tags == "") $tags = " "; - $tags = trim ( $tags ); - } - - $img = array ( - 0 => $fullpath, - 1 => $pathinfo ["basename"], - 2 => $tags - ); - array_push ($this->image_queue, $img ); - } - } - } - - /** - * Adds a message to the info being published at the end - * @param $text string - * @param $addon int Enter a value to modify an existing value (enter value number) - * @return int - */ - private function add_upload_info($text, $addon = 0) { - $info = $this->upload_info; - $time = "[" .date('Y-m-d H:i:s'). "]"; - - // If addon function is not used - if ($addon == 0) { - $this->upload_info .= "$time $text\r\n"; - - // Returns the number of the current line - $currentLine = substr_count($this->upload_info, "\n") -1; - return $currentLine; - } - - // else if addon function is used, select the line & modify it - $lines = substr($info, "\n"); // Seperate the string to array in lines - $lines[$addon] = "$lines[$addon] $text"; // Add the content to the line - $this->upload_info = implode("\n", $lines); // Put string back together & update - - return $addon; // Return line number - } - - /** - * This is run at the end to display & save the log. - */ - private function handle_log() { - global $page; - - // Display message - $page->set_mode("data"); - $page->set_type("text/plain"); - $page->set_data($this->upload_info); - - // Save log - $log_path = $this->root_dir . "/uploads.log"; - - if (file_exists($log_path)) - $prev_content = file_get_contents($log_path); - else $prev_content = ""; - - $content = $prev_content ."\r\n".$this->upload_info; - file_put_contents ($log_path, $content); - } + // TODO: Checkbox option to only allow localhost + a list of additional IP addresses that can be set in /cron_upload + + const QUEUE_DIR = "queue"; + const UPLOADED_DIR = "uploaded"; + const FAILED_DIR = "failed_to_upload"; + + public $output_buffer = []; + + public function onInitExt(InitExtEvent $event) + { + // Set default values + CronUploaderConfig::set_defaults(); + } + + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + if ($event->parent=="system") { + $event->add_nav_link("cron_docs", new Link('cron_upload'), "Cron Upload"); + } + } + + /** + * Checks if the cron upload page has been accessed + * and initializes the upload. + */ + public function onPageRequest(PageRequestEvent $event) + { + global $user; + + if ($event->page_matches("cron_upload")) { + if ($event->count_args() == 1) { + $this->process_upload($event->get_arg(0)); // Start upload + } elseif ($user->can(Permissions::CRON_ADMIN)) { + $this->display_documentation(); + } + } + } + + public function onSetupBuilding(SetupBuildingEvent $event) + { + global $database; + + $documentation_link = make_http(make_link("cron_upload")); + + $users = $database->get_pairs("SELECT name, id FROM users UNION ALL SELECT '', null order by name"); + + $sb = new SetupBlock("Cron Uploader"); + $sb->start_table(); + $sb->add_int_option(CronUploaderConfig::COUNT, "Upload per run", true); + $sb->add_text_option(CronUploaderConfig::DIR, "Root dir", true); + $sb->add_text_option(CronUploaderConfig::KEY, "Key", true); + $sb->add_choice_option(CronUploaderConfig::USER, $users, "User", true); + $sb->end_table(); + $sb->add_label("Read the documentation for cron setup instructions."); + + $event->panel->add_block($sb); + } + + public function onAdminBuilding(AdminBuildingEvent $event) + { + $failed_dir = $this->get_failed_dir(); + $results = get_dir_contents($failed_dir); + + $failed_dirs = []; + foreach ($results as $result) { + $path = join_path($failed_dir, $result); + if (is_dir($path)) { + $failed_dirs[] = $result; + } + } + + $this->theme->display_form($failed_dirs); + } + + public function onAdminAction(AdminActionEvent $event) + { + $action = $event->action; + switch ($action) { + case "cron_uploader_clear_queue": + $event->redirect = true; + $this->clear_folder(self::QUEUE_DIR); + break; + case "cron_uploader_clear_uploaded": + $event->redirect = true; + $this->clear_folder(self::UPLOADED_DIR); + break; + case "cron_uploader_clear_failed": + $event->redirect = true; + $this->clear_folder(self::FAILED_DIR); + break; + case "cron_uploader_restage": + $event->redirect = true; + if (array_key_exists("failed_dir", $_POST) && !empty($_POST["failed_dir"])) { + $this->restage_folder($_POST["failed_dir"]); + } + break; + } + } + + private function restage_folder(string $folder) + { + global $page; + if (empty($folder)) { + throw new SCoreException("folder empty"); + } + $queue_dir = $this->get_queue_dir(); + $stage_dir = join_path($this->get_failed_dir(), $folder); + + if (!is_dir($stage_dir)) { + throw new SCoreException("Could not find $stage_dir"); + } + + $this->prep_root_dir(); + + $results = get_dir_contents($queue_dir); + + if (count($results) > 0) { + $page->flash("Queue folder must be empty to re-stage"); + return; + } + + $results = get_dir_contents($stage_dir); + + if (count($results) == 0) { + if (rmdir($stage_dir)===false) { + $page->flash("Nothing to stage from $folder, cannot remove folder"); + } else { + $page->flash("Nothing to stage from $folder, removing folder"); + } + return; + } + + foreach ($results as $result) { + $original_path = join_path($stage_dir, $result); + $new_path = join_path($queue_dir, $result); + + rename($original_path, $new_path); + } + + $page->flash("Re-staged $folder to queue"); + rmdir($stage_dir); + } + + private function clear_folder($folder) + { + global $page; + $path = join_path(CronUploaderConfig::get_dir(), $folder); + deltree($path); + $page->flash("Cleared $path"); + } + + + private function get_cron_url() + { + return make_http(make_link("/cron_upload/" . CronUploaderConfig::get_key())); + } + + private function get_cron_cmd() + { + return "curl --silent " . $this->get_cron_url(); + } + + private function display_documentation() + { + global $database; + + $this->prep_root_dir(); + + $queue_dir = $this->get_queue_dir(); + $uploaded_dir = $this->get_uploaded_dir(); + $failed_dir = $this->get_failed_dir(); + + $queue_dirinfo = scan_dir($queue_dir); + $uploaded_dirinfo = scan_dir($uploaded_dir); + $failed_dirinfo = scan_dir($failed_dir); + + + $running = false; + $lockfile = fopen($this->get_lock_file(), "w"); + try { + if (!flock($lockfile, LOCK_EX | LOCK_NB)) { + $running = true; + } else { + flock($lockfile, LOCK_UN); + } + } finally { + fclose($lockfile); + } + + $logs = []; + if (Extension::is_enabled(LogDatabaseInfo::KEY)) { + $logs = $database->get_all( + "SELECT * FROM score_log WHERE section = :section ORDER BY date_sent DESC LIMIT 100", + ["section" => self::NAME] + ); + } + + $this->theme->display_documentation( + $running, + $queue_dirinfo, + $uploaded_dirinfo, + $failed_dirinfo, + $this->get_cron_cmd(), + $this->get_cron_url(), + $logs + ); + } + + public function get_queue_dir() + { + $dir = CronUploaderConfig::get_dir(); + return join_path($dir, self::QUEUE_DIR); + } + + public function get_uploaded_dir() + { + $dir = CronUploaderConfig::get_dir(); + return join_path($dir, self::UPLOADED_DIR); + } + + public function get_failed_dir() + { + $dir = CronUploaderConfig::get_dir(); + return join_path($dir, self::FAILED_DIR); + } + + private function prep_root_dir(): string + { + // Determine directory (none = default) + $dir = CronUploaderConfig::get_dir(); + + // Make the directory if it doesn't exist yet + if (!is_dir($this->get_queue_dir())) { + mkdir($this->get_queue_dir(), 0775, true); + } + if (!is_dir($this->get_uploaded_dir())) { + mkdir($this->get_uploaded_dir(), 0775, true); + } + if (!is_dir($this->get_failed_dir())) { + mkdir($this->get_failed_dir(), 0775, true); + } + + return $dir; + } + + private function get_lock_file(): string + { + $root_dir = CronUploaderConfig::get_dir(); + return join_path($root_dir, ".lock"); + } + + /** + * Uploads the image & handles everything + */ + public function process_upload(string $key, ?int $upload_count = null): bool + { + global $database; + + if ($key!=CronUploaderConfig::get_key()) { + throw new SCoreException("Cron upload key incorrect"); + } + $user_id = CronUploaderConfig::get_user(); + if (empty($user_id)) { + throw new SCoreException("Cron upload user not set"); + } + $my_user = User::by_id($user_id); + if ($my_user == null) { + throw new SCoreException("No user found for cron upload user $user_id"); + } + + send_event(new UserLoginEvent($my_user)); + $this->log_message(SCORE_LOG_INFO, "Logged in as user {$my_user->name}"); + + $lockfile = fopen($this->get_lock_file(), "w"); + if (!flock($lockfile, LOCK_EX | LOCK_NB)) { + throw new SCoreException("Cron upload process is already running"); + } + + try { + //set_time_limit(0); + + // Gets amount of imgs to upload + if ($upload_count == null) { + $upload_count = CronUploaderConfig::get_count(); + } + + $output_subdir = date('Ymd-His', time()); + $image_queue = $this->generate_image_queue(CronUploaderConfig::get_dir(), $upload_count); + + + // Throw exception if there's nothing in the queue + if (count($image_queue) == 0) { + $this->log_message(SCORE_LOG_WARNING, "Your queue is empty so nothing could be uploaded."); + $this->handle_log(); + return false; + } + + // Randomize Images + //shuffle($this->image_queue); + + $merged = 0; + $added = 0; + $failed = 0; + + // Upload the file(s) + for ($i = 0; $i < $upload_count && sizeof($image_queue) > 0; $i++) { + $img = array_pop($image_queue); + + try { + $database->beginTransaction(); + $this->log_message(SCORE_LOG_INFO, "Adding file: {$img[0]} - tags: {$img[2]}"); + $result = $this->add_image($img[0], $img[1], $img[2]); + $database->commit(); + $this->move_uploaded($img[0], $img[1], $output_subdir, false); + if ($result->merged) { + $merged++; + } else { + $added++; + } + } catch (Exception $e) { + try { + $database->rollback(); + } catch (Exception $e) { + } + + $failed++; + $this->move_uploaded($img[0], $img[1], $output_subdir, true); + $this->log_message(SCORE_LOG_ERROR, "(" . gettype($e) . ") " . $e->getMessage()); + $this->log_message(SCORE_LOG_ERROR, $e->getTraceAsString()); + } + } + + + $this->log_message(SCORE_LOG_INFO, "Items added: $added"); + $this->log_message(SCORE_LOG_INFO, "Items merged: $merged"); + $this->log_message(SCORE_LOG_INFO, "Items failed: $failed"); + + + // Display upload log + $this->handle_log(); + + return true; + } finally { + flock($lockfile, LOCK_UN); + fclose($lockfile); + } + } + + private function move_uploaded(string $path, string $filename, string $output_subdir, bool $corrupt = false) + { + $relativeDir = dirname(substr($path, strlen(CronUploaderConfig::get_dir()) + 7)); + + if ($relativeDir==".") { + $relativeDir = ""; + } + + // Determine which dir to move to + if ($corrupt) { + // Move to corrupt dir + $newDir = join_path($this->get_failed_dir(), $output_subdir, $relativeDir); + $info = "ERROR: Image was not uploaded. "; + } else { + $newDir = join_path($this->get_uploaded_dir(), $output_subdir, $relativeDir); + $info = "Image successfully uploaded. "; + } + $newDir = str_replace(DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, $newDir); + + if (!is_dir($newDir)) { + mkdir($newDir, 0775, true); + } + + $newFile = join_path($newDir, $filename); + // move file to correct dir + rename($path, $newFile); + + $this->log_message(SCORE_LOG_INFO, $info . "Image \"$filename\" moved from queue to \"$newDir\"."); + } + + /** + * Generate the necessary DataUploadEvent for a given image and tags. + */ + private function add_image(string $tmpname, string $filename, string $tags): DataUploadEvent + { + assert(file_exists($tmpname)); + + $tagArray = Tag::explode($tags); + if (count($tagArray) == 0) { + $tagArray[] = "tagme"; + } + + $pathinfo = pathinfo($filename); + $metadata = []; + $metadata ['filename'] = $pathinfo ['basename']; + if (array_key_exists('extension', $pathinfo)) { + $metadata ['extension'] = $pathinfo ['extension']; + } + $metadata ['tags'] = $tagArray; // doesn't work when not logged in here, handled below + $metadata ['source'] = null; + $event = new DataUploadEvent($tmpname, $metadata); + send_event($event); + + // Generate info message + if ($event->image_id == -1) { + throw new UploadException("File type not recognised. Filename: {$filename}"); + } elseif ($event->merged === true) { + $infomsg = "Image merged. ID: {$event->image_id} - Filename: {$filename}"; + } else { + $infomsg = "Image uploaded. ID: {$event->image_id} - Filename: {$filename}"; + } + $this->log_message(SCORE_LOG_INFO, $infomsg); + + // Set tags + $img = Image::by_id($event->image_id); + $img->set_tags(array_merge($tagArray, $img->get_tag_array())); + + return $event; + } + + private const PARTIAL_DOWNLOAD_EXTENSIONS = ['crdownload','part']; + + private function is_skippable_file(string $path) + { + $info = pathinfo($path); + + if (in_array(strtolower($info['extension']), self::PARTIAL_DOWNLOAD_EXTENSIONS)) { + return true; + } + + return false; + } + + private function generate_image_queue(string $root_dir, ?int $limit = null): array + { + $base = $this->get_queue_dir(); + $output = []; + + if (!is_dir($base)) { + $this->log_message(SCORE_LOG_WARNING, "Image Queue Directory could not be found at \"$base\"."); + return []; + } + + $ite = new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS); + foreach (new RecursiveIteratorIterator($ite) as $fullpath => $cur) { + if (!is_link($fullpath) && !is_dir($fullpath) && !$this->is_skippable_file($fullpath)) { + $pathinfo = pathinfo($fullpath); + + $relativePath = substr($fullpath, strlen($base)); + $tags = path_to_tags($relativePath); + + $img = [ + 0 => $fullpath, + 1 => $pathinfo ["basename"], + 2 => $tags + ]; + $output[] = $img; + if (!empty($limit) && count($output) >= $limit) { + break; + } + } + } + return $output; + } + + + private function log_message(int $severity, string $message): void + { + log_msg(self::NAME, $severity, $message); + + $time = "[" . date('Y-m-d H:i:s') . "]"; + $this->output_buffer[] = $time . " " . $message; + + $log_path = $this->get_log_file(); + + file_put_contents($log_path, $time . " " . $message); + } + + private function get_log_file(): string + { + return join_path(CronUploaderConfig::get_dir(), "uploads.log"); + } + + /** + * This is run at the end to display & save the log. + */ + private function handle_log() + { + global $page; + + // Display message + $page->set_mode(PageMode::DATA); + $page->set_type("text/plain"); + $page->set_data(implode("\r\n", $this->output_buffer)); + } } - diff --git a/ext/cron_uploader/style.css b/ext/cron_uploader/style.css new file mode 100644 index 00000000..2643a6a1 --- /dev/null +++ b/ext/cron_uploader/style.css @@ -0,0 +1,3 @@ +table.log th { + width: 200px; +} \ No newline at end of file diff --git a/ext/cron_uploader/theme.php b/ext/cron_uploader/theme.php new file mode 100644 index 00000000..ea09f6fb --- /dev/null +++ b/ext/cron_uploader/theme.php @@ -0,0 +1,132 @@ +Information +
    + + " . ($running ? "" : "") . " + + + + + + + + + + + + + + + + + + + + +
    Cron upload is currently running
    DirectoryFilesSize (MB)Directory Path
    Queue{$queue_dirinfo['total_files']}{$queue_dirinfo['total_mb']}{$queue_dirinfo['path']}
    Uploaded{$uploaded_dirinfo['total_files']}{$uploaded_dirinfo['total_mb']}{$uploaded_dirinfo['path']}
    Failed{$failed_dirinfo['total_files']}{$failed_dirinfo['total_mb']}{$failed_dirinfo['path']}
    + +
    Cron Command:
    + Create a cron job with the command above.
    + Read the documentation if you're not sure what to do.
    "; + + $install_html = " + This cron uploader is fairly easy to use but has to be configured first. +
      +
    1. Install & activate this plugin.
    2. +
    3. Go to the Board Config and change any settings to match your preference.
    4. +
    5. Copy the cron command above.
    6. +
    7. Create a cron job or something else that can open a url on specified times. +
      cron is a service that runs commands over and over again on a a schedule. You can set up cron (or any similar tool) to run the command above to trigger the import on whatever schedule you desire. +
      If you're not sure how to do this, you can give the command to your web host and you can ask them to create the cron job for you. +
      When you create the cron job, you choose when to upload new images.
    8. +
    "; + + $usage_html = "Upload your images you want to be uploaded to the queue directory using your FTP client or other means. +
    ({$queue_dirinfo['path']}) +
      +
    1. Any sub-folders will be turned into tags.
    2. +
    3. If the file name matches \"## - tag1 tag2.png\" the tags will be used.
    4. +
    5. If both are found, they will all be used.
    6. +
    7. The character \";\" will be changed into \":\" in any tags.
    8. +
    9. You can inherit categories by creating a folder that ends with \";\". For instance category;\\tag1 would result in the tag category:tag1. This allows creating a category folder, then creating many subfolders that will use that category.
    10. +
    + The cron uploader works by importing files from the queue folder whenever this url is visited: +
    $cron_url
    + +
      +
    • If an import is already running, another cannot start until it is done.
    • +
    • Each time it runs it will import up to ".CronUploaderConfig::get_count()." file(s). This is controlled from Board Config.
    • +
    • Uploaded images will be moved to the 'uploaded' directory into a subfolder named after the time the import started. It's recommended that you remove everything out of this directory from time to time. If you have admin controls enabled, this can be done from Board Admin.
    • +
    • If you enable the db logging extension, you can view the log output on this screen. Otherwise the log will be written to a file at ".CronUploaderConfig::get_dir().DIRECTORY_SEPARATOR."uploads.log
    • +
    + "; + + $page->set_title("Cron Uploader"); + $page->set_heading("Cron Uploader"); + + $block = new Block("Cron Uploader", $info_html, "main", 10); + $block_install = new Block("Setup Guide", $install_html, "main", 30); + $block_usage= new Block("Usage Guide", $usage_html, "main", 20); + $page->add_block($block); + $page->add_block($block_install); + $page->add_block($block_usage); + + if (!empty($log_entries)) { + $log_html = ""; + foreach ($log_entries as $entry) { + $log_html .= ""; + } + $log_html .= "
    {$entry["date_sent"]}{$entry["message"]}
    "; + $block = new Block("Log", $log_html, "main", 40); + $page->add_block($block); + } + } + + public function display_form(array $failed_dirs) + { + global $page; + + $link = make_http(make_link("cron_upload")); + $html = "Cron uploader documentation"; + + $html .= make_form(make_link("admin/cron_uploader_restage")); + $html .= ""; + $html .= ""; + $html .= ""; + $html .= "
    Failed dir
    "; + + $html .= make_form(make_link("admin/cron_uploader_clear_queue"), "POST", false, "", "return confirm('Are you sure you want to delete everything in the queue folder?');") + ."
    " + ."
    "; + $html .= make_form(make_link("admin/cron_uploader_clear_uploaded"), "POST", false, "", "return confirm('Are you sure you want to delete everything in the uploaded folder?');") + ."
    " + ."
    "; + $html .= make_form(make_link("admin/cron_uploader_clear_failed"), "POST", false, "", "return confirm('Are you sure you want to delete everything in the failed folder?');") + ."
    " + ."
    "; + $html .= "\n"; + $page->add_block(new Block("Cron Upload", $html)); + } +} diff --git a/ext/custom_html_headers/info.php b/ext/custom_html_headers/info.php new file mode 100644 index 00000000..08f14a36 --- /dev/null +++ b/ext/custom_html_headers/info.php @@ -0,0 +1,21 @@ +"support@drudexsoftware.com"]; + public $license = self::LICENSE_GPLV2; + public $description = "Allows admins to modify & set custom <head> content"; + public $documentation = +"When you go to board config you can find a block named Custom HTML Headers. +In that block you can simply place any thing you can place within <head></head> + +This can be useful if you want to add website tracking code or other javascript. +NOTE: Only use if you know what you're doing. + +You can also add your website name as prefix or suffix to the title of each page on your website."; +} diff --git a/ext/custom_html_headers/main.php b/ext/custom_html_headers/main.php index df04b436..ffeb938e 100644 --- a/ext/custom_html_headers/main.php +++ b/ext/custom_html_headers/main.php @@ -1,72 +1,68 @@ - - * Link: http://www.drudexsoftware.com - * License: GPLv2 - * Description: Allows admins to modify & set custom <head> content - * Documentation: - * When you go to board config you can find a block named Custom HTML Headers. - * In that block you can simply place any thing you can place within <head></head> - * - * This can be useful if you want to add website tracking code or other javascript. - * NOTE: Only use if you know what you're doing. - * - * You can also add your website name as prefix or suffix to the title of each page on your website. - */ -class custom_html_headers extends Extension { + content - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Custom HTML Headers"); + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Custom HTML Headers"); - // custom headers - $sb->add_longtext_option("custom_html_headers", - "HTML Code to place within <head></head> on all pages
    "); + // custom headers + $sb->add_longtext_option( + "custom_html_headers", + "HTML Code to place within <head></head> on all pages
    " + ); - // modified title - $sb->add_choice_option("sitename_in_title", array( - "none" => 0, - "as prefix" => 1, - "as suffix" => 2 - ), "
    Add website name in title"); + // modified title + $sb->add_choice_option("sitename_in_title", [ + "none" => "none", + "as prefix" => "prefix", + "as suffix" => "suffix" + ], "
    Add website name in title"); - $event->panel->add_block($sb); - } - - public function onInitExt(InitExtEvent $event) { - global $config; - $config->set_default_int("sitename_in_title", 0); + $event->panel->add_block($sb); + } + + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_string("sitename_in_title", "none"); + } + + # Load Analytics tracking code on page request + public function onPageRequest(PageRequestEvent $event) + { + $this->handle_custom_html_headers(); + $this->handle_modified_page_title(); + } + + private function handle_custom_html_headers() + { + global $config, $page; + + $header = $config->get_string('custom_html_headers', ''); + if ($header!='') { + $page->add_html_header($header); } - - # Load Analytics tracking code on page request - public function onPageRequest(PageRequestEvent $event) { - $this->handle_custom_html_headers(); - $this->handle_modified_page_title(); + } + + private function handle_modified_page_title() + { + global $config, $page; + + // get config values + $site_title = $config->get_string(SetupConfig::TITLE); + $sitename_in_title = $config->get_string("sitename_in_title"); + + // sitename is already in title (can occur on index & other pages) + if (strstr($page->title, $site_title)) { + return; } - - private function handle_custom_html_headers() { - global $config, $page; - - $header = $config->get_string('custom_html_headers',''); - if ($header!='') $page->add_html_header($header); - } - - private function handle_modified_page_title() { - global $config, $page; - - // get config values - $site_title = $config->get_string("title"); - $sitename_in_title = $config->get_int("sitename_in_title"); - - // if feature is enabled & sitename isn't already in title - // (can occur on index & other pages) - if ($sitename_in_title != 0 && !strstr($page->title, $site_title)) - { - if ($sitename_in_title == 1) - $page->title = "$site_title - $page->title"; // as prefix - else if ($sitename_in_title == 2) - $page->title = "$page->title - $site_title"; // as suffix - } + + if ($sitename_in_title == "prefix") { + $page->title = "$site_title - $page->title"; + } elseif ($sitename_in_title == "suffix") { + $page->title = "$page->title - $site_title"; } + } } - diff --git a/ext/danbooru_api/info.php b/ext/danbooru_api/info.php new file mode 100644 index 00000000..0e958d72 --- /dev/null +++ b/ext/danbooru_api/info.php @@ -0,0 +1,53 @@ +"jsutinen@gmail.com"]; + public $description = "Allow Danbooru apps like Danbooru Uploader for Firefox to communicate with Shimmie"; + public $documentation = +"

    Notes: +
    danbooru API based on documentation from danbooru 1.0 - + http://attachr.com/7569 +
    I've only been able to test add_post and find_tags because I use the + old danbooru firefox extension for firefox 1.5 +

    Functions currently implemented: +

      +
    • add_post - title and rating are currently ignored because shimmie does not support them +
    • find_posts - sort of works, filename is returned as the original filename and probably won't help when it comes to actually downloading it +
    • find_tags - id, name, and after_id all work but the tags parameter is ignored just like danbooru 1.0 ignores it +
    + +CHANGELOG +13-OCT-08 8:00PM CST - JJS +Bugfix - Properly escape source attribute + +17-SEP-08 10:00PM CST - JJS +Bugfix for changed page name checker in PageRequestEvent + +13-APR-08 10:00PM CST - JJS +Properly escape the tags returned in find_tags and find_posts - Caught by ATravelingGeek +Updated extension info to be a bit more clear about its purpose +Deleted add_comment code as it didn't do anything anyway + +01-MAR-08 7:00PM CST - JJS +Rewrote to make it compatible with Shimmie trunk again (r723 at least) +It may or may not support the new file handling stuff correctly, I'm only testing with images and the danbooru uploader for firefox + +21-OCT-07 9:07PM CST - JJS +Turns out I actually did need to implement the new parameter names +for danbooru api v1.8.1. Now danbooruup should work when used with /api/danbooru/post/create.xml +Also correctly redirects the url provided by danbooruup in the event +of a duplicate image. + +19-OCT-07 4:46PM CST - JJS +Add compatibility with danbooru api v1.8.1 style urls +for find_posts and add_post. NOTE: This does not implement +the changes to the parameter names, it is simply a +workaround for the latest danbooruup firefox extension. +Completely compatibility will probably involve a rewrite with a different URL +"; +} diff --git a/ext/danbooru_api/main.php b/ext/danbooru_api/main.php index 3bbad680..d85319da 100644 --- a/ext/danbooru_api/main.php +++ b/ext/danbooru_api/main.php @@ -1,269 +1,217 @@ - -Description: Allow Danbooru apps like Danbooru Uploader for Firefox to communicate with Shimmie -Documentation: -

    Notes: -
    danbooru API based on documentation from danbooru 1.0 - - http://attachr.com/7569 -
    I've only been able to test add_post and find_tags because I use the - old danbooru firefox extension for firefox 1.5 -

    Functions currently implemented: -

      -
    • add_post - title and rating are currently ignored because shimmie does not support them -
    • find_posts - sort of works, filename is returned as the original filename and probably won't help when it comes to actually downloading it -
    • find_tags - id, name, and after_id all work but the tags parameter is ignored just like danbooru 1.0 ignores it -
    +page_matches("api/danbooru")) { + global $page; + $page->set_mode(PageMode::DATA); -17-SEP-08 10:00PM CST - JJS -Bugfix for changed page name checker in PageRequestEvent + if ($event->page_matches("api/danbooru/add_post") || $event->page_matches("api/danbooru/post/create.xml")) { + // No XML data is returned from this function + $page->set_type("text/plain"); + $this->api_add_post(); + } elseif ($event->page_matches("api/danbooru/find_posts") || $event->page_matches("api/danbooru/post/index.xml")) { + $page->set_type("application/xml"); + $page->set_data($this->api_find_posts()); + } elseif ($event->page_matches("api/danbooru/find_tags")) { + $page->set_type("application/xml"); + $page->set_data($this->api_find_tags()); + } -13-APR-08 10:00PM CST - JJS -Properly escape the tags returned in find_tags and find_posts - Caught by ATravelingGeek -Updated extension info to be a bit more clear about its purpose -Deleted add_comment code as it didn't do anything anyway + // Hackery for danbooruup 0.3.2 providing the wrong view url. This simply redirects to the proper + // Shimmie view page + // Example: danbooruup says the url is http://shimmie/api/danbooru/post/show/123 + // This redirects that to http://shimmie/post/view/123 + elseif ($event->page_matches("api/danbooru/post/show")) { + $fixedlocation = make_link("post/view/" . $event->get_arg(0)); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect($fixedlocation); + } + } + } -01-MAR-08 7:00PM CST - JJS -Rewrote to make it compatible with Shimmie trunk again (r723 at least) -It may or may not support the new file handling stuff correctly, I'm only testing with images and the danbooru uploader for firefox + /** + * Turns out I use this a couple times so let's make it a utility function + * Authenticates a user based on the contents of the login and password parameters + * or makes them anonymous. Does not set any cookies or anything permanent. + */ + private function authenticate_user() + { + global $config, $user; -21-OCT-07 9:07PM CST - JJS -Turns out I actually did need to implement the new parameter names -for danbooru api v1.8.1. Now danbooruup should work when used with /api/danbooru/post/create.xml -Also correctly redirects the url provided by danbooruup in the event -of a duplicate image. + if (isset($_REQUEST['login']) && isset($_REQUEST['password'])) { + // Get this user from the db, if it fails the user becomes anonymous + // Code borrowed from /ext/user + $name = $_REQUEST['login']; + $pass = $_REQUEST['password']; + $duser = User::by_name_and_pass($name, $pass); + if (!is_null($duser)) { + $user = $duser; + } else { + $user = User::by_id($config->get_int("anon_id", 0)); + } + send_event(new UserLoginEvent($user)); + } + } -19-OCT-07 4:46PM CST - JJS -Add compatibility with danbooru api v1.8.1 style urls -for find_posts and add_post. NOTE: This does not implement -the changes to the parameter names, it is simply a -workaround for the latest danbooruup firefox extension. -Completely compatibility will probably involve a rewrite with a different URL - -*/ - -class DanbooruApi extends Extension { - public function onPageRequest(PageRequestEvent $event) { - if($event->page_matches("api") && ($event->get_arg(0) == 'danbooru')) { - $this->api_danbooru($event); - } - } - - // Danbooru API - private function api_danbooru(PageRequestEvent $event) { - global $page; - $page->set_mode("data"); - - if(($event->get_arg(1) == 'add_post') || (($event->get_arg(1) == 'post') && ($event->get_arg(2) == 'create.xml'))) { - // No XML data is returned from this function - $page->set_type("text/plain"); - $this->api_add_post(); - } - - elseif(($event->get_arg(1) == 'find_posts') || (($event->get_arg(1) == 'post') && ($event->get_arg(2) == 'index.xml'))) { - $page->set_type("application/xml"); - $page->set_data($this->api_find_posts()); - } - - elseif($event->get_arg(1) == 'find_tags') { - $page->set_type("application/xml"); - $page->set_data($this->api_find_tags()); - } - - // Hackery for danbooruup 0.3.2 providing the wrong view url. This simply redirects to the proper - // Shimmie view page - // Example: danbooruup says the url is http://shimmie/api/danbooru/post/show/123 - // This redirects that to http://shimmie/post/view/123 - elseif(($event->get_arg(1) == 'post') && ($event->get_arg(2) == 'show')) { - $fixedlocation = make_link("post/view/" . $event->get_arg(3)); - $page->set_mode("redirect"); - $page->set_redirect($fixedlocation); - } - } - - /** - * Turns out I use this a couple times so let's make it a utility function - * Authenticates a user based on the contents of the login and password parameters - * or makes them anonymous. Does not set any cookies or anything permanent. - */ - private function authenticate_user() { - global $config, $user; - - if(isset($_REQUEST['login']) && isset($_REQUEST['password'])) { - // Get this user from the db, if it fails the user becomes anonymous - // Code borrowed from /ext/user - $name = $_REQUEST['login']; - $pass = $_REQUEST['password']; - $duser = User::by_name_and_pass($name, $pass); - if(!is_null($duser)) { - $user = $duser; - } - else { - $user = User::by_id($config->get_int("anon_id", 0)); - } - } - } - - /** + /** * find_tags() - * Find all tags that match the search criteria. - * + * Find all tags that match the search criteria. + * * Parameters * - id: A comma delimited list of tag id numbers. * - name: A comma delimited list of tag names. * - tags: any typical tag query. See Tag#parse_query for details. * - after_id: limit results to tags with an id number after after_id. Useful if you only want to refresh - * - * @return string - */ - private function api_find_tags() { - global $database; - $results = array(); - if(isset($_GET['id'])) { - $idlist = explode(",", $_GET['id']); - foreach ($idlist as $id) { - $sqlresult = $database->get_all( - "SELECT id,tag,count FROM tags WHERE id = ?", - array($id)); - foreach ($sqlresult as $row) { - $results[] = array($row['count'], $row['tag'], $row['id']); - } - } - } - elseif(isset($_GET['name'])) { - $namelist = explode(",", $_GET['name']); - foreach ($namelist as $name) { - $sqlresult = $database->get_all( - "SELECT id,tag,count FROM tags WHERE tag = ?", - array($name)); - foreach ($sqlresult as $row) { - $results[] = array($row['count'], $row['tag'], $row['id']); - } - } - } - // Currently disabled to maintain identical functionality to danbooru 1.0's own "broken" find_tags - elseif(false && isset($_GET['tags'])) { - $start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0; - $tags = Tag::explode($_GET['tags']); - } - else { - $start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0; - $sqlresult = $database->get_all( - "SELECT id,tag,count FROM tags WHERE count > 0 AND id >= ? ORDER BY id DESC", - array($start)); - foreach ($sqlresult as $row) { - $results[] = array($row['count'], $row['tag'], $row['id']); - } - } + */ + private function api_find_tags(): string + { + global $database; + $results = []; + if (isset($_GET['id'])) { + $idlist = explode(",", $_GET['id']); + foreach ($idlist as $id) { + $sqlresult = $database->get_all( + "SELECT id,tag,count FROM tags WHERE id = :id", + ['id'=>$id] + ); + foreach ($sqlresult as $row) { + $results[] = [$row['count'], $row['tag'], $row['id']]; + } + } + } elseif (isset($_GET['name'])) { + $namelist = explode(",", $_GET['name']); + foreach ($namelist as $name) { + $sqlresult = $database->get_all( + "SELECT id,tag,count FROM tags WHERE LOWER(tag) = LOWER(:tag)", + ['tag'=>$name] + ); + foreach ($sqlresult as $row) { + $results[] = [$row['count'], $row['tag'], $row['id']]; + } + } + } + // Currently disabled to maintain identical functionality to danbooru 1.0's own "broken" find_tags + elseif (false && isset($_GET['tags'])) { + $start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0; + $tags = Tag::explode($_GET['tags']); + assert(!is_null($start) && !is_null($tags)); + } else { + $start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0; + $sqlresult = $database->get_all( + "SELECT id,tag,count FROM tags WHERE count > 0 AND id >= :id ORDER BY id DESC", + ['id'=>$start] + ); + foreach ($sqlresult as $row) { + $results[] = [$row['count'], $row['tag'], $row['id']]; + } + } - // Tag results collected, build XML output - $xml = "\n"; - foreach ($results as $tag) { - $xml .= xml_tag("tag", array( - "type" => "0", - "counts" => $tag[0], - "name" => $tag[1], - "id" => $tag[2], - )); - } - $xml .= ""; - return $xml; - } + // Tag results collected, build XML output + $xml = "\n"; + foreach ($results as $tag) { + $xml .= xml_tag("tag", [ + "type" => "0", + "counts" => $tag[0], + "name" => $tag[1], + "id" => $tag[2], + ]); + } + $xml .= ""; + return $xml; + } - /** - * find_posts() - * Find all posts that match the search criteria. Posts will be ordered by id descending. - * - * Parameters: - * - md5: md5 hash to search for (comma delimited) - * - id: id to search for (comma delimited) - * - tags: what tags to search for - * - limit: limit - * - page: page number - * - after_id: limit results to posts added after this id - * - * @return string - * @throws SCoreException - */ - private function api_find_posts() { - $results = array(); + /** + * find_posts() + * Find all posts that match the search criteria. Posts will be ordered by id descending. + * + * Parameters: + * - md5: md5 hash to search for (comma delimited) + * - id: id to search for (comma delimited) + * - tags: what tags to search for + * - limit: limit + * - page: page number + * - after_id: limit results to posts added after this id + * + * #return string + */ + private function api_find_posts() + { + $results = []; - $this->authenticate_user(); - $start = 0; + $this->authenticate_user(); + $start = 0; - if(isset($_GET['md5'])) { - $md5list = explode(",", $_GET['md5']); - foreach ($md5list as $md5) { - $results[] = Image::by_hash($md5); - } - $count = count($results); - } - elseif(isset($_GET['id'])) { - $idlist = explode(",", $_GET['id']); - foreach ($idlist as $id) { - $results[] = Image::by_id($id); - } - $count = count($results); - } - else { - $limit = isset($_GET['limit']) ? int_escape($_GET['limit']) : 100; + if (isset($_GET['md5'])) { + $md5list = explode(",", $_GET['md5']); + foreach ($md5list as $md5) { + $results[] = Image::by_hash($md5); + } + $count = count($results); + } elseif (isset($_GET['id'])) { + $idlist = explode(",", $_GET['id']); + foreach ($idlist as $id) { + $results[] = Image::by_id(int_escape($id)); + } + $count = count($results); + } else { + $limit = isset($_GET['limit']) ? int_escape($_GET['limit']) : 100; - // Calculate start offset. - if (isset($_GET['page'])) // Danbooru API uses 'page' >= 1 - $start = (int_escape($_GET['page']) - 1) * $limit; - else if (isset($_GET['pid'])) // Gelbooru API uses 'pid' >= 0 - $start = int_escape($_GET['pid']) * $limit; - else - $start = 0; + // Calculate start offset. + if (isset($_GET['page'])) { // Danbooru API uses 'page' >= 1 + $start = (int_escape($_GET['page']) - 1) * $limit; + } elseif (isset($_GET['pid'])) { // Gelbooru API uses 'pid' >= 0 + $start = int_escape($_GET['pid']) * $limit; + } else { + $start = 0; + } - $tags = isset($_GET['tags']) ? Tag::explode($_GET['tags']) : array(); - $count = Image::count_images($tags); - $results = Image::find_images(max($start, 0), min($limit, 100), $tags); - } + $tags = isset($_GET['tags']) ? Tag::explode($_GET['tags']) : []; + $count = Image::count_images($tags); + $results = Image::find_images(max($start, 0), min($limit, 100), $tags); + } - // Now we have the array $results filled with Image objects - // Let's display them - $xml = "\n"; - foreach ($results as $img) { - // Sanity check to see if $img is really an image object - // If it isn't (e.g. someone requested an invalid md5 or id), break out of the this - if (!is_object($img)) - continue; - $taglist = $img->get_tag_list(); - $owner = $img->get_owner(); - $previewsize = get_thumbnail_size($img->width, $img->height); - $xml .= xml_tag("post", array( - "id" => $img->id, - "md5" => $img->hash, - "file_name" => $img->filename, - "file_url" => $img->get_image_link(), - "height" => $img->height, - "width" => $img->width, - "preview_url" => $img->get_thumb_link(), - "preview_height" => $previewsize[1], - "preview_width" => $previewsize[0], - "rating" => "u", - "date" => $img->posted, - "is_warehoused" => false, - "tags" => $taglist, - "source" => $img->source, - "score" => 0, - "author" => $owner->name - )); - } - $xml .= ""; - return $xml; - } + // Now we have the array $results filled with Image objects + // Let's display them + $xml = "\n"; + foreach ($results as $img) { + // Sanity check to see if $img is really an image object + // If it isn't (e.g. someone requested an invalid md5 or id), break out of the this + if (!is_object($img)) { + continue; + } + $taglist = $img->get_tag_list(); + $owner = $img->get_owner(); + $previewsize = get_thumbnail_size($img->width, $img->height); + $xml .= xml_tag("post", [ + "id" => $img->id, + "md5" => $img->hash, + "file_name" => $img->filename, + "file_url" => $img->get_image_link(), + "height" => $img->height, + "width" => $img->width, + "preview_url" => $img->get_thumb_link(), + "preview_height" => $previewsize[1], + "preview_width" => $previewsize[0], + "rating" => "?", + "date" => $img->posted, + "is_warehoused" => false, + "tags" => $taglist, + "source" => $img->source, + "score" => 0, + "author" => $owner->name + ]); + } + $xml .= ""; + return $xml; + } - /** + /** * add_post() * Adds a post to the database. - * + * * Parameters: * - login: login * - password: password @@ -273,124 +221,129 @@ class DanbooruApi extends Extension { * - tags: list of tags as a string, delimited by whitespace * - md5: MD5 hash of upload in hexadecimal format * - rating: rating of the post. can be explicit, questionable, or safe. **IGNORED** - * + * * Notes: * - The only necessary parameter is tags and either file or source. * - If you want to sign your post, you need a way to authenticate your account, either by supplying login and password, or by supplying a cookie. * - If an account is not supplied or if it doesn‘t authenticate, he post will be added anonymously. * - If the md5 parameter is supplied and does not match the hash of what‘s on the server, the post is rejected. - * + * * Response * The response depends on the method used: * Post: * - X-Danbooru-Location set to the URL for newly uploaded post. * Get: * - Redirected to the newly uploaded post. - */ - private function api_add_post() { - global $user, $config, $page; - $danboorup_kludge = 1; // danboorup for firefox makes broken links out of location: /path + */ + private function api_add_post() + { + global $user, $page; + $danboorup_kludge = 1; // danboorup for firefox makes broken links out of location: /path - // Check first if a login was supplied, if it wasn't check if the user is logged in via cookie - // If all that fails, it's an anonymous upload - $this->authenticate_user(); - // Now we check if a file was uploaded or a url was provided to transload - // Much of this code is borrowed from /ext/upload + // Check first if a login was supplied, if it wasn't check if the user is logged in via cookie + // If all that fails, it's an anonymous upload + $this->authenticate_user(); + // Now we check if a file was uploaded or a url was provided to transload + // Much of this code is borrowed from /ext/upload - if (!$user->can("create_image")) { - $page->set_code(409); - $page->add_http_header("X-Danbooru-Errors: authentication error"); - return; - } + if (!$user->can(Permissions::CREATE_IMAGE)) { + $page->set_code(409); + $page->add_http_header("X-Danbooru-Errors: authentication error"); + return; + } - if (isset($_FILES['file'])) { // A file was POST'd in - $file = $_FILES['file']['tmp_name']; - $filename = $_FILES['file']['name']; - // If both a file is posted and a source provided, I'm assuming source is the source of the file - if (isset($_REQUEST['source']) && !empty($_REQUEST['source'])) { - $source = $_REQUEST['source']; - } else { - $source = null; - } - } elseif (isset($_FILES['post'])) { - $file = $_FILES['post']['tmp_name']['file']; - $filename = $_FILES['post']['name']['file']; - if (isset($_REQUEST['post']['source']) && !empty($_REQUEST['post']['source'])) { - $source = $_REQUEST['post']['source']; - } else { - $source = null; - } - } elseif (isset($_REQUEST['source']) || isset($_REQUEST['post']['source'])) { // A url was provided - $source = isset($_REQUEST['source']) ? $_REQUEST['source'] : $_REQUEST['post']['source']; - $file = tempnam("/tmp", "shimmie_transload"); - $ok = transload($source, $file); - if (!$ok) { - $page->set_code(409); - $page->add_http_header("X-Danbooru-Errors: fopen read error"); - return; - } - $filename = basename($source); - } else { // Nothing was specified at all - $page->set_code(409); - $page->add_http_header("X-Danbooru-Errors: no input files"); - return; - } + if (isset($_FILES['file'])) { // A file was POST'd in + $file = $_FILES['file']['tmp_name']; + $filename = $_FILES['file']['name']; + // If both a file is posted and a source provided, I'm assuming source is the source of the file + if (isset($_REQUEST['source']) && !empty($_REQUEST['source'])) { + $source = $_REQUEST['source']; + } else { + $source = null; + } + } elseif (isset($_FILES['post'])) { + $file = $_FILES['post']['tmp_name']['file']; + $filename = $_FILES['post']['name']['file']; + if (isset($_REQUEST['post']['source']) && !empty($_REQUEST['post']['source'])) { + $source = $_REQUEST['post']['source']; + } else { + $source = null; + } + } elseif (isset($_REQUEST['source']) || isset($_REQUEST['post']['source'])) { // A url was provided + $source = isset($_REQUEST['source']) ? $_REQUEST['source'] : $_REQUEST['post']['source']; + $file = tempnam("/tmp", "shimmie_transload"); + $ok = transload($source, $file); + if (!$ok) { + $page->set_code(409); + $page->add_http_header("X-Danbooru-Errors: fopen read error"); + return; + } + $filename = basename($source); + } else { // Nothing was specified at all + $page->set_code(409); + $page->add_http_header("X-Danbooru-Errors: no input files"); + return; + } - // Get tags out of url - $posttags = Tag::explode(isset($_REQUEST['tags']) ? $_REQUEST['tags'] : $_REQUEST['post']['tags']); + // Get tags out of url + $posttags = Tag::explode(isset($_REQUEST['tags']) ? $_REQUEST['tags'] : $_REQUEST['post']['tags']); - // Was an md5 supplied? Does it match the file hash? - $hash = md5_file($file); - if (isset($_REQUEST['md5']) && strtolower($_REQUEST['md5']) != $hash) { - $page->set_code(409); - $page->add_http_header("X-Danbooru-Errors: md5 mismatch"); - return; - } - // Upload size checking is now performed in the upload extension - // It is also currently broken due to some confusion over file variable ($tmp_filename?) + // Was an md5 supplied? Does it match the file hash? + $hash = md5_file($file); + if (isset($_REQUEST['md5']) && strtolower($_REQUEST['md5']) != $hash) { + $page->set_code(409); + $page->add_http_header("X-Danbooru-Errors: md5 mismatch"); + return; + } + // Upload size checking is now performed in the upload extension + // It is also currently broken due to some confusion over file variable ($tmp_filename?) - // Does it exist already? - $existing = Image::by_hash($hash); - if (!is_null($existing)) { - $page->set_code(409); - $page->add_http_header("X-Danbooru-Errors: duplicate"); - $existinglink = make_link("post/view/" . $existing->id); - if ($danboorup_kludge) $existinglink = make_http($existinglink); - $page->add_http_header("X-Danbooru-Location: $existinglink"); - return; - } + // Does it exist already? + $existing = Image::by_hash($hash); + if (!is_null($existing)) { + $page->set_code(409); + $page->add_http_header("X-Danbooru-Errors: duplicate"); + $existinglink = make_link("post/view/" . $existing->id); + if ($danboorup_kludge) { + $existinglink = make_http($existinglink); + } + $page->add_http_header("X-Danbooru-Location: $existinglink"); + return; + } - // Fire off an event which should process the new file and add it to the db - $fileinfo = pathinfo($filename); - $metadata = array(); - $metadata['filename'] = $fileinfo['basename']; - $metadata['extension'] = $fileinfo['extension']; - $metadata['tags'] = $posttags; - $metadata['source'] = $source; - //log_debug("danbooru_api","========== NEW($filename) ========="); - //log_debug("danbooru_api", "upload($filename): fileinfo(".var_export($fileinfo,TRUE)."), metadata(".var_export($metadata,TRUE).")..."); + // Fire off an event which should process the new file and add it to the db + $fileinfo = pathinfo($filename); + $metadata = []; + $metadata['filename'] = $fileinfo['basename']; + if (array_key_exists('extension', $fileinfo)) { + $metadata['extension'] = $fileinfo['extension']; + } + $metadata['tags'] = $posttags; + $metadata['source'] = $source; + //log_debug("danbooru_api","========== NEW($filename) ========="); + //log_debug("danbooru_api", "upload($filename): fileinfo(".var_export($fileinfo,TRUE)."), metadata(".var_export($metadata,TRUE).")..."); - try { - $nevent = new DataUploadEvent($file, $metadata); - //log_debug("danbooru_api", "send_event(".var_export($nevent,TRUE).")"); - send_event($nevent); - // If it went ok, grab the id for the newly uploaded image and pass it in the header - $newimg = Image::by_hash($hash); // FIXME: Unsupported file doesn't throw an error? - $newid = make_link("post/view/" . $newimg->id); - if ($danboorup_kludge) $newid = make_http($newid); + try { + $nevent = new DataUploadEvent($file, $metadata); + //log_debug("danbooru_api", "send_event(".var_export($nevent,TRUE).")"); + send_event($nevent); + // If it went ok, grab the id for the newly uploaded image and pass it in the header + $newimg = Image::by_hash($hash); // FIXME: Unsupported file doesn't throw an error? + $newid = make_link("post/view/" . $newimg->id); + if ($danboorup_kludge) { + $newid = make_http($newid); + } - // Did we POST or GET this call? - if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $page->add_http_header("X-Danbooru-Location: $newid"); - } else { - $page->add_http_header("Location: $newid"); - } - } catch (UploadException $ex) { - // Did something screw up? - $page->set_code(409); - $page->add_http_header("X-Danbooru-Errors: exception - " . $ex->getMessage()); - } - } + // Did we POST or GET this call? + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $page->add_http_header("X-Danbooru-Location: $newid"); + } else { + $page->add_http_header("Location: $newid"); + } + } catch (UploadException $ex) { + // Did something screw up? + $page->set_code(409); + $page->add_http_header("X-Danbooru-Errors: exception - " . $ex->getMessage()); + } + } } - - diff --git a/ext/danbooru_api/test.php b/ext/danbooru_api/test.php index 6ea0fef7..f2a82e0a 100644 --- a/ext/danbooru_api/test.php +++ b/ext/danbooru_api/test.php @@ -1,23 +1,25 @@ -log_in_as_admin(); +log_in_as_admin(); - $image_id = $this->post_image("tests/bedroom_workshop.jpg", "data"); + $image_id = $this->post_image("tests/bedroom_workshop.jpg", "data"); - $this->get_page("api/danbooru/find_posts"); - $this->get_page("api/danbooru/find_posts?id=$image_id"); - $this->get_page("api/danbooru/find_posts?md5=17fc89f372ed3636e28bd25cc7f3bac1"); + $this->get_page("api/danbooru/find_posts"); + $this->get_page("api/danbooru/find_posts?id=$image_id"); + $this->get_page("api/danbooru/find_posts?md5=17fc89f372ed3636e28bd25cc7f3bac1"); - $this->get_page("api/danbooru/find_tags"); - $this->get_page("api/danbooru/find_tags?id=1"); - $this->get_page("api/danbooru/find_tags?name=data"); + $this->get_page("api/danbooru/find_tags"); + $this->get_page("api/danbooru/find_tags?id=1"); + $this->get_page("api/danbooru/find_tags?name=data"); - $this->get_page("api/danbooru/post/show/$image_id"); - //$this->assert_response(302); // FIXME + $page = $this->get_page("api/danbooru/post/show/$image_id"); + $this->assertEquals(302, $page->code); - $this->get_page("post/list/md5:17fc89f372ed3636e28bd25cc7f3bac1/1"); - //$this->assert_title(new PatternExpectation("/^Image \d+: data/")); - //$this->click("Delete"); - } + $this->get_page("post/list/md5:17fc89f372ed3636e28bd25cc7f3bac1/1"); + //$this->assert_title(new PatternExpectation("/^Image \d+: data/")); + //$this->click("Delete"); + } } diff --git a/ext/downtime/info.php b/ext/downtime/info.php new file mode 100644 index 00000000..a47dc7f0 --- /dev/null +++ b/ext/downtime/info.php @@ -0,0 +1,18 @@ + - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Show a "down for maintenance" page - * Documentation: - * Once installed there will be some more options on the config page -- - * Ticking "disable non-admin access" will mean that regular and anonymous - * users will be blocked from accessing the site, only able to view the - * message specified in the box. - */ +add_bool_option("downtime", "Disable non-admin access: "); - $sb->add_longtext_option("downtime_message", "
    "); - $event->panel->add_block($sb); - } + public function get_priority(): int + { + return 10; + } - public function onPageRequest(PageRequestEvent $event) { - global $config, $page, $user; + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Downtime"); + $sb->add_bool_option("downtime", "Disable non-admin access: "); + $sb->add_longtext_option("downtime_message", "
    "); + $event->panel->add_block($sb); + } - if($config->get_bool("downtime")) { - if(!$user->can("ignore_downtime") && !$this->is_safe_page($event)) { - $msg = $config->get_string("downtime_message"); - $this->theme->display_message($msg); - if(!defined("UNITTEST")) { // hax D: - header("HTTP/1.0 {$page->code} Downtime"); - print($page->data); - exit; - } - } - $this->theme->display_notification($page); - } - } + public function onPageRequest(PageRequestEvent $event) + { + global $config, $page, $user; - private function is_safe_page(PageRequestEvent $event) { - if($event->page_matches("user_admin/login")) return true; - else return false; - } + if ($config->get_bool("downtime")) { + if (!$user->can(Permissions::IGNORE_DOWNTIME) && !$this->is_safe_page($event)) { + $msg = $config->get_string("downtime_message"); + $this->theme->display_message($msg); + if (!defined("UNITTEST")) { // hax D: + header("HTTP/1.0 {$page->code} Downtime"); + print($page->data); + exit; + } + } + $this->theme->display_notification($page); + } + } + + private function is_safe_page(PageRequestEvent $event) + { + if ($event->page_matches("user_admin/login")) { + return true; + } else { + return false; + } + } } diff --git a/ext/downtime/test.php b/ext/downtime/test.php index 4331e27f..f2184e34 100644 --- a/ext/downtime/test.php +++ b/ext/downtime/test.php @@ -1,39 +1,42 @@ -set_bool("downtime", false); - } +set_bool("downtime", false); + } - public function testDowntime() { - global $config; + public function testDowntime() + { + global $config; - $config->set_string("downtime_message", "brb, unit testing"); + $config->set_string("downtime_message", "brb, unit testing"); - // downtime on - $config->set_bool("downtime", true); + // downtime on + $config->set_bool("downtime", true); - $this->log_in_as_admin(); - $this->get_page("post/list"); - $this->assert_text("DOWNTIME MODE IS ON!"); - $this->assert_response(200); + $this->log_in_as_admin(); + $this->get_page("post/list"); + $this->assert_text("DOWNTIME MODE IS ON!"); + $this->assert_response(200); - $this->log_in_as_user(); - $this->get_page("post/list"); - $this->assert_content("brb, unit testing"); - $this->assert_response(503); + $this->log_in_as_user(); + $this->get_page("post/list"); + $this->assert_content("brb, unit testing"); + $this->assert_response(503); - // downtime off - $config->set_bool("downtime", false); + // downtime off + $config->set_bool("downtime", false); - $this->log_in_as_admin(); - $this->get_page("post/list"); - $this->assert_no_text("DOWNTIME MODE IS ON!"); - $this->assert_response(200); + $this->log_in_as_admin(); + $this->get_page("post/list"); + $this->assert_no_text("DOWNTIME MODE IS ON!"); + $this->assert_response(200); - $this->log_in_as_user(); - $this->get_page("post/list"); - $this->assert_no_content("brb, unit testing"); - $this->assert_response(200); - } + $this->log_in_as_user(); + $this->get_page("post/list"); + $this->assert_no_content("brb, unit testing"); + $this->assert_response(200); + } } diff --git a/ext/downtime/theme.php b/ext/downtime/theme.php index 965b10b2..c1a752bc 100644 --- a/ext/downtime/theme.php +++ b/ext/downtime/theme.php @@ -1,32 +1,36 @@ -add_block(new Block("Downtime", - "
    DOWNTIME MODE IS ON!
    ", "left", 0)); - } +class DowntimeTheme extends Themelet +{ + /** + * Show the admin that downtime mode is enabled + */ + public function display_notification(Page $page) + { + $page->add_block(new Block( + "Downtime", + "DOWNTIME MODE IS ON!", + "left", + 0 + )); + } - /** - * Display $message and exit - * - * @param string $message - */ - public function display_message(/*string*/ $message) { - global $config, $user, $page; - $theme_name = $config->get_string('theme'); - $data_href = get_base_href(); - $login_link = make_link("user_admin/login"); - $auth = $user->get_auth_html(); + /** + * Display $message and exit + */ + public function display_message(string $message) + { + global $config, $user, $page; + $theme_name = $config->get_string(SetupConfig::THEME); + $data_href = get_base_href(); + $login_link = make_link("user_admin/login"); + $auth = $user->get_auth_html(); - $page->set_mode('data'); - $page->set_code(503); - $page->set_data(<< + $page->set_mode(PageMode::DATA); + $page->set_code(503); + $page->set_data( + << Downtime @@ -34,7 +38,7 @@ class DowntimeTheme extends Themelet {
    -

    Down for Maintenance

    +

    Down for Maintenance

    $message
    @@ -62,6 +66,6 @@ class DowntimeTheme extends Themelet { EOD -); - } + ); + } } diff --git a/ext/emoticons/info.php b/ext/emoticons/info.php new file mode 100644 index 00000000..eadcbf67 --- /dev/null +++ b/ext/emoticons/info.php @@ -0,0 +1,20 @@ +Images are stored in /ext/emoticons/default/, and you can +add more emoticons by uploading images into that folder."; +} diff --git a/ext/emoticons/main.php b/ext/emoticons/main.php index e6245daf..38563341 100644 --- a/ext/emoticons/main.php +++ b/ext/emoticons/main.php @@ -1,49 +1,20 @@ - - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Lets users use graphical smilies - * Documentation: - * This extension will turn colon-something-colon into a link - * to an image with that something as the name, eg :smile: - * becomes a link to smile.gif - *

    Images are stored in /ext/emoticons/default/, and you can - * add more emoticons by uploading images into that folder. - */ +", $text); - return $text; - } +class Emoticons extends FormatterExtension +{ + public function format(string $text): string + { + $data_href = get_base_href(); + $text = preg_replace("/:([a-z]*?):/s", "\1", $text); + return $text; + } - /** - * @param string $text - * @return string - */ - public function strip(/*string*/ $text) { - return $text; - } + public function strip(string $text): string + { + return $text; + } } - -/** - * Class EmoticonList - */ -class EmoticonList extends Extension { - public function onPageRequest(PageRequestEvent $event) { - if($event->page_matches("emote/list")) { - $this->theme->display_emotes(glob("ext/emoticons/default/*")); - } - } -} - diff --git a/ext/emoticons/test.php b/ext/emoticons/test.php index bc4a8af9..47df8ac4 100644 --- a/ext/emoticons/test.php +++ b/ext/emoticons/test.php @@ -1,19 +1,20 @@ -log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - send_event(new CommentPostingEvent($image_id, $user, ":cool: :beans:")); + send_event(new CommentPostingEvent($image_id, $user, ":cool: :beans:")); - $this->get_page("post/view/$image_id"); - $this->assert_no_text(":cool:"); # FIXME: test for working image link - //$this->assert_text(":beans:"); # FIXME: this should be left as-is + $this->get_page("post/view/$image_id"); + $this->assert_no_text(":cool:"); # FIXME: test for working image link + //$this->assert_text(":beans:"); # FIXME: this should be left as-is - $this->get_page("emote/list"); - //$this->assert_text(":arrow:"); - } + $this->get_page("emote/list"); + //$this->assert_text(":arrow:"); + } } - diff --git a/ext/emoticons/theme.php b/ext/emoticons/theme.php deleted file mode 100644 index 07f033dd..00000000 --- a/ext/emoticons/theme.php +++ /dev/null @@ -1,24 +0,0 @@ -Emoticon list"; - $html .= ""; - $n = 1; - foreach($list as $item) { - $pathinfo = pathinfo($item); - $name = $pathinfo["filename"]; - $html .= ""; - if($n++ % 3 == 0) $html .= ""; - } - $html .= "
    :$name:
    "; - $html .= ""; - $page->set_mode("data"); - $page->set_data($html); - } -} - diff --git a/ext/emoticons_list/info.php b/ext/emoticons_list/info.php new file mode 100644 index 00000000..2bf25bc3 --- /dev/null +++ b/ext/emoticons_list/info.php @@ -0,0 +1,15 @@ +page_matches("emote/list")) { + $this->theme->display_emotes(glob("ext/emoticons/default/*")); + } + } +} diff --git a/ext/emoticons_list/theme.php b/ext/emoticons_list/theme.php new file mode 100644 index 00000000..17fb1f8a --- /dev/null +++ b/ext/emoticons_list/theme.php @@ -0,0 +1,24 @@ +Emoticon list"; + $html .= ""; + $n = 1; + foreach ($list as $item) { + $pathinfo = pathinfo($item); + $name = $pathinfo["filename"]; + $html .= ""; + if ($n++ % 3 == 0) { + $html .= ""; + } + } + $html .= "
    $name :$name:
    "; + $html .= ""; + $page->set_mode(PageMode::DATA); + $page->set_data($html); + } +} diff --git a/ext/et/info.php b/ext/et/info.php new file mode 100644 index 00000000..5bc82c42 --- /dev/null +++ b/ext/et/info.php @@ -0,0 +1,17 @@ + - * License: GPLv2 - * Description: Show various bits of system information - * Documentation: - * Knowing the information that this extension shows can be - * very useful for debugging. There's also an option to send - * your stats to my database, so I can get some idea of how - * shimmie is used, which servers I need to support, which - * versions of PHP I should test with, etc. - */ +page_matches("system_info")) { - if($user->can("view_sysinfo")) { - $this->theme->display_info_page($this->get_info()); - } - } - } +class ET extends Extension +{ + /** @var ETTheme */ + protected $theme; - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("view_sysinfo")) { - $event->add_link("System Info", make_link("system_info")); - } - } + public function onPageRequest(PageRequestEvent $event) + { + global $user; + if ($event->page_matches("system_info")) { + if ($user->can(Permissions::VIEW_SYSINTO)) { + $this->theme->display_info_page($this->get_info()); + } + } + } - /** - * Collect the information and return it in a keyed array. - */ - private function get_info() { - global $config, $database; + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::VIEW_SYSINTO)) { + $event->add_nav_link("system_info", new Link('system_info'), "System Info", null, 10); + } + } + } - $info = array(); - $info['site_title'] = $config->get_string("title"); - $info['site_theme'] = $config->get_string("theme"); - $info['site_url'] = "http://" . $_SERVER["HTTP_HOST"] . get_base_href(); + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::VIEW_SYSINTO)) { + $event->add_link("System Info", make_link("system_info")); + } + } - $info['sys_shimmie'] = VERSION; - $info['sys_schema'] = $config->get_string("db_version"); - $info['sys_php'] = phpversion(); - $info['sys_db'] = $database->get_driver_name(); - $info['sys_os'] = php_uname(); - $info['sys_disk'] = to_shorthand_int(disk_total_space("./") - disk_free_space("./")) . " / " . - to_shorthand_int(disk_total_space("./")); - $info['sys_server'] = isset($_SERVER["SERVER_SOFTWARE"]) ? $_SERVER["SERVER_SOFTWARE"] : 'unknown'; - - $info['thumb_engine'] = $config->get_string("thumb_engine"); - $info['thumb_quality'] = $config->get_int('thumb_quality'); - $info['thumb_width'] = $config->get_int('thumb_width'); - $info['thumb_height'] = $config->get_int('thumb_height'); - $info['thumb_mem'] = $config->get_int("thumb_mem_limit"); + public function onCommand(CommandEvent $event) + { + if ($event->cmd == "help") { + print "\tget-info\n"; + print "\t\tList a bunch of info\n\n"; + } + if ($event->cmd == "info") { + foreach ($this->get_info() as $k => $v) { + print("$k = $v\n"); + } + } + } - $info['stat_images'] = $database->get_one("SELECT COUNT(*) FROM images"); - $info['stat_comments'] = $database->get_one("SELECT COUNT(*) FROM comments"); - $info['stat_users'] = $database->get_one("SELECT COUNT(*) FROM users"); - $info['stat_tags'] = $database->get_one("SELECT COUNT(*) FROM tags"); - $info['stat_image_tags'] = $database->get_one("SELECT COUNT(*) FROM image_tags"); + /** + * Collect the information and return it in a keyed array. + */ + private function get_info(): array + { + global $config, $database; - $els = array(); - foreach(get_declared_classes() as $class) { - $rclass = new ReflectionClass($class); - if($rclass->isAbstract()) { - // don't do anything - } - elseif(is_subclass_of($class, "Extension")) { - $els[] = $class; - } - } - $info['sys_extensions'] = join(', ', $els); + $info = []; + $info['site_title'] = $config->get_string(SetupConfig::TITLE); + $info['site_theme'] = $config->get_string(SetupConfig::THEME); + $info['site_url'] = "http://" . $_SERVER["HTTP_HOST"] . get_base_href(); - //$cfs = array(); - //foreach($database->get_all("SELECT name, value FROM config") as $pair) { - // $cfs[] = $pair['name']."=".$pair['value']; - //} - //$info[''] = "Config: ".join(", ", $cfs); + $info['sys_shimmie'] = VERSION; + $info['sys_schema'] = $config->get_int("db_version"); + $info['sys_php'] = phpversion(); + $info['sys_db'] = $database->get_driver_name(); + $info['sys_os'] = php_uname(); + $info['sys_disk'] = to_shorthand_int((int)disk_total_space("./") - (int)disk_free_space("./")) . " / " . + to_shorthand_int((int)disk_total_space("./")); + $info['sys_server'] = isset($_SERVER["SERVER_SOFTWARE"]) ? $_SERVER["SERVER_SOFTWARE"] : 'unknown'; - return $info; - } + $info[MediaConfig::FFMPEG_PATH] = $config->get_string(MediaConfig::FFMPEG_PATH); + $info[MediaConfig::CONVERT_PATH] = $config->get_string(MediaConfig::CONVERT_PATH); + $info[MediaConfig::MEM_LIMIT] = $config->get_int(MediaConfig::MEM_LIMIT); + + $info[ImageConfig::THUMB_ENGINE] = $config->get_string(ImageConfig::THUMB_ENGINE); + $info[ImageConfig::THUMB_QUALITY] = $config->get_int(ImageConfig::THUMB_QUALITY); + $info[ImageConfig::THUMB_WIDTH] = $config->get_int(ImageConfig::THUMB_WIDTH); + $info[ImageConfig::THUMB_HEIGHT] = $config->get_int(ImageConfig::THUMB_HEIGHT); + $info[ImageConfig::THUMB_SCALING] = $config->get_int(ImageConfig::THUMB_SCALING); + $info[ImageConfig::THUMB_TYPE] = $config->get_string(ImageConfig::THUMB_TYPE); + + $info['stat_images'] = $database->get_one("SELECT COUNT(*) FROM images"); + $info['stat_comments'] = $database->get_one("SELECT COUNT(*) FROM comments"); + $info['stat_users'] = $database->get_one("SELECT COUNT(*) FROM users"); + $info['stat_tags'] = $database->get_one("SELECT COUNT(*) FROM tags"); + $info['stat_image_tags'] = $database->get_one("SELECT COUNT(*) FROM image_tags"); + + $els = []; + foreach (getSubclassesOf("Extension") as $class) { + $els[] = $class; + } + $info['sys_extensions'] = join(', ', $els); + + $info['handled_extensions'] = join(', ', DataHandlerExtension::get_all_supported_exts()); + + //$cfs = array(); + //foreach($database->get_all("SELECT name, value FROM config") as $pair) { + // $cfs[] = $pair['name']."=".$pair['value']; + //} + //$info[''] = "Config: ".join(", ", $cfs); + + return $info; + } } - diff --git a/ext/et/test.php b/ext/et/test.php index 1d741eda..4cb1b512 100644 --- a/ext/et/test.php +++ b/ext/et/test.php @@ -1,8 +1,10 @@ -log_in_as_admin(); - $this->get_page("system_info"); - $this->assert_title("System Info"); - } +log_in_as_admin(); + $this->get_page("system_info"); + $this->assert_title("System Info"); + } } diff --git a/ext/et/theme.php b/ext/et/theme.php index 2239807b..9fd0c156 100644 --- a/ext/et/theme.php +++ b/ext/et/theme.php @@ -1,22 +1,25 @@ - $value) - */ - public function display_info_page($info) { - global $page; +class ETTheme extends Themelet +{ + /* + * Create a page showing info + * + * $info = an array of ($name => $value) + */ + public function display_info_page($info) + { + global $page; - $page->set_title("System Info"); - $page->set_heading("System Info"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Information:", $this->build_data_form($info))); - } + $page->set_title("System Info"); + $page->set_heading("System Info"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Information:", $this->build_data_form($info))); + } - protected function build_data_form($info) { - $data = << @@ -56,7 +63,5 @@ EOD; of web servers / databases / etc I need to support. EOD; - return $html; - } + } } - diff --git a/ext/ext_manager/baseline_open_in_new_black_18dp.png b/ext/ext_manager/baseline_open_in_new_black_18dp.png new file mode 100644 index 00000000..48a6da8e Binary files /dev/null and b/ext/ext_manager/baseline_open_in_new_black_18dp.png differ diff --git a/ext/ext_manager/info.php b/ext/ext_manager/info.php new file mode 100644 index 00000000..71b54b95 --- /dev/null +++ b/ext/ext_manager/info.php @@ -0,0 +1,16 @@ + - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Visibility: admin - * Description: A thing for point & click extension management - * Documentation: - * Allows the admin to view a list of all extensions and enable or - * disable them; also allows users to view the list of activated - * extensions and read their documentation - */ +name, $b->name); + +function __extman_extcmp(ExtensionInfo $a, ExtensionInfo $b): int +{ + if ($a->beta===true&&$b->beta===false) { + return 1; + } + if ($a->beta===false&&$b->beta===true) { + return -1; + } + + return strcmp($a->name, $b->name); } -class ExtensionInfo { - public $ext_name, $name, $link, $author, $email; - public $description, $documentation, $version, $visibility; - public $enabled; - - public function __construct($main) { - $matches = array(); - $lines = file($main); - $number_of_lines = count($lines); - preg_match("#ext/(.*)/main.php#", $main, $matches); - $this->ext_name = $matches[1]; - $this->name = $this->ext_name; - $this->enabled = $this->is_enabled($this->ext_name); - - for($i=0; $i<$number_of_lines; $i++) { - $line = $lines[$i]; - if(preg_match("/Name: (.*)/", $line, $matches)) { - $this->name = $matches[1]; - } - else if(preg_match("/Visibility: (.*)/", $line, $matches)) { - $this->visibility = $matches[1]; - } - else if(preg_match("/Link: (.*)/", $line, $matches)) { - $this->link = $matches[1]; - if($this->link[0] == "/") { - $this->link = make_link(substr($this->link, 1)); - } - } - else if(preg_match("/Version: (.*)/", $line, $matches)) { - $this->version = $matches[1]; - } - else if(preg_match("/Author: (.*) [<\(](.*@.*)[>\)]/", $line, $matches)) { - $this->author = $matches[1]; - $this->email = $matches[2]; - } - else if(preg_match("/Author: (.*)/", $line, $matches)) { - $this->author = $matches[1]; - } - else if(preg_match("/(.*)Description: ?(.*)/", $line, $matches)) { - $this->description = $matches[2]; - $start = $matches[1]." "; - $start_len = strlen($start); - while(substr($lines[$i+1], 0, $start_len) == $start) { - $this->description .= " ".substr($lines[$i+1], $start_len); - $i++; - } - } - else if(preg_match("/(.*)Documentation: ?(.*)/", $line, $matches)) { - $this->documentation = $matches[2]; - $start = $matches[1]." "; - $start_len = strlen($start); - while(substr($lines[$i+1], 0, $start_len) == $start) { - $this->documentation .= " ".substr($lines[$i+1], $start_len); - $i++; - } - $this->documentation = str_replace('$site', make_http(get_base_href()), $this->documentation); - } - else if(preg_match("/\*\//", $line, $matches)) { - break; - } - } - } - - /** - * @param string $fname - * @return bool|null - */ - private function is_enabled(/*string*/ $fname) { - $core = explode(",", CORE_EXTS); - $extra = explode(",", EXTRA_EXTS); - - if(in_array($fname, $extra)) return true; // enabled - if(in_array($fname, $core)) return null; // core - return false; // not enabled - } +function __extman_extactive(ExtensionInfo $a): bool +{ + return Extension::is_enabled($a->key); } -class ExtManager extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; - if($event->page_matches("ext_manager")) { - if($user->can("manage_extension_list")) { - if($event->get_arg(0) == "set" && $user->check_auth_token()) { - if(is_writable("data/config")) { - $this->set_things($_POST); - log_warning("ext_manager", "Active extensions changed", true); - $page->set_mode("redirect"); - $page->set_redirect(make_link("ext_manager")); - } - else { - $this->theme->display_error(500, "File Operation Failed", - "The config file (data/config/extensions.conf.php) isn't writable by the web server :("); - } - } - else { - $this->theme->display_table($page, $this->get_extensions(true), true); - } - } - else { - $this->theme->display_table($page, $this->get_extensions(false), false); - } - } - if($event->page_matches("ext_doc")) { - $ext = $event->get_arg(0); - if(file_exists("ext/$ext/main.php")) { - $info = new ExtensionInfo("ext/$ext/main.php"); - $this->theme->display_doc($page, $info); - } - else { - $this->theme->display_table($page, $this->get_extensions(false), false); - } - } - } +class ExtensionAuthor +{ + public $name; + public $email; - public function onCommand(CommandEvent $event) { - if($event->cmd == "help") { - print "\tdisable-all-ext\n"; - print "\t\tdisable all extensions\n\n"; - } - if($event->cmd == "disable-all-ext") { - $this->write_config(array()); - } - } + public function __construct(string $name, ?string $email) + { + $this->name = $name; + $this->email = $email; + } +} +class ExtManager extends Extension +{ + /** @var ExtManagerTheme */ + protected $theme; - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("manage_extension_list")) { - $event->add_link("Extension Manager", make_link("ext_manager")); - } - else { - $event->add_link("Help", make_link("ext_doc")); - } - } + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; + if ($event->page_matches("ext_manager")) { + if ($user->can(Permissions::MANAGE_EXTENSION_LIST)) { + if ($event->count_args() == 1 && $event->get_arg(0) == "set" && $user->check_auth_token()) { + if (is_writable("data/config")) { + $this->set_things($_POST); + log_warning("ext_manager", "Active extensions changed", "Active extensions changed"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("ext_manager")); + } else { + $this->theme->display_error( + 500, + "File Operation Failed", + "The config file (data/config/extensions.conf.php) isn't writable by the web server :(" + ); + } + } else { + $this->theme->display_table($page, $this->get_extensions(true), true); + } + } else { + $this->theme->display_table($page, $this->get_extensions(false), false); + } + } - /** - * @param bool $all - * @return ExtensionInfo[] - */ - private function get_extensions(/*bool*/ $all) { - $extensions = array(); - if($all) { - $exts = zglob("ext/*/main.php"); - } - else { - $exts = zglob("ext/{".ENABLED_EXTS."}/main.php"); - } - foreach($exts as $main) { - $extensions[] = new ExtensionInfo($main); - } - usort($extensions, "__extman_extcmp"); - return $extensions; - } + if ($event->page_matches("ext_doc")) { + if ($event->count_args() == 1) { + $ext = $event->get_arg(0); + if (file_exists("ext/$ext/info.php")) { + $info = ExtensionInfo::get_by_key($ext); + $this->theme->display_doc($page, $info); + } + } else { + $this->theme->display_table($page, $this->get_extensions(false), false); + } + } + } - private function set_things($settings) { - $core = explode(",", CORE_EXTS); - $extras = array(); + public function onCommand(CommandEvent $event) + { + if ($event->cmd == "help") { + print "\tdisable-all-ext\n"; + print "\t\tdisable all extensions\n\n"; + } + if ($event->cmd == "disable-all-ext") { + $this->write_config([]); + } + } - foreach(glob("ext/*/main.php") as $main) { - $matches = array(); - preg_match("#ext/(.*)/main.php#", $main, $matches); - $fname = $matches[1]; + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::MANAGE_EXTENSION_LIST)) { + $event->add_nav_link("ext_manager", new Link('ext_manager'), "Extension Manager"); + } else { + $event->add_nav_link("ext_doc", new Link('ext_doc'), "Board Help"); + } + } + } - if(!in_array($fname, $core) && isset($settings["ext_$fname"])) { - $extras[] = $fname; - } - } - - $this->write_config($extras); - } + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::MANAGE_EXTENSION_LIST)) { + $event->add_link("Extension Manager", make_link("ext_manager")); + } + } /** - * @param string[] $extras + * #return ExtensionInfo[] */ - private function write_config($extras) { - file_put_contents( - "data/config/extensions.conf.php", - '<'.'?php'."\n". - 'define("EXTRA_EXTS", "'.implode(",", $extras).'");'."\n". - '?'.">" - ); + private function get_extensions(bool $all): array + { + $extensions = ExtensionInfo::get_all(); + if (!$all) { + $extensions = array_filter($extensions, "__extman_extactive"); + } + usort($extensions, "__extman_extcmp"); + return $extensions; + } - // when the list of active extensions changes, we can be - // pretty sure that the list of who reacts to what will - // change too - if(file_exists("data/cache/event_listeners.php")) { - unlink("data/cache/event_listeners.php"); - } - } + private function set_things($settings) + { + $core = ExtensionInfo::get_core_extensions(); + $extras = []; + + foreach (ExtensionInfo::get_all_keys() as $key) { + if (!in_array($key, $core) && isset($settings["ext_$key"])) { + $extras[] = $key; + } + } + + $this->write_config($extras); + } + + /** + * #param string[] $extras + */ + private function write_config(array $extras) + { + file_put_contents( + "data/config/extensions.conf.php", + '<' . '?php' . "\n" . + 'define("EXTRA_EXTS", "' . implode(",", $extras) . '");' . "\n" + ); + + // when the list of active extensions changes, we can be + // pretty sure that the list of who reacts to what will + // change too + _clear_cached_event_listeners(); + } } diff --git a/ext/ext_manager/test.php b/ext/ext_manager/test.php index 850abc27..be90033a 100644 --- a/ext/ext_manager/test.php +++ b/ext/ext_manager/test.php @@ -1,25 +1,27 @@ -get_page('ext_manager'); - $this->assert_title("Extensions"); +get_page('ext_manager'); + $this->assert_title("Extensions"); - $this->get_page('ext_doc'); - $this->assert_title("Extensions"); + $this->get_page('ext_doc'); + $this->assert_title("Extensions"); - $this->get_page('ext_doc/ext_manager'); - $this->assert_title("Documentation for Extension Manager"); - $this->assert_text("view a list of all extensions"); + $this->get_page('ext_doc/ext_manager'); + $this->assert_title("Documentation for Extension Manager"); + $this->assert_text("view a list of all extensions"); - # test author without email - $this->get_page('ext_doc/user'); + # test author without email + $this->get_page('ext_doc/user'); - $this->log_in_as_admin(); - $this->get_page('ext_manager'); - $this->assert_title("Extensions"); - //$this->assert_text("SimpleTest integration"); // FIXME: something which still exists - $this->log_out(); + $this->log_in_as_admin(); + $this->get_page('ext_manager'); + $this->assert_title("Extensions"); + //$this->assert_text("SimpleTest integration"); // FIXME: something which still exists + $this->log_out(); - # FIXME: test that some extensions can be added and removed? :S - } + # FIXME: test that some extensions can be added and removed? :S + } } diff --git a/ext/ext_manager/theme.php b/ext/ext_manager/theme.php index 53732529..e9235f46 100644 --- a/ext/ext_manager/theme.php +++ b/ext/ext_manager/theme.php @@ -1,143 +1,122 @@ -Enabled" : ""; - $html = " - ".make_form(make_link("ext_manager/set"))." - - - - $h_en - - - - - - - "; - foreach($extensions as $extension) { - if(!$editable && $extension->visibility == "admin") continue; +use function MicroHTML\LABEL; +use function MicroHTML\A; +use function MicroHTML\B; +use function MicroHTML\IMG; +use function MicroHTML\TABLE; +use function MicroHTML\THEAD; +use function MicroHTML\TFOOT; +use function MicroHTML\TBODY; +use function MicroHTML\TH; +use function MicroHTML\TR; +use function MicroHTML\TD; +use function MicroHTML\INPUT; +use function MicroHTML\DIV; +use function MicroHTML\P; +use function MicroHTML\BR; +use function MicroHTML\emptyHTML; +use function MicroHTML\rawHTML; - $h_name = html_escape(empty($extension->name) ? $extension->ext_name : $extension->name); - $h_description = html_escape($extension->description); - $h_link = make_link("ext_doc/".url_escape($extension->ext_name)); - $h_enabled = ($extension->enabled === TRUE ? " checked='checked'" : ($extension->enabled === FALSE ? "" : " disabled checked='checked'")); - $h_enabled_box = $editable ? "" : ""; - $h_docs = ($extension->documentation ? "" : ""); //TODO: A proper "docs" symbol would be preferred here. +class ExtManagerTheme extends Themelet +{ + /** + * #param ExtensionInfo[] $extensions + */ + public function display_table(Page $page, array $extensions, bool $editable) + { + $tbody = TBODY(); - $html .= " - - {$h_enabled_box} - - - - "; - } - $h_set = $editable ? "" : ""; - $html .= " - - $h_set -
    NameDocsDescription
    {$h_name}{$h_docs}{$h_description}
    - - "; + $form = SHM_SIMPLE_FORM( + "ext_manager/set", + TABLE( + ["id"=>'extensions', "class"=>'zebra sortable'], + THEAD(TR( + $editable ? TH("Enabled") : null, + TH("Name"), + TH("Docs"), + TH("Description") + )), + $tbody, + $editable ? TFOOT(TR(TD(["colspan"=>'5'], INPUT(["type"=>'submit', "value"=>'Set Extensions'])))) : null + ) + ); - $page->set_title("Extensions"); - $page->set_heading("Extensions"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Extension Manager", $html)); - } + foreach ($extensions as $extension) { + if ((!$editable && $extension->visibility === ExtensionInfo::VISIBLE_ADMIN) + || $extension->visibility === ExtensionInfo::VISIBLE_HIDDEN) { + continue; + } - /* - public function display_blocks(Page $page, $extensions) { - global $user; - $col_1 = ""; - $col_2 = ""; - foreach($extensions as $extension) { - $ext_name = $extension->ext_name; - $h_name = empty($extension->name) ? $ext_name : html_escape($extension->name); - $h_email = html_escape($extension->email); - $h_link = isset($extension->link) ? - "link)."\">Original Site" : ""; - $h_doc = isset($extension->documentation) ? - "ext_name))."\">Documentation" : ""; - $h_author = html_escape($extension->author); - $h_description = html_escape($extension->description); - $h_enabled = $extension->enabled ? " checked='checked'" : ""; - $h_author_link = empty($h_email) ? - "$h_author" : - "$h_author"; + $tbody->appendChild(TR( + ["data-ext"=>$extension->name], + $editable ? TD(INPUT([ + "type"=>'checkbox', + "name"=>"ext_{$extension->key}", + "id"=>"ext_{$extension->key}", + "checked"=>($extension->is_enabled() === true), + "disabled"=>($extension->is_supported()===false || $extension->core===true) + ])) : null, + TD(LABEL( + ["for"=>"ext_{$extension->key}"], + ( + ($extension->beta===true ? "[BETA] ":""). + (empty($extension->name) ? $extension->key : $extension->name) + ) + )), + TD( + // TODO: A proper "docs" symbol would be preferred here. + $extension->documentation ? + A( + ["href"=>make_link("ext_doc/" . url_escape($extension->key))], + IMG(["src"=>'ext/ext_manager/baseline_open_in_new_black_18dp.png']) + ) : + null + ), + TD( + ["style"=>'text-align: left;'], + $extension->description, + " ", + B(["style"=>'color:red'], $extension->get_support_info()) + ), + )); + } - $html = " -

    - - - - - - - - - - -
    $h_name
    By $h_author_linkEnabled: 
    $h_description

    $h_link $h_doc

    - "; - if($n++ % 2 == 0) { - $col_1 .= $html; - } - else { - $col_2 .= $html; - } - } - $html = " - ".make_form(make_link("ext_manager/set"))." - ".$user->get_auth_html()." - - - -
    $col_1$col_2
    - - "; + $page->set_title("Extensions"); + $page->set_heading("Extensions"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Extension Manager", (string)$form)); + } - $page->set_title("Extensions"); - $page->set_heading("Extensions"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Extension Manager", $html)); - } - */ + public function display_doc(Page $page, ExtensionInfo $info) + { + $author = emptyHTML(); + if (count($info->authors) > 0) { + $author->appendChild(BR()); + $author->appendChild(B(count($info->authors) > 1 ? "Authors: " : "Author: ")); + foreach ($info->authors as $auth=>$email) { + if (!empty($email)) { + $author->appendChild(A(["href"=>"mailto:$email"], $auth)); + } else { + $author->appendChild($auth); + } + $author->appendChild(BR()); + } + } - public function display_doc(Page $page, ExtensionInfo $info) { - $author = ""; - if($info->author) { - if($info->email) { - $author = "
    Author: email)."\">".html_escape($info->author).""; - } - else { - $author = "
    Author: ".html_escape($info->author); - } - } - $version = ($info->version) ? "
    Version: ".html_escape($info->version) : ""; - $link = ($info->link) ? "
    Home Page: link)."\">Link" : ""; - $doc = $info->documentation; - $html = " -

    - $author - $version - $link -

    $doc -


    -

    Back to the list -

    "; + $html = DIV( + ["style"=>'margin: auto; text-align: left; width: 512px;'], + $author, + ($info->version ? emptyHTML(BR(), B("Version"), $info->version) : null), + ($info->link ? emptyHTML(BR(), B("Home Page"), A(["href"=>$info->link], "Link")) : null), + P(rawHTML($info->documentation ?? "(This extension has no documentation)")), + //
    , + P(A(["href"=>make_link("ext_manager")], "Back to the list")) + ); - $page->set_title("Documentation for ".html_escape($info->name)); - $page->set_heading(html_escape($info->name)); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Documentation", $html)); - } + $page->set_title("Documentation for " . html_escape($info->name)); + $page->set_heading(html_escape($info->name)); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Documentation", (string)$html)); + } } - diff --git a/ext/favorites/info.php b/ext/favorites/info.php new file mode 100644 index 00000000..baab8e90 --- /dev/null +++ b/ext/favorites/info.php @@ -0,0 +1,17 @@ +"info@daniel-marschall.de"]; + public $license = self::LICENSE_GPLV2; + public $description = "Allow users to favorite images"; + public $documentation = +"Gives users a \"favorite this image\" button that they can press +

    Favorites for a user can then be retrieved by searching for \"favorited_by=UserName\" +

    Popular images can be searched for by eg. \"favorites>5\" +

    Favorite info can be added to an image's filename or tooltip using the \$favorites placeholder"; +} diff --git a/ext/favorites/main.php b/ext/favorites/main.php index 8e6af251..631e5875 100644 --- a/ext/favorites/main.php +++ b/ext/favorites/main.php @@ -1,217 +1,265 @@ - - * License: GPLv2 - * Description: Allow users to favorite images - * Documentation: - * Gives users a "favorite this image" button that they can press - *

    Favorites for a user can then be retrieved by searching for - * "favorited_by=UserName" - *

    Popular images can be searched for by eg. "favorites>5" - *

    Favorite info can be added to an image's filename or tooltip - * using the $favorites placeholder - */ +image_id = $image_id; - $this->user = $user; - $this->do_set = $do_set; - } + $this->image_id = $image_id; + $this->user = $user; + $this->do_set = $do_set; + } } -class Favorites extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config; - if($config->get_int("ext_favorites_version", 0) < 1) { - $this->install(); - } - } +class Favorites extends Extension +{ + /** @var FavoritesTheme */ + protected $theme; - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { - global $database, $user; - if(!$user->is_anonymous()) { - $user_id = $user->id; - $image_id = $event->image->id; + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) + { + global $database, $user; + if (!$user->is_anonymous()) { + $user_id = $user->id; + $image_id = $event->image->id; - $is_favorited = $database->get_one( - "SELECT COUNT(*) AS ct FROM user_favorites WHERE user_id = :user_id AND image_id = :image_id", - array("user_id"=>$user_id, "image_id"=>$image_id)) > 0; - - $event->add_part($this->theme->get_voter_html($event->image, $is_favorited)); - } - } + $is_favorited = $database->get_one( + "SELECT COUNT(*) AS ct FROM user_favorites WHERE user_id = :user_id AND image_id = :image_id", + ["user_id"=>$user_id, "image_id"=>$image_id] + ) > 0; - public function onDisplayingImage(DisplayingImageEvent $event) { - $people = $this->list_persons_who_have_favorited($event->image); - if(count($people) > 0) { - $this->theme->display_people($people); - } - } + $event->add_part((string)$this->theme->get_voter_html($event->image, $is_favorited)); + } + } - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; - if($event->page_matches("change_favorite") && !$user->is_anonymous() && $user->check_auth_token()) { - $image_id = int_escape($_POST['image_id']); - if((($_POST['favorite_action'] == "set") || ($_POST['favorite_action'] == "unset")) && ($image_id > 0)) { - if($_POST['favorite_action'] == "set") { - send_event(new FavoriteSetEvent($image_id, $user, true)); - log_debug("favourite", "Favourite set for $image_id", "Favourite added"); - } - else { - send_event(new FavoriteSetEvent($image_id, $user, false)); - log_debug("favourite", "Favourite removed for $image_id", "Favourite removed"); - } - } - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/$image_id")); - } - } + public function onDisplayingImage(DisplayingImageEvent $event) + { + $people = $this->list_persons_who_have_favorited($event->image); + if (count($people) > 0) { + $this->theme->display_people($people); + } + } - public function onUserPageBuilding(UserPageBuildingEvent $event) { - $i_favorites_count = Image::count_images(array("favorited_by={$event->display_user->name}")); - $i_days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1; - $h_favorites_rate = sprintf("%.1f", ($i_favorites_count / $i_days_old)); - $favorites_link = make_link("post/list/favorited_by={$event->display_user->name}/1"); - $event->add_stats("Images favorited: $i_favorites_count, $h_favorites_rate per day"); - } + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; + if ($event->page_matches("change_favorite") && !$user->is_anonymous() && $user->check_auth_token()) { + $image_id = int_escape($_POST['image_id']); + if ((($_POST['favorite_action'] == "set") || ($_POST['favorite_action'] == "unset")) && ($image_id > 0)) { + if ($_POST['favorite_action'] == "set") { + send_event(new FavoriteSetEvent($image_id, $user, true)); + log_debug("favourite", "Favourite set for $image_id", "Favourite added"); + } else { + send_event(new FavoriteSetEvent($image_id, $user, false)); + log_debug("favourite", "Favourite removed for $image_id", "Favourite removed"); + } + } + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/$image_id")); + } + } - public function onImageInfoSet(ImageInfoSetEvent $event) { - global $user; - if( - in_array('favorite_action', $_POST) && - (($_POST['favorite_action'] == "set") || ($_POST['favorite_action'] == "unset")) - ) { - send_event(new FavoriteSetEvent($event->image->id, $user, ($_POST['favorite_action'] == "set"))); - } - } + public function onUserPageBuilding(UserPageBuildingEvent $event) + { + $i_favorites_count = Image::count_images(["favorited_by={$event->display_user->name}"]); + $i_days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1; + $h_favorites_rate = sprintf("%.1f", ($i_favorites_count / $i_days_old)); + $favorites_link = make_link("post/list/favorited_by={$event->display_user->name}/1"); + $event->add_stats("Images favorited: $i_favorites_count, $h_favorites_rate per day"); + } - public function onFavoriteSet(FavoriteSetEvent $event) { - global $user; - $this->add_vote($event->image_id, $user->id, $event->do_set); - } + public function onImageInfoSet(ImageInfoSetEvent $event) + { + global $user; + if ( + $user->can(Permissions::EDIT_FAVOURITES) && + in_array('favorite_action', $_POST) && + (($_POST['favorite_action'] == "set") || ($_POST['favorite_action'] == "unset")) + ) { + send_event(new FavoriteSetEvent($event->image->id, $user, ($_POST['favorite_action'] == "set"))); + } + } - // FIXME: this should be handled by the foreign key. Check that it - // is, and then remove this - public function onImageDeletion(ImageDeletionEvent $event) { - global $database; - $database->execute("DELETE FROM user_favorites WHERE image_id=:image_id", array("image_id"=>$event->image->id)); - } + public function onFavoriteSet(FavoriteSetEvent $event) + { + global $user; + $this->add_vote($event->image_id, $user->id, $event->do_set); + } - public function onParseLinkTemplate(ParseLinkTemplateEvent $event) { - $event->replace('$favorites', $event->image->favorites); - } + // FIXME: this should be handled by the foreign key. Check that it + // is, and then remove this + public function onImageDeletion(ImageDeletionEvent $event) + { + global $database; + $database->execute("DELETE FROM user_favorites WHERE image_id=:image_id", ["image_id"=>$event->image->id]); + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; + public function onParseLinkTemplate(ParseLinkTemplateEvent $event) + { + $event->replace('$favorites', (string)$event->image->favorites); + } - $username = url_escape($user->name); - $event->add_link("My Favorites", make_link("post/list/favorited_by=$username/1"), 20); - } + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; - public function onSearchTermParse(SearchTermParseEvent $event) { - $matches = array(); - if(preg_match("/^favorites([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { - $cmp = ltrim($matches[1], ":") ?: "="; - $favorites = $matches[2]; - $event->add_querylet(new Querylet("images.id IN (SELECT id FROM images WHERE favorites $cmp $favorites)")); - } - else if(preg_match("/^favorited_by[=|:](.*)$/i", $event->term, $matches)) { - $user = User::by_name($matches[1]); - if(!is_null($user)) { - $user_id = $user->id; - } - else { - $user_id = -1; - } + $username = url_escape($user->name); + $event->add_link("My Favorites", make_link("post/list/favorited_by=$username/1"), 20); + } - $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM user_favorites WHERE user_id = $user_id)")); - } - else if(preg_match("/^favorited_by_userno[=|:](\d+)$/i", $event->term, $matches)) { - $user_id = int_escape($matches[1]); - $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM user_favorites WHERE user_id = $user_id)")); - } - } + public function onSearchTermParse(SearchTermParseEvent $event) + { + if (is_null($event->term)) { + return; + } + $matches = []; + if (preg_match("/^favorites([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { + $cmp = ltrim($matches[1], ":") ?: "="; + $favorites = $matches[2]; + $event->add_querylet(new Querylet("images.id IN (SELECT id FROM images WHERE favorites $cmp $favorites)")); + } elseif (preg_match("/^favorited_by[=|:](.*)$/i", $event->term, $matches)) { + $user_id = User::name_to_id($matches[1]); + $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM user_favorites WHERE user_id = $user_id)")); + } elseif (preg_match("/^favorited_by_userno[=|:](\d+)$/i", $event->term, $matches)) { + $user_id = int_escape($matches[1]); + $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM user_favorites WHERE user_id = $user_id)")); + } + } - private function install() { - global $database; - global $config; + public function onHelpPageBuilding(HelpPageBuildingEvent $event) + { + if ($event->key===HelpPages::SEARCH) { + $event->add_block(new Block("Favorites", (string)$this->theme->get_help_html())); + } + } - if($config->get_int("ext_favorites_version") < 1) { - $database->Execute("ALTER TABLE images ADD COLUMN favorites INTEGER NOT NULL DEFAULT 0"); - $database->Execute("CREATE INDEX images__favorites ON images(favorites)"); - $database->create_table("user_favorites", " + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent=="posts") { + $event->add_nav_link("posts_favorites", new Link("post/list/favorited_by={$user->name}/1"), "My Favorites"); + } + + if ($event->parent==="user") { + if ($user->can(Permissions::MANAGE_ADMINTOOLS)) { + $username = url_escape($user->name); + $event->add_nav_link("favorites", new Link("post/list/favorited_by=$username/1"), "My Favorites"); + } + } + } + + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) + { + global $user; + + if (!$user->is_anonymous()) { + $event->add_action("bulk_favorite", "Favorite"); + $event->add_action("bulk_unfavorite", "Un-Favorite"); + } + } + + public function onBulkAction(BulkActionEvent $event) + { + global $page, $user; + + switch ($event->action) { + case "bulk_favorite": + if (!$user->is_anonymous()) { + $total = 0; + foreach ($event->items as $image) { + send_event(new FavoriteSetEvent($image->id, $user, true)); + $total++; + } + $page->flash("Added $total items to favorites"); + } + break; + case "bulk_unfavorite": + if (!$user->is_anonymous()) { + $total = 0; + foreach ($event->items as $image) { + send_event(new FavoriteSetEvent($image->id, $user, false)); + $total++; + } + $page->flash("Removed $total items from favorites"); + } + break; + } + } + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $database; + + if ($this->get_version("ext_favorites_version") < 1) { + $database->Execute("ALTER TABLE images ADD COLUMN favorites INTEGER NOT NULL DEFAULT 0"); + $database->Execute("CREATE INDEX images__favorites ON images(favorites)"); + $database->create_table("user_favorites", " image_id INTEGER NOT NULL, user_id INTEGER NOT NULL, - created_at SCORE_DATETIME NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(image_id, user_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE "); - $database->execute("CREATE INDEX user_favorites_image_id_idx ON user_favorites(image_id)", array()); - $config->set_int("ext_favorites_version", 2); - } + $database->execute("CREATE INDEX user_favorites_image_id_idx ON user_favorites(image_id)", []); + $this->set_version("ext_favorites_version", 2); + } - if($config->get_int("ext_favorites_version") < 2) { - log_info("favorites", "Cleaning user favourites"); - $database->Execute("DELETE FROM user_favorites WHERE user_id NOT IN (SELECT id FROM users)"); - $database->Execute("DELETE FROM user_favorites WHERE image_id NOT IN (SELECT id FROM images)"); + if ($this->get_version("ext_favorites_version") < 2) { + log_info("favorites", "Cleaning user favourites"); + $database->Execute("DELETE FROM user_favorites WHERE user_id NOT IN (SELECT id FROM users)"); + $database->Execute("DELETE FROM user_favorites WHERE image_id NOT IN (SELECT id FROM images)"); - log_info("favorites", "Adding foreign keys to user favourites"); - $database->Execute("ALTER TABLE user_favorites ADD FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;"); - $database->Execute("ALTER TABLE user_favorites ADD FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE;"); - $config->set_int("ext_favorites_version", 2); - } - } + log_info("favorites", "Adding foreign keys to user favourites"); + $database->Execute("ALTER TABLE user_favorites ADD FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;"); + $database->Execute("ALTER TABLE user_favorites ADD FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE;"); + $this->set_version("ext_favorites_version", 2); + } + } - /** - * @param int $image_id - * @param int $user_id - * @param bool $do_set - */ - private function add_vote(/*int*/ $image_id, /*int*/ $user_id, /*bool*/ $do_set) { - global $database; - if ($do_set) { - $database->Execute( - "INSERT INTO user_favorites(image_id, user_id, created_at) VALUES(:image_id, :user_id, NOW())", - array("image_id"=>$image_id, "user_id"=>$user_id)); - } else { - $database->Execute( - "DELETE FROM user_favorites WHERE image_id = :image_id AND user_id = :user_id", - array("image_id"=>$image_id, "user_id"=>$user_id)); - } - $database->Execute( - "UPDATE images SET favorites=(SELECT COUNT(*) FROM user_favorites WHERE image_id=:image_id) WHERE id=:user_id", - array("image_id"=>$image_id, "user_id"=>$user_id)); - } + private function add_vote(int $image_id, int $user_id, bool $do_set) + { + global $database; + if ($do_set) { + if (!$database->get_row("select 1 from user_favorites where image_id=:image_id and user_id=:user_id", ["image_id"=>$image_id, "user_id"=>$user_id])) { + $database->Execute( + "INSERT INTO user_favorites(image_id, user_id, created_at) VALUES(:image_id, :user_id, NOW())", + ["image_id"=>$image_id, "user_id"=>$user_id] + ); + } + } else { + $database->Execute( + "DELETE FROM user_favorites WHERE image_id = :image_id AND user_id = :user_id", + ["image_id"=>$image_id, "user_id"=>$user_id] + ); + } + $database->Execute( + "UPDATE images SET favorites=(SELECT COUNT(*) FROM user_favorites WHERE image_id=:image_id) WHERE id=:user_id", + ["image_id"=>$image_id, "user_id"=>$user_id] + ); + } - /** - * @param Image $image - * @return string[] - */ - private function list_persons_who_have_favorited(Image $image) { - global $database; + /** + * #return string[] + */ + private function list_persons_who_have_favorited(Image $image): array + { + global $database; - return $database->get_col( - "SELECT name FROM users WHERE id IN (SELECT user_id FROM user_favorites WHERE image_id = :image_id) ORDER BY name", - array("image_id"=>$image->id)); - } + return $database->get_col( + "SELECT name FROM users WHERE id IN (SELECT user_id FROM user_favorites WHERE image_id = :image_id) ORDER BY name", + ["image_id"=>$image->id] + ); + } } - diff --git a/ext/favorites/test.php b/ext/favorites/test.php index cb6c09c7..7df3e0eb 100644 --- a/ext/favorites/test.php +++ b/ext/favorites/test.php @@ -1,29 +1,39 @@ -log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test"); +log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test"); - $this->get_page("post/view/$image_id"); - $this->assert_title("Image $image_id: test"); - $this->assert_no_text("Favorited By"); + # No favourites + $this->get_page("post/view/$image_id"); + $this->assert_title("Image $image_id: test"); + $this->assert_no_text("Favorited By"); - $this->markTestIncomplete(); + # Add a favourite + send_event(new FavoriteSetEvent($image_id, $user, true)); - $this->click("Favorite"); - $this->assert_text("Favorited By"); + # Favourite shown on page + $this->get_page("post/view/$image_id"); + $this->assert_title("Image $image_id: test"); + $this->assert_text("Favorited By"); - $this->get_page("post/list/favorited_by=test/1"); - $this->assert_title("Image $image_id: test"); - $this->assert_text("Favorited By"); + # Favourite shown on index + $page = $this->get_page("post/list/favorited_by=test/1"); + $this->assertEquals(PageMode::REDIRECT, $page->mode); - $this->get_page("user/test"); - $this->assert_text("Images favorited: 1"); - $this->click("Images favorited"); - $this->assert_title("Image $image_id: test"); + # Favourite shown on user page + $this->get_page("user/test"); + $this->assert_text("Images favorited: 1"); - $this->click("Un-Favorite"); - $this->assert_no_text("Favorited By"); - } + # Delete a favourite + send_event(new FavoriteSetEvent($image_id, $user, false)); + + # No favourites + $this->get_page("post/view/$image_id"); + $this->assert_title("Image $image_id: test"); + $this->assert_no_text("Favorited By"); + } } - diff --git a/ext/favorites/theme.php b/ext/favorites/theme.php index ae502ab2..b2c3ca23 100644 --- a/ext/favorites/theme.php +++ b/ext/favorites/theme.php @@ -1,36 +1,57 @@ -id); - $name = $is_favorited ? "unset" : "set"; - $label = $is_favorited ? "Un-Favorite" : "Favorite"; - $html = " - ".make_form(make_link("change_favorite"))." - - - - - "; +class FavoritesTheme extends Themelet +{ + public function get_voter_html(Image $image, $is_favorited) + { + $name = $is_favorited ? "unset" : "set"; + $label = $is_favorited ? "Un-Favorite" : "Favorite"; + return SHM_SIMPLE_FORM( + "change_favorite", + INPUT(["type"=>"hidden", "name"=>"image_id", "value"=>$image->id]), + INPUT(["type"=>"hidden", "name"=>"favorite_action", "value"=>$name]), + INPUT(["type"=>"submit", "value"=>$label]), + ); + } - return $html; - } + public function display_people($username_array) + { + global $page; - public function display_people($username_array) { - global $page; + $i_favorites = count($username_array); + $html = "$i_favorites people:"; - $i_favorites = count($username_array); - $html = "$i_favorites people:"; + reset($username_array); // rewind to first element in array. - reset($username_array); // rewind to first element in array. - - foreach($username_array as $row) { - $username = html_escape($row); - $html .= "
    $username"; - } + foreach ($username_array as $row) { + $username = html_escape($row); + $html .= "
    $username"; + } - $page->add_block(new Block("Favorited By", $html, "left", 25)); - } + $page->add_block(new Block("Favorited By", $html, "left", 25)); + } + + public function get_help_html() + { + return '

    Search for images that have been favorited a certain number of times, or favorited by a particular individual.

    +
    +
    favorites=1
    +

    Returns images that have been favorited once.

    +
    +
    +
    favorites>0
    +

    Returns images that have been favorited 1 or more times

    +
    +

    Can use <, <=, >, >=, or =.

    +
    +
    favorited_by:username
    +

    Returns images that have been favorited by "username".

    +
    +
    +
    favorited_by_userno:123
    +

    Returns images that have been favorited by user 123.

    +
    + '; + } } - - diff --git a/ext/featured/info.php b/ext/featured/info.php new file mode 100644 index 00000000..23e42a90 --- /dev/null +++ b/ext/featured/info.php @@ -0,0 +1,25 @@ +Viewing a featured image +
    Visit /featured_image/view +

    Downloading a featured image +
    Link to /featured_image/download. This will give +the raw data for an image (no HTML). This is useful so that you +can set your desktop wallpaper to be the download URL, refreshed +every couple of hours."; +} diff --git a/ext/featured/main.php b/ext/featured/main.php index 85e2459e..5979ba21 100644 --- a/ext/featured/main.php +++ b/ext/featured/main.php @@ -1,89 +1,77 @@ - - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Bring a specific image to the users' attentions - * Documentation: - * Once enabled, a new "feature this" button will appear next - * to the other image control buttons (delete, rotate, etc). - * Clicking it will set the image as the site's current feature, - * which will be shown in the side bar of the post list. - *

    Viewing a featured image - *
    Visit /featured_image/view - *

    Downloading a featured image - *
    Link to /featured_image/download. This will give - * the raw data for an image (no HTML). This is useful so that you - * can set your desktop wallpaper to be the download URL, refreshed - * every couple of hours. - */ +set_default_int('featured_id', 0); - } +class Featured extends Extension +{ + /** @var FeaturedTheme */ + protected $theme; - public function onPageRequest(PageRequestEvent $event) { - global $config, $page, $user; - if($event->page_matches("featured_image")) { - if($event->get_arg(0) == "set" && $user->check_auth_token()) { - if($user->can("edit_feature") && isset($_POST['image_id'])) { - $id = int_escape($_POST['image_id']); - if($id > 0) { - $config->set_int("featured_id", $id); - log_info("featured", "Featured image set to $id", "Featured image set"); - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/$id")); - } - } - } - if($event->get_arg(0) == "download") { - $image = Image::by_id($config->get_int("featured_id")); - if(!is_null($image)) { - $page->set_mode("data"); - $page->set_type($image->get_mime_type()); - $page->set_data(file_get_contents($image->get_image_filename())); - } - } - if($event->get_arg(0) == "view") { - $image = Image::by_id($config->get_int("featured_id")); - if(!is_null($image)) { - send_event(new DisplayingImageEvent($image, $page)); - } - } - } - } + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_int('featured_id', 0); + } - public function onPostListBuilding(PostListBuildingEvent $event) { - global $config, $database, $page, $user; - $fid = $config->get_int("featured_id"); - if($fid > 0) { - $image = $database->cache->get("featured_image_object:$fid"); - if($image === false) { - $image = Image::by_id($fid); - if($image) { // make sure the object is fully populated before saving - $image->get_tag_array(); - } - $database->cache->set("featured_image_object:$fid", $image, 600); - } - if(!is_null($image)) { - if(ext_is_live("Ratings")) { - if(strpos(Ratings::get_user_privs($user), $image->rating) === FALSE) { - return; - } - } - $this->theme->display_featured($page, $image); - } - } - } + public function onPageRequest(PageRequestEvent $event) + { + global $config, $page, $user; + if ($event->page_matches("featured_image")) { + if ($event->get_arg(0) == "set" && $user->check_auth_token()) { + if ($user->can(Permissions::EDIT_FEATURE) && isset($_POST['image_id'])) { + $id = int_escape($_POST['image_id']); + if ($id > 0) { + $config->set_int("featured_id", $id); + log_info("featured", "Featured image set to $id", "Featured image set"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/$id")); + } + } + } + if ($event->get_arg(0) == "download") { + $image = Image::by_id($config->get_int("featured_id")); + if (!is_null($image)) { + $page->set_mode(PageMode::DATA); + $page->set_type($image->get_mime_type()); + $page->set_data(file_get_contents($image->get_image_filename())); + } + } + if ($event->get_arg(0) == "view") { + $image = Image::by_id($config->get_int("featured_id")); + if (!is_null($image)) { + send_event(new DisplayingImageEvent($image)); + } + } + } + } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { - global $user; - if($user->can("edit_feature")) { - $event->add_part($this->theme->get_buttons_html($event->image->id)); - } - } + public function onPostListBuilding(PostListBuildingEvent $event) + { + global $cache, $config, $page, $user; + $fid = $config->get_int("featured_id"); + if ($fid > 0) { + $image = $cache->get("featured_image_object:$fid"); + if ($image === false) { + $image = Image::by_id($fid); + if ($image) { // make sure the object is fully populated before saving + $image->get_tag_array(); + } + $cache->set("featured_image_object:$fid", $image, 600); + } + if (!is_null($image)) { + if (Extension::is_enabled(RatingsInfo::KEY)) { + if (!in_array($image->rating, Ratings::get_user_class_privs($user))) { + return; + } + } + $this->theme->display_featured($page, $image); + } + } + } + + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::EDIT_FEATURE)) { + $event->add_part($this->theme->get_buttons_html($event->image->id)); + } + } } - diff --git a/ext/featured/test.php b/ext/featured/test.php index 74aa5678..bd3a7c1f 100644 --- a/ext/featured/test.php +++ b/ext/featured/test.php @@ -1,35 +1,36 @@ -log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); +log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - $this->log_in_as_admin(); - $this->get_page("post/view/$image_id"); - $this->assert_title("Image $image_id: pbx"); + # FIXME: test that regular users can't feature things - $this->markTestIncomplete(); + // Admin can feature things + // FIXME: use Event rather than modifying database + // $this->log_in_as_admin(); + // send_event(new SetFeaturedEvent($image_id)); + $config->set_int("featured_id", $image_id); - $this->click("Feature This"); - $this->get_page("post/list"); - $this->assert_text("Featured Image"); + $this->get_page("post/list"); + $this->assert_text("Featured Image"); - # FIXME: test changing from one feature to another + # FIXME: test changing from one feature to another - $this->get_page("featured_image/download"); - $this->assert_response(200); + $page = $this->get_page("featured_image/download"); + $this->assertEquals(200, $page->code); - $this->get_page("featured_image/view"); - $this->assert_response(200); + $page = $this->get_page("featured_image/view"); + $this->assertEquals(200, $page->code); - $this->delete_image($image_id); - $this->log_out(); - - # after deletion, there should be no feature - $this->get_page("post/list"); - $this->assert_no_text("Featured Image"); - } + // after deletion, there should be no feature + $this->delete_image($image_id); + $this->get_page("post/list"); + $this->assert_no_text("Featured Image"); + } } - diff --git a/ext/featured/theme.php b/ext/featured/theme.php index 9fc6a74f..ee797c1e 100644 --- a/ext/featured/theme.php +++ b/ext/featured/theme.php @@ -1,48 +1,34 @@ -add_block(new Block("Featured Image", $this->build_featured_html($image), "left", 3)); - } +class FeaturedTheme extends Themelet +{ + public function display_featured(Page $page, Image $image): void + { + $page->add_block(new Block("Featured Image", $this->build_featured_html($image), "left", 3)); + } - /** - * @param int $image_id - * @return string - */ - public function get_buttons_html(/*int*/ $image_id) { - global $user; - return " - ".make_form(make_link("featured_image/set"))." - ".$user->get_auth_html()." - - - - "; - } + public function get_buttons_html(int $image_id): string + { + return (string)SHM_SIMPLE_FORM( + "featured_image/set", + INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image_id]), + INPUT(["type"=>'submit', "value"=>'Feature This']), + ); + } - /** - * @param Image $image - * @param null|string $query - * @return string - */ - public function build_featured_html(Image $image, $query=null) { - $i_id = int_escape($image->id); - $h_view_link = make_link("post/view/$i_id", $query); - $h_thumb_link = $image->get_thumb_link(); - $h_tip = html_escape($image->get_tooltip()); - $tsize = get_thumbnail_size($image->width, $image->height); + public function build_featured_html(Image $image, ?string $query=null): string + { + $i_id = $image->id; + $h_view_link = make_link("post/view/$i_id", $query); + $h_thumb_link = $image->get_thumb_link(); + $h_tip = html_escape($image->get_tooltip()); + $tsize = get_thumbnail_size($image->width, $image->height); - return " + return " {$h_tip} "; - } + } } - diff --git a/ext/forum/info.php b/ext/forum/info.php new file mode 100644 index 00000000..d5772cb7 --- /dev/null +++ b/ext/forum/info.php @@ -0,0 +1,12 @@ +"mail@seinkraft.info","Alpha"=>"alpha@furries.com.ar"]; + public $license = self::LICENSE_GPLV2; + public $description = "Rough forum extension"; +} diff --git a/ext/forum/main.php b/ext/forum/main.php index 7432225c..95945df4 100644 --- a/ext/forum/main.php +++ b/ext/forum/main.php @@ -1,12 +1,4 @@ - - * Alpha - * License: GPLv2 - * Description: Rough forum extension - * Documentation: - */ +get_int("forum_version") < 1) { - $database->create_table("forum_threads", " + // shortcut to latest + + if ($config->get_int("forum_version") < 1) { + $database->create_table("forum_threads", " id SCORE_AIPK, sticky SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N, title VARCHAR(255) NOT NULL, user_id INTEGER NOT NULL, - date SCORE_DATETIME NOT NULL, - uptodate SCORE_DATETIME NOT NULL, + date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + uptodate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE RESTRICT "); - $database->execute("CREATE INDEX forum_threads_date_idx ON forum_threads(date)", array()); - - $database->create_table("forum_posts", " + $database->execute("CREATE INDEX forum_threads_date_idx ON forum_threads(date)", []); + + $database->create_table("forum_posts", " id SCORE_AIPK, thread_id INTEGER NOT NULL, user_id INTEGER NOT NULL, - date SCORE_DATETIME NOT NULL, + date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, message TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE RESTRICT, FOREIGN KEY (thread_id) REFERENCES forum_threads (id) ON UPDATE CASCADE ON DELETE CASCADE "); - $database->execute("CREATE INDEX forum_posts_date_idx ON forum_posts(date)", array()); + $database->execute("CREATE INDEX forum_posts_date_idx ON forum_posts(date)", []); - $config->set_int("forum_version", 2); - $config->set_int("forumTitleSubString", 25); - $config->set_int("forumThreadsPerPage", 15); - $config->set_int("forumPostsPerPage", 15); + $config->set_int("forum_version", 2); + $config->set_int("forumTitleSubString", 25); + $config->set_int("forumThreadsPerPage", 15); + $config->set_int("forumPostsPerPage", 15); - $config->set_int("forumMaxCharsPerPost", 512); + $config->set_int("forumMaxCharsPerPost", 512); + + log_info("forum", "extension installed"); + } + if ($config->get_int("forum_version") < 2) { + $database->execute("ALTER TABLE forum_threads ADD FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE RESTRICT"); + $database->execute("ALTER TABLE forum_posts ADD FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE RESTRICT"); + $config->set_int("forum_version", 2); + } + } + + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Forum"); + $sb->add_int_option("forumTitleSubString", "Title max long: "); + $sb->add_int_option("forumThreadsPerPage", "
    Threads per page: "); + $sb->add_int_option("forumPostsPerPage", "
    Posts per page: "); + + $sb->add_int_option("forumMaxCharsPerPost", "
    Max chars per post: "); + $event->panel->add_block($sb); + } + + public function onUserPageBuilding(UserPageBuildingEvent $event) + { + global $database; + + $threads_count = $database->get_one("SELECT COUNT(*) FROM forum_threads WHERE user_id=:user_id", ['user_id'=>$event->display_user->id]); + $posts_count = $database->get_one("SELECT COUNT(*) FROM forum_posts WHERE user_id=:user_id", ['user_id'=>$event->display_user->id]); - log_info("forum", "extension installed"); - } - if ($config->get_int("forum_version") < 2) { - $database->execute("ALTER TABLE forum_threads ADD FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE RESTRICT"); - $database->execute("ALTER TABLE forum_posts ADD FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE RESTRICT"); - $config->set_int("forum_version", 2); - } - } - - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Forum"); - $sb->add_int_option("forumTitleSubString", "Title max long: "); - $sb->add_int_option("forumThreadsPerPage", "
    Threads per page: "); - $sb->add_int_option("forumPostsPerPage", "
    Posts per page: "); - - $sb->add_int_option("forumMaxCharsPerPost", "
    Max chars per post: "); - $event->panel->add_block($sb); - } - - public function onUserPageBuilding(UserPageBuildingEvent $event) { - global $database; - - $threads_count = $database->get_one("SELECT COUNT(*) FROM forum_threads WHERE user_id=?", array($event->display_user->id)); - $posts_count = $database->get_one("SELECT COUNT(*) FROM forum_posts WHERE user_id=?", array($event->display_user->id)); - $days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1; - + $threads_rate = sprintf("%.1f", ($threads_count / $days_old)); - $posts_rate = sprintf("%.1f", ($posts_count / $days_old)); - - $event->add_stats("Forum threads: $threads_count, $threads_rate per day"); + $posts_rate = sprintf("%.1f", ($posts_count / $days_old)); + + $event->add_stats("Forum threads: $threads_count, $threads_rate per day"); $event->add_stats("Forum posts: $posts_count, $posts_rate per day"); - } + } - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; - if($event->page_matches("forum")) { - switch($event->get_arg(0)) { - case "index": - $this->show_last_threads($page, $event, $user->is_admin()); - if(!$user->is_anonymous()) $this->theme->display_new_thread_composer($page); - break; - case "view": - $threadID = int_escape($event->get_arg(1)); - $pageNumber = int_escape($event->get_arg(2)); - list($errors) = $this->sanity_check_viewed_thread($threadID); + if ($event->page_matches("forum")) { + switch ($event->get_arg(0)) { + case "index": + $this->show_last_threads($page, $event, $user->can(Permissions::FORUM_ADMIN)); + if (!$user->is_anonymous()) { + $this->theme->display_new_thread_composer($page); + } + break; + case "view": + $threadID = int_escape($event->get_arg(1)); + // $pageNumber = int_escape($event->get_arg(2)); + list($errors) = $this->sanity_check_viewed_thread($threadID); - if($errors!=null) - { - $this->theme->display_error(500, "Error", $errors); - break; - } + if ($errors!=null) { + $this->theme->display_error(500, "Error", $errors); + break; + } - $this->show_posts($event, $user->is_admin()); - if($user->is_admin()) $this->theme->add_actions_block($page, $threadID); - if(!$user->is_anonymous()) $this->theme->display_new_post_composer($page, $threadID); - break; - case "new": - global $page; - $this->theme->display_new_thread_composer($page); - break; - case "create": - $redirectTo = "forum/index"; - if (!$user->is_anonymous()) - { - list($errors) = $this->sanity_check_new_thread(); + $this->show_posts($event, $user->can(Permissions::FORUM_ADMIN)); + if ($user->can(Permissions::FORUM_ADMIN)) { + $this->theme->add_actions_block($page, $threadID); + } + if (!$user->is_anonymous()) { + $this->theme->display_new_post_composer($page, $threadID); + } + break; + case "new": + global $page; + $this->theme->display_new_thread_composer($page); + break; + case "create": + $redirectTo = "forum/index"; + if (!$user->is_anonymous()) { + list($errors) = $this->sanity_check_new_thread(); - if($errors!=null) - { - $this->theme->display_error(500, "Error", $errors); - break; - } + if ($errors!=null) { + $this->theme->display_error(500, "Error", $errors); + break; + } - $newThreadID = $this->save_new_thread($user); - $this->save_new_post($newThreadID, $user); - $redirectTo = "forum/view/".$newThreadID."/1"; - } + $newThreadID = $this->save_new_thread($user); + $this->save_new_post($newThreadID, $user); + $redirectTo = "forum/view/".$newThreadID."/1"; + } - $page->set_mode("redirect"); - $page->set_redirect(make_link($redirectTo)); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link($redirectTo)); - break; - case "delete": - $threadID = int_escape($event->get_arg(1)); - $postID = int_escape($event->get_arg(2)); + break; + case "delete": + $threadID = int_escape($event->get_arg(1)); + $postID = int_escape($event->get_arg(2)); - if ($user->is_admin()) {$this->delete_post($postID);} + if ($user->can(Permissions::FORUM_ADMIN)) { + $this->delete_post($postID); + } - $page->set_mode("redirect"); - $page->set_redirect(make_link("forum/view/".$threadID)); - break; - case "nuke": - $threadID = int_escape($event->get_arg(1)); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("forum/view/".$threadID)); + break; + case "nuke": + $threadID = int_escape($event->get_arg(1)); - if ($user->is_admin()) - $this->delete_thread($threadID); + if ($user->can(Permissions::FORUM_ADMIN)) { + $this->delete_thread($threadID); + } - $page->set_mode("redirect"); - $page->set_redirect(make_link("forum/index")); - break; - case "answer": - $threadID = int_escape($_POST["threadID"]); - $total_pages = $this->get_total_pages_for_thread($threadID); - if (!$user->is_anonymous()) - { - list($errors) = $this->sanity_check_new_post(); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("forum/index")); + break; + case "answer": + $threadID = int_escape($_POST["threadID"]); + $total_pages = $this->get_total_pages_for_thread($threadID); + if (!$user->is_anonymous()) { + list($errors) = $this->sanity_check_new_post(); - if ($errors!=null) - { - $this->theme->display_error(500, "Error", $errors); - break; - } - $this->save_new_post($threadID, $user); - } - $page->set_mode("redirect"); - $page->set_redirect(make_link("forum/view/".$threadID."/".$total_pages)); - break; - default: - $page->set_mode("redirect"); - $page->set_redirect(make_link("forum/index")); - //$this->theme->display_error(400, "Invalid action", "You should check forum/index."); - break; - } - } - } + if ($errors!=null) { + $this->theme->display_error(500, "Error", $errors); + break; + } + $this->save_new_post($threadID, $user); + } + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("forum/view/".$threadID."/".$total_pages)); + break; + default: + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("forum/index")); + //$this->theme->display_error(400, "Invalid action", "You should check forum/index."); + break; + } + } + } - /** - * @param int $threadID - */ - private function get_total_pages_for_thread($threadID) - { - global $database, $config; - $result = $database->get_row("SELECT COUNT(1) AS count FROM forum_posts WHERE thread_id = ?", array($threadID)); + private function get_total_pages_for_thread(int $threadID) + { + global $database, $config; + $result = $database->get_row("SELECT COUNT(1) AS count FROM forum_posts WHERE thread_id = :thread_id", ['thread_id'=>$threadID]); - return ceil($result["count"] / $config->get_int("forumPostsPerPage")); - } + return ceil($result["count"] / $config->get_int("forumPostsPerPage")); + } - private function sanity_check_new_thread() - { - $errors = null; - if (!array_key_exists("title", $_POST)) - { - $errors .= "

    No title supplied.
    "; - } - else if (strlen($_POST["title"]) == 0) - { - $errors .= "
    You cannot have an empty title.
    "; - } - else if (strlen(html_escape($_POST["title"])) > 255) - { - $errors .= "
    Your title is too long.
    "; - } + private function sanity_check_new_thread() + { + $errors = null; + if (!array_key_exists("title", $_POST)) { + $errors .= "
    No title supplied.
    "; + } elseif (strlen($_POST["title"]) == 0) { + $errors .= "
    You cannot have an empty title.
    "; + } elseif (strlen(html_escape($_POST["title"])) > 255) { + $errors .= "
    Your title is too long.
    "; + } - if (!array_key_exists("message", $_POST)) - { - $errors .= "
    No message supplied.
    "; - } - else if (strlen($_POST["message"]) == 0) - { - $errors .= "
    You cannot have an empty message.
    "; - } + if (!array_key_exists("message", $_POST)) { + $errors .= "
    No message supplied.
    "; + } elseif (strlen($_POST["message"]) == 0) { + $errors .= "
    You cannot have an empty message.
    "; + } - return array($errors); - } + return [$errors]; + } - private function sanity_check_new_post() - { - $errors = null; - if (!array_key_exists("threadID", $_POST)) - { - $errors = "
    No thread ID supplied.
    "; - } - else if (strlen($_POST["threadID"]) == 0) - { - $errors = "
    No thread ID supplied.
    "; - } - else if (is_numeric($_POST["threadID"])) + private function sanity_check_new_post() + { + $errors = null; + if (!array_key_exists("threadID", $_POST)) { + $errors = "
    No thread ID supplied.
    "; + } elseif (strlen($_POST["threadID"]) == 0) { + $errors = "
    No thread ID supplied.
    "; + } elseif (is_numeric($_POST["threadID"])) { + if (!array_key_exists("message", $_POST)) { + $errors .= "
    No message supplied.
    "; + } elseif (strlen($_POST["message"]) == 0) { + $errors .= "
    You cannot have an empty message.
    "; + } + } - if (!array_key_exists("message", $_POST)) - { - $errors .= "
    No message supplied.
    "; - } - else if (strlen($_POST["message"]) == 0) - { - $errors .= "
    You cannot have an empty message.
    "; - } + return [$errors]; + } - return array($errors); - } + private function sanity_check_viewed_thread(int $threadID) + { + $errors = null; + if (!$this->threadExists($threadID)) { + $errors = "
    Inexistent thread.
    "; + } + return [$errors]; + } - /** - * @param int $threadID - */ - private function sanity_check_viewed_thread($threadID) - { - $errors = null; - if (!$this->threadExists($threadID)) - { - $errors = "
    Inexistent thread.
    "; - } - return array($errors); - } + private function get_thread_title(int $threadID) + { + global $database; + $result = $database->get_row("SELECT t.title FROM forum_threads AS t WHERE t.id = :id ", ['id'=>$threadID]); + return $result["title"]; + } - /** - * @param int $threadID - */ - private function get_thread_title($threadID) - { - global $database; - $result = $database->get_row("SELECT t.title FROM forum_threads AS t WHERE t.id = ? ", array($threadID)); - return $result["title"]; - } - - private function show_last_threads(Page $page, PageRequestEvent $event, $showAdminOptions = false) - { - global $config, $database; - $pageNumber = $event->get_arg(1); - $threadsPerPage = $config->get_int('forumThreadsPerPage', 15); - $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM forum_threads") / $threadsPerPage); + private function show_last_threads(Page $page, PageRequestEvent $event, $showAdminOptions = false) + { + global $config, $database; + $threadsPerPage = $config->get_int('forumThreadsPerPage', 15); + $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM forum_threads") / $threadsPerPage); - if(is_null($pageNumber) || !is_numeric($pageNumber)) - $pageNumber = 0; - else if ($pageNumber <= 0) - $pageNumber = 0; - else if ($pageNumber >= $totalPages) - $pageNumber = $totalPages - 1; - else - $pageNumber--; + if ($event->count_args() >= 2) { + $pageNumber = $event->get_arg(1); + if (!is_numeric($pageNumber)) { + $pageNumber = 0; + } elseif ($pageNumber <= 0) { + $pageNumber = 0; + } elseif ($pageNumber >= $totalPages) { + $pageNumber = $totalPages - 1; + } else { + $pageNumber--; + } + } else { + $pageNumber = 0; + } - $threads = $database->get_all( - "SELECT f.id, f.sticky, f.title, f.date, f.uptodate, u.name AS user_name, u.email AS user_email, u.class AS user_class, sum(1) - 1 AS response_count ". - "FROM forum_threads AS f ". - "INNER JOIN users AS u ". - "ON f.user_id = u.id ". - "INNER JOIN forum_posts AS p ". - "ON p.thread_id = f.id ". - "GROUP BY f.id, f.sticky, f.title, f.date, u.name, u.email, u.class ". - "ORDER BY f.sticky ASC, f.uptodate DESC LIMIT :limit OFFSET :offset" - , array("limit"=>$threadsPerPage, "offset"=>$pageNumber * $threadsPerPage) - ); + $threads = $database->get_all( + "SELECT f.id, f.sticky, f.title, f.date, f.uptodate, u.name AS user_name, u.email AS user_email, u.class AS user_class, sum(1) - 1 AS response_count ". + "FROM forum_threads AS f ". + "INNER JOIN users AS u ". + "ON f.user_id = u.id ". + "INNER JOIN forum_posts AS p ". + "ON p.thread_id = f.id ". + "GROUP BY f.id, f.sticky, f.title, f.date, u.name, u.email, u.class ". + "ORDER BY f.sticky ASC, f.uptodate DESC LIMIT :limit OFFSET :offset", + ["limit"=>$threadsPerPage, "offset"=>$pageNumber * $threadsPerPage] + ); - $this->theme->display_thread_list($page, $threads, $showAdminOptions, $pageNumber + 1, $totalPages); - } + $this->theme->display_thread_list($page, $threads, $showAdminOptions, $pageNumber + 1, $totalPages); + } - private function show_posts(PageRequestEvent $event, $showAdminOptions = false) - { - global $config, $database; - $threadID = $event->get_arg(1); - $pageNumber = $event->get_arg(2); - $postsPerPage = $config->get_int('forumPostsPerPage', 15); - $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM forum_posts WHERE thread_id = ?", array($threadID)) / $postsPerPage); - $threadTitle = $this->get_thread_title($threadID); + private function show_posts(PageRequestEvent $event, $showAdminOptions = false) + { + global $config, $database; + $threadID = int_escape($event->get_arg(1)); + $postsPerPage = $config->get_int('forumPostsPerPage', 15); + $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM forum_posts WHERE thread_id = :id", ['id'=>$threadID]) / $postsPerPage); + $threadTitle = $this->get_thread_title($threadID); - if(is_null($pageNumber) || !is_numeric($pageNumber)) - $pageNumber = 0; - else if ($pageNumber <= 0) - $pageNumber = 0; - else if ($pageNumber >= $totalPages) - $pageNumber = $totalPages - 1; - else - $pageNumber--; + if ($event->count_args() >= 3) { + $pageNumber = $event->get_arg(2); + if (!is_numeric($pageNumber)) { + $pageNumber = 0; + } elseif ($pageNumber <= 0) { + $pageNumber = 0; + } elseif ($pageNumber >= $totalPages) { + $pageNumber = $totalPages - 1; + } else { + $pageNumber--; + } + } else { + $pageNumber = 0; + } - $posts = $database->get_all( - "SELECT p.id, p.date, p.message, u.name as user_name, u.email AS user_email, u.class AS user_class ". - "FROM forum_posts AS p ". - "INNER JOIN users AS u ". - "ON p.user_id = u.id ". - "WHERE thread_id = :thread_id ". - "ORDER BY p.date ASC ". - "LIMIT :limit OFFSET :offset" - , array("thread_id"=>$threadID, "offset"=>$pageNumber * $postsPerPage, "limit"=>$postsPerPage) - ); - $this->theme->display_thread($posts, $showAdminOptions, $threadTitle, $threadID, $pageNumber + 1, $totalPages); - } + $posts = $database->get_all( + "SELECT p.id, p.date, p.message, u.name as user_name, u.email AS user_email, u.class AS user_class ". + "FROM forum_posts AS p ". + "INNER JOIN users AS u ". + "ON p.user_id = u.id ". + "WHERE thread_id = :thread_id ". + "ORDER BY p.date ASC ". + "LIMIT :limit OFFSET :offset", + ["thread_id"=>$threadID, "offset"=>$pageNumber * $postsPerPage, "limit"=>$postsPerPage] + ); + $this->theme->display_thread($posts, $showAdminOptions, $threadTitle, $threadID, $pageNumber + 1, $totalPages); + } - private function save_new_thread(User $user) - { - $title = html_escape($_POST["title"]); - $sticky = !empty($_POST["sticky"]) ? html_escape($_POST["sticky"]) : "N"; + private function save_new_thread(User $user) + { + $title = html_escape($_POST["title"]); + $sticky = !empty($_POST["sticky"]) ? html_escape($_POST["sticky"]) : "N"; - if($sticky == ""){ - $sticky = "N"; - } + if ($sticky == "") { + $sticky = "N"; + } - global $database; - $database->execute(" + global $database; + $database->execute( + " INSERT INTO forum_threads (title, sticky, user_id, date, uptodate) VALUES - (?, ?, ?, now(), now())", - array($title, $sticky, $user->id)); + (:title, :sticky, :user_id, now(), now())", + ['title'=>$title, 'sticky'=>$sticky, 'user_id'=>$user->id] + ); - $threadID = $database->get_last_insert_id("forum_threads_id_seq"); + $threadID = $database->get_last_insert_id("forum_threads_id_seq"); - log_info("forum", "Thread {$threadID} created by {$user->name}"); + log_info("forum", "Thread {$threadID} created by {$user->name}"); - return $threadID; - } + return $threadID; + } - /** - * @param int $threadID - */ - private function save_new_post($threadID, User $user) - { - global $config; - $userID = $user->id; - $message = html_escape($_POST["message"]); + private function save_new_post(int $threadID, User $user) + { + global $config; + $userID = $user->id; + $message = html_escape($_POST["message"]); - $max_characters = $config->get_int('forumMaxCharsPerPost'); - $message = substr($message, 0, $max_characters); + $max_characters = $config->get_int('forumMaxCharsPerPost'); + $message = substr($message, 0, $max_characters); - global $database; - $database->execute("INSERT INTO forum_posts - (thread_id, user_id, date, message) - VALUES - (?, ?, now(), ?)" - , array($threadID, $userID, $message)); + global $database; + $database->execute(" + INSERT INTO forum_posts (thread_id, user_id, date, message) + VALUES (:thread_id, :user_id, now(), :message) + ", ['thread_id'=>$threadID, 'user_id'=>$userID, 'message'=>$message]); - $postID = $database->get_last_insert_id("forum_posts_id_seq"); + $postID = $database->get_last_insert_id("forum_posts_id_seq"); - log_info("forum", "Post {$postID} created by {$user->name}"); + log_info("forum", "Post {$postID} created by {$user->name}"); - $database->execute("UPDATE forum_threads SET uptodate=now() WHERE id=?", array ($threadID)); - } + $database->execute("UPDATE forum_threads SET uptodate=now() WHERE id=:id", ['id'=>$threadID]); + } - /** - * @param int $threadID - * @param int $pageNumber - */ - private function retrieve_posts($threadID, $pageNumber) - { - global $database, $config; - $postsPerPage = $config->get_int('forumPostsPerPage', 15); + private function retrieve_posts(int $threadID, int $pageNumber) + { + global $database, $config; + $postsPerPage = $config->get_int('forumPostsPerPage', 15); - return $database->get_all( - "SELECT p.id, p.date, p.message, u.name as user_name, u.email AS user_email, u.class AS user_class ". - "FROM forum_posts AS p ". - "INNER JOIN users AS u ". - "ON p.user_id = u.id ". - "WHERE thread_id = :thread_id ". - "ORDER BY p.date ASC ". - "LIMIT :limit OFFSET :offset " - , array("thread_id"=>$threadID, "offset"=>($pageNumber - 1) * $postsPerPage, "limit"=>$postsPerPage)); - } + return $database->get_all( + "SELECT p.id, p.date, p.message, u.name as user_name, u.email AS user_email, u.class AS user_class ". + "FROM forum_posts AS p ". + "INNER JOIN users AS u ". + "ON p.user_id = u.id ". + "WHERE thread_id = :thread_id ". + "ORDER BY p.date ASC ". + "LIMIT :limit OFFSET :offset ", + ["thread_id"=>$threadID, "offset"=>($pageNumber - 1) * $postsPerPage, "limit"=>$postsPerPage] + ); + } - /** - * @param int $threadID - */ - private function delete_thread($threadID) - { - global $database; - $database->execute("DELETE FROM forum_threads WHERE id = ?", array($threadID)); - $database->execute("DELETE FROM forum_posts WHERE thread_id = ?", array($threadID)); - } + private function delete_thread(int $threadID) + { + global $database; + $database->execute("DELETE FROM forum_threads WHERE id = :id", ['id'=>$threadID]); + $database->execute("DELETE FROM forum_posts WHERE thread_id = :thread_id", ['thread_id'=>$threadID]); + } - /** - * @param int $postID - */ - private function delete_post($postID) - { - global $database; - $database->execute("DELETE FROM forum_posts WHERE id = ?", array($postID)); - } + private function delete_post(int $postID) + { + global $database; + $database->execute("DELETE FROM forum_posts WHERE id = :id", ['id'=>$postID]); + } - /** - * @param int $threadID - */ - private function threadExists($threadID) - { - global $database; - $result=$database->get_one("SELECT EXISTS (SELECT * FROM forum_threads WHERE id= ?)", array($threadID)); - if ($result==1){ - return true; - }else{ - return false; - } - } + private function threadExists(int $threadID) + { + global $database; + $result=$database->get_one("SELECT EXISTS (SELECT * FROM forum_threads WHERE id=:id)", ['id'=>$threadID]); + return $result == 1; + } } diff --git a/ext/forum/theme.php b/ext/forum/theme.php index 74d7c5df..4e7a9e25 100644 --- a/ext/forum/theme.php +++ b/ext/forum/theme.php @@ -1,17 +1,18 @@ -make_thread_list($threads, $showAdminOptions); + } - $page->set_title(html_escape("Forum")); - $page->set_heading(html_escape("Forum")); + $page->set_title(html_escape("Forum")); + $page->set_heading(html_escape("Forum")); $page->add_block(new Block("Forum", $html, "main", 10)); - + $this->display_paginator($page, "forum/index", null, $pageNumber, $totalPages); } @@ -19,55 +20,57 @@ class ForumTheme extends Themelet { public function display_new_thread_composer(Page $page, $threadText = null, $threadTitle = null) { - global $config, $user; - $max_characters = $config->get_int('forumMaxCharsPerPost'); - $html = make_form(make_link("forum/create")); + global $config, $user; + $max_characters = $config->get_int('forumMaxCharsPerPost'); + $html = make_form(make_link("forum/create")); - if (!is_null($threadTitle)) - $threadTitle = html_escape($threadTitle); + if (!is_null($threadTitle)) { + $threadTitle = html_escape($threadTitle); + } - if(!is_null($threadText)) - $threadText = html_escape($threadText); - - $html .= " + if (!is_null($threadText)) { + $threadText = html_escape($threadText); + } + + $html .= " "; - if($user->is_admin()){ - $html .= ""; - } - $html .= " + if ($user->can(Permissions::FORUM_ADMIN)) { + $html .= ""; + } + $html .= "
    Title:
    Message:
    Max characters alowed: $max_characters.
    "; $blockTitle = "Write a new thread"; - $page->set_title(html_escape($blockTitle)); - $page->set_heading(html_escape($blockTitle)); + $page->set_title(html_escape($blockTitle)); + $page->set_heading(html_escape($blockTitle)); $page->add_block(new Block($blockTitle, $html, "main", 120)); } - - - + + + public function display_new_post_composer(Page $page, $threadID) { - global $config; - - $max_characters = $config->get_int('forumMaxCharsPerPost'); - - $html = make_form(make_link("forum/answer")); + global $config; + + $max_characters = $config->get_int('forumMaxCharsPerPost'); + + $html = make_form(make_link("forum/answer")); $html .= ''; - - $html .= " + + $html .= " "; - - $html .= " + + $html .= "
    Message:
    Max characters alowed: $max_characters.
    "; @@ -78,67 +81,66 @@ class ForumTheme extends Themelet { - public function display_thread($posts, $showAdminOptions, $threadTitle, $threadID, $pageNumber, $totalPages) + public function display_thread($posts, $showAdminOptions, $threadTitle, $threadID, $pageNumber, $totalPages) { - global $config, $page/*, $user*/; - - $posts_per_page = $config->get_int('forumPostsPerPage'); - + global $config, $page/*, $user*/; + + $posts_per_page = $config->get_int('forumPostsPerPage'); + $current_post = 0; $html = - "

    ". - "". - "". + "

    ". + "
    ". + "". "". "". - ""; - - foreach ($posts as $post) - { - $current_post++; + ""; + + foreach ($posts as $post) { + $current_post++; $message = $post["message"]; $tfe = new TextFormattingEvent($message); send_event($tfe); $message = $tfe->formatted; - - $message = str_replace('\n\r', '
    ', $message); + + $message = str_replace('\n\r', '
    ', $message); $message = str_replace('\r\n', '
    ', $message); $message = str_replace('\n', '
    ', $message); $message = str_replace('\r', '
    ', $message); - - $message = stripslashes($message); - - $user = "".$post["user_name"].""; + + $message = stripslashes($message); + + $userLink = "".$post["user_name"].""; $poster = User::by_name($post["user_name"]); - $gravatar = $poster->get_avatar_html(); + $gravatar = $poster->get_avatar_html(); - $rank = "{$post["user_class"]}"; - - $postID = $post['id']; - - //if($user->is_admin()){ - //$delete_link = "Delete"; - //} else { - //$delete_link = ""; - //} - - if($showAdminOptions){ - $delete_link = "Delete"; - }else{ - $delete_link = ""; - } + $rank = "{$post["user_class"]}"; + + $postID = $post['id']; + + //if($user->can(Permissions::FORUM_ADMIN)){ + //$delete_link = "Delete"; + //} else { + //$delete_link = ""; + //} + + if ($showAdminOptions) { + $delete_link = "Delete"; + } else { + $delete_link = ""; + } - $post_number = (($pageNumber-1)*$posts_per_page)+$current_post; + $post_number = (($pageNumber-1)*$posts_per_page)+$current_post; $html .= " - + "; - } - + $html .= "
    UserMessage
    ".$user."
    ".$rank."
    ".$gravatar."
    ".$userLink."
    ".$rank."
    ".$gravatar."
    #".$post_number."
    @@ -149,20 +151,18 @@ class ForumTheme extends Themelet {
    "; $this->display_paginator($page, "forum/view/".$threadID, null, $pageNumber, $totalPages); - $page->set_title(html_escape($threadTitle)); - $page->set_heading(html_escape($threadTitle)); + $page->set_title(html_escape($threadTitle)); + $page->set_heading(html_escape($threadTitle)); $page->add_block(new Block($threadTitle, $html, "main", 20)); - } - - + + public function add_actions_block(Page $page, $threadID) { @@ -179,11 +179,10 @@ class ForumTheme extends Themelet { "". "Title". "Author". - "Updated". + "Updated". "Responses"; - if($showAdminOptions) - { + if ($showAdminOptions) { $html .= "Actions"; } @@ -191,35 +190,34 @@ class ForumTheme extends Themelet { $current_post = 0; - foreach($threads as $thread) - { + foreach ($threads as $thread) { $oe = ($current_post++ % 2 == 0) ? "even" : "odd"; - - global $config; - $titleSubString = $config->get_int('forumTitleSubString'); - - if ($titleSubString < strlen($thread["title"])) - { - $title = substr($thread["title"], 0, $titleSubString); - $title = $title."..."; - } else { - $title = $thread["title"]; - } - - if($thread["sticky"] == "Y"){ - $sticky = "Sticky: "; - } else { - $sticky = ""; - } + + global $config; + $titleSubString = $config->get_int('forumTitleSubString'); + + if ($titleSubString < strlen($thread["title"])) { + $title = substr($thread["title"], 0, $titleSubString); + $title = $title."..."; + } else { + $title = $thread["title"]; + } + + if ($thread["sticky"] == "Y") { + $sticky = "Sticky: "; + } else { + $sticky = ""; + } $html .= "". ''.$sticky.''.$title."". - ''.$thread["user_name"]."". - "".autodate($thread["uptodate"])."". + ''.$thread["user_name"]."". + "".autodate($thread["uptodate"])."". "".$thread["response_count"].""; - if ($showAdminOptions) + if ($showAdminOptions) { $html .= 'Delete'; + } $html .= ""; } @@ -229,4 +227,3 @@ class ForumTheme extends Themelet { return $html; } } - diff --git a/ext/four_oh_four/info.php b/ext/four_oh_four/info.php new file mode 100644 index 00000000..4fba0195 --- /dev/null +++ b/ext/four_oh_four/info.php @@ -0,0 +1,15 @@ +mode == PageMode::PAGE && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) { + $h_pagename = html_escape(implode('/', $event->args)); + log_debug("four_oh_four", "Hit 404: $h_pagename"); + $page->set_code(404); + $page->set_title("404"); + $page->set_heading("404 - No Handler Found"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Explanation", "No handler could be found for the page '$h_pagename'")); + } + } + + private function count_main($blocks) + { + $n = 0; + foreach ($blocks as $block) { + if ($block->section == "main" && $block->is_content) { + $n++; + } // more hax. + } + return $n; + } + + public function get_priority(): int + { + return 99; + } +} diff --git a/ext/four_oh_four/test.php b/ext/four_oh_four/test.php new file mode 100644 index 00000000..fd9761b2 --- /dev/null +++ b/ext/four_oh_four/test.php @@ -0,0 +1,13 @@ +get_page('not/a/page'); + // most descriptive error first + $this->assert_text("No handler could be found for the page 'not/a/page'"); + $this->assert_title('404'); + $this->assert_response(404); + } +} diff --git a/ext/google_analytics/info.php b/ext/google_analytics/info.php new file mode 100644 index 00000000..fc7c47be --- /dev/null +++ b/ext/google_analytics/info.php @@ -0,0 +1,15 @@ +"support@drudexsoftware.com"]; + public $license = self::LICENSE_GPLV2; + public $description = "Integrates Google Analytics tracking"; + public $documentation = +"User has to enter their Google Analytics ID in the Board Config to use this extension."; +} diff --git a/ext/google_analytics/main.php b/ext/google_analytics/main.php index 3f0f8608..c38f8100 100644 --- a/ext/google_analytics/main.php +++ b/ext/google_analytics/main.php @@ -1,29 +1,24 @@ - - * Link: http://drudexsoftware.com - * License: GPLv2 - * Description: Integrates Google Analytics tracking - * Documentation: - * User has to enter their Google Analytics ID in the Board Config to use this extention. - */ -class google_analytics extends Extension { - # Add analytics to config - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Google Analytics"); - $sb->add_text_option("google_analytics_id", "Analytics ID: "); - $sb->add_label("
    (eg. UA-xxxxxxxx-x)"); - $event->panel->add_block($sb); - } - - # Load Analytics tracking code on page request - public function onPageRequest(PageRequestEvent $event) { - global $config, $page; - - $google_analytics_id = $config->get_string('google_analytics_id',''); - if (stristr($google_analytics_id, "UA-")) { - $page->add_html_header(""); - } } + } } - diff --git a/ext/handle_404/main.php b/ext/handle_404/main.php deleted file mode 100644 index a17e80f7..00000000 --- a/ext/handle_404/main.php +++ /dev/null @@ -1,53 +0,0 @@ - - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Visibility: admin - * Description: If Shimmie can't handle a request, check static files; if that fails, show a 404 - */ - -class Handle404 extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $config, $page; - // hax. - if($page->mode == "page" && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) { - $h_pagename = html_escape(implode('/', $event->args)); - $f_pagename = preg_replace("/[^a-z_\-\.]+/", "_", $h_pagename); - $theme_name = $config->get_string("theme", "default"); - - if(file_exists("themes/$theme_name/$f_pagename") || file_exists("lib/static/$f_pagename")) { - $filename = file_exists("themes/$theme_name/$f_pagename") ? - "themes/$theme_name/$f_pagename" : "lib/static/$f_pagename"; - - $page->add_http_header("Cache-control: public, max-age=600"); - $page->add_http_header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 600) . ' GMT'); - $page->set_mode("data"); - $page->set_data(file_get_contents($filename)); - if(endsWith($filename, ".ico")) $page->set_type("image/x-icon"); - if(endsWith($filename, ".png")) $page->set_type("image/png"); - if(endsWith($filename, ".txt")) $page->set_type("text/plain"); - } - else { - log_debug("handle_404", "Hit 404: $h_pagename"); - $page->set_code(404); - $page->set_title("404"); - $page->set_heading("404 - No Handler Found"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Explanation", "No handler could be found for the page '$h_pagename'")); - } - } - } - - private function count_main($blocks) { - $n = 0; - foreach($blocks as $block) { - if($block->section == "main" && $block->is_content) $n++; // more hax. - } - return $n; - } - - public function get_priority() {return 99;} -} - diff --git a/ext/handle_404/test.php b/ext/handle_404/test.php deleted file mode 100644 index 2d7c9f73..00000000 --- a/ext/handle_404/test.php +++ /dev/null @@ -1,14 +0,0 @@ -get_page('not/a/page'); - // most descriptive error first - $this->assert_text("No handler could be found for the page 'not/a/page'"); - $this->assert_title('404'); - $this->assert_response(404); - - $this->get_page('favicon.ico'); - $this->assert_response(200); - } -} - diff --git a/ext/handle_archive/info.php b/ext/handle_archive/info.php new file mode 100644 index 00000000..066cf1b0 --- /dev/null +++ b/ext/handle_archive/info.php @@ -0,0 +1,17 @@ +Any command line unzipper should work, some examples: +

    unzip: unzip -d \"%d\" \"%f\" +
    7-zip: 7zr x -o\"%d\" \"%f\""; +} diff --git a/ext/handle_archive/main.php b/ext/handle_archive/main.php index ad43c4ac..04937c90 100644 --- a/ext/handle_archive/main.php +++ b/ext/handle_archive/main.php @@ -1,56 +1,59 @@ - - * Description: Allow users to upload archives (zip, etc) - * Documentation: - * Note: requires exec() access and an external unzip command - *

    Any command line unzipper should work, some examples: - *

    unzip: unzip -d "%d" "%f" - *
    7-zip: 7zr x -o"%d" "%f" - */ +set_default_string('archive_extract_command', 'unzip -d "%d" "%f"'); - } +class ArchiveFileHandler extends DataHandlerExtension +{ + protected $SUPPORTED_EXT = ["zip"]; - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Archive Handler Options"); - $sb->add_text_option("archive_tmp_dir", "Temporary folder: "); - $sb->add_text_option("archive_extract_command", "
    Extraction command: "); - $sb->add_label("
    %f for archive, %d for temporary directory"); - $event->panel->add_block($sb); - } + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_string('archive_extract_command', 'unzip -d "%d" "%f"'); + } - public function onDataUpload(DataUploadEvent $event) { - if($this->supported_ext($event->type)) { - global $config; - $tmp = sys_get_temp_dir(); - $tmpdir = "$tmp/shimmie-archive-{$event->hash}"; - $cmd = $config->get_string('archive_extract_command'); - $cmd = str_replace('%f', $event->tmpname, $cmd); - $cmd = str_replace('%d', $tmpdir, $cmd); - exec($cmd); - $results = add_dir($tmpdir); - if(count($results) > 0) { - // Not all themes have the add_status() method, so need to check before calling. - if (method_exists($this->theme, "add_status")) { - $this->theme->add_status("Adding files", $results); - } - } - deltree($tmpdir); - $event->image_id = -2; // default -1 = upload wasn't handled - } - } + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Archive Handler Options"); + $sb->add_text_option("archive_tmp_dir", "Temporary folder: "); + $sb->add_text_option("archive_extract_command", "
    Extraction command: "); + $sb->add_label("
    %f for archive, %d for temporary directory"); + $event->panel->add_block($sb); + } - /** - * @param string $ext - * @return bool - */ - private function supported_ext($ext) { - $exts = array("zip"); - return in_array(strtolower($ext), $exts); - } + public function onDataUpload(DataUploadEvent $event) + { + if ($this->supported_ext($event->type)) { + global $config, $page; + $tmp = sys_get_temp_dir(); + $tmpdir = "$tmp/shimmie-archive-{$event->hash}"; + $cmd = $config->get_string('archive_extract_command'); + $cmd = str_replace('%f', $event->tmpname, $cmd); + $cmd = str_replace('%d', $tmpdir, $cmd); + exec($cmd); + $results = add_dir($tmpdir); + if (count($results) > 0) { + $page->flash("Adding files" . implode("\n", $results)); + } + deltree($tmpdir); + $event->image_id = -2; // default -1 = upload wasn't handled + } + } + + public function onDisplayingImage(DisplayingImageEvent $event) + { + } + + // we don't actually do anything, just accept one upload and spawn several + protected function media_check_properties(MediaCheckPropertiesEvent $event): void + { + } + + protected function check_contents(string $tmpname): bool + { + return false; + } + + protected function create_thumb(string $hash, string $type): bool + { + return false; + } } diff --git a/ext/handle_cbz/comic.js b/ext/handle_cbz/comic.js new file mode 100644 index 00000000..8d0f12ee --- /dev/null +++ b/ext/handle_cbz/comic.js @@ -0,0 +1,80 @@ +function Comic(root, comicURL) { + let self = this; + + this.root = document.getElementById(root); + this.comicPages = []; + this.comicPage = 0; + this.comicZip = null; + + this.setComic = function(zip) { + let self = this; + self.comicZip = zip; + + // Shimmie-specific; nullify existing back / forward + $("[rel='previous']").remove(); + $("[rel='next']").remove(); + + zip.forEach(function (relativePath, file){ + self.comicPages.push(relativePath); + }); + self.comicPages.sort(); + for(let i=0; i 0) { + self.setPage(self.comicPage-1); + document.getElementById("comicMain").scrollIntoView(); + } + }; + + this.next = function() { + if(self.comicPage < self.comicPages.length) { + self.setPage(self.comicPage+1); + document.getElementById("comicMain").scrollIntoView(); + } + }; + + this.onKeyUp = function(e) { + if ($(e.target).is('input,textarea')) { return; } + if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) { return; } + if (e.keyCode === 37) {self.prev();} + else if (e.keyCode === 39) {self.next();} + }; + + this.onPageChanged = function(e) { + self.setPage(parseInt(document.getElementById("comicPageList").value)); + }; + + JSZipUtils.getBinaryContent(comicURL, function(err, data) { + if(err) {throw err;} + JSZip.loadAsync(data).then(function (zip) { + self.setComic(zip); + }); + }); + + document.addEventListener("keyup", this.onKeyUp); + document.getElementById("comicNext").addEventListener("click", this.next); + document.getElementById("comicPrev").addEventListener("click", this.prev); + document.getElementById("comicPageList").addEventListener("change", this.onPageChanged); + document.getElementById("comicPageList").addEventListener("keyup", function(e) {e.stopPropagation();}); + + return this; +} diff --git a/ext/handle_cbz/info.php b/ext/handle_cbz/info.php new file mode 100644 index 00000000..25b72125 --- /dev/null +++ b/ext/handle_cbz/info.php @@ -0,0 +1,12 @@ +\r\nFunction IEBinaryToArray_ByteStr(Binary)\r\n IEBinaryToArray_ByteStr = CStr(Binary)\r\nEnd Function\r\nFunction IEBinaryToArray_ByteStr_Last(Binary)\r\n Dim lastIndex\r\n lastIndex = LenB(Binary)\r\n if lastIndex mod 2 Then\r\n IEBinaryToArray_ByteStr_Last = Chr( AscB( MidB( Binary, lastIndex, 1 ) ) )\r\n Else\r\n IEBinaryToArray_ByteStr_Last = \"\"\r\n End If\r\nEnd Function\r\n<\/script>\r\n"),e.JSZipUtils._getBinaryFromXHR=function(r){for(var n=r.responseBody,t={},e=0;e<256;e++)for(var i=0;i<256;i++)t[String.fromCharCode(e+(i<<8))]=String.fromCharCode(e)+String.fromCharCode(i);var o=IEBinaryToArray_ByteStr(n),a=IEBinaryToArray_ByteStr_Last(n);return o.replace(/[\s\S]/g,function(r){return t[r]})+a}},{}]},{},[1]); \ No newline at end of file diff --git a/ext/handle_cbz/jszip-utils.min.js b/ext/handle_cbz/jszip-utils.min.js new file mode 100644 index 00000000..aa910525 --- /dev/null +++ b/ext/handle_cbz/jszip-utils.min.js @@ -0,0 +1 @@ +!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.JSZipUtils=e():"undefined"!=typeof global?global.JSZipUtils=e():"undefined"!=typeof self&&(self.JSZipUtils=e())}(function(){return function o(i,f,u){function s(n,e){if(!f[n]){if(!i[n]){var t="function"==typeof require&&require;if(!e&&t)return t(n,!0);if(a)return a(n,!0);throw new Error("Cannot find module '"+n+"'")}var r=f[n]={exports:{}};i[n][0].call(r.exports,function(e){var t=i[n][1][e];return s(t||e)},r,r.exports,o,i,f,u)}return f[n].exports}for(var a="function"==typeof require&&require,e=0;e + +(c) 2009-2016 Stuart Knightley +Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/master/LICENSE.markdown. + +JSZip uses the library pako released under the MIT license : +https://github.com/nodeca/pako/blob/master/LICENSE +*/ + +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=t()}}(function(){return function s(a,o,h){function u(r,t){if(!o[r]){if(!a[r]){var e="function"==typeof require&&require;if(!t&&e)return e(r,!0);if(l)return l(r,!0);var i=new Error("Cannot find module '"+r+"'");throw i.code="MODULE_NOT_FOUND",i}var n=o[r]={exports:{}};a[r][0].call(n.exports,function(t){var e=a[r][1][t];return u(e||t)},n,n.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,t=0;t>2,s=(3&e)<<4|r>>4,a=1>6:64,o=2>4,r=(15&n)<<4|(s=p.indexOf(t.charAt(o++)))>>2,i=(3&s)<<6|(a=p.indexOf(t.charAt(o++))),l[h++]=e,64!==s&&(l[h++]=r),64!==a&&(l[h++]=i);return l}},{"./support":30,"./utils":32}],2:[function(t,e,r){"use strict";var i=t("./external"),n=t("./stream/DataWorker"),s=t("./stream/DataLengthProbe"),a=t("./stream/Crc32Probe");s=t("./stream/DataLengthProbe");function o(t,e,r,i,n){this.compressedSize=t,this.uncompressedSize=e,this.crc32=r,this.compression=i,this.compressedContent=n}o.prototype={getContentWorker:function(){var t=new n(i.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new s("data_length")),e=this;return t.on("end",function(){if(this.streamInfo.data_length!==e.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),t},getCompressedWorker:function(){return new n(i.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(t,e,r){return t.pipe(new a).pipe(new s("uncompressedSize")).pipe(e.compressWorker(r)).pipe(new s("compressedSize")).withStreamInfo("compression",e)},e.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(t,e,r){"use strict";var i=t("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(t){return new i("STORE compression")},uncompressWorker:function(){return new i("STORE decompression")}},r.DEFLATE=t("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(t,e,r){"use strict";var i=t("./utils");var o=function(){for(var t,e=[],r=0;r<256;r++){t=r;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[r]=t}return e}();e.exports=function(t,e){return void 0!==t&&t.length?"string"!==i.getTypeOf(t)?function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e[a])];return-1^t}(0|e,t,t.length,0):function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e.charCodeAt(a))];return-1^t}(0|e,t,t.length,0):0}},{"./utils":32}],5:[function(t,e,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(t,e,r){"use strict";var i=null;i="undefined"!=typeof Promise?Promise:t("lie"),e.exports={Promise:i}},{lie:37}],7:[function(t,e,r){"use strict";var i="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,n=t("pako"),s=t("./utils"),a=t("./stream/GenericWorker"),o=i?"uint8array":"array";function h(t,e){a.call(this,"FlateWorker/"+t),this._pako=null,this._pakoAction=t,this._pakoOptions=e,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(t){this.meta=t.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,t.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new n[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var e=this;this._pako.onData=function(t){e.push({data:t,meta:e.meta})}},r.compressWorker=function(t){return new h("Deflate",t)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(t,e,r){"use strict";function A(t,e){var r,i="";for(r=0;r>>=8;return i}function i(t,e,r,i,n,s){var a,o,h=t.file,u=t.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),d=I.transformTo("string",O.utf8encode(h.name)),c=h.comment,p=I.transformTo("string",s(c)),m=I.transformTo("string",O.utf8encode(c)),_=d.length!==h.name.length,g=m.length!==c.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};e&&!r||(x.crc32=t.crc32,x.compressedSize=t.compressedSize,x.uncompressedSize=t.uncompressedSize);var S=0;e&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===n?(C=798,z|=function(t,e){var r=t;return t||(r=e?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(t){return 63&(t||0)}(h.dosPermissions)),a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+d,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\0",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+A(p.length,2)+"\0\0\0\0"+A(z,4)+A(i,4)+f+b+p}}var I=t("../utils"),n=t("../stream/GenericWorker"),O=t("../utf8"),B=t("../crc32"),R=t("../signature");function s(t,e,r,i){n.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=e,this.zipPlatform=r,this.encodeFileName=i,this.streamFiles=t,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,n),s.prototype.push=function(t){var e=t.meta.percent||0,r=this.entriesCount,i=this._sources.length;this.accumulate?this.contentBuffer.push(t):(this.bytesWritten+=t.data.length,n.prototype.push.call(this,{data:t.data,meta:{currentFile:this.currentFile,percent:r?(e+100*(r-i-1))/r:100}}))},s.prototype.openedSource=function(t){this.currentSourceOffset=this.bytesWritten,this.currentFile=t.file.name;var e=this.streamFiles&&!t.file.dir;if(e){var r=i(t,e,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate=!0},s.prototype.closedSource=function(t){this.accumulate=!1;var e=this.streamFiles&&!t.file.dir,r=i(t,e,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),e)this.push({data:function(t){return R.DATA_DESCRIPTOR+A(t.crc32,4)+A(t.compressedSize,4)+A(t.uncompressedSize,4)}(t),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush=function(){for(var t=this.bytesWritten,e=0;e=this.index;e--)r=(r<<8)+this.byteAt(e);return this.index+=t,r},readString:function(t){return i.transformTo("string",this.readData(t))},readData:function(t){},lastIndexOfSignature:function(t){},readAndCheckSignature:function(t){},readDate:function(){var t=this.readInt(4);return new Date(Date.UTC(1980+(t>>25&127),(t>>21&15)-1,t>>16&31,t>>11&31,t>>5&63,(31&t)<<1))}},e.exports=n},{"../utils":32}],19:[function(t,e,r){"use strict";var i=t("./Uint8ArrayReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(t,e,r){"use strict";var i=t("./DataReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.byteAt=function(t){return this.data.charCodeAt(this.zero+t)},n.prototype.lastIndexOfSignature=function(t){return this.data.lastIndexOf(t)-this.zero},n.prototype.readAndCheckSignature=function(t){return t===this.readData(4)},n.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./DataReader":18}],21:[function(t,e,r){"use strict";var i=t("./ArrayReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.readData=function(t){if(this.checkOffset(t),0===t)return new Uint8Array(0);var e=this.data.subarray(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./ArrayReader":17}],22:[function(t,e,r){"use strict";var i=t("../utils"),n=t("../support"),s=t("./ArrayReader"),a=t("./StringReader"),o=t("./NodeBufferReader"),h=t("./Uint8ArrayReader");e.exports=function(t){var e=i.getTypeOf(t);return i.checkSupport(e),"string"!==e||n.uint8array?"nodebuffer"===e?new o(t):n.uint8array?new h(i.transformTo("uint8array",t)):new s(i.transformTo("array",t)):new a(t)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(t,e,r){"use strict";r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(t,e,r){"use strict";var i=t("./GenericWorker"),n=t("../utils");function s(t){i.call(this,"ConvertWorker to "+t),this.destType=t}n.inherits(s,i),s.prototype.processChunk=function(t){this.push({data:n.transformTo(this.destType,t.data),meta:t.meta})},e.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(t,e,r){"use strict";var i=t("./GenericWorker"),n=t("../crc32");function s(){i.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}t("../utils").inherits(s,i),s.prototype.processChunk=function(t){this.streamInfo.crc32=n(t.data,this.streamInfo.crc32||0),this.push(t)},e.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(t,e,r){"use strict";var i=t("../utils"),n=t("./GenericWorker");function s(t){n.call(this,"DataLengthProbe for "+t),this.propName=t,this.withStreamInfo(t,0)}i.inherits(s,n),s.prototype.processChunk=function(t){if(t){var e=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=e+t.data.length}n.prototype.processChunk.call(this,t)},e.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(t,e,r){"use strict";var i=t("../utils"),n=t("./GenericWorker");function s(t){n.call(this,"DataWorker");var e=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,t.then(function(t){e.dataIsReady=!0,e.data=t,e.max=t&&t.length||0,e.type=i.getTypeOf(t),e.isPaused||e._tickAndRepeat()},function(t){e.error(t)})}i.inherits(s,n),s.prototype.cleanUp=function(){n.prototype.cleanUp.call(this),this.data=null},s.prototype.resume=function(){return!!n.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,i.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(i.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var t=null,e=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":t=this.data.substring(this.index,e);break;case"uint8array":t=this.data.subarray(this.index,e);break;case"array":case"nodebuffer":t=this.data.slice(this.index,e)}return this.index=e,this.push({data:t,meta:{percent:this.max?this.index/this.max*100:0}})},e.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(t,e,r){"use strict";function i(t){this.name=t||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}i.prototype={push:function(t){this.emit("data",t)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(t){this.emit("error",t)}return!0},error:function(t){return!this.isFinished&&(this.isPaused?this.generatedError=t:(this.isFinished=!0,this.emit("error",t),this.previous&&this.previous.error(t),this.cleanUp()),!0)},on:function(t,e){return this._listeners[t].push(e),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(t,e){if(this._listeners[t])for(var r=0;r "+t:t}},e.exports=i},{}],29:[function(t,e,r){"use strict";var h=t("../utils"),n=t("./ConvertWorker"),s=t("./GenericWorker"),u=t("../base64"),i=t("../support"),a=t("../external"),o=null;if(i.nodestream)try{o=t("../nodejs/NodejsStreamOutputAdapter")}catch(t){}function l(t,o){return new a.Promise(function(e,r){var i=[],n=t._internalType,s=t._outputType,a=t._mimeType;t.on("data",function(t,e){i.push(t),o&&o(e)}).on("error",function(t){i=[],r(t)}).on("end",function(){try{var t=function(t,e,r){switch(t){case"blob":return h.newBlob(h.transformTo("arraybuffer",e),r);case"base64":return u.encode(e);default:return h.transformTo(t,e)}}(s,function(t,e){var r,i=0,n=null,s=0;for(r=0;r>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63),e[s++]=128|63&r);return e}(t)},s.utf8decode=function(t){return h.nodebuffer?o.transformTo("nodebuffer",t).toString("utf-8"):function(t){var e,r,i,n,s=t.length,a=new Array(2*s);for(e=r=0;e>10&1023,a[r++]=56320|1023&i)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(t=o.transformTo(h.uint8array?"uint8array":"array",t))},o.inherits(a,i),a.prototype.processChunk=function(t){var e=o.transformTo(h.uint8array?"uint8array":"array",t.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=e;(e=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),e.set(r,this.leftOver.length)}else e=this.leftOver.concat(e);this.leftOver=null}var i=function(t,e){var r;for((e=e||t.length)>t.length&&(e=t.length),r=e-1;0<=r&&128==(192&t[r]);)r--;return r<0?e:0===r?e:r+u[t[r]]>e?r:e}(e),n=e;i!==e.length&&(h.uint8array?(n=e.subarray(0,i),this.leftOver=e.subarray(i,e.length)):(n=e.slice(0,i),this.leftOver=e.slice(i,e.length))),this.push({data:s.utf8decode(n),meta:t.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,i),l.prototype.processChunk=function(t){this.push({data:s.utf8encode(t.data),meta:t.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(t,e,a){"use strict";var o=t("./support"),h=t("./base64"),r=t("./nodejsUtils"),i=t("set-immediate-shim"),u=t("./external");function n(t){return t}function l(t,e){for(var r=0;r>8;this.dir=!!(16&this.externalFileAttributes),0==t&&(this.dosPermissions=63&this.externalFileAttributes),3==t&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(t){if(this.extraFields[1]){var e=i(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(t){var e,r,i,n=t.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});t.index>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63),e[s++]=128|63&r);return e},r.buf2binstring=function(t){return l(t,t.length)},r.binstring2buf=function(t){for(var e=new h.Buf8(t.length),r=0,i=e.length;r>10&1023,o[i++]=56320|1023&n)}return l(o,i)},r.utf8border=function(t,e){var r;for((e=e||t.length)>t.length&&(e=t.length),r=e-1;0<=r&&128==(192&t[r]);)r--;return r<0?e:0===r?e:r+u[t[r]]>e?r:e}},{"./common":41}],43:[function(t,e,r){"use strict";e.exports=function(t,e,r,i){for(var n=65535&t|0,s=t>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:t>>>1;e[r]=t}return e}();e.exports=function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e[a])];return-1^t}},{}],46:[function(t,e,r){"use strict";var h,d=t("../utils/common"),u=t("./trees"),c=t("./adler32"),p=t("./crc32"),i=t("./messages"),l=0,f=4,m=0,_=-2,g=-1,b=4,n=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(t,e){return t.msg=i[e],e}function T(t){return(t<<1)-(4t.avail_out&&(r=t.avail_out),0!==r&&(d.arraySet(t.output,e.pending_buf,e.pending_out,r,t.next_out),t.next_out+=r,e.pending_out+=r,t.total_out+=r,t.avail_out-=r,e.pending-=r,0===e.pending&&(e.pending_out=0))}function N(t,e){u._tr_flush_block(t,0<=t.block_start?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,F(t.strm)}function U(t,e){t.pending_buf[t.pending++]=e}function P(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function L(t,e){var r,i,n=t.max_chain_length,s=t.strstart,a=t.prev_length,o=t.nice_match,h=t.strstart>t.w_size-z?t.strstart-(t.w_size-z):0,u=t.window,l=t.w_mask,f=t.prev,d=t.strstart+S,c=u[s+a-1],p=u[s+a];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(u[(r=e)+a]===p&&u[r+a-1]===c&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&sh&&0!=--n);return a<=t.lookahead?a:t.lookahead}function j(t){var e,r,i,n,s,a,o,h,u,l,f=t.w_size;do{if(n=t.window_size-t.lookahead-t.strstart,t.strstart>=f+(f-z)){for(d.arraySet(t.window,t.window,f,f,0),t.match_start-=f,t.strstart-=f,t.block_start-=f,e=r=t.hash_size;i=t.head[--e],t.head[e]=f<=i?i-f:0,--r;);for(e=r=f;i=t.prev[--e],t.prev[e]=f<=i?i-f:0,--r;);n+=f}if(0===t.strm.avail_in)break;if(a=t.strm,o=t.window,h=t.strstart+t.lookahead,u=n,l=void 0,l=a.avail_in,u=x)for(s=t.strstart-t.insert,t.ins_h=t.window[s],t.ins_h=(t.ins_h<=x&&(t.ins_h=(t.ins_h<=x)if(i=u._tr_tally(t,t.strstart-t.match_start,t.match_length-x),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=x){for(t.match_length--;t.strstart++,t.ins_h=(t.ins_h<=x&&(t.ins_h=(t.ins_h<=x&&t.match_length<=t.prev_length){for(n=t.strstart+t.lookahead-x,i=u._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-x),t.lookahead-=t.prev_length-1,t.prev_length-=2;++t.strstart<=n&&(t.ins_h=(t.ins_h<t.pending_buf_size-5&&(r=t.pending_buf_size-5);;){if(t.lookahead<=1){if(j(t),0===t.lookahead&&e===l)return A;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=0;var i=t.block_start+r;if((0===t.strstart||t.strstart>=i)&&(t.lookahead=t.strstart-i,t.strstart=i,N(t,!1),0===t.strm.avail_out))return A;if(t.strstart-t.block_start>=t.w_size-z&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):(t.strstart>t.block_start&&(N(t,!1),t.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,258,258,4096,W)],r.deflateInit=function(t,e){return Y(t,e,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(t,e){return t&&t.state?2!==t.state.wrap?_:(t.state.gzhead=e,m):_},r.deflate=function(t,e){var r,i,n,s;if(!t||!t.state||5>8&255),U(i,i.gzhead.time>>16&255),U(i,i.gzhead.time>>24&255),U(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),U(i,255&i.gzhead.os),i.gzhead.extra&&i.gzhead.extra.length&&(U(i,255&i.gzhead.extra.length),U(i,i.gzhead.extra.length>>8&255)),i.gzhead.hcrc&&(t.adler=p(t.adler,i.pending_buf,i.pending,0)),i.gzindex=0,i.status=69):(U(i,0),U(i,0),U(i,0),U(i,0),U(i,0),U(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),U(i,3),i.status=E);else{var a=v+(i.w_bits-8<<4)<<8;a|=(2<=i.strategy||i.level<2?0:i.level<6?1:6===i.level?2:3)<<6,0!==i.strstart&&(a|=32),a+=31-a%31,i.status=E,P(i,a),0!==i.strstart&&(P(i,t.adler>>>16),P(i,65535&t.adler)),t.adler=1}if(69===i.status)if(i.gzhead.extra){for(n=i.pending;i.gzindex<(65535&i.gzhead.extra.length)&&(i.pending!==i.pending_buf_size||(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending!==i.pending_buf_size));)U(i,255&i.gzhead.extra[i.gzindex]),i.gzindex++;i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),i.gzindex===i.gzhead.extra.length&&(i.gzindex=0,i.status=73)}else i.status=73;if(73===i.status)if(i.gzhead.name){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending===i.pending_buf_size)){s=1;break}s=i.gzindexn&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),0===s&&(i.gzindex=0,i.status=91)}else i.status=91;if(91===i.status)if(i.gzhead.comment){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending===i.pending_buf_size)){s=1;break}s=i.gzindexn&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),0===s&&(i.status=103)}else i.status=103;if(103===i.status&&(i.gzhead.hcrc?(i.pending+2>i.pending_buf_size&&F(t),i.pending+2<=i.pending_buf_size&&(U(i,255&t.adler),U(i,t.adler>>8&255),t.adler=0,i.status=E)):i.status=E),0!==i.pending){if(F(t),0===t.avail_out)return i.last_flush=-1,m}else if(0===t.avail_in&&T(e)<=T(r)&&e!==f)return R(t,-5);if(666===i.status&&0!==t.avail_in)return R(t,-5);if(0!==t.avail_in||0!==i.lookahead||e!==l&&666!==i.status){var o=2===i.strategy?function(t,e){for(var r;;){if(0===t.lookahead&&(j(t),0===t.lookahead)){if(e===l)return A;break}if(t.match_length=0,r=u._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,r&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}(i,e):3===i.strategy?function(t,e){for(var r,i,n,s,a=t.window;;){if(t.lookahead<=S){if(j(t),t.lookahead<=S&&e===l)return A;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=x&&0t.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=x?(r=u._tr_tally(t,1,t.match_length-x),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(r=u._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),r&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}(i,e):h[i.level].func(i,e);if(o!==O&&o!==B||(i.status=666),o===A||o===O)return 0===t.avail_out&&(i.last_flush=-1),m;if(o===I&&(1===e?u._tr_align(i):5!==e&&(u._tr_stored_block(i,0,0,!1),3===e&&(D(i.head),0===i.lookahead&&(i.strstart=0,i.block_start=0,i.insert=0))),F(t),0===t.avail_out))return i.last_flush=-1,m}return e!==f?m:i.wrap<=0?1:(2===i.wrap?(U(i,255&t.adler),U(i,t.adler>>8&255),U(i,t.adler>>16&255),U(i,t.adler>>24&255),U(i,255&t.total_in),U(i,t.total_in>>8&255),U(i,t.total_in>>16&255),U(i,t.total_in>>24&255)):(P(i,t.adler>>>16),P(i,65535&t.adler)),F(t),0=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new d.Buf8(r.w_size),d.arraySet(u,e,l-r.w_size,r.w_size,0),e=u,l=r.w_size),a=t.avail_in,o=t.next_in,h=t.input,t.avail_in=l,t.next_in=0,t.input=e,j(r);r.lookahead>=x;){for(i=r.strstart,n=r.lookahead-(x-1);r.ins_h=(r.ins_h<>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(c&(1<>>=y,p-=y),p<15&&(c+=z[i++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(c&(1<>>=y,p-=y,(y=s-a)>3,c&=(1<<(p-=w<<3))-1,t.next_in=i,t.next_out=s,t.avail_in=i>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=P,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new I.Buf32(i),e.distcode=e.distdyn=new I.Buf32(n),e.sane=1,e.back=-1,N):U}function o(t){var e;return t&&t.state?((e=t.state).wsize=0,e.whave=0,e.wnext=0,a(t)):U}function h(t,e){var r,i;return t&&t.state?(i=t.state,e<0?(r=0,e=-e):(r=1+(e>>4),e<48&&(e&=15)),e&&(e<8||15=s.wsize?(I.arraySet(s.window,e,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(i<(n=s.wsize-s.wnext)&&(n=i),I.arraySet(s.window,e,r-i,n,s.wnext),(i-=n)?(I.arraySet(s.window,e,r-i,i,0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){t.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){t.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){t.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(c=r.length)&&(c=o),c&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,i,s,c,k)),512&r.flags&&(r.check=B(r.check,i,c,s)),o-=c,s+=c,r.length-=c),r.length))break t;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break t;for(c=0;k=i[s+c++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&c>9&1,r.head.done=!0),t.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break t;o--,u+=i[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==e)break;u>>>=2,l-=2;break t;case 2:r.mode=17;break;case 3:t.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break t;o--,u+=i[s++]<>>16^65535)){t.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===e)break t;case 15:r.mode=16;case 16:if(c=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){t.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],c=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)),u>>>=7,l-=7}if(r.have+c>r.nlen+r.ndist){t.msg="invalid bit length repeat",r.mode=30;break}for(;c--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){t.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){t.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===e)break t;case 20:r.mode=21;case 21:if(6<=o&&258<=h){t.next_out=a,t.avail_out=h,t.next_in=s,t.avail_in=o,r.hold=u,r.bits=l,R(t,d),a=t.next_out,n=t.output,h=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){t.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){t.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){t.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break t;if(c=d-h,r.offset>c){if((c=r.offset-c)>r.whave&&r.sane){t.msg="invalid distance too far back",r.mode=30;break}p=c>r.wnext?(c-=r.wnext,r.wsize-c):r.wnext-c,c>r.length&&(c=r.length),m=r.window}else m=n,p=a-r.offset,c=r.length;for(hc?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=e[r+a[v]]}if(k>>7)]}function U(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function P(t,e,r){t.bi_valid>c-r?(t.bi_buf|=e<>c-t.bi_valid,t.bi_valid+=r-c):(t.bi_buf|=e<>>=1,r<<=1,0<--e;);return r>>>1}function Z(t,e,r){var i,n,s=new Array(g+1),a=0;for(i=1;i<=g;i++)s[i]=a=a+r[i-1]<<1;for(n=0;n<=e;n++){var o=t[2*n+1];0!==o&&(t[2*n]=j(s[o]++,o))}}function W(t){var e;for(e=0;e>1;1<=r;r--)G(t,s,r);for(n=h;r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],G(t,s,1),i=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=i,s[2*n]=s[2*r]+s[2*i],t.depth[n]=(t.depth[r]>=t.depth[i]?t.depth[r]:t.depth[i])+1,s[2*r+1]=s[2*i+1]=n,t.heap[1]=n++,G(t,s,1),2<=t.heap_len;);t.heap[--t.heap_max]=t.heap[1],function(t,e){var r,i,n,s,a,o,h=e.dyn_tree,u=e.max_code,l=e.stat_desc.static_tree,f=e.stat_desc.has_stree,d=e.stat_desc.extra_bits,c=e.stat_desc.extra_base,p=e.stat_desc.max_length,m=0;for(s=0;s<=g;s++)t.bl_count[s]=0;for(h[2*t.heap[t.heap_max]+1]=0,r=t.heap_max+1;r<_;r++)p<(s=h[2*h[2*(i=t.heap[r])+1]+1]+1)&&(s=p,m++),h[2*i+1]=s,u>=7;i>>=1)if(1&r&&0!==t.dyn_ltree[2*e])return o;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return h;for(e=32;e>>3,(s=t.static_len+3+7>>>3)<=n&&(n=s)):n=s=r+5,r+4<=n&&-1!==e?J(t,e,r,i):4===t.strategy||s===n?(P(t,2+(i?1:0),3),K(t,z,C)):(P(t,4+(i?1:0),3),function(t,e,r,i){var n;for(P(t,e-257,5),P(t,r-1,5),P(t,i-4,4),n=0;n>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&r,t.last_lit++,0===e?t.dyn_ltree[2*r]++:(t.matches++,e--,t.dyn_ltree[2*(A[r]+u+1)]++,t.dyn_dtree[2*N(e)]++),t.last_lit===t.lit_bufsize-1},r._tr_align=function(t){P(t,2,3),L(t,m,z),function(t){16===t.bi_valid?(U(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):8<=t.bi_valid&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}(t)}},{"../utils/common":41}],53:[function(t,e,r){"use strict";e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(t,e,r){"use strict";e.exports="function"==typeof setImmediate?setImmediate:function(){var t=[].slice.apply(arguments);t.splice(1,0,0),setTimeout.apply(null,t)}},{}]},{},[10])(10)}); \ No newline at end of file diff --git a/ext/handle_cbz/main.php b/ext/handle_cbz/main.php new file mode 100644 index 00000000..b9011c52 --- /dev/null +++ b/ext/handle_cbz/main.php @@ -0,0 +1,66 @@ +image->lossless = false; + $event->image->video = false; + $event->image->audio = false; + $event->image->image = false; + + $tmp = $this->get_representative_image($event->file_name); + $info = getimagesize($tmp); + if ($info) { + $event->image->width = $info[0]; + $event->image->height = $info[1]; + } + unlink($tmp); + } + + protected function create_thumb(string $hash, string $type): bool + { + $cover = $this->get_representative_image(warehouse_path(Image::IMAGE_DIR, $hash)); + create_scaled_image( + $cover, + warehouse_path(Image::THUMBNAIL_DIR, $hash), + get_thumbnail_max_size_scaled(), + get_extension(getMimeType($cover)), + null + ); + return true; + } + + protected function check_contents(string $tmpname): bool + { + $fp = fopen($tmpname, "r"); + $head = fread($fp, 4); + fclose($fp); + return $head == "PK\x03\x04"; + } + + private function get_representative_image(string $archive): string + { + $out = "data/comic-cover-FIXME.jpg"; // TODO: random + + $za = new ZipArchive(); + $za->open($archive); + $names = []; + for ($i=0; $i<$za->numFiles;$i++) { + $file = $za->statIndex($i); + $names[] = $file['name']; + } + sort($names); + $cover = $names[0]; + foreach ($names as $name) { + if (strpos(strtolower($name), "cover") !== false) { + $cover = $name; + break; + } + } + file_put_contents($out, $za->getFromName($cover)); + return $out; + } +} diff --git a/ext/handle_cbz/spinner.gif b/ext/handle_cbz/spinner.gif new file mode 100644 index 00000000..5f45c3e0 Binary files /dev/null and b/ext/handle_cbz/spinner.gif differ diff --git a/ext/handle_cbz/style.css b/ext/handle_cbz/style.css new file mode 100644 index 00000000..4813925c --- /dev/null +++ b/ext/handle_cbz/style.css @@ -0,0 +1,26 @@ +#comicMain { + background: black; + color: white; + font-size: 3em; +} +#comicPageList { + width: 90%; +} + +#comicView { + display: flex; + flex-flow: row; +} +#comicView #comicPage { + flex: 10 auto; +} +#comicView #comicPrev, +#comicView #comicNext { + flex: 1 auto; + padding-top: 45%; +} +#comicView .comicPager { + position: absolute; + top: 0; + margin: auto; +} diff --git a/ext/handle_cbz/theme.php b/ext/handle_cbz/theme.php new file mode 100644 index 00000000..382f4401 --- /dev/null +++ b/ext/handle_cbz/theme.php @@ -0,0 +1,27 @@ +get_image_link(); + $html = " +

    +
    + +
    +
    + < + comic + > +
    +
    + + + + + "; + $page->add_block(new Block("Comic", $html, "main", 10, "comicBlock")); + } +} diff --git a/ext/handle_flash/info.php b/ext/handle_flash/info.php new file mode 100644 index 00000000..448996bc --- /dev/null +++ b/ext/handle_flash/info.php @@ -0,0 +1,12 @@ + - * Link: http://code.shishnet.org/shimmie2/ - * Description: Handle Flash files. (No thumbnail is generated for flash files) - */ +image->lossless = true; + $event->image->video = true; + $event->image->image = false; - /** - * @param string $filename - * @param array $metadata - * @return Image|null - */ - protected function create_image_from_data(/*string*/ $filename, /*array*/ $metadata) { - $image = new Image(); + $info = getimagesize($event->file_name); + if ($info) { + $event->image->width = $info[0]; + $event->image->height = $info[1]; + } + } - $image->filesize = $metadata['size']; - $image->hash = $metadata['hash']; - $image->filename = $metadata['filename']; - $image->ext = $metadata['extension']; - $image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']); - $image->source = $metadata['source']; + protected function create_thumb(string $hash, string $type): bool + { + if (!Media::create_thumbnail_ffmpeg($hash)) { + copy("ext/handle_flash/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash)); + } + return true; + } - $info = getimagesize($filename); - if(!$info) return null; - - $image->width = $info[0]; - $image->height = $info[1]; - - return $image; - } - - /** - * @param string $file - * @return bool - */ - protected function check_contents(/*string*/ $file) { - if (!file_exists($file)) return false; - - $fp = fopen($file, "r"); - $head = fread($fp, 3); - fclose($fp); - if (!in_array($head, array("CWS", "FWS"))) return false; - - return true; - } + protected function check_contents(string $tmpname): bool + { + $fp = fopen($tmpname, "r"); + $head = fread($fp, 3); + fclose($fp); + return in_array($head, ["CWS", "FWS"]); + } } - diff --git a/ext/handle_flash/theme.php b/ext/handle_flash/theme.php index e4557088..d1ce90be 100644 --- a/ext/handle_flash/theme.php +++ b/ext/handle_flash/theme.php @@ -1,10 +1,12 @@ -get_image_link(); - // FIXME: object and embed have "height" and "width" - $html = " +class FlashFileHandlerTheme extends Themelet +{ + public function display_image(Page $page, Image $image) + { + $ilink = $image->get_image_link(); + // FIXME: object and embed have "height" and "width" + $html = " + type='application/x-shockwave-flash' /> "; - $page->add_block(new Block("Flash Animation", $html, "main", 10)); - } + $page->add_block(new Block("Flash Animation", $html, "main", 10)); + } } - diff --git a/ext/handle_ico/info.php b/ext/handle_ico/info.php new file mode 100644 index 00000000..6c932f0c --- /dev/null +++ b/ext/handle_ico/info.php @@ -0,0 +1,12 @@ + - * Description: Handle windows icons - */ +supported_ext($event->type) && $this->check_contents($event->tmpname)) { - $hash = $event->hash; - $ha = substr($hash, 0, 2); - move_upload_to_archive($event); - send_event(new ThumbnailGenerationEvent($event->hash, $event->type)); - $image = $this->create_image_from_data("images/$ha/$hash", $event->metadata); - if(is_null($image)) { - throw new UploadException("Icon handler failed to create image object from data"); - } - $iae = new ImageAdditionEvent($image); - send_event($iae); - $event->image_id = $iae->image->id; - } - } +class IcoFileHandler extends DataHandlerExtension +{ + protected $SUPPORTED_EXT = ["ico", "ani", "cur"]; - public function onThumbnailGeneration(ThumbnailGenerationEvent $event) { - if($this->supported_ext($event->type)) { - $this->create_thumb($event->hash); - } - } + protected function media_check_properties(MediaCheckPropertiesEvent $event): void + { + $event->image->lossless = true; + $event->image->video = false; + $event->image->audio = false; + $event->image->image = ($event->ext!="ani"); - public function onDisplayingImage(DisplayingImageEvent $event) { - global $page; - if($this->supported_ext($event->image->ext)) { - $this->theme->display_image($page, $event->image); - } - } + $fp = fopen($event->file_name, "r"); + try { + unpack("Snull/Stype/Scount", fread($fp, 6)); + $subheader = unpack("Cwidth/Cheight/Ccolours/Cnull/Splanes/Sbpp/Lsize/loffset", fread($fp, 16)); + } finally { + fclose($fp); + } - /** - * @param string $ext - * @return bool - */ - private function supported_ext($ext) { - $exts = array("ico", "ani", "cur"); - return in_array(strtolower($ext), $exts); - } + $width = $subheader['width']; + $height = $subheader['height']; + $event->image->width = $width == 0 ? 256 : $width; + $event->image->height = $height == 0 ? 256 : $height; + } - /** - * @param string $filename - * @param mixed[] $metadata - * @return Image - */ - private function create_image_from_data($filename, $metadata) { - $image = new Image(); + protected function create_thumb(string $hash, string $type): bool + { + try { + create_image_thumb($hash, $type, MediaEngine::IMAGICK); + return true; + } catch (MediaException $e) { + log_warning("handle_ico", "Could not generate thumbnail. " . $e->getMessage()); + return false; + } + } - $fp = fopen($filename, "r"); - $header = unpack("Snull/Stype/Scount", fread($fp, 6)); - - $subheader = unpack("Cwidth/Cheight/Ccolours/Cnull/Splanes/Sbpp/Lsize/loffset", fread($fp, 16)); - fclose($fp); - - $width = $subheader['width']; - $height = $subheader['height']; - $image->width = $width == 0 ? 256 : $width; - $image->height = $height == 0 ? 256 : $height; - - $image->filesize = $metadata['size']; - $image->hash = $metadata['hash']; - $image->filename = $metadata['filename']; - $image->ext = $metadata['extension']; - $image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']); - $image->source = $metadata['source']; - - return $image; - } - - /** - * @param string $file - * @return bool - */ - private function check_contents($file) { - if(!file_exists($file)) return false; - $fp = fopen($file, "r"); - $header = unpack("Snull/Stype/Scount", fread($fp, 6)); - fclose($fp); - return ($header['null'] == 0 && ($header['type'] == 0 || $header['type'] == 1)); - } - - /** - * @param string $hash - * @return bool - */ - private function create_thumb($hash) { - global $config; - - $inname = warehouse_path("images", $hash); - $outname = warehouse_path("thumbs", $hash); - - $w = $config->get_int("thumb_width"); - $h = $config->get_int("thumb_height"); - $q = $config->get_int("thumb_quality"); - $mem = $config->get_int("thumb_mem_limit") / 1024 / 1024; // IM takes memory in MB - - if($config->get_bool("ico_convert")) { - // "-limit memory $mem" broken? - exec("convert {$inname}[0] -geometry {$w}x{$h} -quality {$q} jpg:$outname"); - } - else { - copy($inname, $outname); - } - - return true; - } + protected function check_contents(string $file): bool + { + $fp = fopen($file, "r"); + $header = unpack("Snull/Stype/Scount", fread($fp, 6)); + fclose($fp); + return ($header['null'] == 0 && ($header['type'] == 0 || $header['type'] == 1)); + } } - diff --git a/ext/handle_ico/test.php b/ext/handle_ico/test.php index fa130100..db0aea34 100644 --- a/ext/handle_ico/test.php +++ b/ext/handle_ico/test.php @@ -1,12 +1,15 @@ -log_in_as_user(); - $image_id = $this->post_image("lib/static/favicon.ico", "shimmie favicon"); - $this->get_page("post/view/$image_id"); // test for no crash +log_in_as_user(); + $image_id = $this->post_image("ext/static_files/static/favicon.ico", "shimmie favicon"); - # FIXME: test that the thumb works - # FIXME: test that it gets displayed properly - } + $page = $this->get_page("post/view/$image_id"); + $this->assertEquals(200, $page->code); + + # FIXME: test that the thumb works + # FIXME: test that it gets displayed properly + } } - diff --git a/ext/handle_ico/theme.php b/ext/handle_ico/theme.php index 36daa9c2..6a99d0a3 100644 --- a/ext/handle_ico/theme.php +++ b/ext/handle_ico/theme.php @@ -1,13 +1,14 @@ -get_image_link(); - $html = " +class IcoFileHandlerTheme extends Themelet +{ + public function display_image(Page $page, Image $image) + { + $ilink = $image->get_image_link(); + $html = " main image "; - $page->add_block(new Block("Image", $html, "main", 10)); - } + $page->add_block(new Block("Image", $html, "main", 10)); + } } - diff --git a/ext/handle_mp3/info.php b/ext/handle_mp3/info.php new file mode 100644 index 00000000..ba5ede9b --- /dev/null +++ b/ext/handle_mp3/info.php @@ -0,0 +1,12 @@ + - * Description: Handle MP3 files - */ +image->audio = true; + $event->image->video = false; + $event->image->lossless = false; + $event->image->image = false; + $event->image->width = 0; + $event->image->height = 0; + // TODO: ->length = ??? + } - /** - * @param string $filename - * @param mixed[] $metadata - * @return Image|null - */ - protected function create_image_from_data($filename, $metadata) { - $image = new Image(); + protected function create_thumb(string $hash, string $type): bool + { + copy("ext/handle_mp3/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash)); + return true; + } - //NOTE: No need to set width/height as we don't use it. - $image->width = 1; - $image->height = 1; - - $image->filesize = $metadata['size']; - $image->hash = $metadata['hash']; - - //Filename is renamed to "artist - title.mp3" when the user requests download by using the download attribute & jsmediatags.js - $image->filename = $metadata['filename']; - - $image->ext = $metadata['extension']; - $image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']); - $image->source = $metadata['source']; - - return $image; - } - - /** - * @param $file - * @return bool - */ - protected function check_contents($file) { - $success = FALSE; - - if (file_exists($file)) { - $mimeType = mime_content_type($file); - - $success = ($mimeType == 'audio/mpeg'); - } - - return $success; - } + protected function check_contents(string $tmpname): bool + { + return getMimeType($tmpname) == 'audio/mpeg'; + } } - diff --git a/ext/handle_mp3/theme.php b/ext/handle_mp3/theme.php index 95868018..e5118a7a 100644 --- a/ext/handle_mp3/theme.php +++ b/ext/handle_mp3/theme.php @@ -1,11 +1,12 @@ -get_image_link(); - $fname = url_escape($image->filename); //Most of the time this will be the title/artist of the song. - $html = " +class MP3FileHandlerTheme extends Themelet +{ + public function display_image(Page $page, Image $image) + { + $data_href = get_base_href(); + $ilink = $image->get_image_link(); + $html = "
    "; - $page->add_block(new Block(null, $html, "main", 1, 'note_system')); - } + $page->add_block(new Block(null, $html, "main", 1, 'note_system')); + } - public function display_note_list($images, $pageNumber, $totalPages) { - global $page; - $pool_images = ''; - foreach($images as $pair) { - $image = $pair[0]; + public function display_note_list($images, $pageNumber, $totalPages) + { + global $page; + $pool_images = ''; + foreach ($images as $pair) { + $image = $pair[0]; - $thumb_html = $this->build_thumb_html($image); + $thumb_html = $this->build_thumb_html($image); - $pool_images .= ''. - ' '.$thumb_html.''. - ''; + $pool_images .= ''. + ' '.$thumb_html.''. + ''; + } + $this->display_paginator($page, "note/list", null, $pageNumber, $totalPages); + $page->set_title("Notes"); + $page->set_heading("Notes"); + $page->add_block(new Block("Notes", $pool_images, "main", 20)); + } - } - $this->display_paginator($page, "note/list", null, $pageNumber, $totalPages); + public function display_note_requests($images, $pageNumber, $totalPages) + { + global $page; - $page->set_title("Notes"); - $page->set_heading("Notes"); - $page->add_block(new Block("Notes", $pool_images, "main", 20)); - } + $pool_images = ''; + foreach ($images as $pair) { + $image = $pair[0]; - public function display_note_requests($images, $pageNumber, $totalPages) { - global $page; + $thumb_html = $this->build_thumb_html($image); - $pool_images = ''; - foreach($images as $pair) { - $image = $pair[0]; + $pool_images .= ''. + ' '.$thumb_html.''. + ''; + } + $this->display_paginator($page, "requests/list", null, $pageNumber, $totalPages); - $thumb_html = $this->build_thumb_html($image); + $page->set_title("Note Requests"); + $page->set_heading("Note Requests"); + $page->add_block(new Block("Note Requests", $pool_images, "main", 20)); + } - $pool_images .= ''. - ' '.$thumb_html.''. - ''; + private function get_history($histories) + { + global $user; + $html = "". + "". + "". + "". + "". + "". + ""; - } - $this->display_paginator($page, "requests/list", null, $pageNumber, $totalPages); + if (!$user->is_anonymous()) { + $html .= ""; + } - $page->set_title("Note Requests"); - $page->set_heading("Note Requests"); - $page->add_block(new Block("Note Requests", $pool_images, "main", 20)); - } + $html .= "". + ""; - private function get_history($histories) { - global $user; + foreach ($histories as $history) { + $image_link = "".$history['image_id'].""; + $history_link = "".$history['note_id'].".".$history['review_id'].""; + $user_link = "".$history['user_name'].""; + $revert_link = "Revert"; - $html = "
    ImageNoteBodyUpdaterDateAction
    ". - "". - "". - "". - "". - "". - ""; + $html .= "". + "". + "". + "". + "". + ""; - if(!$user->is_anonymous()){ - $html .= ""; - } + if (!$user->is_anonymous()) { + $html .= ""; + } + } - $html .= "". - ""; + $html .= "
    ImageNoteBodyUpdaterDate
    ".$image_link."".$history_link."".$history['note']."".$user_link."".autodate($history['date'])."Action".$revert_link."
    "; - foreach($histories as $history) { - $image_link = "".$history['image_id'].""; - $history_link = "".$history['note_id'].".".$history['review_id'].""; - $user_link = "".$history['user_name'].""; - $revert_link = "Revert"; + return $html; + } - $html .= "". - "".$image_link."". - "".$history_link."". - "".$history['note']."". - "".$user_link."". - "".autodate($history['date']).""; + public function display_histories($histories, $pageNumber, $totalPages) + { + global $page; - if(!$user->is_anonymous()){ - $html .= "".$revert_link.""; - } + $html = $this->get_history($histories); - } + $page->set_title("Note Updates"); + $page->set_heading("Note Updates"); + $page->add_block(new Block("Note Updates", $html, "main", 10)); - $html .= ""; + $this->display_paginator($page, "note/updated", null, $pageNumber, $totalPages); + } - return $html; - } + public function display_history($histories, $pageNumber, $totalPages) + { + global $page; - public function display_histories($histories, $pageNumber, $totalPages) { - global $page; + $html = $this->get_history($histories); - $html = $this->get_history($histories); + $page->set_title("Note History"); + $page->set_heading("Note History"); + $page->add_block(new Block("Note History", $html, "main", 10)); - $page->set_title("Note Updates"); - $page->set_heading("Note Updates"); - $page->add_block(new Block("Note Updates", $html, "main", 10)); + $this->display_paginator($page, "note/updated", null, $pageNumber, $totalPages); + } - $this->display_paginator($page, "note/updated", null, $pageNumber, $totalPages); - } - - public function display_history($histories, $pageNumber, $totalPages) { - global $page; - - $html = $this->get_history($histories); - - $page->set_title("Note History"); - $page->set_heading("Note History"); - $page->add_block(new Block("Note History", $html, "main", 10)); - - $this->display_paginator($page, "note/updated", null, $pageNumber, $totalPages); - } + public function get_help_html() + { + return '

    Search for images with notes.

    +
    +
    note=noted
    +

    Returns images with a note matching "noted".

    +
    +
    +
    notes>0
    +

    Returns images with 1 or more notes.

    +
    +

    Can use <, <=, >, >=, or =.

    +
    +
    notes_by=username
    +

    Returns images with note(s) by "username".

    +
    +
    +
    notes_by_user_id=123
    +

    Returns images with note(s) by user 123.

    +
    + '; + } } diff --git a/ext/numeric_score/info.php b/ext/numeric_score/info.php new file mode 100644 index 00000000..beb2059c --- /dev/null +++ b/ext/numeric_score/info.php @@ -0,0 +1,14 @@ + - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Allow users to score images - * Documentation: - * Each registered user may vote an image +1 or -1, the - * image's score is the sum of all votes. - */ +image_id = $image_id; - $this->user = $user; - $this->score = $score; - } + public function __construct(int $image_id, User $user, int $score) + { + parent::__construct(); + $this->image_id = $image_id; + $this->user = $user; + $this->score = $score; + } } -class NumericScore extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config; - if($config->get_int("ext_numeric_score_version", 0) < 1) { - $this->install(); - } - } +class NumericScore extends Extension +{ + /** @var NumericScoreTheme */ + protected $theme; - public function onDisplayingImage(DisplayingImageEvent $event) { - global $user; - if(!$user->is_anonymous()) { - $this->theme->get_voter($event->image); - } - } + public function onDisplayingImage(DisplayingImageEvent $event) + { + global $user; + if (!$user->is_anonymous()) { + $this->theme->get_voter($event->image); + } + } - public function onUserPageBuilding(UserPageBuildingEvent $event) { - global $user; - if($user->can("edit_other_vote")) { - $this->theme->get_nuller($event->display_user); - } - } + public function onUserPageBuilding(UserPageBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::EDIT_OTHER_VOTE)) { + $this->theme->get_nuller($event->display_user); + } - public function onPageRequest(PageRequestEvent $event) { - global $config, $database, $user, $page; + $u_name = url_escape($event->display_user->name); + $n_up = Image::count_images(["upvoted_by={$event->display_user->name}"]); + $link_up = make_link("post/list/upvoted_by=$u_name/1"); + $n_down = Image::count_images(["downvoted_by={$event->display_user->name}"]); + $link_down = make_link("post/list/downvoted_by=$u_name/1"); + $event->add_stats("$n_up Upvotes / $n_down Downvotes"); + } - if($event->page_matches("numeric_score_votes")) { - $image_id = int_escape($event->get_arg(0)); - $x = $database->get_all( - "SELECT users.name as username, user_id, score - FROM numeric_score_votes + public function onPageRequest(PageRequestEvent $event) + { + global $config, $database, $user, $page; + + if ($event->page_matches("numeric_score_votes")) { + $image_id = int_escape($event->get_arg(0)); + $x = $database->get_all( + "SELECT users.name as username, user_id, score + FROM numeric_score_votes JOIN users ON numeric_score_votes.user_id=users.id - WHERE image_id=?", - array($image_id)); - $html = ""; - foreach($x as $vote) { - $html .= ""; - } - die($html); - } - else if($event->page_matches("numeric_score_vote") && $user->check_auth_token()) { - if(!$user->is_anonymous()) { - $image_id = int_escape($_POST['image_id']); - $char = $_POST['vote']; - $score = null; - if($char == "up") $score = 1; - else if($char == "null") $score = 0; - else if($char == "down") $score = -1; - if(!is_null($score) && $image_id>0) send_event(new NumericScoreSetEvent($image_id, $user, $score)); - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/$image_id")); - } - } - else if($event->page_matches("numeric_score/remove_votes_on") && $user->check_auth_token()) { - if($user->can("edit_other_vote")) { - $image_id = int_escape($_POST['image_id']); - $database->execute( - "DELETE FROM numeric_score_votes WHERE image_id=?", - array($image_id)); - $database->execute( - "UPDATE images SET numeric_score=0 WHERE id=?", - array($image_id)); - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/$image_id")); - } - } - else if($event->page_matches("numeric_score/remove_votes_by") && $user->check_auth_token()) { - if($user->can("edit_other_vote")) { - $this->delete_votes_by(int_escape($_POST['user_id'])); - $page->set_mode("redirect"); - $page->set_redirect(make_link()); - } - } - else if($event->page_matches("popular_by_day") || $event->page_matches("popular_by_month") || $event->page_matches("popular_by_year")) { - //FIXME: popular_by isn't linked from anywhere - list($day, $month, $year) = array(date("d"), date("m"), date("Y")); + WHERE image_id=:image_id", + ['image_id'=>$image_id] + ); + $html = "
    "; - $html .= "{$vote['username']}"; - $html .= ""; - $html .= $vote['score']; - $html .= "
    "; + foreach ($x as $vote) { + $html .= ""; + } + die($html); + } elseif ($event->page_matches("numeric_score_vote") && $user->check_auth_token()) { + if (!$user->is_anonymous()) { + $image_id = int_escape($_POST['image_id']); + $char = $_POST['vote']; + $score = null; + if ($char == "up") { + $score = 1; + } elseif ($char == "null") { + $score = 0; + } elseif ($char == "down") { + $score = -1; + } + if (!is_null($score) && $image_id>0) { + send_event(new NumericScoreSetEvent($image_id, $user, $score)); + } + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/$image_id")); + } + } elseif ($event->page_matches("numeric_score/remove_votes_on") && $user->check_auth_token()) { + if ($user->can(Permissions::EDIT_OTHER_VOTE)) { + $image_id = int_escape($_POST['image_id']); + $database->execute( + "DELETE FROM numeric_score_votes WHERE image_id=:image_id", + ['image_id'=>$image_id] + ); + $database->execute( + "UPDATE images SET numeric_score=0 WHERE id=:id", + ['id'=>$image_id] + ); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/$image_id")); + } + } elseif ($event->page_matches("numeric_score/remove_votes_by") && $user->check_auth_token()) { + if ($user->can(Permissions::EDIT_OTHER_VOTE)) { + $this->delete_votes_by(int_escape($_POST['user_id'])); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link()); + } + } elseif ($event->page_matches("popular_by_day") || $event->page_matches("popular_by_month") || $event->page_matches("popular_by_year")) { + //FIXME: popular_by isn't linked from anywhere + list($day, $month, $year) = [date("d"), date("m"), date("Y")]; - if(!empty($_GET['day'])){ - $D = (int) $_GET['day']; - $day = clamp($D, 1, 31); - } - if(!empty($_GET['month'])){ - $M = (int) $_GET['month']; - $month = clamp($M, 1 ,12); - } - if(!empty($_GET['year'])){ - $Y = (int) $_GET['year']; - $year = clamp($Y, 1970, 2100); - } + if (!empty($_GET['day'])) { + $D = (int) $_GET['day']; + $day = clamp($D, 1, 31); + } + if (!empty($_GET['month'])) { + $M = (int) $_GET['month']; + $month = clamp($M, 1, 12); + } + if (!empty($_GET['year'])) { + $Y = (int) $_GET['year']; + $year = clamp($Y, 1970, 2100); + } - $totaldate = $year."/".$month."/".$day; + $totaldate = $year."/".$month."/".$day; - $sql = "SELECT id FROM images + $sql = "SELECT id FROM images WHERE EXTRACT(YEAR FROM posted) = :year "; - $args = array("limit" => $config->get_int("index_images"), "year" => $year); + $args = ["limit" => $config->get_int(IndexConfig::IMAGES), "year" => $year]; - if($event->page_matches("popular_by_day")){ - $sql .= - "AND EXTRACT(MONTH FROM posted) = :month + if ($event->page_matches("popular_by_day")) { + $sql .= + "AND EXTRACT(MONTH FROM posted) = :month AND EXTRACT(DAY FROM posted) = :day"; - $args = array_merge($args, array("month" => $month, "day" => $day)); - $dte = array($totaldate, date("F jS, Y", (strtotime($totaldate))), "\\y\\e\\a\\r\\=Y\\&\\m\\o\\n\\t\\h\\=m\\&\\d\\a\\y\\=d", "day"); - } - else if($event->page_matches("popular_by_month")){ - $sql .= "AND EXTRACT(MONTH FROM posted) = :month"; + $args = array_merge($args, ["month" => $month, "day" => $day]); + $dte = [$totaldate, date("F jS, Y", (strtotime($totaldate))), "\\y\\e\\a\\r\\=Y\\&\\m\\o\\n\\t\\h\\=m\\&\\d\\a\\y\\=d", "day"]; + } elseif ($event->page_matches("popular_by_month")) { + $sql .= "AND EXTRACT(MONTH FROM posted) = :month"; - $args = array_merge($args, array("month" => $month)); - $dte = array($totaldate, date("F Y", (strtotime($totaldate))), "\\y\\e\\a\\r\\=Y\\&\\m\\o\\n\\t\\h\\=m", "month"); - } - else if($event->page_matches("popular_by_year")){ - $dte = array($totaldate, $year, "\\y\\e\\a\\r\=Y", "year"); - } - else { - // this should never happen due to the fact that the page event is already matched against earlier. - throw new UnexpectedValueException("Error: Invalid page event."); - } - $sql .= " AND NOT numeric_score=0 ORDER BY numeric_score DESC LIMIT :limit OFFSET 0"; + $args = array_merge($args, ["month" => $month]); + $dte = [$totaldate, date("F Y", (strtotime($totaldate))), "\\y\\e\\a\\r\\=Y\\&\\m\\o\\n\\t\\h\\=m", "month"]; + } elseif ($event->page_matches("popular_by_year")) { + $dte = [$totaldate, $year, "\\y\\e\\a\\r\=Y", "year"]; + } else { + // this should never happen due to the fact that the page event is already matched against earlier. + throw new UnexpectedValueException("Error: Invalid page event."); + } + $sql .= " AND NOT numeric_score=0 ORDER BY numeric_score DESC LIMIT :limit OFFSET 0"; - //filter images by score != 0 + date > limit to max images on one page > order from highest to lowest score + //filter images by score != 0 + date > limit to max images on one page > order from highest to lowest score - $result = $database->get_col($sql, $args); - $images = array(); - foreach($result as $id) { $images[] = Image::by_id($id); } + $result = $database->get_col($sql, $args); + $images = []; + foreach ($result as $id) { + $images[] = Image::by_id($id); + } - $this->theme->view_popular($images, $dte); - } - } + $this->theme->view_popular($images, $dte); + } + } - public function onNumericScoreSet(NumericScoreSetEvent $event) { - global $user; - log_debug("numeric_score", "Rated Image #{$event->image_id} as {$event->score}", true, array("image_id"=>$event->image_id)); - $this->add_vote($event->image_id, $user->id, $event->score); - } + public function onNumericScoreSet(NumericScoreSetEvent $event) + { + global $user; + log_debug("numeric_score", "Rated Image #{$event->image_id} as {$event->score}", "Rated Image"); + $this->add_vote($event->image_id, $user->id, $event->score); + } - public function onImageDeletion(ImageDeletionEvent $event) { - global $database; - $database->execute("DELETE FROM numeric_score_votes WHERE image_id=:id", array("id" => $event->image->id)); - } + public function onImageDeletion(ImageDeletionEvent $event) + { + global $database; + $database->execute("DELETE FROM numeric_score_votes WHERE image_id=:id", ["id" => $event->image->id]); + } - public function onUserDeletion(UserDeletionEvent $event) { - $this->delete_votes_by($event->id); - } + public function onUserDeletion(UserDeletionEvent $event) + { + $this->delete_votes_by($event->id); + } - /** - * @param int $user_id - */ - public function delete_votes_by($user_id) { - global $database; + public function delete_votes_by(int $user_id) + { + global $database; - $image_ids = $database->get_col("SELECT image_id FROM numeric_score_votes WHERE user_id=?", array($user_id)); + $image_ids = $database->get_col("SELECT image_id FROM numeric_score_votes WHERE user_id=:user_id", ['user_id'=>$user_id]); - if(count($image_ids) == 0) return; + if (count($image_ids) == 0) { + return; + } - // vote recounting is pretty heavy, and often hits statement timeouts - // if you try to recount all the images in one go - foreach(array_chunk($image_ids, 20) as $chunk) { - $id_list = implode(",", $chunk); - $database->execute( - "DELETE FROM numeric_score_votes WHERE user_id=? AND image_id IN (".$id_list.")", - array($user_id)); - $database->execute(" + // vote recounting is pretty heavy, and often hits statement timeouts + // if you try to recount all the images in one go + foreach (array_chunk($image_ids, 20) as $chunk) { + $id_list = implode(",", $chunk); + $database->execute( + "DELETE FROM numeric_score_votes WHERE user_id=:user_id AND image_id IN (".$id_list.")", + ['user_id'=>$user_id] + ); + $database->execute(" UPDATE images SET numeric_score=COALESCE( ( @@ -201,82 +206,114 @@ class NumericScore extends Extension { 0 ) WHERE images.id IN (".$id_list.")"); - } - } + } + } - public function onParseLinkTemplate(ParseLinkTemplateEvent $event) { - $event->replace('$score', $event->image->numeric_score); - } + public function onParseLinkTemplate(ParseLinkTemplateEvent $event) + { + $event->replace('$score', (string)$event->image->numeric_score); + } - public function onSearchTermParse(SearchTermParseEvent $event) { - $matches = array(); - if(preg_match("/^score([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(-?\d+)$/i", $event->term, $matches)) { - $cmp = ltrim($matches[1], ":") ?: "="; - $score = $matches[2]; - $event->add_querylet(new Querylet("numeric_score $cmp $score")); - } - else if(preg_match("/^upvoted_by[=|:](.*)$/i", $event->term, $matches)) { - $duser = User::by_name($matches[1]); - if(is_null($duser)) { - throw new SearchTermParseException( - "Can't find the user named ".html_escape($matches[1])); - } - $event->add_querylet(new Querylet( - "images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=1)", - array("ns_user_id"=>$duser->id))); - } - else if(preg_match("/^downvoted_by[=|:](.*)$/i", $event->term, $matches)) { - $duser = User::by_name($matches[1]); - if(is_null($duser)) { - throw new SearchTermParseException( - "Can't find the user named ".html_escape($matches[1])); - } - $event->add_querylet(new Querylet( - "images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=-1)", - array("ns_user_id"=>$duser->id))); - } - else if(preg_match("/^upvoted_by_id[=|:](\d+)$/i", $event->term, $matches)) { - $iid = int_escape($matches[1]); - $event->add_querylet(new Querylet( - "images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=1)", - array("ns_user_id"=>$iid))); - } - else if(preg_match("/^downvoted_by_id[=|:](\d+)$/i", $event->term, $matches)) { - $iid = int_escape($matches[1]); - $event->add_querylet(new Querylet( - "images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=-1)", - array("ns_user_id"=>$iid))); - } - else if(preg_match("/^order[=|:](?:numeric_)?(score)(?:_(desc|asc))?$/i", $event->term, $matches)){ - $default_order_for_column = "DESC"; - $sort = isset($matches[2]) ? strtoupper($matches[2]) : $default_order_for_column; - Image::$order_sql = "images.numeric_score $sort"; - $event->add_querylet(new Querylet("1=1")); //small hack to avoid metatag being treated as normal tag - } - } + public function onHelpPageBuilding(HelpPageBuildingEvent $event) + { + if ($event->key===HelpPages::SEARCH) { + $block = new Block(); + $block->header = "Numeric Score"; + $block->body = $this->theme->get_help_html(); + $event->add_block($block); + } + } - public function onTagTermParse(TagTermParseEvent $event) { - $matches = array(); + public function onSearchTermParse(SearchTermParseEvent $event) + { + if (is_null($event->term)) { + return; + } - if(preg_match("/^vote[=|:](up|down|remove)$/", $event->term, $matches) && $event->parse) { - global $user; - $score = ($matches[1] == "up" ? 1 : ($matches[1] == "down" ? -1 : 0)); - if(!$user->is_anonymous()) { - send_event(new NumericScoreSetEvent($event->id, $user, $score)); - } - } + $matches = []; + if (preg_match("/^score([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(-?\d+)$/i", $event->term, $matches)) { + $cmp = ltrim($matches[1], ":") ?: "="; + $score = $matches[2]; + $event->add_querylet(new Querylet("numeric_score $cmp $score")); + } elseif (preg_match("/^upvoted_by[=|:](.*)$/i", $event->term, $matches)) { + $duser = User::by_name($matches[1]); + if (is_null($duser)) { + throw new SearchTermParseException( + "Can't find the user named ".html_escape($matches[1]) + ); + } + $event->add_querylet(new Querylet( + "images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=1)", + ["ns_user_id"=>$duser->id] + )); + } elseif (preg_match("/^downvoted_by[=|:](.*)$/i", $event->term, $matches)) { + $duser = User::by_name($matches[1]); + if (is_null($duser)) { + throw new SearchTermParseException( + "Can't find the user named ".html_escape($matches[1]) + ); + } + $event->add_querylet(new Querylet( + "images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=-1)", + ["ns_user_id"=>$duser->id] + )); + } elseif (preg_match("/^upvoted_by_id[=|:](\d+)$/i", $event->term, $matches)) { + $iid = int_escape($matches[1]); + $event->add_querylet(new Querylet( + "images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=1)", + ["ns_user_id"=>$iid] + )); + } elseif (preg_match("/^downvoted_by_id[=|:](\d+)$/i", $event->term, $matches)) { + $iid = int_escape($matches[1]); + $event->add_querylet(new Querylet( + "images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=-1)", + ["ns_user_id"=>$iid] + )); + } elseif (preg_match("/^order[=|:](?:numeric_)?(score)(?:_(desc|asc))?$/i", $event->term, $matches)) { + $default_order_for_column = "DESC"; + $sort = isset($matches[2]) ? strtoupper($matches[2]) : $default_order_for_column; + Image::$order_sql = "images.numeric_score $sort"; + $event->add_querylet(new Querylet("1=1")); //small hack to avoid metatag being treated as normal tag + } + } - if(!empty($matches)) $event->metatag = true; - } + public function onTagTermCheck(TagTermCheckEvent $event) + { + if (preg_match("/^vote[=|:](up|down|remove)$/i", $event->term)) { + $event->metatag = true; + } + } - private function install() { - global $database; - global $config; + public function onTagTermParse(TagTermParseEvent $event) + { + $matches = []; - if($config->get_int("ext_numeric_score_version") < 1) { - $database->execute("ALTER TABLE images ADD COLUMN numeric_score INTEGER NOT NULL DEFAULT 0"); - $database->execute("CREATE INDEX images__numeric_score ON images(numeric_score)"); - $database->create_table("numeric_score_votes", " + if (preg_match("/^vote[=|:](up|down|remove)$/", $event->term, $matches)) { + global $user; + $score = ($matches[1] == "up" ? 1 : ($matches[1] == "down" ? -1 : 0)); + if (!$user->is_anonymous()) { + send_event(new NumericScoreSetEvent($event->image_id, $user, $score)); + } + } + } + + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + if ($event->parent=="posts") { + $event->add_nav_link("numeric_score_day", new Link('popular_by_day'), "Popular by Day"); + $event->add_nav_link("numeric_score_month", new Link('popular_by_month'), "Popular by Month"); + $event->add_nav_link("numeric_score_year", new Link('popular_by_year'), "Popular by Year"); + } + } + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $database; + + if ($this->get_version("ext_numeric_score_version") < 1) { + $database->execute("ALTER TABLE images ADD COLUMN numeric_score INTEGER NOT NULL DEFAULT 0"); + $database->execute("CREATE INDEX images__numeric_score ON images(numeric_score)"); + $database->create_table("numeric_score_votes", " image_id INTEGER NOT NULL, user_id INTEGER NOT NULL, score INTEGER NOT NULL, @@ -284,38 +321,36 @@ class NumericScore extends Extension { FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE "); - $database->execute("CREATE INDEX numeric_score_votes_image_id_idx ON numeric_score_votes(image_id)", array()); - $config->set_int("ext_numeric_score_version", 1); - } - if($config->get_int("ext_numeric_score_version") < 2) { - $database->execute("CREATE INDEX numeric_score_votes__user_votes ON numeric_score_votes(user_id, score)"); - $config->set_int("ext_numeric_score_version", 2); - } - } + $database->execute("CREATE INDEX numeric_score_votes_image_id_idx ON numeric_score_votes(image_id)", []); + $this->set_version("ext_numeric_score_version", 1); + } + if ($this->get_version("ext_numeric_score_version") < 2) { + $database->execute("CREATE INDEX numeric_score_votes__user_votes ON numeric_score_votes(user_id, score)"); + $this->set_version("ext_numeric_score_version", 2); + } + } - /** - * @param int $image_id - * @param int $user_id - * @param int $score - */ - private function add_vote($image_id, $user_id, $score) { - global $database; - $database->execute( - "DELETE FROM numeric_score_votes WHERE image_id=:imageid AND user_id=:userid", - array("imageid" => $image_id, "userid" => $user_id)); - if($score != 0) { - $database->execute( - "INSERT INTO numeric_score_votes(image_id, user_id, score) VALUES(:imageid, :userid, :score)", - array("imageid" => $image_id, "userid" => $user_id, "score" => $score)); - } - $database->Execute( - "UPDATE images SET numeric_score=( + private function add_vote(int $image_id, int $user_id, int $score) + { + global $database; + $database->execute( + "DELETE FROM numeric_score_votes WHERE image_id=:imageid AND user_id=:userid", + ["imageid" => $image_id, "userid" => $user_id] + ); + if ($score != 0) { + $database->execute( + "INSERT INTO numeric_score_votes(image_id, user_id, score) VALUES(:imageid, :userid, :score)", + ["imageid" => $image_id, "userid" => $user_id, "score" => $score] + ); + } + $database->Execute( + "UPDATE images SET numeric_score=( COALESCE( (SELECT SUM(score) FROM numeric_score_votes WHERE image_id=:imageid), 0 ) ) WHERE id=:id", - array("imageid" => $image_id, "id" => $image_id)); - } + ["imageid" => $image_id, "id" => $image_id] + ); + } } - diff --git a/ext/numeric_score/test.php b/ext/numeric_score/test.php index f492acdb..ac0d1f4b 100644 --- a/ext/numeric_score/test.php +++ b/ext/numeric_score/test.php @@ -1,60 +1,57 @@ -log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - $this->get_page("post/view/$image_id"); +markTestIncomplete(); + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + $this->get_page("post/view/$image_id"); + $this->assert_text("Current Score: 0"); - $this->assert_text("Current Score: 0"); - $this->click("Vote Down"); - $this->assert_text("Current Score: -1"); - $this->click("Vote Up"); - $this->assert_text("Current Score: 1"); - # FIXME: "remove vote" button? - # FIXME: test that up and down are hidden if already voted up or down + send_event(new NumericScoreSetEvent($image_id, $user, -1)); + $this->get_page("post/view/$image_id"); + $this->assert_text("Current Score: -1"); - # test search by score - $this->get_page("post/list/score=1/1"); - $this->assert_title("Image $image_id: pbx"); + send_event(new NumericScoreSetEvent($image_id, $user, 1)); + $this->get_page("post/view/$image_id"); + $this->assert_text("Current Score: 1"); - $this->get_page("post/list/score>0/1"); - $this->assert_title("Image $image_id: pbx"); + # FIXME: test that up and down are hidden if already voted up or down - $this->get_page("post/list/score>-5/1"); - $this->assert_title("Image $image_id: pbx"); + # test search by score + $page = $this->get_page("post/list/score=1/1"); + $this->assertEquals(PageMode::REDIRECT, $page->mode); - $this->get_page("post/list/-score>5/1"); - $this->assert_title("Image $image_id: pbx"); + $page = $this->get_page("post/list/score>0/1"); + $this->assertEquals(PageMode::REDIRECT, $page->mode); - $this->get_page("post/list/-score<-5/1"); - $this->assert_title("Image $image_id: pbx"); + $page = $this->get_page("post/list/score>-5/1"); + $this->assertEquals(PageMode::REDIRECT, $page->mode); - # test search by vote - $this->get_page("post/list/upvoted_by=test/1"); - $this->assert_title("Image $image_id: pbx"); - $this->assert_no_text("No Images Found"); + $page = $this->get_page("post/list/-score>5/1"); + $this->assertEquals(PageMode::REDIRECT, $page->mode); - # and downvote - $this->get_page("post/list/downvoted_by=test/1"); - $this->assert_text("No Images Found"); + $page = $this->get_page("post/list/-score<-5/1"); + $this->assertEquals(PageMode::REDIRECT, $page->mode); - # test errors - $this->get_page("post/list/upvoted_by=asdfasdf/1"); - $this->assert_text("No Images Found"); - $this->get_page("post/list/downvoted_by=asdfasdf/1"); - $this->assert_text("No Images Found"); - $this->get_page("post/list/upvoted_by_id=0/1"); - $this->assert_text("No Images Found"); - $this->get_page("post/list/downvoted_by_id=0/1"); - $this->assert_text("No Images Found"); + # test search by vote + $page = $this->get_page("post/list/upvoted_by=test/1"); + $this->assertEquals(PageMode::REDIRECT, $page->mode); - $this->log_out(); + # and downvote + $page = $this->get_page("post/list/downvoted_by=test/1"); + $this->assertEquals(404, $page->code); - $this->log_in_as_admin(); - $this->delete_image($image_id); - $this->log_out(); - } + # test errors + $page = $this->get_page("post/list/upvoted_by=asdfasdf/1"); + $this->assertEquals(404, $page->code); + $page = $this->get_page("post/list/downvoted_by=asdfasdf/1"); + $this->assertEquals(404, $page->code); + $page = $this->get_page("post/list/upvoted_by_id=0/1"); + $this->assertEquals(404, $page->code); + $page = $this->get_page("post/list/downvoted_by_id=0/1"); + $this->assertEquals(404, $page->code); + } } - diff --git a/ext/numeric_score/theme.php b/ext/numeric_score/theme.php index d1a9f38b..da9b1d9c 100644 --- a/ext/numeric_score/theme.php +++ b/ext/numeric_score/theme.php @@ -1,12 +1,17 @@ -id); - $i_score = int_escape($image->numeric_score); +class NumericScoreTheme extends Themelet +{ + public function get_voter(Image $image) + { + global $user, $page; + $i_image_id = $image->id; + if (is_string($image->numeric_score)) { + $image->numeric_score = (int)$image->numeric_score; + } + $i_score = $image->numeric_score; - $html = " + $html = " Current Score: $i_score

    @@ -30,8 +35,8 @@ class NumericScoreTheme extends Themelet { "; - if($user->can("edit_other_vote")) { - $html .= " + if ($user->can(Permissions::EDIT_OTHER_VOTE)) { + $html .= "
    ".$user->get_auth_html()." @@ -45,48 +50,88 @@ class NumericScoreTheme extends Themelet { >See All Votes "; - } - $page->add_block(new Block("Image Score", $html, "left", 20)); - } + } + $page->add_block(new Block("Image Score", $html, "left", 20)); + } - public function get_nuller(User $duser) { - global $user, $page; - $html = " + public function get_nuller(User $duser) + { + global $user, $page; + $html = " ".$user->get_auth_html()." "; - $page->add_block(new Block("Votes", $html, "main", 80)); - } + $page->add_block(new Block("Votes", $html, "main", 80)); + } - public function view_popular($images, $dte) { - global $page, $config; + public function view_popular($images, $dte) + { + global $page, $config; - $pop_images = ""; - foreach($images as $image) { - $pop_images .= $this->build_thumb_html($image)."\n"; - } + $pop_images = ""; + foreach ($images as $image) { + $pop_images .= $this->build_thumb_html($image)."\n"; + } - $b_dte = make_link("popular_by_".$dte[3]."?".date($dte[2], (strtotime('-1 '.$dte[3], strtotime($dte[0]))))); - $f_dte = make_link("popular_by_".$dte[3]."?".date($dte[2], (strtotime('+1 '.$dte[3], strtotime($dte[0]))))); + $b_dte = make_link("popular_by_".$dte[3]."?".date($dte[2], (strtotime('-1 '.$dte[3], strtotime($dte[0]))))); + $f_dte = make_link("popular_by_".$dte[3]."?".date($dte[2], (strtotime('+1 '.$dte[3], strtotime($dte[0]))))); - $html = "\n". - "

    \n". - "

    \n". - " « {$dte[1]} »\n". - "

    \n". - "
    \n". - "
    \n".$pop_images; + $html = "\n". + "

    \n". + " « {$dte[1]} »\n". + "

    \n". + "
    \n".$pop_images; - $nav_html = "Index"; + $nav_html = "Index"; - $page->set_heading($config->get_string('title')); - $page->add_block(new Block("Navigation", $nav_html, "left", 10)); - $page->add_block(new Block(null, $html, "main", 30)); - } + $page->set_heading($config->get_string(SetupConfig::TITLE)); + $page->add_block(new Block("Navigation", $nav_html, "left", 10)); + $page->add_block(new Block(null, $html, "main", 30)); + } + + + public function get_help_html() + { + return '

    Search for images that have received numeric scores by the score or by the scorer.

    +
    +
    score=1
    +

    Returns images with a score of 1.

    +
    +
    +
    score>0
    +

    Returns images with a score of 1 or more.

    +
    +

    Can use <, <=, >, >=, or =.

    + +
    +
    upvoted_by=username
    +

    Returns images upvoted by "username".

    +
    +
    +
    upvoted_by_id=123
    +

    Returns images upvoted by user 123.

    +
    +
    +
    downvoted_by=username
    +

    Returns images downvoted by "username".

    +
    +
    +
    downvoted_by_id=123
    +

    Returns images downvoted by user 123.

    +
    + +
    +
    order:score_desc
    +

    Sorts the search results by score, descending.

    +
    +
    +
    order:score_asc
    +

    Sorts the search results by score, ascending.

    +
    + '; + } } - - diff --git a/ext/oekaki/chibipaint.jar b/ext/oekaki/chibipaint.jar deleted file mode 100644 index 81cbc78b..00000000 Binary files a/ext/oekaki/chibipaint.jar and /dev/null differ diff --git a/ext/oekaki/license.txt b/ext/oekaki/license.txt deleted file mode 100644 index 94a9ed02..00000000 --- a/ext/oekaki/license.txt +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. 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 -them 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 prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. 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. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey 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; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If 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 convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU 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 that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - 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. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -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. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - 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 -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - 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 3 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, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program 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, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU 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. But first, please read -. diff --git a/ext/oekaki/main.php b/ext/oekaki/main.php deleted file mode 100644 index c26c1019..00000000 --- a/ext/oekaki/main.php +++ /dev/null @@ -1,90 +0,0 @@ -page_matches("oekaki")) { - if($user->can("create_image")) { - if($event->get_arg(0) == "create") { - $this->theme->display_page(); - $this->theme->display_block(); - } - if($event->get_arg(0) == "claim") { - // FIXME: move .chi to data/oekaki/$ha/$hash mirroring images and thumbs - // FIXME: .chi viewer? - // FIXME: clean out old unclaimed images? - $pattern = data_path('oekaki_unclaimed/' . $_SERVER['REMOTE_ADDR'] . ".*.png"); - foreach(glob($pattern) as $tmpname) { - assert(file_exists($tmpname)); - - $pathinfo = pathinfo($tmpname); - if(!array_key_exists('extension', $pathinfo)) { - throw new UploadException("File has no extension"); - } - log_info("oekaki", "Processing file [{$pathinfo['filename']}]"); - $metadata = array(); - $metadata['filename'] = 'oekaki.png'; - $metadata['extension'] = $pathinfo['extension']; - $metadata['tags'] = Tag::explode('oekaki tagme'); - $metadata['source'] = null; - $duev = new DataUploadEvent($tmpname, $metadata); - send_event($duev); - if($duev->image_id == -1) { - throw new UploadException("File type not recognised"); - } - else { - unlink($tmpname); - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/".$duev->image_id)); - } - } - } - } - if($event->get_arg(0) == "upload") { - // FIXME: this allows anyone to upload anything to /data ... - // hardcoding the ext to .png should stop the obvious exploit, - // but more checking may be wise - if(isset($_FILES["picture"])) { - header('Content-type: text/plain'); - - $file = $_FILES['picture']['name']; - //$ext = (strpos($file, '.') === FALSE) ? '' : substr($file, strrpos($file, '.')); - $uploadname = $_SERVER['REMOTE_ADDR'] . "." . time(); - $uploadfile = data_path('oekaki_unclaimed/'.$uploadname); - - log_info("oekaki", "Uploading file [$uploadname]"); - - $success = TRUE; - if (isset($_FILES["chibifile"])) - $success = $success && move_uploaded_file($_FILES['chibifile']['tmp_name'], $uploadfile . ".chi"); - - // hardcode the ext, so nobody can upload "foo.php" - $success = $success && move_uploaded_file($_FILES['picture']['tmp_name'], $uploadfile . ".png"); # $ext); - if ($success) { - echo "CHIBIOK\n"; - } else { - echo "CHIBIERROR\n"; - } - } - else { - echo "CHIBIERROR No Data\n"; - } - } - } - } - - // FIXME: "edit this image" button on existing images? - function onPostListBuilding(PostListBuildingEvent $event) { - global $user; - if($user->can("create_image")) { - $this->theme->display_block(); - } - } -} - diff --git a/ext/oekaki/readme.txt b/ext/oekaki/readme.txt deleted file mode 100644 index b5e292b9..00000000 --- a/ext/oekaki/readme.txt +++ /dev/null @@ -1,124 +0,0 @@ - ChibiPaint - - Original version of ChibiPaint: - Copyright (c) 2006-2008 Marc Schefer - http://www.chibipaint.com/ - - Some icons taken from the GNU Image Manipulation Program. - Art contributors: http://git.gnome.org/browse/gimp/tree/AUTHORS - Lapo Calamandrei - Paul Davey - Alexia Death - Aurore Derriennic - Tuomas Kuosmanen - Karl La Rocca - Andreas Nilsson - Ville Pätsi - Mike Schaeffer - Carol Spears - Jakub Steiner - William Szilveszter - - - This file is part of ChibiPaint. - - ChibiPaint 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 3 of the License, or - (at your option) any later version. - - ChibiPaint 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 ChibiPaint. If not, see . - - CHIBIPAINT - - ChibiPaint is an oekaki applet. A software that allows people to draw and paint online and share the result with - other art enthusiasts. It's designed to be integrated with an oekaki board, a web server running dedicated software. - Several are available but we don't currently provide an integrated solution for ChibiPaint. - - INTEGRATION - - ChibiPaint is still in the alpha stage of its development and the following integration specs are likely to evolve - in the future. - - APPLET PARAMETERS - - Here's an example on how to integrate the applet in a html webpage - - - - - - - - - - JAVA NOT SUPPORTED! - - - The parameters are: - canvasWidth - width of the area on which users can draw (currently capped to 1024) - canvasHeight - height of the area on which users can draw (currently capped to 1024) - - postUrl - url that will be used to post the resulting files (see below for more details) - exitUrl - after sending the oekaki the user will be redirected to that url - exitUrlTarget - optional target to allow different frames configuration - - loadImage - an image (png format) that will be loaded in the applet to be edited - loadChibiFile - a chibifile format (.chi) multi-layer image that will be loaded in the applet to be edited - - NOTE: The last two parameters can be omited when they don't apply. If both loadImage and loadChibiFile are specified, - loadChibiFile takes precedence - - POST FORMAT - - The applet will send the resulting png file and optionally a multi-layer chibifile format file. - - The files are sent as a regular multipart HTTP POST file upload, similar to the one used by form based file uploads - for ease of processing by the server side script. - - The form data name for the png file is 'picture' and 'chibifile' for the multilayer file. The recommended extension - for chibifiles is '.chi' - - The applet expects the server to answer with the single line reply "CHIBIOK" followed by a newline character. - - "CHIBIERROR" followed by an error message on the same list is the planned way to report an error but currently the - applet will just ignore the error message and report a failure on any reply except CHIBIOK. - - PHP EXAMPLE - - Here's an example of how a php script might handle the applet's POST - - - - CONTACT INFORMATION - - Author: Marc Schefer (codexus@codexus.com) diff --git a/ext/oekaki/test.php b/ext/oekaki/test.php deleted file mode 100644 index 1061595c..00000000 --- a/ext/oekaki/test.php +++ /dev/null @@ -1,7 +0,0 @@ -log_in_as_user(); - $this->get_page("oekaki/create"); - } -} diff --git a/ext/oekaki/theme.php b/ext/oekaki/theme.php deleted file mode 100644 index 8a0ee9b9..00000000 --- a/ext/oekaki/theme.php +++ /dev/null @@ -1,64 +0,0 @@ -get_int("oekaki_width", 400); - $oekH = $config->get_int("oekaki_height", 400); - if(isset($_POST['oekW']) && isset($_POST['oekH'])) { - $oekW = int_escape($_POST['oekW']); - $oekH = int_escape($_POST['oekH']); - } - - $html = " - - - - - - - JAVA NOT INSTALLED :( - - "; - -# -# - // FIXME: prevent oekaki block from collapsing on click in cerctain themes. This causes canvas reset - $page->set_title("Oekaki"); - $page->set_heading("Oekaki"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Oekaki", $html, "main", 20)); - } - - public function display_block() { - global $config, $page; - //FIXME: input field alignment could be done more elegantly, without inline styling - //FIXME: autocomplete='off' seems to be an invalid HTML tag - - $oekW = $config->get_int("oekaki_width", 400); - $oekH = $config->get_int("oekaki_height", 400); - if(isset($_POST['oekW']) && isset($_POST['oekH'])) { - $oekW = int_escape($_POST['oekW']); - $oekH = int_escape($_POST['oekH']); - } - - $page->add_block(new Block("Oekaki", - " -
    - ". - "x". - "". - " - - " - , "left", 21)); // upload is 20 - } -} - diff --git a/ext/ouroboros_api/info.php b/ext/ouroboros_api/info.php new file mode 100644 index 00000000..9ff2b872 --- /dev/null +++ b/ext/ouroboros_api/info.php @@ -0,0 +1,32 @@ +"diftraku[at]derpy.me"]; + public $description = "Ouroboros-like API for Shimmie"; + public $version = "0.2"; + public $documentation = +"Currently working features +
      +
    • Post: +
        +
      • Index/List
      • +
      • Show
      • +
      • Create
      • +
      +
    • +
    • Tag: +
        +
      • Index/List
      • +
      +
    • +
    +Tested to work with CartonBox using \"Danbooru 1.18.x\" as site type. +Does not work with Andbooru or Danbooru Gallery for reasons beyond me, took me a while to figure rating \"u\" is bad... +Lots of Ouroboros/Danbooru specific values use their defaults (or what I gathered them to be default) +and tons of stuff not supported directly in Shimmie is botched to work"; +} diff --git a/ext/ouroboros_api/main.php b/ext/ouroboros_api/main.php index 963832a2..f0f983bf 100644 --- a/ext/ouroboros_api/main.php +++ b/ext/ouroboros_api/main.php @@ -1,31 +1,4 @@ - - * Description: Ouroboros-like API for Shimmie - * Version: 0.2 - * Documentation: - * Currently working features - *
      - *
    • Post: - *
        - *
      • Index/List
      • - *
      • Show
      • - *
      • Create
      • - *
      - *
    • - *
    • Tag: - *
        - *
      • Index/List
      • - *
      - *
    • - *
    - * Tested to work with CartonBox using "Danbooru 1.18.x" as site type. - * Does not work with Andbooru or Danbooru Gallery for reasons beyond me, took me a while to figure rating "u" is bad... - * Lots of Ouroboros/Danbooru specific values use their defaults (or what I gathered them to be default) - * and tons of stuff not supported directly in Shimmie is botched to work - */ +change = intval($img->id); //DaFug is this even supposed to do? ChangeID? // Should be JSON specific, just strip this when converting to XML - $this->created_at = array('n' => 123456789, 's' => strtotime($img->posted), 'json_class' => 'Time'); + $this->created_at = ['n' => 123456789, 's' => strtotime($img->posted), 'json_class' => 'Time']; $this->id = intval($img->id); $this->parent_id = null; - if (defined('ENABLED_EXTS')) { - if (strstr(ENABLED_EXTS, 'rating') !== false) { - // 'u' is not a "valid" rating - if ($img->rating == 's' || $img->rating == 'q' || $img->rating == 'e') { - $this->rating = $img->rating; - } - } - if (strstr(ENABLED_EXTS, 'numeric_score') !== false) { - $this->score = $img->numeric_score; + + if (Extension::is_enabled(RatingsInfo::KEY)!== false) { + // 'u' is not a "valid" rating + if ($img->rating == 's' || $img->rating == 'q' || $img->rating == 'e') { + $this->rating = $img->rating; } } + if (Extension::is_enabled(NumericScoreInfo::KEY)!== false) { + $this->score = $img->numeric_score; + } + $this->source = $img->source; $this->status = 'active'; //not supported in Shimmie... yet $this->tags = $img->get_tag_list(); @@ -235,8 +204,8 @@ class _SafeOuroborosImage $this->has_notes = false; // thumb - $this->preview_height = $config->get_int('thumb_height'); - $this->preview_width = $config->get_int('thumb_width'); + $this->preview_height = $config->get_int(ImageConfig::THUMB_HEIGHT); + $this->preview_width = $config->get_int(ImageConfig::THUMB_WIDTH); $this->preview_url = make_http($img->get_thumb_link()); // sample (use the full image here) @@ -252,7 +221,7 @@ class OuroborosPost extends _SafeOuroborosImage * Multipart File * @var array */ - public $file = array(); + public $file = []; /** * Create with rating locked @@ -270,10 +239,9 @@ class OuroborosPost extends _SafeOuroborosImage /** * Initialize an OuroborosPost for creation * Mainly just acts as a wrapper and validation layer - * @param array $post - * @param string $md5 + * @noinspection PhpMissingParentConstructorInspection */ - public function __construct(array $post, $md5 = '') + public function __construct(array $post, string $md5 = '') { if (array_key_exists('tags', $post)) { // implode(explode()) to resolve aliases and sanitise @@ -410,22 +378,20 @@ class OuroborosAPI extends Extension } elseif ($this->type == 'xml') { $page->set_type('text/xml; charset=utf-8'); } - $page->set_mode('data'); + $page->set_mode(PageMode::DATA); $this->tryAuth(); if ($event->page_matches('post')) { if ($this->match('create')) { // Create - if ($user->can("create_image")) { + if ($user->can(Permissions::CREATE_IMAGE)) { $md5 = !empty($_REQUEST['md5']) ? filter_var($_REQUEST['md5'], FILTER_SANITIZE_STRING) : null; $this->postCreate(new OuroborosPost($_REQUEST['post']), $md5); } else { $this->sendResponse(403, 'You cannot create new posts'); } - } elseif ($this->match('update')) { - // Update - //@todo add post update + throw new SCoreException("update not implemented"); } elseif ($this->match('show')) { // Show $id = !empty($_REQUEST['id']) ? filter_var($_REQUEST['id'], FILTER_SANITIZE_NUMBER_INT) : null; @@ -438,8 +404,8 @@ class OuroborosAPI extends Extension $p = !empty($_REQUEST['page']) ? intval( filter_var($_REQUEST['page'], FILTER_SANITIZE_NUMBER_INT) ) : 1; - $tags = !empty($_REQUEST['tags']) ? filter_var($_REQUEST['tags'], FILTER_SANITIZE_STRING) : array(); - if (!empty($tags)) { + $tags = !empty($_REQUEST['tags']) ? filter_var($_REQUEST['tags'], FILTER_SANITIZE_STRING) : []; + if (is_string($tags)) { $tags = Tag::explode($tags); } $this->postIndex($limit, $p, $tags); @@ -471,12 +437,11 @@ class OuroborosAPI extends Extension } } } elseif ($event->page_matches('post/show')) { - $page->set_mode('redirect'); + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link(str_replace('post/show', 'post/view', implode('/', $event->args)))); $page->display(); die(); } - } /** @@ -485,33 +450,29 @@ class OuroborosAPI extends Extension /** * Wrapper for post creation - * @param OuroborosPost $post - * @param string $md5 */ - protected function postCreate(OuroborosPost $post, $md5 = '') + protected function postCreate(OuroborosPost $post, ?string $md5 = '') { global $config; - $handler = $config->get_string("upload_collision_handler"); - if (!empty($md5) && !($handler == 'merge')) { + $handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER); + if (!empty($md5) && !($handler == ImageConfig::COLLISION_MERGE)) { $img = Image::by_hash($md5); if (!is_null($img)) { $this->sendResponse(420, self::ERROR_POST_CREATE_DUPE); return; } } - $meta = array(); + $meta = []; $meta['tags'] = is_array($post->tags) ? $post->tags : Tag::explode($post->tags); $meta['source'] = $post->source; - if (defined('ENABLED_EXTS')) { - if (strstr(ENABLED_EXTS, 'rating') !== false) { - $meta['rating'] = $post->rating; - } + if (Extension::is_enabled(RatingsInfo::KEY)!== false) { + $meta['rating'] = $post->rating; } // Check where we should try for the file if (empty($post->file) && !empty($post->file_url) && filter_var( - $post->file_url, - FILTER_VALIDATE_URL - ) !== false + $post->file_url, + FILTER_VALIDATE_URL + ) !== false ) { // Transload from source $meta['file'] = tempnam('/tmp', 'shimmie_transload_' . $config->get_string('transload_engine')); @@ -534,20 +495,19 @@ class OuroborosAPI extends Extension if (!empty($meta['hash'])) { $img = Image::by_hash($meta['hash']); if (!is_null($img)) { - $handler = $config->get_string("upload_collision_handler"); - if($handler == "merge") { + $handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER); + if ($handler == ImageConfig::COLLISION_MERGE) { $postTags = is_array($post->tags) ? $post->tags : Tag::explode($post->tags); $merged = array_merge($postTags, $img->get_tag_array()); send_event(new TagSetEvent($img, $merged)); // This is really the only thing besides tags we should care - if(isset($meta['source'])){ + if (isset($meta['source'])) { send_event(new SourceSetEvent($img, $meta['source'])); } $this->sendResponse(200, self::OK_POST_CREATE_UPDATE . ' ID: ' . $img->id); return; - } - else { + } else { $this->sendResponse(420, self::ERROR_POST_CREATE_DUPE); return; } @@ -575,13 +535,12 @@ class OuroborosAPI extends Extension /** * Wrapper for getting a single post - * @param int $id */ - protected function postShow($id = null) + protected function postShow(int $id = null) { if (!is_null($id)) { $post = new _SafeOuroborosImage(Image::by_id($id)); - $this->sendData('post', $post); + $this->sendData('post', [$post]); } else { $this->sendResponse(424, 'ID is mandatory'); } @@ -589,15 +548,13 @@ class OuroborosAPI extends Extension /** * Wrapper for getting a list of posts - * @param int $limit - * @param int $page - * @param string[] $tags + * #param string[] $tags */ - protected function postIndex($limit, $page, $tags) + protected function postIndex(int $limit, int $page, array $tags) { $start = ($page - 1) * $limit; $results = Image::find_images(max($start, 0), min($limit, 100), $tags); - $posts = array(); + $posts = []; foreach ($results as $img) { if (!is_object($img)) { continue; @@ -611,37 +568,25 @@ class OuroborosAPI extends Extension * Tag */ - /** - * Wrapper for getting a list of tags - * @param int $limit - * @param int $page - * @param string $order - * @param int $id - * @param int $after_id - * @param string $name - * @param string $name_pattern - */ - protected function tagIndex($limit, $page, $order, $id, $after_id, $name, $name_pattern) + protected function tagIndex(int $limit, int $page, string $order, int $id, int $after_id, string $name, string $name_pattern) { global $database, $config; $start = ($page - 1) * $limit; - $tag_data = array(); switch ($order) { case 'name': $tag_data = $database->get_col( - $database->scoreql_to_sql( - " - SELECT DISTINCT - id, SCORE_STRNORM(substr(tag, 1, 1)), count - FROM tags - WHERE count >= :tags_min - ORDER BY SCORE_STRNORM(substr(tag, 1, 1)) LIMIT :start, :max_items - " - ), - array('tags_min' => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit) + " + SELECT DISTINCT + id, LOWER(substr(tag, 1, 1)), count + FROM tags + WHERE count >= :tags_min + ORDER BY LOWER(substr(tag, 1, 1)) LIMIT :start, :max_items + ", + ['tags_min' => $config->get_int(TagListConfig::TAGS_MIN), 'start' => $start, 'max_items' => $limit] ); break; case 'count': + default: $tag_data = $database->get_all( " SELECT id, tag, count @@ -649,22 +594,11 @@ class OuroborosAPI extends Extension WHERE count >= :tags_min ORDER BY count DESC, tag ASC LIMIT :start, :max_items ", - array('tags_min' => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit) - ); - break; - case 'date': - $tag_data = $database->get_all( - " - SELECT id, tag, count - FROM tags - WHERE count >= :tags_min - ORDER BY count DESC, tag ASC LIMIT :start, :max_items - ", - array('tags_min' => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit) + ['tags_min' => $config->get_int(TagListConfig::TAGS_MIN), 'start' => $start, 'max_items' => $limit] ); break; } - $tags = array(); + $tags = []; foreach ($tag_data as $tag) { if (!is_array($tag)) { continue; @@ -680,12 +614,8 @@ class OuroborosAPI extends Extension /** * Sends a simple {success,reason} message to browser - * - * @param int $code HTTP equivalent code for the message - * @param string $reason Reason for the code - * @param bool $location Is $reason a location? (used mainly for post/create) */ - private function sendResponse($code = 200, $reason = '', $location = false) + private function sendResponse(int $code = 200, string $reason = '', bool $location = false) { global $page; if ($code == 200) { @@ -711,7 +641,7 @@ class OuroborosAPI extends Extension } header("{$proto} {$code} {$header}", true); } - $response = array('success' => $success, 'reason' => $reason); + $response = ['success' => $success, 'reason' => $reason]; if ($this->type == 'json') { if ($location !== false) { $response['location'] = $response['reason']; @@ -738,13 +668,7 @@ class OuroborosAPI extends Extension $page->set_data($response); } - /** - * Send data to the browser - * @param string $type - * @param mixed $data - * @param int $offset - */ - private function sendData($type = '', $data = array(), $offset = 0) + private function sendData(string $type = '', array $data = [], int $offset = 0) { global $page; $response = ''; @@ -777,10 +701,7 @@ class OuroborosAPI extends Extension $page->set_data($response); } - /** - * @param string $type - */ - private function createItemXML(XMLWriter &$xml, $type, $item) + private function createItemXML(XMLWriter &$xml, string $type, $item) { $xml->startElement($type); foreach ($item as $key => $val) { @@ -801,8 +722,6 @@ class OuroborosAPI extends Extension * * Currently checks for either user & session in request or cookies * and initializes a global User - * @param void - * @return void */ private function tryAuth() { @@ -818,6 +737,7 @@ class OuroborosAPI extends Extension } else { $user = User::by_id($config->get_int("anon_id", 0)); } + send_event(new UserLoginEvent($user)); } elseif (isset($_COOKIE[$config->get_string('cookie_prefix', 'shm') . '_' . 'session']) && isset($_COOKIE[$config->get_string('cookie_prefix', 'shm') . '_' . 'user']) ) { @@ -830,15 +750,14 @@ class OuroborosAPI extends Extension } else { $user = User::by_id($config->get_int("anon_id", 0)); } + send_event(new UserLoginEvent($user)); } } /** * Helper for matching API methods from event - * @param string $page - * @return bool */ - private function match($page) + private function match(string $page): bool { return (preg_match("%{$page}\.(xml|json)$%", implode('/', $this->event->args), $matches) === 1); } diff --git a/ext/pm/info.php b/ext/pm/info.php new file mode 100644 index 00000000..79b83204 --- /dev/null +++ b/ext/pm/info.php @@ -0,0 +1,17 @@ + - * License: GPLv2 - * Description: Allow users to send messages to eachother - * Documentation: - * PMs show up on a user's profile page, readable by that user - * as well as board admins. To send a PM, visit another user's - * profile page and a box will be shown. - */ +pm = $pm; - } + public function __construct(PM $pm) + { + parent::__construct(); + $this->pm = $pm; + } } -class PM { - public $id, $from_id, $from_ip, $to_id, $sent_date, $subject, $message, $is_read; +class PM +{ + /** @var int */ + public $id; + /** @var int */ + public $from_id; + /** @var string */ + public $from_ip; + /** @var int */ + public $to_id; + /** @var mixed */ + public $sent_date; + /** @var string */ + public $subject; + /** @var string */ + public $message; + /** @var bool */ + public $is_read; - public function __construct($from_id=0, $from_ip="0.0.0.0", $to_id=0, $subject="A Message", $message="Some Text", $read=False) { - # PHP: the P stands for "really", the H stands for "awful" and the other P stands for "language" - if(is_array($from_id)) { - $a = $from_id; - $this->id = $a["id"]; - $this->from_id = $a["from_id"]; - $this->from_ip = $a["from_ip"]; - $this->to_id = $a["to_id"]; - $this->sent_date = $a["sent_date"]; - $this->subject = $a["subject"]; - $this->message = $a["message"]; - $this->is_read = bool_escape($a["is_read"]); - } - else { - $this->id = -1; - $this->from_id = $from_id; - $this->from_ip = $from_ip; - $this->to_id = $to_id; - $this->subject = $subject; - $this->message = $message; - $this->is_read = $read; - } - } + public function __construct($from_id=0, string $from_ip="0.0.0.0", int $to_id=0, string $subject="A Message", string $message="Some Text", bool $read=false) + { + # PHP: the P stands for "really", the H stands for "awful" and the other P stands for "language" + if (is_array($from_id)) { + $a = $from_id; + $this->id = (int)$a["id"]; + $this->from_id = (int)$a["from_id"]; + $this->from_ip = $a["from_ip"]; + $this->to_id = (int)$a["to_id"]; + $this->sent_date = $a["sent_date"]; + $this->subject = $a["subject"]; + $this->message = $a["message"]; + $this->is_read = bool_escape($a["is_read"]); + } else { + $this->id = -1; + $this->from_id = $from_id; + $this->from_ip = $from_ip; + $this->to_id = $to_id; + $this->subject = $subject; + $this->message = $message; + $this->is_read = $read; + } + } } -class PrivMsg extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config, $database; +class PrivMsg extends Extension +{ + /** @var PrivMsgTheme */ + protected $theme; - // shortcut to latest - if($config->get_int("pm_version") < 1) { - $database->create_table("private_message", " + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $config, $database; + + // shortcut to latest + if ($config->get_int("pm_version") < 1) { + $database->create_table("private_message", " id SCORE_AIPK, from_id INTEGER NOT NULL, from_ip SCORE_INET NOT NULL, to_id INTEGER NOT NULL, - sent_date SCORE_DATETIME NOT NULL, + sent_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, subject VARCHAR(64) NOT NULL, message TEXT NOT NULL, is_read SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N, FOREIGN KEY (from_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (to_id) REFERENCES users(id) ON DELETE CASCADE "); - $database->execute("CREATE INDEX private_message__to_id ON private_message(to_id)"); - $config->set_int("pm_version", 2); - log_info("pm", "extension installed"); - } + $database->execute("CREATE INDEX private_message__to_id ON private_message(to_id)"); + $config->set_int("pm_version", 2); + log_info("pm", "extension installed"); + } - if($config->get_int("pm_version") < 2) { - log_info("pm", "Adding foreign keys to private messages"); - $database->Execute("delete from private_message where to_id not in (select id from users);"); - $database->Execute("delete from private_message where from_id not in (select id from users);"); - $database->Execute("ALTER TABLE private_message + if ($config->get_int("pm_version") < 2) { + log_info("pm", "Adding foreign keys to private messages"); + $database->Execute("delete from private_message where to_id not in (select id from users);"); + $database->Execute("delete from private_message where from_id not in (select id from users);"); + $database->Execute("ALTER TABLE private_message ADD FOREIGN KEY (from_id) REFERENCES users(id) ON DELETE CASCADE, ADD FOREIGN KEY (to_id) REFERENCES users(id) ON DELETE CASCADE;"); - $config->set_int("pm_version", 2); - log_info("pm", "extension installed"); - } - } + $config->set_int("pm_version", 2); + log_info("pm", "extension installed"); + } + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if(!$user->is_anonymous()) { - $count = $this->count_pms($user); - $h_count = $count > 0 ? " ($count)" : ""; - $event->add_link("Private Messages$h_count", make_link("user#private-messages")); - } - } + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="user") { + if ($user->can(Permissions::READ_PM)) { + $count = $this->count_pms($user); + $h_count = $count > 0 ? " ($count)" : ""; + $event->add_nav_link("pm", new Link('user#private-messages'), "Private Messages$h_count"); + } + } + } - public function onUserPageBuilding(UserPageBuildingEvent $event) { - global $page, $user; - $duser = $event->display_user; - if(!$user->is_anonymous() && !$duser->is_anonymous()) { - if(($user->id == $duser->id) || $user->can("view_other_pms")) { - $this->theme->display_pms($page, $this->get_pms($duser)); - } - if($user->id != $duser->id) { - $this->theme->display_composer($page, $user, $duser); - } - } - } - public function onPageRequest(PageRequestEvent $event) { - global $database, $page, $user; - if($event->page_matches("pm")) { - if(!$user->is_anonymous()) { - switch($event->get_arg(0)) { - case "read": - $pm_id = int_escape($event->get_arg(1)); - $pm = $database->get_row("SELECT * FROM private_message WHERE id = :id", array("id" => $pm_id)); - if(is_null($pm)) { - $this->theme->display_error(404, "No such PM", "There is no PM #$pm_id"); - } - else if(($pm["to_id"] == $user->id) || $user->can("view_other_pms")) { - $from_user = User::by_id(int_escape($pm["from_id"])); - if($pm["to_id"] == $user->id) { - $database->execute("UPDATE private_message SET is_read='Y' WHERE id = :id", array("id" => $pm_id)); - $database->cache->delete("pm-count-{$user->id}"); - } - $this->theme->display_message($page, $from_user, $user, new PM($pm)); - } - else { - // permission denied - } - break; - case "delete": - if($user->check_auth_token()) { - $pm_id = int_escape($_POST["pm_id"]); - $pm = $database->get_row("SELECT * FROM private_message WHERE id = :id", array("id" => $pm_id)); - if(is_null($pm)) { - $this->theme->display_error(404, "No such PM", "There is no PM #$pm_id"); - } - else if(($pm["to_id"] == $user->id) || $user->can("view_other_pms")) { - $database->execute("DELETE FROM private_message WHERE id = :id", array("id" => $pm_id)); - $database->cache->delete("pm-count-{$user->id}"); - log_info("pm", "Deleted PM #$pm_id", "PM deleted"); - $page->set_mode("redirect"); - $page->set_redirect($_SERVER["HTTP_REFERER"]); - } - } - break; - case "send": - if($user->check_auth_token()) { - $to_id = int_escape($_POST["to_id"]); - $from_id = $user->id; - $subject = $_POST["subject"]; - $message = $_POST["message"]; - send_event(new SendPMEvent(new PM($from_id, $_SERVER["REMOTE_ADDR"], $to_id, $subject, $message))); - flash_message("PM sent"); - $page->set_mode("redirect"); - $page->set_redirect($_SERVER["HTTP_REFERER"]); - } - break; - default: - $this->theme->display_error(400, "Invalid action", "That's not something you can do with a PM"); - break; - } - } - } - } + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::READ_PM)) { + $count = $this->count_pms($user); + $h_count = $count > 0 ? " ($count)" : ""; + $event->add_link("Private Messages$h_count", make_link("user#private-messages")); + } + } - public function onSendPM(SendPMEvent $event) { - global $database; - $database->execute(" + public function onUserPageBuilding(UserPageBuildingEvent $event) + { + global $page, $user; + $duser = $event->display_user; + if (!$user->is_anonymous() && !$duser->is_anonymous()) { + if (($user->id == $duser->id) || $user->can(Permissions::VIEW_OTHER_PMS)) { + $this->theme->display_pms($page, $this->get_pms($duser)); + } + if ($user->id != $duser->id) { + $this->theme->display_composer($page, $user, $duser); + } + } + } + + public function onPageRequest(PageRequestEvent $event) + { + global $cache, $database, $page, $user; + if ($event->page_matches("pm")) { + switch ($event->get_arg(0)) { + case "read": + if ($user->can(Permissions::READ_PM)) { + $pm_id = int_escape($event->get_arg(1)); + $pm = $database->get_row("SELECT * FROM private_message WHERE id = :id", ["id" => $pm_id]); + if (is_null($pm)) { + $this->theme->display_error(404, "No such PM", "There is no PM #$pm_id"); + } elseif (($pm["to_id"] == $user->id) || $user->can(Permissions::VIEW_OTHER_PMS)) { + $from_user = User::by_id((int)$pm["from_id"]); + if ($pm["to_id"] == $user->id) { + $database->execute("UPDATE private_message SET is_read='Y' WHERE id = :id", ["id" => $pm_id]); + $cache->delete("pm-count-{$user->id}"); + } + $this->theme->display_message($page, $from_user, $user, new PM($pm)); + } else { + $this->theme->display_permission_denied(); + } + } + break; + case "delete": + if ($user->can(Permissions::READ_PM)) { + if ($user->check_auth_token()) { + $pm_id = int_escape($_POST["pm_id"]); + $pm = $database->get_row("SELECT * FROM private_message WHERE id = :id", ["id" => $pm_id]); + if (is_null($pm)) { + $this->theme->display_error(404, "No such PM", "There is no PM #$pm_id"); + } elseif (($pm["to_id"] == $user->id) || $user->can(Permissions::VIEW_OTHER_PMS)) { + $database->execute("DELETE FROM private_message WHERE id = :id", ["id" => $pm_id]); + $cache->delete("pm-count-{$user->id}"); + log_info("pm", "Deleted PM #$pm_id", "PM deleted"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect($_SERVER["HTTP_REFERER"]); + } + } + } + break; + case "send": + if ($user->can(Permissions::SEND_PM)) { + if ($user->check_auth_token()) { + $to_id = int_escape($_POST["to_id"]); + $from_id = $user->id; + $subject = $_POST["subject"]; + $message = $_POST["message"]; + send_event(new SendPMEvent(new PM($from_id, $_SERVER["REMOTE_ADDR"], $to_id, $subject, $message))); + $page->flash("PM sent"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect($_SERVER["HTTP_REFERER"]); + } + } + break; + default: + $this->theme->display_error(400, "Invalid action", "That's not something you can do with a PM"); + break; + } + } + } + + public function onSendPM(SendPMEvent $event) + { + global $cache, $database; + $database->execute( + " INSERT INTO private_message( from_id, from_ip, to_id, sent_date, subject, message) VALUES(:fromid, :fromip, :toid, now(), :subject, :message)", - array("fromid" => $event->pm->from_id, "fromip" => $event->pm->from_ip, - "toid" => $event->pm->to_id, "subject" => $event->pm->subject, "message" => $event->pm->message) - ); - $database->cache->delete("pm-count-{$event->pm->to_id}"); - log_info("pm", "Sent PM to User #{$event->pm->to_id}"); - } + ["fromid" => $event->pm->from_id, "fromip" => $event->pm->from_ip, + "toid" => $event->pm->to_id, "subject" => $event->pm->subject, "message" => $event->pm->message] + ); + $cache->delete("pm-count-{$event->pm->to_id}"); + log_info("pm", "Sent PM to User #{$event->pm->to_id}"); + } - private function get_pms(User $user) { - global $database; + private function get_pms(User $user) + { + global $database; - $arr = $database->get_all(" + $arr = $database->get_all( + " SELECT private_message.*,user_from.name AS from_name FROM private_message JOIN users AS user_from ON user_from.id=from_id WHERE to_id = :toid ORDER BY sent_date DESC", - array("toid" => $user->id)); - $pms = array(); - foreach($arr as $pm) { - $pms[] = new PM($pm); - } - return $pms; - } + ["toid" => $user->id] + ); + $pms = []; + foreach ($arr as $pm) { + $pms[] = new PM($pm); + } + return $pms; + } - private function count_pms(User $user) { - global $database; + private function count_pms(User $user) + { + global $cache, $database; - $count = $database->cache->get("pm-count:{$user->id}"); - if(is_null($count) || $count === false) { - $count = $database->get_one(" + $count = $cache->get("pm-count:{$user->id}"); + if (is_null($count) || $count === false) { + $count = $database->get_one(" SELECT count(*) FROM private_message WHERE to_id = :to_id AND is_read = :is_read - ", array("to_id" => $user->id, "is_read" => "N")); - $database->cache->set("pm-count:{$user->id}", $count, 600); - } - return $count; - } + ", ["to_id" => $user->id, "is_read" => "N"]); + $cache->set("pm-count:{$user->id}", $count, 600); + } + return $count; + } } - diff --git a/ext/pm/test.php b/ext/pm/test.php index df065139..60a66c66 100644 --- a/ext/pm/test.php +++ b/ext/pm/test.php @@ -1,59 +1,37 @@ -log_in_as_admin(); - $this->get_page("user/test"); +log_in_as_admin(); + send_event(new SendPMEvent(new PM( + User::by_name(self::$admin_name)->id, + "0.0.0.0", + User::by_name(self::$user_name)->id, + "message demo to test" + ))); - $this->markTestIncomplete(); + // Check that admin can see user's messages + $this->get_page("user/" . self::$user_name); + $this->assert_text("message demo to test"); - $this->set_field('subject', "message demo to test"); - $this->set_field('message', "message contents"); - $this->click("Send"); - $this->log_out(); + // Check that user can see own messages + $this->log_in_as_user(); + $this->get_page("user"); + $this->assert_text("message demo to test"); - $this->log_in_as_user(); - $this->get_page("user"); - $this->assert_text("message demo to test"); - $this->click("message demo to test"); - $this->assert_text("message contents"); - $this->back(); - $this->click("Delete"); - $this->assert_no_text("message demo to test"); + // FIXME: read PM + // $this->get_page("pm/read/0"); + // $this->assert_text("No such PM"); - $this->get_page("pm/read/0"); - $this->assert_text("No such PM"); - // GET doesn't work due to auth token check - //$this->get_page("pm/delete/0"); - //$this->assert_text("No such PM"); - $this->get_page("pm/waffle/0"); - $this->assert_text("Invalid action"); + // FIXME: delete PM + // send_event(); + // $this->get_page("user"); + // $this->assert_no_text("message demo to test"); - $this->log_out(); - } - - public function testAdminAccess() { - $this->log_in_as_admin(); - $this->get_page("user/test"); - - $this->markTestIncomplete(); - - $this->set_field('subject', "message demo to test"); - $this->set_field('message', "message contents"); - $this->click("Send"); - - $this->get_page("user/test"); - $this->assert_text("message demo to test"); - $this->click("message demo to test"); - $this->assert_text("message contents"); - $this->back(); - $this->click("Delete"); - - # simpletest bug? - redirect(referrer) works in opera, not in - # webtestcase, so we end up at the wrong page... - $this->get_page("user/test"); - $this->assert_title("test's Page"); - $this->assert_no_text("message demo to test"); - $this->log_out(); - } + // FIXME: verify deleted + // $this->get_page("pm/read/0"); + // $this->assert_text("No such PM"); + } } - diff --git a/ext/pm/theme.php b/ext/pm/theme.php index 81242c9c..850de817 100644 --- a/ext/pm/theme.php +++ b/ext/pm/theme.php @@ -1,30 +1,34 @@ -
    "; - foreach($pms as $pm) { - $h_subject = html_escape($pm->subject); - if(strlen(trim($h_subject)) == 0) $h_subject = "(No subject)"; - $from = User::by_id($pm->from_id); - $from_name = $from->name; - $h_from = html_escape($from_name); - $from_url = make_link("user/".url_escape($from_name)); - $pm_url = make_link("pm/read/".$pm->id); - $del_url = make_link("pm/delete"); - $h_date = html_escape($pm->sent_date); - $readYN = "Y"; - if(!$pm->is_read) { - $h_subject = "$h_subject"; - $readYN = "N"; - } - $hb = $from->can("hellbanned") ? "hb" : ""; - $html .= " + foreach ($pms as $pm) { + $h_subject = html_escape($pm->subject); + if (strlen(trim($h_subject)) == 0) { + $h_subject = "(No subject)"; + } + $from = User::by_id($pm->from_id); + $from_name = $from->name; + $h_from = html_escape($from_name); + $from_url = make_link("user/".url_escape($from_name)); + $pm_url = make_link("pm/read/".$pm->id); + $del_url = make_link("pm/delete"); + $h_date = html_escape($pm->sent_date); + $readYN = "Y"; + if (!$pm->is_read) { + $h_subject = "$h_subject"; + $readYN = "N"; + } + $hb = $from->can(Permissions::HELLBANNED) ? "hb" : ""; + $html .= " @@ -34,21 +38,22 @@ class PrivMsgTheme extends Themelet { "; - } - $html .= " + } + $html .= "
    "; + $html .= "{$vote['username']}"; + $html .= ""; + $html .= $vote['score']; + $html .= "
    R?SubjectFromDateAction
    $readYN $h_subject $h_from$h_date
    "; - $page->add_block(new Block("Private Messages", $html, "main", 40, "private-messages")); - } + $page->add_block(new Block("Private Messages", $html, "main", 40, "private-messages")); + } - public function display_composer(Page $page, User $from, User $to, $subject="") { - global $user; - $post_url = make_link("pm/send"); - $h_subject = html_escape($subject); - $to_id = $to->id; - $auth = $user->get_auth_html(); - $html = <<id; + $auth = $user->get_auth_html(); + $html = << $auth @@ -59,15 +64,15 @@ $auth EOD; - $page->add_block(new Block("Write a PM", $html, "main", 50)); - } + $page->add_block(new Block("Write a PM", $html, "main", 50)); + } - public function display_message(Page $page, User $from, User $to, PM $pm) { - $this->display_composer($page, $to, $from, "Re: ".$pm->subject); - $page->set_title("Private Message"); - $page->set_heading(html_escape($pm->subject)); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Message from {$from->name}", format_text($pm->message), "main", 10)); - } + public function display_message(Page $page, User $from, User $to, PM $pm) + { + $this->display_composer($page, $to, $from, "Re: ".$pm->subject); + $page->set_title("Private Message"); + $page->set_heading(html_escape($pm->subject)); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Message from {$from->name}", format_text($pm->message), "main", 10)); + } } - diff --git a/ext/pm_triggers/info.php b/ext/pm_triggers/info.php new file mode 100644 index 00000000..b4226c9a --- /dev/null +++ b/ext/pm_triggers/info.php @@ -0,0 +1,14 @@ + - * License: GPLv2 - * Description: Send PMs in response to certain events (eg image deletion) - */ +send( - $event->image->owner_id, - "[System] An image you uploaded has been deleted", - "Image le gone~ (#{$event->image->id}, {$event->image->get_tag_list()})" - ); - } +class PMTrigger extends Extension +{ + public function onImageDeletion(ImageDeletionEvent $event) + { + $this->send( + $event->image->owner_id, + "[System] An image you uploaded has been deleted", + "Image le gone~ (#{$event->image->id}, {$event->image->get_tag_list()})" + ); + } - private function send($to_id, $subject, $body) { - global $user; - send_event(new SendPMEvent(new PM( - $user->id, - $_SERVER["REMOTE_ADDR"], - $to_id, - $subject, - $body - ))); - } + private function send($to_id, $subject, $body) + { + global $user; + send_event(new SendPMEvent(new PM( + $user->id, + $_SERVER["REMOTE_ADDR"], + $to_id, + $subject, + $body + ))); + } } - diff --git a/ext/pools/info.php b/ext/pools/info.php new file mode 100644 index 00000000..ea812426 --- /dev/null +++ b/ext/pools/info.php @@ -0,0 +1,14 @@ +"mail@seinkraft.info", "jgen"=>"jgen.tech@gmail.com", "Daku"=>"admin@codeanimu.net"]; + public $license = self::LICENSE_GPLV2; + public $description = "Allow users to create groups of images and order them."; + public $documentation = +"This extension allows users to created named groups of images, and order the images within the group. Useful for related images like in a comic, etc."; +} diff --git a/ext/pools/main.php b/ext/pools/main.php index 4788de9e..dd4d887b 100644 --- a/ext/pools/main.php +++ b/ext/pools/main.php @@ -1,398 +1,523 @@ -, jgen , Daku - * License: GPLv2 - * Description: Allow users to create groups of images and order them. - * Documentation: This extension allows users to created named groups of - * images, and order the images within the group. - * Useful for related images like in a comic, etc. - */ +error = $error; - } +class PoolCreationException extends SCoreException +{ } -class Pools extends Extension { +class PoolAddPostsEvent extends Event +{ + public $pool_id; - public function onInitExt(InitExtEvent $event) { - global $config, $database; + public $posts = []; - // Set the defaults for the pools extension - $config->set_default_int("poolsMaxImportResults", 1000); - $config->set_default_int("poolsImagesPerPage", 20); - $config->set_default_int("poolsListsPerPage", 20); - $config->set_default_int("poolsUpdatedPerPage", 20); - $config->set_default_bool("poolsInfoOnViewImage", false); - $config->set_default_bool("poolsAdderOnViewImage", false); - $config->set_default_bool("poolsShowNavLinks", false); - $config->set_default_bool("poolsAutoIncrementOrder", false); + public function __construct(int $pool_id, array $posts) + { + parent::__construct(); + $this->pool_id = $pool_id; + $this->posts = $posts; + } +} - // Create the database tables - if ($config->get_int("ext_pools_version") < 1){ - $database->create_table("pools", " +class PoolCreationEvent extends Event +{ + public $title; + public $user; + public $public; + public $description; + + public $new_id = -1; + + public function __construct(string $title, User $pool_user = null, bool $public = false, string $description = "") + { + parent::__construct(); + global $user; + + $this->title = $title; + $this->user = $pool_user ?? $user; + $this->public = $public; + $this->description = $description; + } +} + +class Pools extends Extension +{ + /** @var PoolsTheme */ + protected $theme; + + public function onInitExt(InitExtEvent $event) + { + global $config; + + // Set the defaults for the pools extension + $config->set_default_int(PoolsConfig::MAX_IMPORT_RESULTS, 1000); + $config->set_default_int(PoolsConfig::IMAGES_PER_PAGE, 20); + $config->set_default_int(PoolsConfig::LISTS_PER_PAGE, 20); + $config->set_default_int(PoolsConfig::UPDATED_PER_PAGE, 20); + $config->set_default_bool(PoolsConfig::INFO_ON_VIEW_IMAGE, false); + $config->set_default_bool(PoolsConfig::ADDER_ON_VIEW_IMAGE, false); + $config->set_default_bool(PoolsConfig::SHOW_NAV_LINKS, false); + $config->set_default_bool(PoolsConfig::AUTO_INCREMENT_ORDER, false); + } + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $database; + + // Create the database tables + if ($this->get_version("ext_pools_version") < 1) { + $database->create_table("pools", " id SCORE_AIPK, user_id INTEGER NOT NULL, public SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N, title VARCHAR(255) NOT NULL, description TEXT, - date SCORE_DATETIME NOT NULL, + date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, posts INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE "); - $database->create_table("pool_images", " + $database->create_table("pool_images", " pool_id INTEGER NOT NULL, image_id INTEGER NOT NULL, image_order INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (pool_id) REFERENCES pools(id) ON UPDATE CASCADE ON DELETE CASCADE, FOREIGN KEY (image_id) REFERENCES images(id) ON UPDATE CASCADE ON DELETE CASCADE "); - $database->create_table("pool_history", " + $database->create_table("pool_history", " id SCORE_AIPK, pool_id INTEGER NOT NULL, user_id INTEGER NOT NULL, action INTEGER NOT NULL, images TEXT, count INTEGER NOT NULL DEFAULT 0, - date SCORE_DATETIME NOT NULL, + date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (pool_id) REFERENCES pools(id) ON UPDATE CASCADE ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE "); - $config->set_int("ext_pools_version", 3); + $this->set_version("ext_pools_version", 3); - log_info("pools", "extension installed"); - } + log_info("pools", "extension installed"); + } - if ($config->get_int("ext_pools_version") < 2){ - $database->Execute("ALTER TABLE pools ADD UNIQUE INDEX (title);"); - $database->Execute("ALTER TABLE pools ADD lastupdated TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;"); + if ($this->get_version("ext_pools_version") < 2) { + $database->Execute("ALTER TABLE pools ADD UNIQUE INDEX (title);"); + $database->Execute("ALTER TABLE pools ADD lastupdated TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;"); - $config->set_int("ext_pools_version", 3); // skip 2 - } - } + $this->set_version("ext_pools_version", 3); // skip 2 + } + } - // Add a block to the Board Config / Setup - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Pools"); - $sb->add_int_option("poolsMaxImportResults", "Max results on import: "); - $sb->add_int_option("poolsImagesPerPage", "
    Images per page: "); - $sb->add_int_option("poolsListsPerPage", "
    Index list items per page: "); - $sb->add_int_option("poolsUpdatedPerPage", "
    Updated list items per page: "); - $sb->add_bool_option("poolsInfoOnViewImage", "
    Show pool info on image: "); - $sb->add_bool_option("poolsShowNavLinks", "
    Show 'Prev' & 'Next' links when viewing pool images: "); - $sb->add_bool_option("poolsAutoIncrementOrder", "
    Autoincrement order when post is added to pool:"); - //$sb->add_bool_option("poolsAdderOnViewImage", "
    Show pool adder on image: "); + // Add a block to the Board Config / Setup + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Pools"); + $sb->add_int_option(PoolsConfig::MAX_IMPORT_RESULTS, "Max results on import: "); + $sb->add_int_option(PoolsConfig::IMAGES_PER_PAGE, "
    Images per page: "); + $sb->add_int_option(PoolsConfig::LISTS_PER_PAGE, "
    Index list items per page: "); + $sb->add_int_option(PoolsConfig::UPDATED_PER_PAGE, "
    Updated list items per page: "); + $sb->add_bool_option(PoolsConfig::INFO_ON_VIEW_IMAGE, "
    Show pool info on image: "); + $sb->add_bool_option(PoolsConfig::SHOW_NAV_LINKS, "
    Show 'Prev' & 'Next' links when viewing pool images: "); + $sb->add_bool_option(PoolsConfig::AUTO_INCREMENT_ORDER, "
    Autoincrement order when post is added to pool:"); + //$sb->add_bool_option(PoolsConfig::ADDER_ON_VIEW_IMAGE, "
    Show pool adder on image: "); - $event->panel->add_block($sb); - } + $event->panel->add_block($sb); + } - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; - - if ($event->page_matches("pool")) { - $pool_id = 0; - $pool = array(); + public function onPageNavBuilding(PageNavBuildingEvent $event) + { + $event->add_nav_link("pool", new Link('pool/list'), "Pools"); + } - // Check if we have pool id, since this is most often the case. - if (isset($_POST["pool_id"])) { - $pool_id = int_escape($_POST["pool_id"]); - $pool = $this->get_single_pool($pool_id); - } - - // What action are we trying to perform? - switch($event->get_arg(0)) { - case "list": //index - $this->list_pools($page, int_escape($event->get_arg(1))); - break; - - case "new": // Show form for new pools - if(!$user->is_anonymous()){ - $this->theme->new_pool_composer($page); - } else { - $errMessage = "You must be registered and logged in to create a new pool."; - $this->theme->display_error(401, "Error", $errMessage); - } - break; - - case "create": // ADD _POST - try { - $newPoolID = $this->add_pool(); - $page->set_mode("redirect"); - $page->set_redirect(make_link("pool/view/".$newPoolID)); - } - catch(PoolCreationException $e) { - $this->theme->display_error(400, "Error", $e->error); - } - break; - - case "view": - $poolID = int_escape($event->get_arg(1)); - $this->get_posts($event, $poolID); - break; - - case "updated": - $this->get_history(int_escape($event->get_arg(1))); - break; - - case "revert": - if(!$user->is_anonymous()) { - $historyID = int_escape($event->get_arg(1)); - $this->revert_history($historyID); - $page->set_mode("redirect"); - $page->set_redirect(make_link("pool/updated")); - } - break; - - case "edit": // Edit the pool (remove images) - if ($this->have_permission($user, $pool)) { - $this->theme->edit_pool($page, $this->get_pool($pool_id), $this->edit_posts($pool_id)); - } else { - $page->set_mode("redirect"); - $page->set_redirect(make_link("pool/view/".$pool_id)); - } - break; - - case "order": // Order the pool (view and change the order of images within the pool) - if (isset($_POST["order_view"])) { - if ($this->have_permission($user, $pool)) { - $this->theme->edit_order($page, $this->get_pool($pool_id), $this->edit_order($pool_id)); - } else { - $page->set_mode("redirect"); - $page->set_redirect(make_link("pool/view/".$pool_id)); - } - } - else { - if ($this->have_permission($user, $pool)) { - $this->order_posts(); - $page->set_mode("redirect"); - $page->set_redirect(make_link("pool/view/".$pool_id)); - } else { - $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page"); - } - } - break; - - case "import": - if ($this->have_permission($user, $pool)) { - $this->import_posts($pool_id); - } else { - $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page"); - } - break; - - case "add_posts": - if ($this->have_permission($user, $pool)) { - $this->add_posts(); - $page->set_mode("redirect"); - $page->set_redirect(make_link("pool/view/".$pool_id)); - } else { - $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page"); - } - break; - - case "remove_posts": - if ($this->have_permission($user, $pool)) { - $this->remove_posts(); - $page->set_mode("redirect"); - $page->set_redirect(make_link("pool/view/".$pool_id)); - } else { - $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page"); - } - - break; - - case "edit_description": - if ($this->have_permission($user, $pool)) { - $this->edit_description(); - $page->set_mode("redirect"); - $page->set_redirect(make_link("pool/view/".$pool_id)); - } else { - $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page"); - } - - break; - - case "nuke": - // Completely remove the given pool. - // -> Only admins and owners may do this - if($user->is_admin() || $user->id == $pool['user_id']) { - $this->nuke_pool($pool_id); - $page->set_mode("redirect"); - $page->set_redirect(make_link("pool/list")); - } else { - $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page"); - } - break; - - default: - $page->set_mode("redirect"); - $page->set_redirect(make_link("pool/list")); - break; - } - } - } - - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - $event->add_link("Pools", make_link("pool/list")); - } + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + if ($event->parent=="pool") { + $event->add_nav_link("pool_list", new Link('pool/list'), "List"); + $event->add_nav_link("pool_new", new Link('pool/new'), "Create"); + $event->add_nav_link("pool_updated", new Link('pool/updated'), "Changes"); + $event->add_nav_link("pool_help", new Link('ext_doc/pools'), "Help"); + } + } - /** - * When displaying an image, optionally list all the pools that the - * image is currently a member of on a side panel, as well as a link - * to the Next image in the pool. - * - * @var DisplayingImageEvent $event - */ - public function onDisplayingImage(DisplayingImageEvent $event) { - global $config; - if($config->get_bool("poolsInfoOnViewImage")) { - $imageID = $event->image->id; - $poolsIDs = $this->get_pool_ids($imageID); + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; - $show_nav = $config->get_bool("poolsShowNavLinks", false); + if ($event->page_matches("pool")) { + $pool_id = 0; + $pool = []; - $navInfo = array(); - foreach($poolsIDs as $poolID) { - $pool = $this->get_single_pool($poolID); + // Check if we have pool id, since this is most often the case. + if (isset($_POST["pool_id"])) { + $pool_id = int_escape($_POST["pool_id"]); + $pool = $this->get_single_pool($pool_id); + } - $navInfo[$pool['id']] = array(); - $navInfo[$pool['id']]['info'] = $pool; + // What action are we trying to perform? + switch ($event->get_arg(0)) { + case "list": //index + $this->list_pools($page, $event->try_page_num(1)); + break; - // Optionally show a link the Prev/Next image in the Pool. - if ($show_nav) { - $navInfo[$pool['id']]['nav'] = $this->get_nav_posts($pool, $imageID); - } - } - $this->theme->pool_info($navInfo); - } - } + case "new": // Show form for new pools + if (!$user->is_anonymous()) { + $this->theme->new_pool_composer($page); + } else { + $errMessage = "You must be registered and logged in to create a new pool."; + $this->theme->display_error(401, "Error", $errMessage); + } + break; - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { - global $config, $database, $user; - if($config->get_bool("poolsAdderOnViewImage") && !$user->is_anonymous()) { - if($user->is_admin()) { - $pools = $database->get_all("SELECT * FROM pools"); - } - else { - $pools = $database->get_all("SELECT * FROM pools WHERE user_id=:id", array("id"=>$user->id)); - } - if(count($pools) > 0) { - $event->add_part($this->theme->get_adder_html($event->image, $pools)); - } - } - } + case "create": // ADD _POST + try { + $title = $_POST["title"]; + $event = new PoolCreationEvent( + $title, + $user, + $_POST["public"] === "Y", + $_POST["description"] + ); - public function onSearchTermParse(SearchTermParseEvent $event) { - $matches = array(); - if(preg_match("/^pool[=|:]([0-9]+|any|none)$/i", $event->term, $matches)) { - $poolID = $matches[1]; + send_event($event); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("pool/view/" . $event->new_id)); + } catch (PoolCreationException $e) { + $this->theme->display_error(400, "Error", $e->error); + } + break; - if(preg_match("/^(any|none)$/", $poolID)){ - $not = ($poolID == "none" ? "NOT" : ""); - $event->add_querylet(new Querylet("images.id $not IN (SELECT DISTINCT image_id FROM pool_images)")); - }else{ - $event->add_querylet(new Querylet("images.id IN (SELECT DISTINCT image_id FROM pool_images WHERE pool_id = $poolID)")); - } - } - else if(preg_match("/^pool_by_name[=|:](.*)$/i", $event->term, $matches)) { - $poolTitle = str_replace("_", " ", $matches[1]); + case "view": + $poolID = int_escape($event->get_arg(1)); + $this->get_posts($event, $poolID); + break; - $pool = $this->get_single_pool_from_title($poolTitle); - $poolID = 0; - if ($pool){ $poolID = $pool['id']; } - $event->add_querylet(new Querylet("images.id IN (SELECT DISTINCT image_id FROM pool_images WHERE pool_id = $poolID)")); - } - } + case "updated": + $this->get_history(int_escape($event->get_arg(1))); + break; - public function onTagTermParse(TagTermParseEvent $event) { - $matches = array(); + case "revert": + if (!$user->is_anonymous()) { + $historyID = int_escape($event->get_arg(1)); + $this->revert_history($historyID); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("pool/updated")); + } + break; - if(preg_match("/^pool[=|:]([^:]*|lastcreated):?([0-9]*)$/i", $event->term, $matches)) { - global $user; - $poolTag = (string) str_replace("_", " ", $matches[1]); + case "edit": // Edit the pool (remove images) + if ($this->have_permission($user, $pool)) { + $this->theme->edit_pool($page, $this->get_pool($pool_id), $this->edit_posts($pool_id)); + } else { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("pool/view/" . $pool_id)); + } + break; - $pool = null; - if($poolTag == 'lastcreated'){ - $pool = $this->get_last_userpool($user->id); - } - elseif(ctype_digit($poolTag)){ //If only digits, assume PoolID - $pool = $this->get_single_pool($poolTag); - }else{ //assume PoolTitle - $pool = $this->get_single_pool_from_title($poolTag); - } + case "order": // Order the pool (view and change the order of images within the pool) + if (isset($_POST["order_view"])) { + if ($this->have_permission($user, $pool)) { + $this->theme->edit_order($page, $this->get_pool($pool_id), $this->edit_order($pool_id)); + } else { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("pool/view/" . $pool_id)); + } + } else { + if ($this->have_permission($user, $pool)) { + $this->order_posts(); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("pool/view/" . $pool_id)); + } else { + $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page"); + } + } + break; + + case "import": + if ($this->have_permission($user, $pool)) { + $this->import_posts($pool_id); + } else { + $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page"); + } + break; + + case "add_posts": + if ($this->have_permission($user, $pool)) { + $images = []; + foreach ($_POST['check'] as $imageID) { + $images[] = $imageID; + } + send_event(new PoolAddPostsEvent($pool_id, $images)); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("pool/view/" . $pool_id)); + } else { + $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page"); + } + break; + + case "remove_posts": + if ($this->have_permission($user, $pool)) { + $this->remove_posts(); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("pool/view/" . $pool_id)); + } else { + $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page"); + } + + break; + + case "edit_description": + if ($this->have_permission($user, $pool)) { + $this->edit_description(); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("pool/view/" . $pool_id)); + } else { + $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page"); + } + + break; + + case "nuke": + // Completely remove the given pool. + // -> Only admins and owners may do this + if ($user->can(Permissions::POOLS_ADMIN) || $user->id == $pool['user_id']) { + $this->nuke_pool($pool_id); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("pool/list")); + } else { + $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page"); + } + break; + + default: + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("pool/list")); + break; + } + } + } + + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + $event->add_link("Pools", make_link("pool/list")); + } - if($pool ? $this->have_permission($user, $pool) : FALSE){ - $image_order = ($matches[2] ?: 0); - $this->add_post($pool['id'], $event->id, true, $image_order); - } - } + /** + * When displaying an image, optionally list all the pools that the + * image is currently a member of on a side panel, as well as a link + * to the Next image in the pool. + * + * @var DisplayingImageEvent $event + */ + public function onDisplayingImage(DisplayingImageEvent $event) + { + global $config; - if(!empty($matches)) $event->metatag = true; - } + if ($config->get_bool(PoolsConfig::INFO_ON_VIEW_IMAGE)) { + $imageID = $event->image->id; + $poolsIDs = $this->get_pool_ids($imageID); - /* ------------------------------------------------- */ - /* -------------- Private Functions -------------- */ - /* ------------------------------------------------- */ + $show_nav = $config->get_bool(PoolsConfig::SHOW_NAV_LINKS, false); - /** - * Check if the given user has permission to edit/change the pool. - * - * TODO: Should the user variable be global? - * - * @param \User $user - * @param array $pool - * @return bool - */ - private function have_permission($user, $pool) { - // If the pool is public and user is logged OR if the user is admin OR if the pool is owned by the user. - if ( (($pool['public'] == "Y" || $pool['public'] == "y") && !$user->is_anonymous()) || $user->is_admin() || $user->id == $pool['user_id']) - { - return true; - } else { - return false; - } - } + $navInfo = []; + foreach ($poolsIDs as $poolID) { + $pool = $this->get_single_pool($poolID); - /** - * HERE WE GET THE LIST OF POOLS. - * - * @param \Page $page - * @param int $pageNumber - */ - private function list_pools(Page $page, /*int*/ $pageNumber) { - global $config, $database; + $navInfo[$pool['id']] = []; + $navInfo[$pool['id']]['info'] = $pool; - $pageNumber = clamp($pageNumber, 1, null) - 1; + // Optionally show a link the Prev/Next image in the Pool. + if ($show_nav) { + $navInfo[$pool['id']]['nav'] = $this->get_nav_posts($pool, $imageID); + } + } + $this->theme->pool_info($navInfo); + } + } - $poolsPerPage = $config->get_int("poolsListsPerPage"); + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) + { + global $config, $database, $user; + if ($config->get_bool(PoolsConfig::ADDER_ON_VIEW_IMAGE) && !$user->is_anonymous()) { + if ($user->can(Permissions::POOLS_ADMIN)) { + $pools = $database->get_all("SELECT * FROM pools"); + } else { + $pools = $database->get_all("SELECT * FROM pools WHERE user_id=:id", ["id" => $user->id]); + } + if (count($pools) > 0) { + $event->add_part($this->theme->get_adder_html($event->image, $pools)); + } + } + } - $order_by = ""; - $order = $page->get_cookie("ui-order-pool"); - if($order == "created" || is_null($order)){ - $order_by = "ORDER BY p.date DESC"; - }elseif($order == "updated"){ - $order_by = "ORDER BY p.lastupdated DESC"; - }elseif($order == "name"){ - $order_by = "ORDER BY p.title ASC"; - }elseif($order == "count"){ - $order_by = "ORDER BY p.posts DESC"; - } + public function onHelpPageBuilding(HelpPageBuildingEvent $event) + { + if ($event->key===HelpPages::SEARCH) { + $block = new Block(); + $block->header = "Pools"; + $block->body = $this->theme->get_help_html(); + $event->add_block($block); + } + } - $pools = $database->get_all(" + + public function onSearchTermParse(SearchTermParseEvent $event) + { + if (is_null($event->term)) { + return; + } + + $matches = []; + if (preg_match("/^pool[=|:]([0-9]+|any|none)$/i", $event->term, $matches)) { + $poolID = $matches[1]; + + if (preg_match("/^(any|none)$/", $poolID)) { + $not = ($poolID == "none" ? "NOT" : ""); + $event->add_querylet(new Querylet("images.id $not IN (SELECT DISTINCT image_id FROM pool_images)")); + } else { + $event->add_querylet(new Querylet("images.id IN (SELECT DISTINCT image_id FROM pool_images WHERE pool_id = $poolID)")); + } + } elseif (preg_match("/^pool_by_name[=|:](.*)$/i", $event->term, $matches)) { + $poolTitle = str_replace("_", " ", $matches[1]); + + $pool = $this->get_single_pool_from_title($poolTitle); + $poolID = 0; + if ($pool) { + $poolID = $pool['id']; + } + $event->add_querylet(new Querylet("images.id IN (SELECT DISTINCT image_id FROM pool_images WHERE pool_id = $poolID)")); + } + } + + public function onTagTermCheck(TagTermCheckEvent $event) + { + if (preg_match("/^pool[=|:]([^:]*|lastcreated):?([0-9]*)$/i", $event->term)) { + $event->metatag = true; + } + } + + public function onTagTermParse(TagTermParseEvent $event) + { + $matches = []; + if (preg_match("/^pool[=|:]([^:]*|lastcreated):?([0-9]*)$/i", $event->term, $matches)) { + global $user; + $poolTag = (string)str_replace("_", " ", $matches[1]); + + $pool = null; + if ($poolTag == 'lastcreated') { + $pool = $this->get_last_userpool($user->id); + } elseif (ctype_digit($poolTag)) { //If only digits, assume PoolID + $pool = $this->get_single_pool((int)$poolTag); + } else { //assume PoolTitle + $pool = $this->get_single_pool_from_title($poolTag); + } + + if ($pool ? $this->have_permission($user, $pool) : false) { + $image_order = ($matches[2] ?: 0); + $this->add_post($pool['id'], $event->image_id, true, $image_order); + } + } + } + + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) + { + global $database; + + $pools = $database->get_all("SELECT * FROM pools ORDER BY title "); + + + $event->add_action("bulk_pool_add_existing", "Add To (P)ool", "p", "", $this->theme->get_bulk_pool_selector($pools)); + $event->add_action("bulk_pool_add_new", "Create Pool", "", "", $this->theme->get_bulk_pool_input($event->search_terms)); + } + + public function onBulkAction(BulkActionEvent $event) + { + global $user; + + switch ($event->action) { + case "bulk_pool_add_existing": + if (!isset($_POST['bulk_pool_select'])) { + return; + } + $pool_id = intval($_POST['bulk_pool_select']); + $pool = $this->get_pool($pool_id); + + if ($this->have_permission($user, $pool)) { + send_event( + new PoolAddPostsEvent($pool_id, iterator_map_to_array("image_to_id", $event->items)) + ); + } + break; + case "bulk_pool_add_new": + if (!isset($_POST['bulk_pool_new'])) { + return; + } + $new_pool_title = $_POST['bulk_pool_new']; + $pce = new PoolCreationEvent($new_pool_title); + send_event($pce); + send_event(new PoolAddPostsEvent($pce->new_id, iterator_map_to_array("image_to_id", $event->items))); + break; + } + } + + /* ------------------------------------------------- */ + /* -------------- Private Functions -------------- */ + /* ------------------------------------------------- */ + + /** + * Check if the given user has permission to edit/change the pool. + * + * TODO: Should the user variable be global? + */ + private function have_permission(User $user, array $pool): bool + { + // If the pool is public and user is logged OR if the user is admin OR if the pool is owned by the user. + if ((($pool['public'] == "Y" || $pool['public'] == "y") && !$user->is_anonymous()) || $user->can(Permissions::POOLS_ADMIN) || $user->id == $pool['user_id']) { + return true; + } else { + return false; + } + } + + /** + * HERE WE GET THE LIST OF POOLS. + */ + private function list_pools(Page $page, int $pageNumber) + { + global $config, $database; + + $pageNumber = clamp($pageNumber, 1, null) - 1; + + $poolsPerPage = $config->get_int(PoolsConfig::LISTS_PER_PAGE); + + $order_by = ""; + $order = $page->get_cookie("ui-order-pool"); + if ($order == "created" || is_null($order)) { + $order_by = "ORDER BY p.date DESC"; + } elseif ($order == "updated") { + $order_by = "ORDER BY p.lastupdated DESC"; + } elseif ($order == "name") { + $order_by = "ORDER BY p.title ASC"; + } elseif ($order == "count") { + $order_by = "ORDER BY p.posts DESC"; + } + + $pools = $database->get_all(" SELECT p.id, p.user_id, p.public, p.title, p.description, p.posts, u.name as user_name FROM pools AS p @@ -400,236 +525,217 @@ class Pools extends Extension { ON p.user_id = u.id $order_by LIMIT :l OFFSET :o - ", array("l"=>$poolsPerPage, "o"=>$pageNumber * $poolsPerPage)); + ", ["l" => $poolsPerPage, "o" => $pageNumber * $poolsPerPage]); - $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM pools") / $poolsPerPage); + $totalPages = (int)ceil($database->get_one("SELECT COUNT(*) FROM pools") / $poolsPerPage); - $this->theme->list_pools($page, $pools, $pageNumber + 1, $totalPages); - } + $this->theme->list_pools($page, $pools, $pageNumber + 1, $totalPages); + } - /** - * HERE WE CREATE A NEW POOL - * - * @return int - * @throws PoolCreationException - */ - private function add_pool() { - global $user, $database; + /** + * HERE WE CREATE A NEW POOL + */ + public function onPoolCreation(PoolCreationEvent $event) + { + global $user, $database; - if($user->is_anonymous()) { - throw new PoolCreationException("You must be registered and logged in to add a image."); - } - if(empty($_POST["title"])) { - throw new PoolCreationException("Pool title is empty."); - } - if($this->get_single_pool_from_title($_POST["title"])) { - throw new PoolCreationException("A pool using this title already exists."); - } + if ($user->is_anonymous()) { + throw new PoolCreationException("You must be registered and logged in to add a image."); + } + if (empty($event->title)) { + throw new PoolCreationException("Pool title is empty."); + } + if ($this->get_single_pool_from_title($event->title)) { + throw new PoolCreationException("A pool using this title already exists."); + } - $public = $_POST["public"] === "Y" ? "Y" : "N"; - $database->execute(" + + $database->execute( + " INSERT INTO pools (user_id, public, title, description, date) VALUES (:uid, :public, :title, :desc, now())", - array("uid"=>$user->id, "public"=>$public, "title"=>$_POST["title"], "desc"=>$_POST["description"])); + ["uid" => $event->user->id, "public" => $event->public ? "Y" : "N", "title" => $event->title, "desc" => $event->description] + ); - $poolID = $database->get_last_insert_id('pools_id_seq'); - log_info("pools", "Pool {$poolID} created by {$user->name}"); - return $poolID; - } + $poolID = $database->get_last_insert_id('pools_id_seq'); + log_info("pools", "Pool {$poolID} created by {$user->name}"); - /** - * Retrieve information about pools given multiple pool IDs. - * - * TODO: What is the difference between this and get_single_pool() other than the db query? - * - * @param int $poolID Array of integers - * @return array - */ - private function get_pool(/*int*/ $poolID) { - global $database; - return $database->get_all("SELECT * FROM pools WHERE id=:id", array("id"=>$poolID)); - } + $event->new_id = $poolID; + } - /** - * Retrieve information about a pool given a pool ID. - * @param int $poolID the pool id - * @return array Array with only 1 element in the one dimension - */ - private function get_single_pool(/*int*/ $poolID) { - global $database; - return $database->get_row("SELECT * FROM pools WHERE id=:id", array("id"=>$poolID)); - } + /** + * Retrieve information about pools given multiple pool IDs. + * + * TODO: What is the difference between this and get_single_pool() other than the db query? + */ + private function get_pool(int $poolID): array + { + global $database; + return $database->get_all("SELECT * FROM pools WHERE id=:id", ["id" => $poolID]); + } - /** - * Retrieve information about a pool given a pool title. - * @param string $poolTitle - * @return array Array (with only 1 element in the one dimension) - */ - private function get_single_pool_from_title(/*string*/ $poolTitle) { - global $database; - return $database->get_row("SELECT * FROM pools WHERE title=:title", array("title"=>$poolTitle)); - } + /** + * Retrieve information about a pool given a pool ID. + */ + private function get_single_pool(int $poolID): array + { + global $database; + return $database->get_row("SELECT * FROM pools WHERE id=:id", ["id" => $poolID]); + } - /** - * Get all of the pool IDs that an image is in, given an image ID. - * @param int $imageID Integer ID for the image - * @return int[] - */ - private function get_pool_ids(/*int*/ $imageID) { - global $database; - return $database->get_col("SELECT pool_id FROM pool_images WHERE image_id=:iid", array("iid"=>$imageID)); - } + /** + * Retrieve information about a pool given a pool title. + */ + private function get_single_pool_from_title(string $poolTitle): ?array + { + global $database; + return $database->get_row("SELECT * FROM pools WHERE title=:title", ["title" => $poolTitle]); + } - /** - * Retrieve information about the last pool the given userID created - * @param int $userID - * @return array - */ - private function get_last_userpool(/*int*/ $userID){ - global $database; - return $database->get_row("SELECT * FROM pools WHERE user_id=:uid ORDER BY id DESC", array("uid"=>$userID)); - } + /** + * Get all of the pool IDs that an image is in, given an image ID. + * #return int[] + */ + private function get_pool_ids(int $imageID): array + { + global $database; + return $database->get_col("SELECT pool_id FROM pool_images WHERE image_id=:iid", ["iid" => $imageID]); + } - /** - * HERE WE GET THE IMAGES FROM THE TAG ON IMPORT - * @param int $pool_id - */ - private function import_posts(/*int*/ $pool_id) { - global $page, $config; + /** + * Retrieve information about the last pool the given userID created + */ + private function get_last_userpool(int $userID): array + { + global $database; + return $database->get_row("SELECT * FROM pools WHERE user_id=:uid ORDER BY id DESC", ["uid" => $userID]); + } - $poolsMaxResults = $config->get_int("poolsMaxImportResults", 1000); - - $images = $images = Image::find_images(0, $poolsMaxResults, Tag::explode($_POST["pool_tag"])); - $this->theme->pool_result($page, $images, $this->get_pool($pool_id)); - } + /** + * HERE WE GET THE IMAGES FROM THE TAG ON IMPORT + */ + private function import_posts(int $pool_id) + { + global $page, $config; + + $poolsMaxResults = $config->get_int(PoolsConfig::MAX_IMPORT_RESULTS, 1000); + + $images = $images = Image::find_images(0, $poolsMaxResults, Tag::explode($_POST["pool_tag"])); + $this->theme->pool_result($page, $images, $this->get_pool($pool_id)); + } - /** - * HERE WE ADD CHECKED IMAGES FROM POOL AND UPDATE THE HISTORY - * - * TODO: Fix this so that the pool ID and images are passed as Arguments to the function. - * - * @return int - */ - private function add_posts() { - global $database; + /** + * HERE WE ADD CHECKED IMAGES FROM POOL AND UPDATE THE HISTORY + * + */ + public function onPoolAddPosts(PoolAddPostsEvent $event) + { + global $database, $user; - $poolID = int_escape($_POST['pool_id']); - $images = ""; + $pool = $this->get_single_pool($event->pool_id); + if (!$this->have_permission($user, $pool)) { + return; + } - foreach ($_POST['check'] as $imageID){ - if(!$this->check_post($poolID, $imageID)){ - $database->execute(" - INSERT INTO pool_images (pool_id, image_id) - VALUES (:pid, :iid)", - array("pid"=>$poolID, "iid"=>$imageID)); + $images = " "; + foreach ($event->posts as $post_id) { + if ($this->add_post($event->pool_id, $post_id, false)) { + $images .= " " . $post_id; + } + } - $images .= " ".$imageID; - } - } + if (!strlen($images) == 0) { + $count = int_escape($database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid" => $event->pool_id])); + $this->add_history($event->pool_id, 1, $images, $count); + } + } - if(!strlen($images) == 0) { - $count = int_escape($database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", array("pid"=>$poolID))); - $this->add_history($poolID, 1, $images, $count); - } + /** + * TODO: Fix this so that the pool ID and images are passed as Arguments to the function. + */ + private function order_posts(): int + { + global $database; - $database->Execute(" - UPDATE pools - SET posts=(SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid) - WHERE id=:pid", - array("pid"=>$poolID) - ); - return $poolID; - } + $poolID = int_escape($_POST['pool_id']); - /** - * TODO: Fix this so that the pool ID and images are passed as Arguments to the function. - * @return int - */ - private function order_posts() { - global $database; - - $poolID = int_escape($_POST['pool_id']); - - foreach($_POST['imgs'] as $data) { - list($imageORDER, $imageID) = $data; - $database->Execute(" + foreach ($_POST['imgs'] as $data) { + list($imageORDER, $imageID) = $data; + $database->Execute( + " UPDATE pool_images SET image_order = :ord WHERE pool_id = :pid AND image_id = :iid", - array("ord"=>$imageORDER, "pid"=>$poolID, "iid"=>$imageID) - ); - } + ["ord" => $imageORDER, "pid" => $poolID, "iid" => $imageID] + ); + } - return $poolID; - } + return $poolID; + } - /** - * HERE WE REMOVE CHECKED IMAGES FROM POOL AND UPDATE THE HISTORY - * - * TODO: Fix this so that the pool ID and images are passed as Arguments to the function. - * - * @return int - */ - private function remove_posts() { - global $database; + /** + * HERE WE REMOVE CHECKED IMAGES FROM POOL AND UPDATE THE HISTORY + * + * TODO: Fix this so that the pool ID and images are passed as Arguments to the function. + */ + private function remove_posts(): int + { + global $database; - $poolID = int_escape($_POST['pool_id']); - $images = ""; + $poolID = int_escape($_POST['pool_id']); + $images = ""; - foreach($_POST['check'] as $imageID) { - $database->execute("DELETE FROM pool_images WHERE pool_id = :pid AND image_id = :iid", array("pid"=>$poolID, "iid"=>$imageID)); - $images .= " ".$imageID; - } + foreach ($_POST['check'] as $imageID) { + $database->execute("DELETE FROM pool_images WHERE pool_id = :pid AND image_id = :iid", ["pid" => $poolID, "iid" => $imageID]); + $images .= " " . $imageID; + } - $count = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", array("pid"=>$poolID)); - $this->add_history($poolID, 0, $images, $count); - return $poolID; - } + $count = (int)$database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid" => $poolID]); + $this->add_history($poolID, 0, $images, $count); + return $poolID; + } - /** - * Allows editing of pool description. - * @return int - */ - private function edit_description() { - global $database; + /** + * Allows editing of pool description. + */ + private function edit_description(): int + { + global $database; - $poolID = int_escape($_POST['pool_id']); - $database->execute("UPDATE pools SET description=:dsc WHERE id=:pid", array("dsc"=>$_POST['description'], "pid"=>$poolID)); + $poolID = int_escape($_POST['pool_id']); + $database->execute("UPDATE pools SET description=:dsc WHERE id=:pid", ["dsc" => $_POST['description'], "pid" => $poolID]); - return $poolID; - } + return $poolID; + } - /** - * This function checks if a given image is contained within a given pool. - * Used by add_posts() - * - * @see add_posts() - * @param int $poolID - * @param int $imageID - * @return bool - */ - private function check_post(/*int*/ $poolID, /*int*/ $imageID) { - global $database; - $result = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid AND image_id=:iid", array("pid"=>$poolID, "iid"=>$imageID)); - return ($result != 0); - } + /** + * This function checks if a given image is contained within a given pool. + * Used by add_posts() + */ + private function check_post(int $poolID, int $imageID): bool + { + global $database; + $result = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid AND image_id=:iid", ["pid" => $poolID, "iid" => $imageID]); + return ($result != 0); + } - /** - * Gets the previous and next successive images from a pool, given a pool ID and an image ID. - * - * @param array $pool Array for the given pool - * @param int $imageID Integer - * @return array Array returning two elements (prev, next) in 1 dimension. Each returns ImageID or NULL if none. - */ - private function get_nav_posts(/*array*/ $pool, /*int*/ $imageID) { - global $database; + /** + * Gets the previous and next successive images from a pool, given a pool ID and an image ID. + * + * #return int[] Array returning two elements (prev, next) in 1 dimension. Each returns ImageID or NULL if none. + */ + private function get_nav_posts(array $pool, int $imageID): array + { + global $database; - if (empty($pool) || empty($imageID)) - return null; - - $result = $database->get_row(" + if (empty($pool) || empty($imageID)) { + return null; + } + + $result = $database->get_row( + " SELECT ( SELECT image_id FROM pool_images @@ -658,183 +764,169 @@ class Pools extends Extension { ) AS next LIMIT 1", - array("pid"=>$pool['id'], "iid"=>$imageID) ); + ["pid" => $pool['id'], "iid" => $imageID] + ); - if (empty($result)) { - // assume that we are at the end of the pool - return null; - } else { - return $result; - } - } + if (empty($result)) { + // assume that we are at the end of the pool + return null; + } else { + return $result; + } + } - /** - * Retrieve all the images in a pool, given a pool ID. - * - * @param PageRequestEvent $event - * @param int $poolID - */ - private function get_posts($event, /*int*/ $poolID) { - global $config, $user, $database; + /** + * Retrieve all the images in a pool, given a pool ID. + */ + private function get_posts(PageRequestEvent $event, int $poolID) + { + global $config, $user, $database; - $pageNumber = int_escape($event->get_arg(2)); - if(is_null($pageNumber) || !is_numeric($pageNumber)) - $pageNumber = 0; - else if ($pageNumber <= 0) - $pageNumber = 0; - else - $pageNumber--; + $pageNumber = $event->try_page_num(2) - 1; + $pool = $this->get_pool($poolID); + $imagesPerPage = $config->get_int(PoolsConfig::IMAGES_PER_PAGE); - $poolID = int_escape($poolID); - $pool = $this->get_pool($poolID); + $query = " + INNER JOIN images AS i ON i.id = p.image_id + WHERE p.pool_id = :pid + "; - $imagesPerPage = $config->get_int("poolsImagesPerPage"); + // WE CHECK IF THE EXTENSION RATING IS INSTALLED, WHICH VERSION AND IF IT + // WORKS TO SHOW/HIDE SAFE, QUESTIONABLE, EXPLICIT AND UNRATED IMAGES FROM USER + if (Extension::is_enabled(RatingsInfo::KEY)) { + $query .= "AND i.rating IN (".Ratings::privs_to_sql(Ratings::get_user_class_privs($user)).")"; + } + if (Extension::is_enabled(TrashInfo::KEY)) { + $query .= $database->scoreql_to_sql(" AND trash = SCORE_BOOL_N "); + } - // WE CHECK IF THE EXTENSION RATING IS INSTALLED, WHICH VERSION AND IF IT - // WORKS TO SHOW/HIDE SAFE, QUESTIONABLE, EXPLICIT AND UNRATED IMAGES FROM USER - if(ext_is_live("Ratings")) { - $rating = Ratings::privs_to_sql(Ratings::get_user_privs($user)); - } - if (isset($rating) && !empty($rating)) { - - $result = $database->get_all(" - SELECT p.image_id - FROM pool_images AS p - INNER JOIN images AS i ON i.id = p.image_id - WHERE p.pool_id = :pid AND i.rating IN ($rating) + $result = $database->get_all( + " + SELECT p.image_id FROM pool_images p + $query ORDER BY p.image_order ASC LIMIT :l OFFSET :o", - array("pid"=>$poolID, "l"=>$imagesPerPage, "o"=>$pageNumber * $imagesPerPage)); + ["pid" => $poolID, "l" => $imagesPerPage, "o" => $pageNumber * $imagesPerPage] + ); - $totalPages = ceil($database->get_one(" - SELECT COUNT(*) - FROM pool_images AS p - INNER JOIN images AS i ON i.id = p.image_id - WHERE pool_id=:pid AND i.rating IN ($rating)", - array("pid"=>$poolID)) / $imagesPerPage); - } else { - - $result = $database->get_all(" - SELECT image_id - FROM pool_images - WHERE pool_id=:pid - ORDER BY image_order ASC - LIMIT :l OFFSET :o", - array("pid"=>$poolID, "l"=>$imagesPerPage, "o"=>$pageNumber * $imagesPerPage)); - - $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", array("pid"=>$poolID)) / $imagesPerPage); - } + $totalPages = (int)ceil($database->get_one( + " + SELECT COUNT(*) FROM pool_images p + $query", + ["pid" => $poolID] + ) / $imagesPerPage); - $images = array(); - foreach($result as $singleResult) { - $images[] = Image::by_id($singleResult["image_id"]); - } + $images = []; + foreach ($result as $singleResult) { + $images[] = Image::by_id($singleResult["image_id"]); + } - $this->theme->view_pool($pool, $images, $pageNumber + 1, $totalPages); - } + $this->theme->view_pool($pool, $images, $pageNumber + 1, $totalPages); + } - /** - * This function gets the current order of images from a given pool. - * @param int $poolID - * @return \Image[] Array of image objects. - */ - private function edit_posts(/*int*/ $poolID) { - global $database; + /** + * This function gets the current order of images from a given pool. + * #return Image[] Array of image objects. + */ + private function edit_posts(int $poolID): array + { + global $database; - $result = $database->Execute("SELECT image_id FROM pool_images WHERE pool_id=:pid ORDER BY image_order ASC", array("pid"=>$poolID)); - $images = array(); - - while($row = $result->fetch()) { - $image = Image::by_id($row["image_id"]); - $images[] = array($image); - } - - return $images; - } + $result = $database->Execute("SELECT image_id FROM pool_images WHERE pool_id=:pid ORDER BY image_order ASC", ["pid" => $poolID]); + $images = []; + + while ($row = $result->fetch()) { + $image = Image::by_id($row["image_id"]); + $images[] = [$image]; + } + + return $images; + } - /** - * WE GET THE ORDER OF THE IMAGES BUT HERE WE SEND KEYS ADDED IN ARRAY TO GET THE ORDER IN THE INPUT VALUE. - * - * @param int $poolID - * @return \Image[] - */ - private function edit_order(/*int*/ $poolID) { - global $database; + /** + * WE GET THE ORDER OF THE IMAGES BUT HERE WE SEND KEYS ADDED IN ARRAY TO GET THE ORDER IN THE INPUT VALUE. + * + * #return Image[] + */ + private function edit_order(int $poolID): array + { + global $database; - $result = $database->Execute("SELECT image_id FROM pool_images WHERE pool_id=:pid ORDER BY image_order ASC", array("pid"=>$poolID)); - $images = array(); - - while($row = $result->fetch()) - { - $image = $database->get_row(" + $result = $database->Execute("SELECT image_id FROM pool_images WHERE pool_id=:pid ORDER BY image_order ASC", ["pid" => $poolID]); + $images = []; + + while ($row = $result->fetch()) { + $image = $database->get_row( + " SELECT * FROM images AS i INNER JOIN pool_images AS p ON i.id = p.image_id WHERE pool_id=:pid AND i.id=:iid", - array("pid"=>$poolID, "iid"=>$row['image_id'])); - $image = ($image ? new Image($image) : null); - $images[] = array($image); - } - - return $images; - } + ["pid" => $poolID, "iid" => $row['image_id']] + ); + $image = ($image ? new Image($image) : null); + $images[] = [$image]; + } + + return $images; + } - /** - * HERE WE NUKE ENTIRE POOL. WE REMOVE POOLS AND POSTS FROM REMOVED POOL AND HISTORIES ENTRIES FROM REMOVED POOL. - * - * @param int $poolID - */ - private function nuke_pool(/*int*/ $poolID) { - global $user, $database; + /** + * HERE WE NUKE ENTIRE POOL. WE REMOVE POOLS AND POSTS FROM REMOVED POOL AND HISTORIES ENTRIES FROM REMOVED POOL. + */ + private function nuke_pool(int $poolID) + { + global $user, $database; - $p_id = $database->get_one("SELECT user_id FROM pools WHERE id = :pid", array("pid"=>$poolID)); - if($user->is_admin()) { - $database->execute("DELETE FROM pool_history WHERE pool_id = :pid", array("pid"=>$poolID)); - $database->execute("DELETE FROM pool_images WHERE pool_id = :pid", array("pid"=>$poolID)); - $database->execute("DELETE FROM pools WHERE id = :pid", array("pid"=>$poolID)); - } elseif($user->id == $p_id) { - $database->execute("DELETE FROM pool_history WHERE pool_id = :pid", array("pid"=>$poolID)); - $database->execute("DELETE FROM pool_images WHERE pool_id = :pid", array("pid"=>$poolID)); - $database->execute("DELETE FROM pools WHERE id = :pid AND user_id = :uid", array("pid"=>$poolID, "uid"=>$user->id)); - } - } + $p_id = $database->get_one("SELECT user_id FROM pools WHERE id = :pid", ["pid" => $poolID]); + if ($user->can(Permissions::POOLS_ADMIN)) { + $database->execute("DELETE FROM pool_history WHERE pool_id = :pid", ["pid" => $poolID]); + $database->execute("DELETE FROM pool_images WHERE pool_id = :pid", ["pid" => $poolID]); + $database->execute("DELETE FROM pools WHERE id = :pid", ["pid" => $poolID]); + } elseif ($user->id == $p_id) { + $database->execute("DELETE FROM pool_history WHERE pool_id = :pid", ["pid" => $poolID]); + $database->execute("DELETE FROM pool_images WHERE pool_id = :pid", ["pid" => $poolID]); + $database->execute("DELETE FROM pools WHERE id = :pid AND user_id = :uid", ["pid" => $poolID, "uid" => $user->id]); + } + } - /** - * HERE WE ADD A HISTORY ENTRY. - * - * @param int $poolID - * @param int $action Action=1 (one) MEANS ADDED, Action=0 (zero) MEANS REMOVED - * @param string $images - * @param int $count - */ - private function add_history(/*int*/ $poolID, $action, $images, $count) { - global $user, $database; + /** + * HERE WE ADD A HISTORY ENTRY. + * + * $action Action=1 (one) MEANS ADDED, Action=0 (zero) MEANS REMOVED + */ + private function add_history(int $poolID, int $action, string $images, int $count) + { + global $user, $database; - $database->execute(" + $database->execute( + " INSERT INTO pool_history (pool_id, user_id, action, images, count, date) VALUES (:pid, :uid, :act, :img, :count, now())", - array("pid"=>$poolID, "uid"=>$user->id, "act"=>$action, "img"=>$images, "count"=>$count)); - } + ["pid" => $poolID, "uid" => $user->id, "act" => $action, "img" => $images, "count" => $count] + ); + } - /** - * HERE WE GET THE HISTORY LIST. - * @param int $pageNumber - */ - private function get_history(/*int*/ $pageNumber) { - global $config, $database; + /** + * HERE WE GET THE HISTORY LIST. + */ + private function get_history(int $pageNumber) + { + global $config, $database; - if(is_null($pageNumber) || !is_numeric($pageNumber)) - $pageNumber = 0; - else if ($pageNumber <= 0) - $pageNumber = 0; - else - $pageNumber--; + if (is_null($pageNumber) || !is_numeric($pageNumber)) { + $pageNumber = 0; + } elseif ($pageNumber <= 0) { + $pageNumber = 0; + } else { + $pageNumber--; + } - $historiesPerPage = $config->get_int("poolsUpdatedPerPage"); + $historiesPerPage = $config->get_int(PoolsConfig::UPDATED_PER_PAGE); - $history = $database->get_all(" + $history = $database->get_all(" SELECT h.id, h.pool_id, h.user_id, h.action, h.images, h.count, h.date, u.name as user_name, p.title as title FROM pool_history AS h @@ -844,112 +936,118 @@ class Pools extends Extension { ON h.user_id = u.id ORDER BY h.date DESC LIMIT :l OFFSET :o - ", array("l"=>$historiesPerPage, "o"=>$pageNumber * $historiesPerPage)); + ", ["l" => $historiesPerPage, "o" => $pageNumber * $historiesPerPage]); - $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM pool_history") / $historiesPerPage); + $totalPages = (int)ceil($database->get_one("SELECT COUNT(*) FROM pool_history") / $historiesPerPage); - $this->theme->show_history($history, $pageNumber + 1, $totalPages); - } + $this->theme->show_history($history, $pageNumber + 1, $totalPages); + } - /** - * HERE GO BACK IN HISTORY AND ADD OR REMOVE POSTS TO POOL. - * @param int $historyID - */ - private function revert_history(/*int*/ $historyID) { - global $database; - $status = $database->get_all("SELECT * FROM pool_history WHERE id=:hid", array("hid"=>$historyID)); + /** + * HERE GO BACK IN HISTORY AND ADD OR REMOVE POSTS TO POOL. + */ + private function revert_history(int $historyID) + { + global $database; + $status = $database->get_all("SELECT * FROM pool_history WHERE id=:hid", ["hid" => $historyID]); - foreach($status as $entry) { - $images = trim($entry['images']); - $images = explode(" ", $images); - $poolID = $entry['pool_id']; - $imageArray = ""; - $newAction = -1; + foreach ($status as $entry) { + $images = trim($entry['images']); + $images = explode(" ", $images); + $poolID = $entry['pool_id']; + $imageArray = ""; + $newAction = -1; - if($entry['action'] == 0) { - // READ ENTRIES - foreach($images as $image) { - $imageID = $image; - $this->add_post($poolID, $imageID); + if ($entry['action'] == 0) { + // READ ENTRIES + foreach ($images as $image) { + $imageID = $image; + $this->add_post($poolID, $imageID); - $imageArray .= " ".$imageID; - $newAction = 1; - } - } - else if($entry['action'] == 1) { - // DELETE ENTRIES - foreach($images as $image) { - $imageID = $image; - $this->delete_post($poolID, $imageID); + $imageArray .= " " . $imageID; + $newAction = 1; + } + } elseif ($entry['action'] == 1) { + // DELETE ENTRIES + foreach ($images as $image) { + $imageID = $image; + $this->delete_post($poolID, $imageID); - $imageArray .= " ".$imageID; - $newAction = 0; - } - } else { - // FIXME: should this throw an exception instead? - log_error("pools", "Invalid history action."); - continue; // go on to the next one. - } + $imageArray .= " " . $imageID; + $newAction = 0; + } + } else { + // FIXME: should this throw an exception instead? + log_error("pools", "Invalid history action."); + continue; // go on to the next one. + } - $count = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", array("pid"=>$poolID)); - $this->add_history($poolID, $newAction, $imageArray, $count); - } - } + $count = (int)$database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid" => $poolID]); + $this->add_history($poolID, $newAction, $imageArray, $count); + } + } - /** - * HERE WE ADD A SIMPLE POST FROM POOL. - * USED WITH FOREACH IN revert_history() & onTagTermParse(). - * - * @param int $poolID - * @param int $imageID - * @param bool $history - * @param int $imageOrder - */ - private function add_post(/*int*/ $poolID, /*int*/ $imageID, $history=false, $imageOrder=0) { - global $database, $config; + /** + * HERE WE ADD A SIMPLE POST FROM POOL. + * USED WITH FOREACH IN revert_history() & onTagTermParse(). + */ + private function add_post(int $poolID, int $imageID, bool $history = false, int $imageOrder = 0): bool + { + global $database, $config; - if(!$this->check_post($poolID, $imageID)) { - if($config->get_bool("poolsAutoIncrementOrder") && $imageOrder === 0){ - $imageOrder = $database->get_one(" - SELECT CASE WHEN image_order IS NOT NULL THEN MAX(image_order) + 1 ELSE 0 END + if (!$this->check_post($poolID, $imageID)) { + if ($config->get_bool(PoolsConfig::AUTO_INCREMENT_ORDER) && $imageOrder === 0) { + $imageOrder = $database->get_one( + " + SELECT COALESCE(MAX(image_order),0) + 1 FROM pool_images - WHERE pool_id = :pid", - array("pid"=>$poolID)); - } + WHERE pool_id = :pid AND image_order IS NOT NULL", + ["pid" => $poolID] + ); + } - $database->execute(" + $database->execute( + " INSERT INTO pool_images (pool_id, image_id, image_order) VALUES (:pid, :iid, :ord)", - array("pid"=>$poolID, "iid"=>$imageID, "ord"=>$imageOrder)); - } + ["pid" => $poolID, "iid" => $imageID, "ord" => $imageOrder] + ); + } else { + // If the post is already added, there is nothing else to do + return false; + } - $database->execute("UPDATE pools SET posts=(SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid) WHERE id=:pid", array("pid"=>$poolID)); + $this->update_count($poolID); - if($history){ - $count = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", array("pid"=>$poolID)); - $this->add_history($poolID, 1, $imageID, $count); - } - } + if ($history) { + $count = (int)$database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid" => $poolID]); + $this->add_history($poolID, 1, (string)$imageID, $count); + } + return true; + } - /** - * HERE WE REMOVE A SIMPLE POST FROM POOL. - * USED WITH FOREACH IN revert_history() & onTagTermParse(). - * - * @param int $poolID - * @param int $imageID - * @param bool $history - */ - private function delete_post(/*int*/ $poolID, /*int*/ $imageID, $history=false) { - global $database; - $database->execute("DELETE FROM pool_images WHERE pool_id = :pid AND image_id = :iid", array("pid"=>$poolID, "iid"=>$imageID)); - $database->execute("UPDATE pools SET posts=(SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid) WHERE id=:pid", array("pid"=>$poolID)); + private function update_count($pool_id) + { + global $database; + $database->execute("UPDATE pools SET posts=(SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid) WHERE id=:pid", ["pid" => $pool_id]); + } - if($history){ - $count = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", array("pid"=>$poolID)); - $this->add_history($poolID, 0, $imageID, $count); - } - } + /** + * HERE WE REMOVE A SIMPLE POST FROM POOL. + * USED WITH FOREACH IN revert_history() & onTagTermParse(). + */ + private function delete_post(int $poolID, int $imageID, bool $history = false) + { + global $database; + $database->execute("DELETE FROM pool_images WHERE pool_id = :pid AND image_id = :iid", ["pid" => $poolID, "iid" => $imageID]); + + $this->update_count($poolID); + + if ($history) { + $count = (int)$database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid" => $poolID]); + $this->add_history($poolID, 0, (string)$imageID, $count); + } + } } - diff --git a/ext/pools/script.js b/ext/pools/script.js index 6b45792c..16119d32 100644 --- a/ext/pools/script.js +++ b/ext/pools/script.js @@ -1,6 +1,6 @@ /*jshint bitwise:true, curly:true, forin:false, noarg:true, noempty:true, nonew:true, undef:true, strict:false, browser:true, jquery:true */ -$(function() { +document.addEventListener('DOMContentLoaded', () => { $('#order_pool').change(function(){ var val = $("#order_pool option:selected").val(); Cookies.set("shm_ui-order-pool", val, {path: '/', expires: 365}); //FIXME: This won't play nice if COOKIE_PREFIX is not "shm_". diff --git a/ext/pools/test.php b/ext/pools/test.php index 0a3cc265..06ac0d18 100644 --- a/ext/pools/test.php +++ b/ext/pools/test.php @@ -1,42 +1,12 @@ -get_page('pool/list'); - $this->assert_title("Pools"); +get_page('pool/list'); + $this->assert_title("Pools"); - $this->get_page('pool/new'); - $this->assert_title("Error"); - - $this->log_in_as_user(); - $this->get_page('pool/list'); - - $this->markTestIncomplete(); - - $this->click("Create Pool"); - $this->assert_title("Create Pool"); - $this->click("Create"); - $this->assert_title("Error"); - - $this->get_page('pool/new'); - $this->assert_title("Create Pool"); - $this->set_field("title", "Test Pool Title"); - $this->set_field("description", "Test pool description"); - $this->click("Create"); - $this->assert_title("Pool: Test Pool Title"); - - $this->log_out(); - - - $this->log_in_as_admin(); - - $this->get_page('pool/list'); - $this->click("Test Pool Title"); - $this->assert_title("Pool: Test Pool Title"); - $this->click("Delete Pool"); - $this->assert_title("Pools"); - $this->assert_no_text("Test Pool Title"); - - $this->log_out(); - } + $this->get_page('pool/new'); + $this->assert_title("Error"); + } } - diff --git a/ext/pools/theme.php b/ext/pools/theme.php index 32c5f0f0..da21495f 100644 --- a/ext/pools/theme.php +++ b/ext/pools/theme.php @@ -1,48 +1,46 @@ - $pool){ - $linksPools[] = "".html_escape($pool['info']['title']).""; + $linksPools = []; + foreach ($navIDs as $poolID => $pool) { + $linksPools[] = "" . html_escape($pool['info']['title']) . ""; - if (array_key_exists('nav', $pool)){ - $navlinks = ""; - if (!empty($pool['nav']['prev'])) { - $navlinks .= 'Prev'; - } - if (!empty($pool['nav']['next'])) { - $navlinks .= 'Next'; - } - if(!empty($navlinks)){ - $navlinks .= "
    "; - $linksPools[] = $navlinks; - } - } - } + if (array_key_exists('nav', $pool)) { + $navlinks = ""; + if (!empty($pool['nav']['prev'])) { + $navlinks .= 'Prev'; + } + if (!empty($pool['nav']['next'])) { + $navlinks .= 'Next'; + } + if (!empty($navlinks)) { + $navlinks .= "
    "; + $linksPools[] = $navlinks; + } + } + } - if(count($linksPools) > 0) { - $page->add_block(new Block("Pools", implode("
    ", $linksPools), "left")); - } - } + if (count($linksPools) > 0) { + $page->add_block(new Block("Pools", implode("
    ", $linksPools), "left")); + } + } - /** - * @param Image $image - * @param array $pools - * @return string - */ - public function get_adder_html(Image $image, /*array*/ $pools) { - $h = ""; - foreach($pools as $pool) { - $h .= ""; - } - $editor = "\n".make_form(make_link("pool/add_post"))." + public function get_adder_html(Image $image, array $pools): string + { + $h = ""; + foreach ($pools as $pool) { + $h .= ""; + } + $editor = "\n" . make_form(make_link("pool/add_post")) . " @@ -50,19 +48,15 @@ class PoolsTheme extends Themelet { "; - return $editor; - } + return $editor; + } - /** - * HERE WE SHOWS THE LIST OF POOLS. - * - * @param Page $page - * @param array $pools - * @param int $pageNumber - * @param int $totalPages - */ - public function list_pools(Page $page, /*array*/ $pools, /*int*/ $pageNumber, /*int*/ $totalPages) { - $html = ' + /** + * HERE WE SHOWS THE LIST OF POOLS. + */ + public function list_pools(Page $page, array $pools, int $pageNumber, int $totalPages) + { + $html = ' @@ -71,47 +65,47 @@ class PoolsTheme extends Themelet { '; - // Build up the list of pools. - foreach($pools as $pool) { - $pool_link = ''.html_escape($pool['title']).""; - $user_link = ''.html_escape($pool['user_name']).""; - $public = ($pool['public'] == "Y" ? "Yes" : "No"); + // Build up the list of pools. + foreach ($pools as $pool) { + $pool_link = '' . html_escape($pool['title']) . ""; + $user_link = '' . html_escape($pool['user_name']) . ""; + $public = ($pool['public'] == "Y" ? "Yes" : "No"); - $html .= "". - "". - "". - "". - "". - ""; - } + $html .= "" . + "" . + "" . + "" . + "" . + ""; + } - $html .= "
    NamePublic
    ".$pool_link."".$user_link."".$pool['posts']."".$public."
    " . $pool_link . "" . $user_link . "" . $pool['posts'] . "" . $public . "
    "; + $html .= ""; - $order_html = ''; + $order_html = ''; - $this->display_top(null, "Pools"); - $page->add_block(new Block("Order By", $order_html, "left", 15)); + $this->display_top(null, "Pools"); + $page->add_block(new Block("Order By", $order_html, "left", 15)); - $page->add_block(new Block("Pools", $html, "main", 10)); + $page->add_block(new Block("Pools", $html, "main", 10)); + $this->display_paginator($page, "pool/list", null, $pageNumber, $totalPages); + } - $this->display_paginator($page, "pool/list", null, $pageNumber, $totalPages); - } - - /* - * HERE WE DISPLAY THE NEW POOL COMPOSER - */ - public function new_pool_composer(Page $page) { - $create_html = " - ".make_form(make_link("pool/create"))." + /* + * HERE WE DISPLAY THE NEW POOL COMPOSER + */ + public function new_pool_composer(Page $page) + { + $create_html = " + " . make_form(make_link("pool/create")) . " @@ -121,99 +115,88 @@ class PoolsTheme extends Themelet { "; - $this->display_top(null, "Create Pool"); - $page->add_block(new Block("Create Pool", $create_html, "main", 20)); - } + $this->display_top(null, "Create Pool"); + $page->add_block(new Block("Create Pool", $create_html, "main", 20)); + } - /** - * @param array $pools - * @param string $heading - * @param bool $check_all - */ - private function display_top(/*array*/ $pools, /*string*/ $heading, $check_all=false) { - global $page, $user; + private function display_top(?array $pools, string $heading, bool $check_all = false) + { + global $page, $user; - $page->set_title($heading); - $page->set_heading($heading); + $page->set_title($heading); + $page->set_heading($heading); - $poolnav_html = ' - Pool Index -
    Create Pool -
    Pool Changes + $poolnav_html = ' + Pool Index +
    Create Pool +
    Pool Changes '; - $page->add_block(new NavBlock()); - $page->add_block(new Block("Pool Navigation", $poolnav_html, "left", 10)); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Pool Navigation", $poolnav_html, "left", 10)); - if(count($pools) == 1) { - $pool = $pools[0]; - if($pool['public'] == "Y" || $user->is_admin()) {// IF THE POOL IS PUBLIC OR IS ADMIN SHOW EDIT PANEL - if(!$user->is_anonymous()) {// IF THE USER IS REGISTERED AND LOGGED IN SHOW EDIT PANEL - $this->sidebar_options($page, $pool, $check_all); - } - } + if (!is_null($pools) && count($pools) == 1) { + $pool = $pools[0]; + if ($pool['public'] == "Y" || $user->can(Permissions::POOLS_ADMIN)) {// IF THE POOL IS PUBLIC OR IS ADMIN SHOW EDIT PANEL + if (!$user->is_anonymous()) {// IF THE USER IS REGISTERED AND LOGGED IN SHOW EDIT PANEL + $this->sidebar_options($page, $pool, $check_all); + } + } - $tfe = new TextFormattingEvent($pool['description']); - send_event($tfe); - $page->add_block(new Block(html_escape($pool['title']), $tfe->formatted, "main", 10)); - } - } + $tfe = new TextFormattingEvent($pool['description']); + send_event($tfe); + $page->add_block(new Block(html_escape($pool['title']), $tfe->formatted, "main", 10)); + } + } - /** - * HERE WE DISPLAY THE POOL WITH TITLE DESCRIPTION AND IMAGES WITH PAGINATION. - * - * @param array $pools - * @param array $images - * @param int $pageNumber - * @param int $totalPages - */ - public function view_pool(/*array*/ $pools, /*array*/ $images, /*int*/ $pageNumber, /*int*/ $totalPages) { - global $page; + /** + * HERE WE DISPLAY THE POOL WITH TITLE DESCRIPTION AND IMAGES WITH PAGINATION. + */ + public function view_pool(array $pools, array $images, int $pageNumber, int $totalPages) + { + global $page; - $this->display_top($pools, "Pool: ".html_escape($pools[0]['title'])); + $this->display_top($pools, "Pool: " . html_escape($pools[0]['title'])); - $pool_images = ''; - foreach($images as $image) { - $thumb_html = $this->build_thumb_html($image); - $pool_images .= "\n".$thumb_html."\n"; - } + $pool_images = ''; + foreach ($images as $image) { + $thumb_html = $this->build_thumb_html($image); + $pool_images .= "\n" . $thumb_html . "\n"; + } - $page->add_block(new Block("Viewing Posts", $pool_images, "main", 30)); - $this->display_paginator($page, "pool/view/".$pools[0]['id'], null, $pageNumber, $totalPages); - } + $page->add_block(new Block("Viewing Posts", $pool_images, "main", 30)); + $this->display_paginator($page, "pool/view/" . $pools[0]['id'], null, $pageNumber, $totalPages); + } - /** - * HERE WE DISPLAY THE POOL OPTIONS ON SIDEBAR BUT WE HIDE REMOVE OPTION IF THE USER IS NOT THE OWNER OR ADMIN. - * - * @param Page $page - * @param array $pool - * @param bool $check_all - */ - public function sidebar_options(Page $page, $pool, /*bool*/ $check_all) { - global $user; + /** + * HERE WE DISPLAY THE POOL OPTIONS ON SIDEBAR BUT WE HIDE REMOVE OPTION IF THE USER IS NOT THE OWNER OR ADMIN. + */ + public function sidebar_options(Page $page, array $pool, bool $check_all) + { + global $user; - $editor = "\n".make_form( make_link('pool/import') ).' + $editor = "\n" . make_form(make_link('pool/import')) . ' - + - - '.make_form( make_link('pool/edit') ).' + + ' . make_form(make_link('pool/edit')) . ' - + - - '.make_form( make_link('pool/order') ).' + + ' . make_form(make_link('pool/order')) . ' - + '; - if($user->id == $pool['user_id'] || $user->is_admin()){ - $editor .= " + if ($user->id == $pool['user_id'] || $user->can(Permissions::POOLS_ADMIN)) { + $editor .= " - ".make_form(make_link("pool/nuke"))." + " . make_form(make_link("pool/nuke")) . " - + "; - } + } - if($check_all) { - $editor .= " + if ($check_all) { + $editor .= " "; - $sb = new SetupBlock("General"); - $sb->position = 0; - $sb->add_text_option("title", "Site title: "); - $sb->add_text_option("front_page", "
    Front page: "); - $sb->add_text_option("main_page", "
    Main page: "); - $sb->add_text_option("contact_link", "
    Contact URL: "); - $sb->add_choice_option("theme", $themes, "
    Theme: "); - //$sb->add_multichoice_option("testarray", array("a" => "b", "c" => "d"), "
    Test Array: "); - $sb->add_bool_option("nice_urls", "
    Nice URLs: "); - $sb->add_label("(Javascript inactive, can't test!)$nicescript"); - $event->panel->add_block($sb); + $sb = new SetupBlock("General"); + $sb->position = 0; + $sb->add_text_option(SetupConfig::TITLE, "Site title: "); + $sb->add_text_option(SetupConfig::FRONT_PAGE, "
    Front page: "); + $sb->add_text_option(SetupConfig::MAIN_PAGE, "
    Main page: "); + $sb->add_text_option("contact_link", "
    Contact URL: "); + $sb->add_choice_option(SetupConfig::THEME, $themes, "
    Theme: "); + //$sb->add_multichoice_option("testarray", array("a" => "b", "c" => "d"), "
    Test Array: "); + $sb->add_bool_option("nice_urls", "
    Nice URLs: "); + $sb->add_label("(Javascript inactive, can't test!)$nicescript"); + $event->panel->add_block($sb); - $sb = new SetupBlock("Remote API Integration"); - $sb->add_label("Akismet"); - $sb->add_text_option("comment_wordpress_key", "
    API key: "); - $sb->add_label("
     
    ReCAPTCHA"); - $sb->add_text_option("api_recaptcha_privkey", "
    Secret key: "); - $sb->add_text_option("api_recaptcha_pubkey", "
    Site key: "); - $event->panel->add_block($sb); - } + $sb = new SetupBlock("Remote API Integration"); + $sb->add_label("Akismet"); + $sb->add_text_option("comment_wordpress_key", "
    API key: "); + $sb->add_label("
     
    ReCAPTCHA"); + $sb->add_text_option("api_recaptcha_privkey", "
    Secret key: "); + $sb->add_text_option("api_recaptcha_pubkey", "
    Site key: "); + $event->panel->add_block($sb); + } - public function onConfigSave(ConfigSaveEvent $event) { - global $config; - foreach($_POST as $_name => $junk) { - if(substr($_name, 0, 6) == "_type_") { - $name = substr($_name, 6); - $type = $_POST["_type_$name"]; - $value = isset($_POST["_config_$name"]) ? $_POST["_config_$name"] : null; - switch($type) { - case "string": $config->set_string($name, $value); break; - case "int": $config->set_int($name, $value); break; - case "bool": $config->set_bool($name, $value); break; - case "array": $config->set_array($name, $value); break; - } - } - } - log_warning("setup", "Configuration updated"); - foreach(glob("data/cache/*.css") as $css_cache) { - unlink($css_cache); - } - log_warning("setup", "Cache cleared"); - } + public function onConfigSave(ConfigSaveEvent $event) + { + global $config; + foreach ($_POST as $_name => $junk) { + if (substr($_name, 0, 6) == "_type_") { + $name = substr($_name, 6); + $type = $_POST["_type_$name"]; + $value = isset($_POST["_config_$name"]) ? $_POST["_config_$name"] : null; + switch ($type) { + case "string": $config->set_string($name, $value); break; + case "int": $config->set_int($name, parse_shorthand_int((string)$value)); break; + case "bool": $config->set_bool($name, bool_escape($value)); break; + case "array": $config->set_array($name, $value); break; + } + } + } + log_warning("setup", "Configuration updated"); + foreach (glob("data/cache/*.css") as $css_cache) { + unlink($css_cache); + } + log_warning("setup", "Cache cleared"); + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("change_setting")) { - $event->add_link("Board Config", make_link("setup")); - } - } + public function onCommand(CommandEvent $event) + { + if ($event->cmd == "help") { + print "\tconfig [get|set] \n"; + print "\t\teg 'config get db_version'\n\n"; + } + if ($event->cmd == "config") { + global $cache, $config; + $cmd = $event->args[0]; + $key = $event->args[1]; + switch ($cmd) { + case "get": + print($config->get_string($key) . "\n"); + break; + case "set": + $config->set_string($key, $event->args[2]); + break; + } + $cache->delete("config"); + } + } + + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::CHANGE_SETTING)) { + $event->add_nav_link("setup", new Link('setup'), "Board Config", null, 0); + } + } + } + + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::CHANGE_SETTING)) { + $event->add_link("Board Config", make_link("setup")); + } + } + + public function onParseLinkTemplate(ParseLinkTemplateEvent $event) + { + global $config; + $event->replace('$base', $config->get_string('base_href')); + $event->replace('$title', $config->get_string(SetupConfig::TITLE)); + } } - diff --git a/ext/setup/style.css b/ext/setup/style.css index ad738987..bc460fa4 100644 --- a/ext/setup/style.css +++ b/ext/setup/style.css @@ -38,6 +38,6 @@ background: none; border: none; box-shadow: none; - margin: 0px; - padding: 0px; + margin: 0; + padding: 0; } diff --git a/ext/setup/test.php b/ext/setup/test.php index 2989472d..0b89d2b1 100644 --- a/ext/setup/test.php +++ b/ext/setup/test.php @@ -1,39 +1,44 @@ -get_page('nicetest'); - $this->assert_content("ok"); - $this->assert_no_content("\n"); - } +get_page('nicetest'); + $this->assert_content("ok"); + $this->assert_no_content("\n"); + } - public function testAuthAnon() { - $this->get_page('setup'); - $this->assert_response(403); - $this->assert_title("Permission Denied"); - } + public function testAuthAnon() + { + $this->get_page('setup'); + $this->assert_response(403); + $this->assert_title("Permission Denied"); + } - public function testAuthUser() { - $this->log_in_as_user(); - $this->get_page('setup'); - $this->assert_response(403); - $this->assert_title("Permission Denied"); - } + public function testAuthUser() + { + $this->log_in_as_user(); + $this->get_page('setup'); + $this->assert_response(403); + $this->assert_title("Permission Denied"); + } - public function testAuthAdmin() { - $this->log_in_as_admin(); - $this->get_page('setup'); - $this->assert_title("Shimmie Setup"); - $this->assert_text("General"); - } + public function testAuthAdmin() + { + $this->log_in_as_admin(); + $this->get_page('setup'); + $this->assert_title("Shimmie Setup"); + $this->assert_text("General"); + } - public function testAdvanced() { - $this->log_in_as_admin(); - $this->get_page('setup/advanced'); - $this->assert_title("Shimmie Setup"); - $this->assert_text("thumb_quality"); - } + public function testAdvanced() + { + $this->log_in_as_admin(); + $this->get_page('setup/advanced'); + $this->assert_title("Shimmie Setup"); + $this->assert_text(ImageConfig::THUMB_QUALITY); + } } - diff --git a/ext/setup/theme.php b/ext/setup/theme.php index 6a563890..56e29460 100644 --- a/ext/setup/theme.php +++ b/ext/setup/theme.php @@ -1,61 +1,67 @@ -blocks the blocks to be displayed, unsorted - * - * It's recommented that the theme sort the blocks before doing anything - * else, using: usort($panel->blocks, "blockcmp"); - * - * The page should wrap all the options in a form which links to setup_save - */ - public function display_page(Page $page, SetupPanel $panel) { - usort($panel->blocks, "blockcmp"); +class SetupTheme extends Themelet +{ + /* + * Display a set of setup option blocks + * + * $panel = the container of the blocks + * $panel->blocks the blocks to be displayed, unsorted + * + * It's recommented that the theme sort the blocks before doing anything + * else, using: usort($panel->blocks, "blockcmp"); + * + * The page should wrap all the options in a form which links to setup_save + */ + public function display_page(Page $page, SetupPanel $panel) + { + usort($panel->blocks, "blockcmp"); - /* - * Try and keep the two columns even; count the line breaks in - * each an calculate where a block would work best - */ - $setupblock_html = ""; - foreach($panel->blocks as $block) { - $setupblock_html .= $this->sb_to_html($block); - } + /* + * Try and keep the two columns even; count the line breaks in + * each an calculate where a block would work best + */ + $setupblock_html = ""; + foreach ($panel->blocks as $block) { + $setupblock_html .= $this->sb_to_html($block); + } - $table = " + $table = " ".make_form(make_link("setup/save"))."
    $setupblock_html
    "; - $page->set_title("Shimmie Setup"); - $page->set_heading("Shimmie Setup"); - $page->add_block(new Block("Navigation", $this->build_navigation(), "left", 0)); - $page->add_block(new Block("Setup", $table)); - } + $page->set_title("Shimmie Setup"); + $page->set_heading("Shimmie Setup"); + $page->add_block(new Block("Navigation", $this->build_navigation(), "left", 0)); + $page->add_block(new Block("Setup", $table)); + } - public function display_advanced(Page $page, $options) { - $h_rows = ""; - ksort($options); - foreach($options as $name => $value) { - $h_name = html_escape($name); - $h_value = html_escape($value); + public function display_advanced(Page $page, $options) + { + $h_rows = ""; + ksort($options); + foreach ($options as $name => $value) { + if (is_null($value)) { + $value = ''; + } - $h_box = ""; - if(strpos($value, "\n") > 0) { - $h_box .= ""; - } - else { - $h_box .= ""; - } - $h_box .= ""; - $h_rows .= "
    "; - } + $h_name = html_escape($name); + $h_value = html_escape((string)$value); - $table = " + $h_box = ""; + if (is_string($value) && strpos($value, "\n") > 0) { + $h_box .= ""; + } else { + $h_box .= ""; + } + $h_box .= ""; + $h_rows .= ""; + } + + $table = " ".make_form(make_link("setup/save"))."
    Title:
    Public?
    $h_name$h_box
    $h_name$h_box
    @@ -65,31 +71,32 @@ class SetupTheme extends Themelet { "; - $page->set_title("Shimmie Setup"); - $page->set_heading("Shimmie Setup"); - $page->add_block(new Block("Navigation", $this->build_navigation(), "left", 0)); - $page->add_block(new Block("Setup", $table)); - } + $page->set_title("Shimmie Setup"); + $page->set_heading("Shimmie Setup"); + $page->add_block(new Block("Navigation", $this->build_navigation(), "left", 0)); + $page->add_block(new Block("Setup", $table)); + } - protected function build_navigation() { - return " + protected function build_navigation() + { + return " Index
    Help
    Advanced "; - } + } - protected function sb_to_html(SetupBlock $block) { - $h = $block->header; - $b = $block->body; - $i = preg_replace('/[^a-zA-Z0-9]/', '_', $h) . "-setup"; - $html = " + protected function sb_to_html(SetupBlock $block) + { + $h = $block->header; + $b = $block->body; + $i = preg_replace('/[^a-zA-Z0-9]/', '_', $h) . "-setup"; + $html = "
    $h
    $b
    "; - return $html; - } + return $html; + } } - diff --git a/ext/shimmie_api/info.php b/ext/shimmie_api/info.php new file mode 100644 index 00000000..878a55de --- /dev/null +++ b/ext/shimmie_api/info.php @@ -0,0 +1,24 @@ +Admin Warning - this exposes private data, eg IP addresses +

    Developer Warning - the API is unstable; notably, private data may get hidden +

    Usage: +

    get_tags - List of all tags. (May contain unused tags) +

      tags - Optional - Search for more specific tags (Searchs TAG*)
    +

    get_image - Get image via id. +

      id - Required - User id. (Defaults to id=1 if empty)
    +

    find_images - List of latest 12(?) images. +

    get_user - Get user info. (Defaults to id=2 if both are empty) +

      id - Optional - User id.
    +
      name - Optional - User name.
    "; +} diff --git a/ext/shimmie_api/main.php b/ext/shimmie_api/main.php index a672f85b..31923d46 100644 --- a/ext/shimmie_api/main.php +++ b/ext/shimmie_api/main.php @@ -1,173 +1,162 @@ - - * Description: A JSON interface to shimmie data [WARNING] - * Documentation: - * Admin Warning - this exposes private data, eg IP addresses - *

    Developer Warning - the API is unstable; notably, private data may get hidden - *

    Usage: - *

    get_tags - List of all tags. (May contain unused tags) - *

      tags - Optional - Search for more specific tags (Searchs TAG*)
    - *

    get_image - Get image via id. - *

      id - Required - User id. (Defaults to id=1 if empty)
    - *

    find_images - List of latest 12(?) images. - *

    get_user - Get user info. (Defaults to id=2 if both are empty) - *

      id - Optional - User id.
    - *
      name - Optional - User name.
    - */ +id = $img->id; - $this->height = $img->height; - $this->width = $img->width; - $this->hash = $img->hash; - $this->filesize = $img->filesize; - $this->ext = $img->ext; - $this->posted = strtotime($img->posted); - $this->source = $img->source; - $this->owner_id = $img->owner_id; - $this->tags = $img->get_tag_array(); - } + public function __construct(Image $img) + { + $this->id = $img->id; + $this->height = $img->height; + $this->width = $img->width; + $this->hash = $img->hash; + $this->filesize = $img->filesize; + $this->ext = $img->ext; + $this->posted = strtotime($img->posted); + $this->source = $img->source; + $this->owner_id = $img->owner_id; + $this->tags = $img->get_tag_array(); + } } -class ShimmieApi extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; +class ShimmieApi extends Extension +{ + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; - if($event->page_matches("api/shimmie")) { - $page->set_mode("data"); - $page->set_type("text/plain"); + if ($event->page_matches("api/shimmie")) { + $page->set_mode(PageMode::DATA); + $page->set_type("text/plain"); - if($event->page_matches("api/shimmie/get_tags")){ - $tag = $event->get_arg(0); - if(empty($tag) && isset($_GET['tag'])) $tag = $_GET['tag']; - $res = $this->api_get_tags($tag); - $page->set_data(json_encode($res)); - } + if ($event->page_matches("api/shimmie/get_tags")) { + if ($event->count_args() > 0) { + $tag = $event->get_arg(0); + } elseif (isset($_GET['tag'])) { + $tag = $_GET['tag']; + } else { + $tag = null; + } + $res = $this->api_get_tags($tag); + $page->set_data(json_encode($res)); + } elseif ($event->page_matches("api/shimmie/get_image")) { + $arg = $event->get_arg(0); + if (empty($arg) && isset($_GET['id'])) { + $arg = $_GET['id']; + } + $image = Image::by_id(int_escape($arg)); + // FIXME: handle null image + $image->get_tag_array(); // tag data isn't loaded into the object until necessary + $safe_image = new _SafeImage($image); + $page->set_data(json_encode($safe_image)); + } elseif ($event->page_matches("api/shimmie/find_images")) { + $search_terms = $event->get_search_terms(); + $page_number = $event->get_page_number(); + $page_size = $event->get_page_size(); + $images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms); + $safe_images = []; + foreach ($images as $image) { + $image->get_tag_array(); + $safe_images[] = new _SafeImage($image); + } + $page->set_data(json_encode($safe_images)); + } elseif ($event->page_matches("api/shimmie/get_user")) { + $query = $user->id; + $type = "id"; + if ($event->count_args() == 1) { + $query = $event->get_arg(0); + $type = "name"; + } elseif (isset($_GET['id'])) { + $query = $_GET['id']; + } elseif (isset($_GET['name'])) { + $query = $_GET['name']; + $type = "name"; + } - elseif($event->page_matches("api/shimmie/get_image")) { - $arg = $event->get_arg(0); - if(empty($arg) && isset($_GET['id'])) $arg = $_GET['id']; - $image = Image::by_id(int_escape($arg)); - // FIXME: handle null image - $image->get_tag_array(); // tag data isn't loaded into the object until necessary - $safe_image = new _SafeImage($image); - $page->set_data(json_encode($safe_image)); - } + $all = $this->api_get_user($type, $query); + $page->set_data(json_encode($all)); + } else { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("ext_doc/shimmie_api")); + } + } + } - elseif($event->page_matches("api/shimmie/find_images")) { - $search_terms = $event->get_search_terms(); - $page_number = $event->get_page_number(); - $page_size = $event->get_page_size(); - $images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms); - $safe_images = array(); - foreach($images as $image) { - $image->get_tag_array(); - $safe_images[] = new _SafeImage($image); - } - $page->set_data(json_encode($safe_images)); - } + /** + * #return string[] + */ + private function api_get_tags(?string $arg): array + { + global $database; + if (!empty($arg)) { + $all = $database->get_all("SELECT tag FROM tags WHERE tag LIKE :tag", ['tag'=>$arg . "%"]); + } else { + $all = $database->get_all("SELECT tag FROM tags"); + } + $res = []; + foreach ($all as $row) { + $res[] = $row["tag"]; + } + return $res; + } - elseif($event->page_matches("api/shimmie/get_user")) { - $query = $user->id; - $type = "id"; - if($event->count_args() == 1) { - $query = $event->get_arg(0); - $type = "name"; - } - elseif(isset($_GET['id'])) { - $query = $_GET['id']; - } - elseif(isset($_GET['name'])) { - $query = $_GET['name']; - $type = "name"; - } + private function api_get_user(string $type, string $query): array + { + global $database; + $all = $database->get_row( + "SELECT id, name, joindate, class FROM users WHERE $type=:query", + ['query'=>$query] + ); - $all = $this->api_get_user($type, $query); - $page->set_data(json_encode($all)); - } + if (!empty($all)) { + //FIXME?: For some weird reason, get_all seems to return twice. Unsetting second value to make things look nice.. + // - it returns data as eg array(0=>1234, 'id'=>1234, 1=>'bob', 'name'=>bob, ...); + for ($i = 0; $i < 4; $i++) { + unset($all[$i]); + } + $all['uploadcount'] = Image::count_images(["user_id=" . $all['id']]); + $all['commentcount'] = $database->get_one( + "SELECT COUNT(*) AS count FROM comments WHERE owner_id=:owner_id", + ["owner_id" => $all['id']] + ); - else { - $page->set_mode("redirect"); - $page->set_redirect(make_link("ext_doc/shimmie_api")); - } + if (isset($_GET['recent'])) { + $recents = $database->get_all( + "SELECT * FROM images WHERE owner_id=:owner_id ORDER BY id DESC LIMIT 0, 5", + ['owner_id'=>$all['id']] + ); - } - } + $i = 0; + foreach ($recents as $recent) { + $all['recentposts'][$i] = $recent; + unset($all['recentposts'][$i]['owner_id']); //We already know the owners id.. + unset($all['recentposts'][$i]['owner_ip']); - /** - * @param string $arg - * @return string[] - */ - private function api_get_tags($arg) { - global $database; - if (!empty($arg)) { - $all = $database->get_all("SELECT tag FROM tags WHERE tag LIKE ?", array($arg . "%")); - } else { - $all = $database->get_all("SELECT tag FROM tags"); - } - $res = array(); - foreach ($all as $row) { - $res[] = $row["tag"]; - } - return $res; - } - - /** - * @param string $type - * @param string $query - * @return array - */ - private function api_get_user($type, $query) { - global $database; - $all = $database->get_row( - "SELECT id, name, joindate, class FROM users WHERE $type=?", - array($query) - ); - - if (!empty($all)) { - //FIXME?: For some weird reason, get_all seems to return twice. Unsetting second value to make things look nice.. - // - it returns data as eg array(0=>1234, 'id'=>1234, 1=>'bob', 'name'=>bob, ...); - for ($i = 0; $i < 4; $i++) unset($all[$i]); - $all['uploadcount'] = Image::count_images(array("user_id=" . $all['id'])); - $all['commentcount'] = $database->get_one( - "SELECT COUNT(*) AS count FROM comments WHERE owner_id=:owner_id", - array("owner_id" => $all['id'])); - - if (isset($_GET['recent'])) { - $recent = $database->get_all( - "SELECT * FROM images WHERE owner_id=? ORDER BY id DESC LIMIT 0, 5", - array($all['id'])); - - $i = 0; - foreach ($recent as $all['recentposts'][$i]) { - unset($all['recentposts'][$i]['owner_id']); //We already know the owners id.. - unset($all['recentposts'][$i]['owner_ip']); - - for ($x = 0; $x < 14; $x++) unset($all['recentposts'][$i][$x]); - if (empty($all['recentposts'][$i]['author'])) unset($all['recentposts'][$i]['author']); - if ($all['recentposts'][$i]['notes'] > 0) $all['recentposts'][$i]['has_notes'] = "Y"; - else $all['recentposts'][$i]['has_notes'] = "N"; - unset($all['recentposts'][$i]['notes']); - $i += 1; - } - } - } - return $all; - } + for ($x = 0; $x < 14; $x++) { + unset($all['recentposts'][$i][$x]); + } + if (empty($all['recentposts'][$i]['author'])) { + unset($all['recentposts'][$i]['author']); + } + if ($all['recentposts'][$i]['notes'] > 0) { + $all['recentposts'][$i]['has_notes'] = "Y"; + } else { + $all['recentposts'][$i]['has_notes'] = "N"; + } + unset($all['recentposts'][$i]['notes']); + $i += 1; + } + } + } + return $all; + } } - diff --git a/ext/shimmie_api/test.php b/ext/shimmie_api/test.php index 99327dba..d6339b2f 100644 --- a/ext/shimmie_api/test.php +++ b/ext/shimmie_api/test.php @@ -1,23 +1,28 @@ -log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); +log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - // FIXME: get_page should support GET params - $this->get_page("api/shimmie/get_tags"); - $this->get_page("api/shimmie/get_tags/pb"); - //$this->get_page("api/shimmie/get_tags?tag=pb"); - $this->get_page("api/shimmie/get_image/$image_id"); - //$this->get_page("api/shimmie/get_image?id=$image_id"); - $this->get_page("api/shimmie/find_images"); - $this->get_page("api/shimmie/find_images/pbx"); - $this->get_page("api/shimmie/find_images/pbx/1"); - $this->get_page("api/shimmie/get_user/demo"); - //$this->get_page("api/shimmie/get_user?name=demo"); - //$this->get_page("api/shimmie/get_user?id=2"); + // FIXME: get_page should support GET params + $this->get_page("api/shimmie/get_tags"); + $this->get_page("api/shimmie/get_tags/pb"); + //$this->get_page("api/shimmie/get_tags?tag=pb"); + $this->get_page("api/shimmie/get_image/$image_id"); + //$this->get_page("api/shimmie/get_image?id=$image_id"); + $this->get_page("api/shimmie/find_images"); + $this->get_page("api/shimmie/find_images/pbx"); + $this->get_page("api/shimmie/find_images/pbx/1"); - // FIXME: test unspecified / bad values - // FIXME: test that json is encoded properly - } + $page = $this->get_page("api/shimmie/get_user/demo"); + $this->assertEquals(200, $page->code); + + //$this->get_page("api/shimmie/get_user?name=demo"); + //$this->get_page("api/shimmie/get_user?id=2"); + + // FIXME: test unspecified / bad values + // FIXME: test that json is encoded properly + } } diff --git a/ext/site_description/info.php b/ext/site_description/info.php new file mode 100644 index 00000000..adca4371 --- /dev/null +++ b/ext/site_description/info.php @@ -0,0 +1,16 @@ + - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Visibility: admin - * Description: A description for search engines - * Documentation: - * This extension sets the "description" meta tag in the header - * of pages so that search engines can pick it up - */ -class SiteDescription extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $config, $page; - if(strlen($config->get_string("site_description")) > 0) { - $description = $config->get_string("site_description"); - $page->add_html_header(""); - } - if(strlen($config->get_string("site_keywords")) > 0) { - $keywords = $config->get_string("site_keywords"); - $page->add_html_header(""); - } - } +add_text_option("site_description", "Description: "); - $sb->add_text_option("site_keywords", "
    Keywords: "); - $event->panel->add_block($sb); - } +class SiteDescription extends Extension +{ + public function onPageRequest(PageRequestEvent $event) + { + global $config, $page; + if (!empty($config->get_string("site_description"))) { + $description = $config->get_string("site_description"); + $page->add_html_header(""); + } + if (!empty($config->get_string("site_keywords"))) { + $keywords = $config->get_string("site_keywords"); + $page->add_html_header(""); + } + } + + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Site Description"); + $sb->add_text_option("site_description", "Description: "); + $sb->add_text_option("site_keywords", "
    Keywords: "); + $event->panel->add_block($sb); + } } - diff --git a/ext/site_description/test.php b/ext/site_description/test.php index 073252be..3c79c691 100644 --- a/ext/site_description/test.php +++ b/ext/site_description/test.php @@ -1,23 +1,25 @@ -set_string("site_description", "A Shimmie testbed"); - $this->get_page("post/list"); - $this->assertContains( - '', - $page->get_all_html_headers() - ); - } +set_string("site_description", "A Shimmie testbed"); + $this->get_page("post/list"); + $this->assertStringContainsString( + '', + $page->get_all_html_headers() + ); + } - public function testSiteKeywords() { - global $config, $page; - $config->set_string("site_keywords", "foo,bar,baz"); - $this->get_page("post/list"); - $this->assertContains( - '', - $page->get_all_html_headers() - ); - } + public function testSiteKeywords() + { + global $config, $page; + $config->set_string("site_keywords", "foo,bar,baz"); + $this->get_page("post/list"); + $this->assertStringContainsString( + '', + $page->get_all_html_headers() + ); + } } - diff --git a/ext/sitemap/info.php b/ext/sitemap/info.php new file mode 100644 index 00000000..d230056f --- /dev/null +++ b/ext/sitemap/info.php @@ -0,0 +1,13 @@ +"mail@seinkraft.info","Drudex Software"=>"support@drudexsoftware.com"]; + public $license = self::LICENSE_GPLV2; + public $description = "Sitemap with caching & advanced priorities"; +} diff --git a/ext/sitemap/main.php b/ext/sitemap/main.php index 9c34890a..afa60714 100644 --- a/ext/sitemap/main.php +++ b/ext/sitemap/main.php @@ -1,195 +1,197 @@ - - * Author: Drudex Software - * Link: http://drudexsoftware.com - * License: GPLv2 - * Description: Sitemap with caching & advanced priorities - * Documentation: - */ class XMLSitemap extends Extension { - private $sitemap_queue = ""; - private $sitemap_filepath = ""; // set onPageRequest + private $sitemap_queue = ""; + private $sitemap_filepath = ""; // set onPageRequest - public function onPageRequest(PageRequestEvent $event) - { - if ($event->page_matches("sitemap.xml")) { - global $config; + public function onPageRequest(PageRequestEvent $event) + { + if ($event->page_matches("sitemap.xml")) { + global $config; - $this->sitemap_filepath = data_path("cache/sitemap.xml"); - // determine if new sitemap needs to be generated - if ($this->new_sitemap_needed()) { - // determine which type of sitemap to generate - if ($config->get_bool("sitemap_generatefull", false)) { - $this->handle_full_sitemap(); // default false until cache fixed - } else { - $this->handle_smaller_sitemap(); - } - } else $this->display_existing_sitemap(); - } - } + $this->sitemap_filepath = data_path("cache/sitemap.xml"); + // determine if new sitemap needs to be generated + if ($this->new_sitemap_needed()) { + // determine which type of sitemap to generate + if ($config->get_bool("sitemap_generatefull", false)) { + $this->handle_full_sitemap(); // default false until cache fixed + } else { + $this->handle_smaller_sitemap(); + } + } else { + $this->display_existing_sitemap(); + } + } + } - public function onSetupBuilding(SetupBuildingEvent $event) - { - $sb = new SetupBlock("Sitemap"); + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Sitemap"); - $sb->add_bool_option("sitemap_generatefull", "Generate full sitemap"); - $sb->add_label("
    (Enabled: every image and tag in sitemap, generation takes longer)"); - $sb->add_label("
    (Disabled: only display the last 50 uploads in the sitemap)"); + $sb->add_bool_option("sitemap_generatefull", "Generate full sitemap"); + $sb->add_label("
    (Enabled: every image and tag in sitemap, generation takes longer)"); + $sb->add_label("
    (Disabled: only display the last 50 uploads in the sitemap)"); - $event->panel->add_block($sb); - } + $event->panel->add_block($sb); + } - // sitemap with only the latest 50 images - private function handle_smaller_sitemap() - { - /* --- Add latest images to sitemap with higher priority --- */ - $latestimages = Image::find_images(0, 50, array()); - if(empty($latestimages)) return; - $latestimages_urllist = array(); - foreach ($latestimages as $arrayid => $image) { - // create url from image id's - $latestimages_urllist[$arrayid] = "post/view/$image->id"; - } + // sitemap with only the latest 50 images + private function handle_smaller_sitemap() + { + /* --- Add latest images to sitemap with higher priority --- */ + $latestimages = Image::find_images(0, 50, []); + if (empty($latestimages)) { + return; + } + $latestimages_urllist = []; + $last_image = null; + foreach ($latestimages as $arrayid => $image) { + // create url from image id's + $latestimages_urllist[$arrayid] = "post/view/$image->id"; + $last_image = $image; + } - $this->add_sitemap_queue($latestimages_urllist, "monthly", "0.8", date("Y-m-d", strtotime($image->posted))); + $this->add_sitemap_queue( + $latestimages_urllist, + "monthly", + "0.8", + date("Y-m-d", strtotime($last_image->posted)) + ); - /* --- Display page --- */ - // when sitemap is ok, display it from the file - $this->generate_display_sitemap(); - } + /* --- Display page --- */ + // when sitemap is ok, display it from the file + $this->generate_display_sitemap(); + } - // Full sitemap - private function handle_full_sitemap() - { - global $database, $config; + // Full sitemap + private function handle_full_sitemap() + { + global $database, $config; - // add index - $index = array(); - $index[0] = $config->get_string("front_page"); - $this->add_sitemap_queue($index, "weekly", "1"); + // add index + $index = []; + $index[0] = $config->get_string(SetupConfig::FRONT_PAGE); + $this->add_sitemap_queue($index, "weekly", "1"); - /* --- Add 20 most used tags --- */ - $popular_tags = $database->get_all("SELECT tag, count FROM tags ORDER BY `count` DESC LIMIT 0,20"); - foreach ($popular_tags as $arrayid => $tag) { - $tag = $tag['tag']; - $popular_tags[$arrayid] = "post/list/$tag/"; - } - $this->add_sitemap_queue($popular_tags, "monthly", "0.9" /* not sure how to deal with date here */); + /* --- Add 20 most used tags --- */ + $popular_tags = $database->get_all("SELECT tag, count FROM tags ORDER BY `count` DESC LIMIT 0,20"); + foreach ($popular_tags as $arrayid => $tag) { + $tag = $tag['tag']; + $popular_tags[$arrayid] = "post/list/$tag/"; + } + $this->add_sitemap_queue($popular_tags, "monthly", "0.9" /* not sure how to deal with date here */); - /* --- Add latest images to sitemap with higher priority --- */ - $latestimages = Image::find_images(0, 50, array()); - $latestimages_urllist = array(); - foreach ($latestimages as $arrayid => $image) { - // create url from image id's - $latestimages_urllist[$arrayid] = "post/view/$image->id"; - } - $this->add_sitemap_queue($latestimages_urllist, "monthly", "0.8", date("Y-m-d", strtotime($image->posted))); + /* --- Add latest images to sitemap with higher priority --- */ + $latestimages = Image::find_images(0, 50, []); + $latestimages_urllist = []; + $latest_image = null; + foreach ($latestimages as $arrayid => $image) { + // create url from image id's + $latestimages_urllist[$arrayid] = "post/view/$image->id"; + $latest_image = $image; + } + $this->add_sitemap_queue($latestimages_urllist, "monthly", "0.8", date("Y-m-d", strtotime($latest_image->posted))); - /* --- Add other tags --- */ - $other_tags = $database->get_all("SELECT tag, count FROM tags ORDER BY `count` DESC LIMIT 21,10000000"); - foreach ($other_tags as $arrayid => $tag) { - $tag = $tag['tag']; - // create url from tags (tagme ignored) - if ($tag != "tagme") - $other_tags[$arrayid] = "post/list/$tag/"; - } - $this->add_sitemap_queue($other_tags, "monthly", "0.7" /* not sure how to deal with date here */); + /* --- Add other tags --- */ + $other_tags = $database->get_all("SELECT tag, count FROM tags ORDER BY `count` DESC LIMIT 21,10000000"); + foreach ($other_tags as $arrayid => $tag) { + $tag = $tag['tag']; + // create url from tags (tagme ignored) + if ($tag != "tagme") { + $other_tags[$arrayid] = "post/list/$tag/"; + } + } + $this->add_sitemap_queue($other_tags, "monthly", "0.7" /* not sure how to deal with date here */); - /* --- Add all other images to sitemap with lower priority --- */ - $otherimages = Image::find_images(51, 10000000, array()); - foreach ($otherimages as $arrayid => $image) { - // create url from image id's - $otherimages[$arrayid] = "post/view/$image->id"; - } - $this->add_sitemap_queue($otherimages, "monthly", "0.6", date("Y-m-d", strtotime($image->posted))); + /* --- Add all other images to sitemap with lower priority --- */ + $otherimages = Image::find_images(51, 10000000, []); + $image = null; + foreach ($otherimages as $arrayid => $image) { + // create url from image id's + $otherimages[$arrayid] = "post/view/$image->id"; + } + assert(!is_null($image)); + $this->add_sitemap_queue($otherimages, "monthly", "0.6", date("Y-m-d", strtotime($image->posted))); - /* --- Display page --- */ - // when sitemap is ok, display it from the file - $this->generate_display_sitemap(); - } + /* --- Display page --- */ + // when sitemap is ok, display it from the file + $this->generate_display_sitemap(); + } - /** - * Adds an array of urls to the sitemap with the given information. - * - * @param array $urls - * @param string $changefreq - * @param string $priority - * @param string $date - */ - private function add_sitemap_queue( /*array(urls)*/ $urls, $changefreq = "monthly", - $priority = "0.5", $date = "2013-02-01") - { - foreach ($urls as $url) { - $link = make_http(make_link("$url")); - $this->sitemap_queue .= " + /** + * Adds an array of urls to the sitemap with the given information. + */ + private function add_sitemap_queue( + array $urls, + string $changefreq = "monthly", + string $priority = "0.5", + string $date = "2013-02-01" + ) { + foreach ($urls as $url) { + $link = make_http(make_link("$url")); + $this->sitemap_queue .= " $link $date $changefreq $priority "; - } - } + } + } - // sets sitemap with entries in sitemap_queue - private function generate_display_sitemap() - { - global $page; + // sets sitemap with entries in sitemap_queue + private function generate_display_sitemap() + { + global $page; - $xml = "<" . "?xml version=\"1.0\" encoding=\"utf-8\"?" . "> + $xml = "<" . "?xml version=\"1.0\" encoding=\"utf-8\"?" . "> $this->sitemap_queue "; - // Generate new sitemap - file_put_contents($this->sitemap_filepath, $xml); - $page->set_mode("data"); - $page->set_type("application/xml"); - $page->set_data($xml); - } + // Generate new sitemap + file_put_contents($this->sitemap_filepath, $xml); + $page->set_mode(PageMode::DATA); + $page->set_type("application/xml"); + $page->set_data($xml); + } - /** - * Returns true if a new sitemap is needed. - * - * @return bool - */ - private function new_sitemap_needed() - { - if(!file_exists($this->sitemap_filepath)) { - return true; - } + /** + * Returns true if a new sitemap is needed. + */ + private function new_sitemap_needed(): bool + { + if (!file_exists($this->sitemap_filepath)) { + return true; + } - $sitemap_generation_interval = 86400; // allow new site map every day - $last_generated_time = filemtime($this->sitemap_filepath); + $sitemap_generation_interval = 86400; // allow new site map every day + $last_generated_time = filemtime($this->sitemap_filepath); - // if file doesn't exist, return true - if ($last_generated_time == false) { - return true; - } + // if file doesn't exist, return true + if ($last_generated_time == false) { + return true; + } - // if it's been a day since last sitemap creation, return true - if ($last_generated_time + $sitemap_generation_interval < time()) { - return true; - } else { - return false; - } - } + // if it's been a day since last sitemap creation, return true + if ($last_generated_time + $sitemap_generation_interval < time()) { + return true; + } else { + return false; + } + } - private function display_existing_sitemap() - { - global $page; + private function display_existing_sitemap() + { + global $page; - $xml = file_get_contents($this->sitemap_filepath); + $xml = file_get_contents($this->sitemap_filepath); - $page->set_mode("data"); - $page->set_type("application/xml"); - $page->set_data($xml); - } + $page->set_mode(PageMode::DATA); + $page->set_type("application/xml"); + $page->set_data($xml); + } } - diff --git a/ext/sitemap/test.php b/ext/sitemap/test.php index a2756249..a63c58ad 100644 --- a/ext/sitemap/test.php +++ b/ext/sitemap/test.php @@ -1,8 +1,9 @@ -get_page('sitemap.xml'); - } +get_page('sitemap.xml'); + $this->assertEquals(200, $page->code); + } } diff --git a/ext/source_history/info.php b/ext/source_history/info.php new file mode 100644 index 00000000..2bc0c764 --- /dev/null +++ b/ext/source_history/info.php @@ -0,0 +1,12 @@ +set_default_int("history_limit", -1); + // in before source are actually set, so that "get current source" works + public function get_priority(): int + { + return 40; + } - // shimmie is being installed so call install to create the table. - if($config->get_int("ext_source_history_version") < 3) { - $this->install(); - } - } + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_int("history_limit", -1); + } - public function onAdminBuilding(AdminBuildingEvent $event) { - $this->theme->display_admin_block(); - } + public function onAdminBuilding(AdminBuildingEvent $event) + { + $this->theme->display_admin_block(); + } - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; - if($event->page_matches("source_history/revert")) { - // this is a request to revert to a previous version of the source - if($user->can("edit_image_tag")) { - if(isset($_POST['revert'])) { - $this->process_revert_request($_POST['revert']); - } - } - } - else if($event->page_matches("source_history/bulk_revert")) { - if($user->can("bulk_edit_image_tag") && $user->check_auth_token()) { - $this->process_bulk_revert_request(); - } - } - else if($event->page_matches("source_history/all")) { - $page_id = int_escape($event->get_arg(0)); - $this->theme->display_global_page($page, $this->get_global_source_history($page_id), $page_id); - } - else if($event->page_matches("source_history") && $event->count_args() == 1) { - // must be an attempt to view a source history - $image_id = int_escape($event->get_arg(0)); - $this->theme->display_history_page($page, $image_id, $this->get_source_history_from_id($image_id)); - } - } - - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { - $event->add_part(" + if ($event->page_matches("source_history/revert")) { + // this is a request to revert to a previous version of the source + if ($user->can(Permissions::EDIT_IMAGE_TAG)) { + if (isset($_POST['revert'])) { + $this->process_revert_request((int)$_POST['revert']); + } + } + } elseif ($event->page_matches("source_history/bulk_revert")) { + if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG) && $user->check_auth_token()) { + $this->process_bulk_revert_request(); + } + } elseif ($event->page_matches("source_history/all")) { + $page_id = int_escape($event->get_arg(0)); + $this->theme->display_global_page($page, $this->get_global_source_history($page_id), $page_id); + } elseif ($event->page_matches("source_history") && $event->count_args() == 1) { + // must be an attempt to view a source history + $image_id = int_escape($event->get_arg(0)); + $this->theme->display_history_page($page, $image_id, $this->get_source_history_from_id($image_id)); + } + } + + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) + { + $event->add_part("
    ", 20); - } + } - /* - // disk space is cheaper than manually rebuilding history, - // so let's default to -1 and the user can go advanced if - // they /really/ want to - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Source History"); - $sb->add_label("Limit to "); - $sb->add_int_option("history_limit"); - $sb->add_label(" entires per image"); - $sb->add_label("
    (-1 for unlimited)"); - $event->panel->add_block($sb); - } - */ + /* + // disk space is cheaper than manually rebuilding history, + // so let's default to -1 and the user can go advanced if + // they /really/ want to + public function onSetupBuilding(SetupBuildingEvent $event) { + $sb = new SetupBlock("Source History"); + $sb->add_label("Limit to "); + $sb->add_int_option("history_limit"); + $sb->add_label(" entires per image"); + $sb->add_label("
    (-1 for unlimited)"); + $event->panel->add_block($sb); + } + */ - public function onSourceSet(SourceSetEvent $event) { - $this->add_source_history($event->image, $event->source); - } + public function onSourceSet(SourceSetEvent $event) + { + $this->add_source_history($event->image, $event->source); + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("bulk_edit_image_tag")) { - $event->add_link("Source Changes", make_link("source_history/all/1")); - } - } - - protected function install() { - global $database, $config; + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) { + $event->add_nav_link("source_history", new Link('source_history/all/1'), "Source Changes", NavLink::is_active(["source_history"])); + } + } + } - if($config->get_int("ext_source_history_version") < 1) { - $database->create_table("source_histories", " + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) { + $event->add_link("Source Changes", make_link("source_history/all/1")); + } + } + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $database; + + if ($this->get_version("ext_source_history_version") < 1) { + $database->create_table("source_histories", " id SCORE_AIPK, image_id INTEGER NOT NULL, user_id INTEGER NOT NULL, user_ip SCORE_INET NOT NULL, source TEXT NOT NULL, - date_set SCORE_DATETIME NOT NULL, + date_set TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE "); - $database->execute("CREATE INDEX source_histories_image_id_idx ON source_histories(image_id)", array()); - $config->set_int("ext_source_history_version", 3); - } - - if($config->get_int("ext_source_history_version") == 1) { - $database->Execute("ALTER TABLE source_histories ADD COLUMN user_id INTEGER NOT NULL"); - $database->Execute("ALTER TABLE source_histories ADD COLUMN date_set DATETIME NOT NULL"); - $config->set_int("ext_source_history_version", 2); - } + $database->execute("CREATE INDEX source_histories_image_id_idx ON source_histories(image_id)", []); + $this->set_version("ext_source_history_version", 3); + } - if($config->get_int("ext_source_history_version") == 2) { - $database->Execute("ALTER TABLE source_histories ADD COLUMN user_ip CHAR(15) NOT NULL"); - $config->set_int("ext_source_history_version", 3); - } - } + if ($this->get_version("ext_source_history_version") == 1) { + $database->Execute("ALTER TABLE source_histories ADD COLUMN user_id INTEGER NOT NULL"); + $database->Execute("ALTER TABLE source_histories ADD COLUMN date_set DATETIME NOT NULL"); + $this->set_version("ext_source_history_version", 2); + } - /** - * This function is called when a revert request is received. - * @param int $revert_id - */ - private function process_revert_request($revert_id) { - global $page; + if ($this->get_version("ext_source_history_version") == 2) { + $database->Execute("ALTER TABLE source_histories ADD COLUMN user_ip CHAR(15) NOT NULL"); + $this->set_version("ext_source_history_version", 3); + } + } - $revert_id = int_escape($revert_id); + /** + * This function is called when a revert request is received. + */ + private function process_revert_request(int $revert_id) + { + global $page; - // check for the nothing case - if($revert_id < 1) { - $page->set_mode("redirect"); - $page->set_redirect(make_link()); - return; - } - - // lets get this revert id assuming it exists - $result = $this->get_source_history_from_revert($revert_id); - - if(empty($result)) { - // there is no history entry with that id so either the image was deleted - // while the user was viewing the history, someone is playing with form - // variables or we have messed up in code somewhere. - /* calling die() is probably not a good idea, we should throw an Exception */ - die("Error: No source history with specified id was found."); - } - - // lets get the values out of the result - //$stored_result_id = $result['id']; - $stored_image_id = $result['image_id']; - $stored_source = $result['source']; - - log_debug("source_history", 'Reverting source of Image #'.$stored_image_id.' to ['.$stored_source.']'); + // check for the nothing case + if ($revert_id < 1) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link()); + return; + } - $image = Image::by_id($stored_image_id); - - if (is_null($image)) { - die('Error: No image with the id ('.$stored_image_id.') was found. Perhaps the image was deleted while processing this request.'); - } + // lets get this revert id assuming it exists + $result = $this->get_source_history_from_revert($revert_id); - // all should be ok so we can revert by firing the SetUserSources event. - send_event(new SourceSetEvent($image, $stored_source)); - - // all should be done now so redirect the user back to the image - $page->set_mode("redirect"); - $page->set_redirect(make_link('post/view/'.$stored_image_id)); - } + if (empty($result)) { + // there is no history entry with that id so either the image was deleted + // while the user was viewing the history, someone is playing with form + // variables or we have messed up in code somewhere. + /* calling die() is probably not a good idea, we should throw an Exception */ + die("Error: No source history with specified id was found."); + } - protected function process_bulk_revert_request() { - if (isset($_POST['revert_name']) && !empty($_POST['revert_name'])) { - $revert_name = $_POST['revert_name']; - } - else { - $revert_name = null; - } + // lets get the values out of the result + //$stored_result_id = $result['id']; + $stored_image_id = $result['image_id']; + $stored_source = $result['source']; - if (isset($_POST['revert_ip']) && !empty($_POST['revert_ip'])) { - $revert_ip = filter_var($_POST['revert_ip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE); - - if ($revert_ip === false) { - // invalid ip given. - $this->theme->display_admin_block('Invalid IP'); - return; - } - } - else { - $revert_ip = null; - } - - if (isset($_POST['revert_date']) && !empty($_POST['revert_date'])) { - if (isValidDate($_POST['revert_date']) ){ - $revert_date = addslashes($_POST['revert_date']); // addslashes is really unnecessary since we just checked if valid, but better safe. - } - else { - $this->theme->display_admin_block('Invalid Date'); - return; - } - } - else { - $revert_date = null; - } - - set_time_limit(0); // reverting changes can take a long time, disable php's timelimit if possible. - - // Call the revert function. - $this->process_revert_all_changes($revert_name, $revert_ip, $revert_date); - // output results - $this->theme->display_revert_ip_results(); - } + log_debug("source_history", 'Reverting source of Image #'.$stored_image_id.' to ['.$stored_source.']'); - /** - * @param int $revert_id - * @return mixed|null - */ - public function get_source_history_from_revert(/*int*/ $revert_id) { - global $database; - $row = $database->get_row(" + $image = Image::by_id($stored_image_id); + + if (is_null($image)) { + die('Error: No image with the id ('.$stored_image_id.') was found. Perhaps the image was deleted while processing this request.'); + } + + // all should be ok so we can revert by firing the SetUserSources event. + send_event(new SourceSetEvent($image, $stored_source)); + + // all should be done now so redirect the user back to the image + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link('post/view/'.$stored_image_id)); + } + + protected function process_bulk_revert_request() + { + if (isset($_POST['revert_name']) && !empty($_POST['revert_name'])) { + $revert_name = $_POST['revert_name']; + } else { + $revert_name = null; + } + + if (isset($_POST['revert_ip']) && !empty($_POST['revert_ip'])) { + $revert_ip = filter_var($_POST['revert_ip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE); + + if ($revert_ip === false) { + // invalid ip given. + $this->theme->display_admin_block('Invalid IP'); + return; + } + } else { + $revert_ip = null; + } + + if (isset($_POST['revert_date']) && !empty($_POST['revert_date'])) { + if (isValidDate($_POST['revert_date'])) { + $revert_date = addslashes($_POST['revert_date']); // addslashes is really unnecessary since we just checked if valid, but better safe. + } else { + $this->theme->display_admin_block('Invalid Date'); + return; + } + } else { + $revert_date = null; + } + + set_time_limit(0); // reverting changes can take a long time, disable php's timelimit if possible. + + // Call the revert function. + $this->process_revert_all_changes($revert_name, $revert_ip, $revert_date); + // output results + $this->theme->display_revert_ip_results(); + } + + public function get_source_history_from_revert(int $revert_id): ?array + { + global $database; + $row = $database->get_row(" SELECT source_histories.*, users.name FROM source_histories JOIN users ON source_histories.user_id = users.id - WHERE source_histories.id = ?", array($revert_id)); - return ($row ? $row : null); - } + WHERE source_histories.id = :id", ["id"=>$revert_id]); + return ($row ? $row : null); + } - /** - * @param int $image_id - * @return array - */ - public function get_source_history_from_id(/*int*/ $image_id) { - global $database; - $row = $database->get_all(" + public function get_source_history_from_id(int $image_id): array + { + global $database; + return $database->get_all( + " SELECT source_histories.*, users.name FROM source_histories JOIN users ON source_histories.user_id = users.id - WHERE image_id = ? + WHERE image_id = :image_id ORDER BY source_histories.id DESC", - array($image_id)); - return ($row ? $row : array()); - } + ["image_id"=>$image_id] + ); + } - /** - * @param int $page_id - * @return array - */ - public function get_global_source_history($page_id) { - global $database; - $row = $database->get_all(" + public function get_global_source_history(int $page_id): array + { + global $database; + return $database->get_all(" SELECT source_histories.*, users.name FROM source_histories JOIN users ON source_histories.user_id = users.id ORDER BY source_histories.id DESC LIMIT 100 OFFSET :offset - ", array("offset" => ($page_id-1)*100)); - return ($row ? $row : array()); - } + ", ["offset" => ($page_id-1)*100]); + } - /** - * This function attempts to revert all changes by a given IP within an (optional) timeframe. - * - * @param string $name - * @param string $ip - * @param string $date - */ - public function process_revert_all_changes($name, $ip, $date) { - global $database; - - $select_code = array(); - $select_args = array(); + /** + * This function attempts to revert all changes by a given IP within an (optional) timeframe. + */ + public function process_revert_all_changes(?string $name, ?string $ip, ?string $date) + { + global $database; - if(!is_null($name)) { - $duser = User::by_name($name); - if(is_null($duser)) { - $this->theme->add_status($name, "user not found"); - return; - } - else { - $select_code[] = 'user_id = ?'; - $select_args[] = $duser->id; - } - } + $select_code = []; + $select_args = []; - if(!is_null($date)) { - $select_code[] = 'date_set >= ?'; - $select_args[] = $date; - } + if (!is_null($name)) { + $duser = User::by_name($name); + if (is_null($duser)) { + $this->theme->add_status($name, "user not found"); + return; + } else { + $select_code[] = 'user_id = :user_id'; + $select_args['user_id'] = $duser->id; + } + } - if(!is_null($ip)) { - $select_code[] = 'user_ip = ?'; - $select_args[] = $ip; - } + if (!is_null($ip)) { + $select_code[] = 'user_ip = :user_ip'; + $select_args['user_ip'] = $ip; + } - if(count($select_code) == 0) { - log_error("source_history", "Tried to mass revert without any conditions"); - return; - } + if (!is_null($date)) { + $select_code[] = 'date_set >= :date_set'; + $select_args['date_set'] = $date; + } - log_info("source_history", 'Attempting to revert edits where '.implode(" and ", $select_code)." (".implode(" / ", $select_args).")"); - - // Get all the images that the given IP has changed source on (within the timeframe) that were last editied by the given IP - $result = $database->get_col(' + if (count($select_code) == 0) { + log_error("source_history", "Tried to mass revert without any conditions"); + return; + } + + log_info("source_history", 'Attempting to revert edits where '.implode(" and ", $select_code)." (".implode(" / ", $select_args).")"); + + // Get all the images that the given IP has changed source on (within the timeframe) that were last editied by the given IP + $result = $database->get_col(' SELECT t1.image_id FROM source_histories t1 LEFT JOIN source_histories t2 ON (t1.image_id = t2.image_id AND t1.date_set < t2.date_set) WHERE t2.image_id IS NULL - AND t1.image_id IN ( select image_id from source_histories where '.implode(" AND ", $select_code).') + AND t1.image_id IN ( select image_id from source_histories where '.implode(" AND ", $select_code).') ORDER BY t1.image_id ', $select_args); - - foreach($result as $image_id) { - // Get the first source history that was done before the given IP edit - $row = $database->get_row(' + + foreach ($result as $image_id) { + // Get the first source history that was done before the given IP edit + $row = $database->get_row(' SELECT id, source FROM source_histories WHERE image_id='.$image_id.' AND NOT ('.implode(" AND ", $select_code).') ORDER BY date_set DESC LIMIT 1 ', $select_args); - - if (empty($row)) { - // we can not revert this image based on the date restriction. - // Output a message perhaps? - } - else { - $revert_id = $row['id']; - $result = $this->get_source_history_from_revert($revert_id); - - if(empty($result)) { - // there is no history entry with that id so either the image was deleted - // while the user was viewing the history, or something messed up - /* calling die() is probably not a good idea, we should throw an Exception */ - die('Error: No source history with specified id ('.$revert_id.') was found in the database.'."\n\n". - 'Perhaps the image was deleted while processing this request.'); - } - - // lets get the values out of the result - $stored_result_id = $result['id']; - $stored_image_id = $result['image_id']; - $stored_source = $result['source']; - - log_debug("source_history", 'Reverting source of Image #'.$stored_image_id.' to ['.$stored_source.']'); - $image = Image::by_id($stored_image_id); + if (!empty($row)) { + $revert_id = $row['id']; + $result = $this->get_source_history_from_revert($revert_id); - if (is_null($image)) { - die('Error: No image with the id ('.$stored_image_id.') was found. Perhaps the image was deleted while processing this request.'); - } + if (empty($result)) { + // there is no history entry with that id so either the image was deleted + // while the user was viewing the history, or something messed up + /* calling die() is probably not a good idea, we should throw an Exception */ + die('Error: No source history with specified id ('.$revert_id.') was found in the database.'."\n\n". + 'Perhaps the image was deleted while processing this request.'); + } - // all should be ok so we can revert by firing the SetSources event. - send_event(new SourceSetEvent($image, $stored_source)); - $this->theme->add_status('Reverted Change','Reverted Image #'.$image_id.' to Source History #'.$stored_result_id.' ('.$row['source'].')'); - } - } + // lets get the values out of the result + $stored_result_id = $result['id']; + $stored_image_id = $result['image_id']; + $stored_source = $result['source']; - log_info("source_history", 'Reverted '.count($result).' edits.'); - } + log_debug("source_history", 'Reverting source of Image #'.$stored_image_id.' to ['.$stored_source.']'); - /** - * This function is called just before an images source is changed. - * @param Image $image - * @param string $source - */ - private function add_source_history($image, $source) { - global $database, $config, $user; + $image = Image::by_id($stored_image_id); - $new_source = $source; - $old_source = $image->source; - - if($new_source == $old_source) return; - - if(empty($old_source)) { - /* no old source, so we are probably adding the image for the first time */ - log_debug("source_history", "adding new source history: [$new_source]"); - } - else { - log_debug("source_history", "adding source history: [$old_source] -> [$new_source]"); - } - - $allowed = $config->get_int("history_limit"); - if($allowed == 0) return; - - // if the image has no history, make one with the old source - $entries = $database->get_one("SELECT COUNT(*) FROM source_histories WHERE image_id = ?", array($image->id)); - if($entries == 0 && !empty($old_source)) { - $database->execute(" + if (is_null($image)) { + die('Error: No image with the id ('.$stored_image_id.') was found. Perhaps the image was deleted while processing this request.'); + } + + // all should be ok so we can revert by firing the SetSources event. + send_event(new SourceSetEvent($image, $stored_source)); + $this->theme->add_status('Reverted Change', 'Reverted Image #'.$image_id.' to Source History #'.$stored_result_id.' ('.$row['source'].')'); + } + } + + log_info("source_history", 'Reverted '.count($result).' edits.'); + } + + /** + * This function is called just before an images source is changed. + */ + private function add_source_history(Image $image, string $source) + { + global $database, $config, $user; + + $new_source = $source; + $old_source = $image->source; + + if ($new_source == $old_source) { + return; + } + + if (empty($old_source)) { + /* no old source, so we are probably adding the image for the first time */ + log_debug("source_history", "adding new source history: [$new_source]"); + } else { + log_debug("source_history", "adding source history: [$old_source] -> [$new_source]"); + } + + $allowed = $config->get_int("history_limit"); + if ($allowed == 0) { + return; + } + + // if the image has no history, make one with the old source + $entries = $database->get_one("SELECT COUNT(*) FROM source_histories WHERE image_id = :image_id", ['image_id'=>$image->id]); + if ($entries == 0 && !empty($old_source)) { + $database->execute( + " INSERT INTO source_histories(image_id, source, user_id, user_ip, date_set) - VALUES (?, ?, ?, ?, now())", - array($image->id, $old_source, $config->get_int('anon_id'), '127.0.0.1')); - $entries++; - } + VALUES (:image_id, :source, :user_id, :user_ip, now())", + ["image_id"=>$image->id, "source"=>$old_source, "user_id"=>$config->get_int('anon_id'), "user_ip"=>'127.0.0.1'] + ); + $entries++; + } - // add a history entry - $database->execute(" + // add a history entry + $database->execute( + " INSERT INTO source_histories(image_id, source, user_id, user_ip, date_set) - VALUES (?, ?, ?, ?, now())", - array($image->id, $new_source, $user->id, $_SERVER['REMOTE_ADDR'])); - $entries++; - - // if needed remove oldest one - if($allowed == -1) return; - if($entries > $allowed) { - // TODO: Make these queries better - /* - MySQL does NOT allow you to modify the same table which you use in the SELECT part. - Which means that these will probably have to stay as TWO separate queries... - - http://dev.mysql.com/doc/refman/5.1/en/subquery-restrictions.html - http://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause - */ - $min_id = $database->get_one("SELECT MIN(id) FROM source_histories WHERE image_id = ?", array($image->id)); - $database->execute("DELETE FROM source_histories WHERE id = ?", array($min_id)); - } - } + VALUES (:image_id, :source, :user_id, :user_ip, now())", + ["image_id"=>$image->id, "source"=>$new_source, "user_id"=>$user->id, "user_ip"=>$_SERVER['REMOTE_ADDR']] + ); + $entries++; + + // if needed remove oldest one + if ($allowed == -1) { + return; + } + if ($entries > $allowed) { + // TODO: Make these queries better + /* + MySQL does NOT allow you to modify the same table which you use in the SELECT part. + Which means that these will probably have to stay as TWO separate queries... + + http://dev.mysql.com/doc/refman/5.1/en/subquery-restrictions.html + http://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause + */ + $min_id = $database->get_one("SELECT MIN(id) FROM source_histories WHERE image_id = :image_id", ["image_id"=>$image->id]); + $database->execute("DELETE FROM source_histories WHERE id = :id", ["id"=>$min_id]); + } + } } - diff --git a/ext/source_history/theme.php b/ext/source_history/theme.php index c02ee222..9772737d 100644 --- a/ext/source_history/theme.php +++ b/ext/source_history/theme.php @@ -1,35 +1,31 @@ - ".make_form(make_link("source_history/revert"))."
      "; - $history_list = ""; - $n = 0; - foreach($history as $fields) - { - $n++; - $current_id = $fields['id']; - $current_source = html_escape($fields['source']); - $name = $fields['name']; - $date_set = autodate($fields['date_set']); - $h_ip = $user->can("view_ip") ? " ".show_ip($fields['user_ip'], "Sourcing Image #$image_id as '$current_source'") : ""; - $setter = "".html_escape($name)."$h_ip"; + $history_list = ""; + $n = 0; + foreach ($history as $fields) { + $n++; + $current_id = $fields['id']; + $current_source = html_escape($fields['source']); + $name = $fields['name']; + $date_set = autodate($fields['date_set']); + $h_ip = $user->can(Permissions::VIEW_IP) ? " ".show_ip($fields['user_ip'], "Sourcing Image #$image_id as '$current_source'") : ""; + $setter = "".html_escape($name)."$h_ip"; - $selected = ($n == 2) ? " checked" : ""; + $selected = ($n == 2) ? " checked" : ""; - $history_list .= " + $history_list .= "
    • "; - } + } - $end_string = " + $end_string = "
    "; - $history_html = $start_string . $history_list . $end_string; + $history_html = $start_string . $history_list . $end_string; - $page->set_title('Image '.$image_id.' Source History'); - $page->set_heading('Source History: '.$image_id); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Source History", $history_html, "main", 10)); - } + $page->set_title('Image '.$image_id.' Source History'); + $page->set_heading('Source History: '.$image_id); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Source History", $history_html, "main", 10)); + } - /** - * @param Page $page - * @param array $history - * @param int $page_number - */ - public function display_global_page(Page $page, /*array*/ $history, /*int*/ $page_number) { - $start_string = " + public function display_global_page(Page $page, array $history, int $page_number) + { + $start_string = "
    ".make_form(make_link("source_history/revert"))."
      "; - $end_string = " + $end_string = "
    "; - global $user; - $history_list = ""; - foreach($history as $fields) - { - $current_id = $fields['id']; - $image_id = $fields['image_id']; - $current_source = html_escape($fields['source']); - $name = $fields['name']; - $h_ip = $user->can("view_ip") ? " ".show_ip($fields['user_ip'], "Sourcing Image #$image_id as '$current_source'") : ""; - $setter = "".html_escape($name)."$h_ip"; + global $user; + $history_list = ""; + foreach ($history as $fields) { + $current_id = $fields['id']; + $image_id = $fields['image_id']; + $current_source = html_escape($fields['source']); + $name = $fields['name']; + $h_ip = $user->can(Permissions::VIEW_IP) ? " ".show_ip($fields['user_ip'], "Sourcing Image #$image_id as '$current_source'") : ""; + $setter = "".html_escape($name)."$h_ip"; - $history_list .= ' + $history_list .= '
  • '.$image_id.': '.$current_source.' (Set by '.$setter.')
  • '; - } + } - $history_html = $start_string . $history_list . $end_string; - $page->set_title("Global Source History"); - $page->set_heading("Global Source History"); - $page->add_block(new Block("Source History", $history_html, "main", 10)); + $history_html = $start_string . $history_list . $end_string; + $page->set_title("Global Source History"); + $page->set_heading("Global Source History"); + $page->add_block(new Block("Source History", $history_html, "main", 10)); - $h_prev = ($page_number <= 1) ? "Prev" : - 'Prev'; - $h_index = "Index"; - $h_next = 'Next'; + $h_prev = ($page_number <= 1) ? "Prev" : + 'Prev'; + $h_index = "Index"; + $h_next = 'Next'; - $nav = $h_prev.' | '.$h_index.' | '.$h_next; - $page->add_block(new Block("Navigation", $nav, "left")); - } + $nav = $h_prev.' | '.$h_index.' | '.$h_next; + $page->add_block(new Block("Navigation", $nav, "left")); + } - /** - * Add a section to the admin page. - * @param string $validation_msg - */ - public function display_admin_block(/*string*/ $validation_msg='') { - global $page; - - if (!empty($validation_msg)) { - $validation_msg = '
    '. $validation_msg .''; - } - - $html = ' - Revert source changes/edit by a specific IP address or username. -
    You can restrict the time frame to revert these edits as well. -
    (Date format: 2011-10-23) + /** + * Add a section to the admin page. + */ + public function display_admin_block(string $validation_msg='') + { + global $page; + + if (!empty($validation_msg)) { + $validation_msg = '
    '. $validation_msg .''; + } + + $html = ' + Revert source changes by a specific IP address or username, optionally limited to recent changes. '.$validation_msg.'

    '.make_form(make_link("source_history/bulk_revert"), 'POST')."
    NameValue
    - +
    Username
    IP Address
    Date range
    Since
    "; - $page->add_block(new Block("Mass Source Revert", $html)); - } - - /* - * Show a standard page for results to be put into - */ - public function display_revert_ip_results() { - global $page; - $html = implode($this->messages, "\n"); - $page->add_block(new Block("Bulk Revert Results", $html)); - } + $page->add_block(new Block("Mass Source Revert", $html)); + } + + /* + * Show a standard page for results to be put into + */ + public function display_revert_ip_results() + { + global $page; + $html = implode($this->messages, "\n"); + $page->add_block(new Block("Bulk Revert Results", $html)); + } - /** - * @param string $title - * @param string $body - */ - public function add_status(/*string*/ $title, /*string*/ $body) { - $this->messages[] = '

    '. $title .'
    '. $body .'

    '; - } + public function add_status(string $title, string $body) + { + $this->messages[] = '

    '. $title .'
    '. $body .'

    '; + } } - diff --git a/ext/static_files/info.php b/ext/static_files/info.php new file mode 100644 index 00000000..abfdd7c0 --- /dev/null +++ b/ext/static_files/info.php @@ -0,0 +1,15 @@ +mode == PageMode::PAGE && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) { + $h_pagename = html_escape(implode('/', $event->args)); + $f_pagename = preg_replace("/[^a-z_\-\.]+/", "_", $h_pagename); + $theme_name = $config->get_string(SetupConfig::THEME, "default"); + + $theme_file = "themes/$theme_name/static/$f_pagename"; + $static_file = "ext/static_files/static/$f_pagename"; + + if (file_exists($theme_file) || file_exists($static_file)) { + $filename = file_exists($theme_file) ? $theme_file : $static_file; + + $page->add_http_header("Cache-control: public, max-age=600"); + $page->add_http_header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 600) . ' GMT'); + $page->set_mode(PageMode::DATA); + $page->set_data(file_get_contents($filename)); + if (endsWith($filename, ".ico")) { + $page->set_type("image/x-icon"); + } + if (endsWith($filename, ".png")) { + $page->set_type("image/png"); + } + if (endsWith($filename, ".txt")) { + $page->set_type("text/plain"); + } + } + } + } + + private function count_main($blocks) + { + $n = 0; + foreach ($blocks as $block) { + if ($block->section == "main" && $block->is_content) { + $n++; + } // more hax. + } + return $n; + } + + public function get_priority(): int + { + return 98; + } // before 404 +} diff --git a/lib/vendor/js/modernizr-3.3.1.custom.js b/ext/static_files/modernizr-3.3.1.custom.js similarity index 100% rename from lib/vendor/js/modernizr-3.3.1.custom.js rename to ext/static_files/modernizr-3.3.1.custom.js diff --git a/ext/static_files/script.js b/ext/static_files/script.js new file mode 100644 index 00000000..d0800904 --- /dev/null +++ b/ext/static_files/script.js @@ -0,0 +1,86 @@ +/*jshint bitwise:false, curly:true, eqeqeq:true, evil:true, forin:false, noarg:true, noempty:true, nonew:true, undef:false, strict:false, browser:true */ + +document.addEventListener('DOMContentLoaded', () => { + /** Load jQuery extensions **/ + //Code via: http://stackoverflow.com/a/13106698 + $.fn.highlight = function (fadeOut) { + fadeOut = typeof fadeOut !== 'undefined' ? fadeOut : 5000; + $(this).each(function () { + let el = $(this); + $("
    ") + .width(el.outerWidth()) + .height(el.outerHeight()) + .css({ + "position": "absolute", + "left": el.offset().left, + "top": el.offset().top, + "background-color": "#ffff99", + "opacity": ".7", + "z-index": "9999999", + "border-top-left-radius": parseInt(el.css("borderTopLeftRadius"), 10), + "border-top-right-radius": parseInt(el.css("borderTopRightRadius"), 10), + "border-bottom-left-radius": parseInt(el.css("borderBottomLeftRadius"), 10), + "border-bottom-right-radius": parseInt(el.css("borderBottomRightRadius"), 10) + }).appendTo('body').fadeOut(fadeOut).queue(function () { $(this).remove(); }); + }); + }; + + /** Setup jQuery.timeago **/ + $.timeago.settings.cutoff = 365 * 24 * 60 * 60 * 1000; // Display original dates older than 1 year + $("time").timeago(); + + /** Setup tablesorter **/ + $("table.sortable").tablesorter(); + + /** Setup sidebar toggle **/ + let sidebar_hidden = []; + try { + sidebar_hidden = (Cookies.get("ui-sidebar-hidden") || "").split("|"); + for (let i=0; i 0) { + $(sidebar_hidden[i]+" .blockbody").hide(); + } + } + } + catch(err) {} + $(".shm-toggler").each(function(idx, elm) { + let tid = $(elm).data("toggle-sel"); + let tob = $(tid+" .blockbody"); + $(elm).click(function(e) { + tob.slideToggle("slow"); + if(sidebar_hidden.indexOf(tid) === -1) { + sidebar_hidden.push(tid); + } + else { + for (let i=0; iINPUT[type="button"], +TD>INPUT[type="submit"], +TD>INPUT[type="text"], +TD>INPUT[type="password"], +TD>INPUT[type="email"], +TD>SELECT, +TD>TEXTAREA, +TD>BUTTON {width: 100%;} + +TABLE.form {width: 300px;} +TABLE.form TD, TABLE.form TH {vertical-align: middle;} +TABLE.form TBODY TD {text-align: left;} +TABLE.form TBODY TH {text-align: right; padding-right: 4px; width: 1%; white-space: nowrap;} +TABLE.form TD + TH {padding-left: 8px;} + +*[onclick], +H3[class~="shm-toggler"], +.sortable TH { + cursor: pointer; +} +IMG {border: none;} +FORM {margin: 0;} +IMG.lazy {display: none;} + +#flash { + background: #FF7; + display: block; + padding: 8px; + margin: 8px; + border: 1px solid #882; +} + +#installer { + background: #EEE; + font-family: "Arial", sans-serif; + font-size: 14px; + width: 512px; + margin: 16px auto auto; + border: 1px solid black; + border-radius: 16px; +} +#installer P { + padding: 5px; +} +#installer A { + text-decoration: none; +} +#installer A:hover { + text-decoration: underline; +} +#installer H1, #installer H3 { + background: #DDD; + text-align: center; + margin: 0; + padding: 2px; +} +#installer H1 { + border-bottom: 1px solid black; + border-radius: 16px 16px 0 0; +} +#installer H3 { + border-bottom: 1px solid black; +} +#installer TH { + text-align: right; +} +#installer INPUT, +#installer SELECT { + width: 100%; + box-sizing: border-box; +} diff --git a/ext/static_files/test.php b/ext/static_files/test.php new file mode 100644 index 00000000..f33b7281 --- /dev/null +++ b/ext/static_files/test.php @@ -0,0 +1,9 @@ +get_page('favicon.ico'); + $this->assert_response(200); + } +} diff --git a/ext/statsd/info.php b/ext/statsd/info.php new file mode 100644 index 00000000..42463632 --- /dev/null +++ b/ext/statsd/info.php @@ -0,0 +1,15 @@ + -* License: GPLv2 -* Visibility: admin -* Description: Sends Shimmie stats to a StatsD server -* Documentation: -* define('STATSD_HOST', 'my.server.com:8125'); in shimmie.conf.php to set the host -*/ +dbtime}|ms"; - StatsDInterface::$stats["shimmie.$type.memory"] = memory_get_peak_usage(true)."|c"; - StatsDInterface::$stats["shimmie.$type.files"] = count(get_included_files())."|c"; - StatsDInterface::$stats["shimmie.$type.queries"] = $database->query_count."|c"; - StatsDInterface::$stats["shimmie.$type.events"] = $_shm_event_count."|c"; - StatsDInterface::$stats["shimmie.$type.cache-hits"] = $database->cache->get_hits()."|c"; - StatsDInterface::$stats["shimmie.$type.cache-misses"] = $database->cache->get_misses()."|c"; - } + private function _stats(string $type) + { + global $_shm_event_count, $cache, $database, $_shm_load_start; + $time = microtime(true) - $_shm_load_start; + StatsDInterface::$stats["shimmie.$type.hits"] = "1|c"; + StatsDInterface::$stats["shimmie.$type.time"] = "$time|ms"; + StatsDInterface::$stats["shimmie.$type.time-db"] = "{$database->dbtime}|ms"; + StatsDInterface::$stats["shimmie.$type.memory"] = memory_get_peak_usage(true)."|c"; + StatsDInterface::$stats["shimmie.$type.files"] = count(get_included_files())."|c"; + StatsDInterface::$stats["shimmie.$type.queries"] = $database->query_count."|c"; + StatsDInterface::$stats["shimmie.$type.events"] = $_shm_event_count."|c"; + StatsDInterface::$stats["shimmie.$type.cache-hits"] = $cache->get_hits()."|c"; + StatsDInterface::$stats["shimmie.$type.cache-misses"] = $cache->get_misses()."|c"; + } - public function onPageRequest(PageRequestEvent $event) { - $this->_stats("overall"); + public function onPageRequest(PageRequestEvent $event) + { + $this->_stats("overall"); - if($event->page_matches("post/view")) { # 40% - $this->_stats("post-view"); - } - else if($event->page_matches("post/list")) { # 30% - $this->_stats("post-list"); - } - else if($event->page_matches("user")) { - $this->_stats("user"); - } - else if($event->page_matches("upload")) { - $this->_stats("upload"); - } - else if($event->page_matches("rss")) { - $this->_stats("rss"); - } - else if($event->page_matches("api")) { - $this->_stats("api"); - } - else { - #global $_shm_load_start; - #$time = microtime(true) - $_shm_load_start; - #file_put_contents("data/other.log", "{$_SERVER['REQUEST_URI']} $time\n", FILE_APPEND); - $this->_stats("other"); - } + if ($event->page_matches("post/view")) { # 40% + $this->_stats("post-view"); + } elseif ($event->page_matches("post/list")) { # 30% + $this->_stats("post-list"); + } elseif ($event->page_matches("user")) { + $this->_stats("user"); + } elseif ($event->page_matches("upload")) { + $this->_stats("upload"); + } elseif ($event->page_matches("rss")) { + $this->_stats("rss"); + } elseif ($event->page_matches("api")) { + $this->_stats("api"); + } else { + #global $_shm_load_start; + #$time = microtime(true) - $_shm_load_start; + #file_put_contents("data/other.log", "{$_SERVER['REQUEST_URI']} $time\n", FILE_APPEND); + $this->_stats("other"); + } - $this->send(StatsDInterface::$stats, 1.0); - StatsDInterface::$stats = array(); - } + $this->send(StatsDInterface::$stats, 1.0); + StatsDInterface::$stats = []; + } - public function onUserCreation(UserCreationEvent $event) { - StatsDInterface::$stats["shimmie.events.user_creations"] = "1|c"; - } + public function onUserCreation(UserCreationEvent $event) + { + StatsDInterface::$stats["shimmie_events.user_creations"] = "1|c"; + } - public function onDataUpload(DataUploadEvent $event) { - StatsDInterface::$stats["shimmie.events.uploads"] = "1|c"; - } + public function onDataUpload(DataUploadEvent $event) + { + StatsDInterface::$stats["shimmie_events.uploads"] = "1|c"; + } - public function onCommentPosting(CommentPostingEvent $event) { - StatsDInterface::$stats["shimmie.events.comments"] = "1|c"; - } + public function onCommentPosting(CommentPostingEvent $event) + { + StatsDInterface::$stats["shimmie_events.comments"] = "1|c"; + } - public function onImageInfoSet(ImageInfoSetEvent $event) { - StatsDInterface::$stats["shimmie.events.info-sets"] = "1|c"; - } + public function onImageInfoSet(ImageInfoSetEvent $event) + { + StatsDInterface::$stats["shimmie_events.info-sets"] = "1|c"; + } - /** - * @return int - */ - public function get_priority() {return 99;} + public function get_priority(): int + { + return 99; + } - /** - * @param array $data - * @param int $sampleRate - */ - private function send($data, $sampleRate=1) { - if (!STATSD_HOST) { return; } + private function send(array $data, float $sampleRate=1) + { + if (!STATSD_HOST) { + return; + } // sampling - $sampledData = array(); + $sampledData = []; if ($sampleRate < 1) { foreach ($data as $stat => $value) { @@ -106,15 +97,19 @@ class StatsDInterface extends Extension { $sampledData = $data; } - if (empty($sampledData)) { return; } + if (empty($sampledData)) { + return; + } // Wrap this in a try/catch - failures in any of this should be silently ignored try { $parts = explode(":", STATSD_HOST); $host = $parts[0]; - $port = $parts[1]; + $port = (int)$parts[1]; $fp = fsockopen("udp://$host", $port, $errno, $errstr); - if (! $fp) { return; } + if (! $fp) { + return; + } foreach ($sampledData as $stat => $value) { fwrite($fp, "$stat:$value"); } diff --git a/ext/system/info.php b/ext/system/info.php new file mode 100644 index 00000000..a3fcd9ef --- /dev/null +++ b/ext/system/info.php @@ -0,0 +1,13 @@ +"matthew@darkholme.net"]; + public $license = self::LICENSE_WTFPL; + public $description = "Provides system screen"; + public $core = true; +} diff --git a/ext/system/main.php b/ext/system/main.php new file mode 100644 index 00000000..aec77745 --- /dev/null +++ b/ext/system/main.php @@ -0,0 +1,23 @@ +page_matches("system")) { + $e = new PageSubNavBuildingEvent("system"); + send_event($e); + usort($e->links, "sort_nav_links"); + $link = $e->links[0]->link; + + $page->set_redirect($link->make_link()); + $page->set_mode(PageMode::REDIRECT); + } + } + public function onPageNavBuilding(PageNavBuildingEvent $event) + { + $event->add_nav_link("system", new Link('system'), "System"); + } +} diff --git a/ext/tag_categories/config.php b/ext/tag_categories/config.php new file mode 100644 index 00000000..fe9a8854 --- /dev/null +++ b/ext/tag_categories/config.php @@ -0,0 +1,8 @@ +"danneh@danneh.net"]; + public $description = "Let tags be split into 'categories', like Danbooru's tagging"; +} diff --git a/ext/tag_categories/main.php b/ext/tag_categories/main.php index e1dca36e..0ca1a7c9 100644 --- a/ext/tag_categories/main.php +++ b/ext/tag_categories/main.php @@ -1,158 +1,190 @@ - - * Link: http://code.shishnet.org/shimmie2/ - * Description: Let tags be split into 'categories', like Danbooru's tagging - */ +set_default_bool("tag_categories_split_on_view", true); - if($config->get_int("ext_tag_categories_version") < 1) { - // primary extension database, holds all our stuff! - $database->create_table('image_tag_categories', - 'category VARCHAR(60) PRIMARY KEY, +require_once "config.php"; + +class TagCategories extends Extension +{ + /** @var TagCategoriesTheme */ + protected $theme; + + public function onInitExt(InitExtEvent $event) + { + global $config; + + // whether we split out separate categories on post view by default + // note: only takes effect if /post/view shows the image's exact tags + $config->set_default_bool(TagCategoriesConfig::SPLIT_ON_VIEW, true); + } + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $database; + + if ($this->get_version(TagCategoriesConfig::VERSION) < 1) { + // primary extension database, holds all our stuff! + $database->create_table( + 'image_tag_categories', + 'category VARCHAR(60) PRIMARY KEY, display_singular VARCHAR(60), display_multiple VARCHAR(60), - color VARCHAR(7)'); + color VARCHAR(7)' + ); - $config->set_int("ext_tag_categories_version", 1); + $this->set_version(TagCategoriesConfig::VERSION, 1); log_info("tag_categories", "extension installed"); - } + } - // if empty, add our default values - $number_of_db_rows = $database->execute('SELECT COUNT(*) FROM image_tag_categories;')->fetchColumn(); + // if empty, add our default values + $number_of_db_rows = $database->execute('SELECT COUNT(*) FROM image_tag_categories;')->fetchColumn(); - if ($number_of_db_rows == 0) { - $database->execute( - 'INSERT INTO image_tag_categories VALUES (?, ?, ?, ?)', - array("artist", "Artist", "Artists", "#BB6666") - ); - $database->execute( - 'INSERT INTO image_tag_categories VALUES (?, ?, ?, ?)', - array("series", "Series", "Series", "#AA00AA") - ); - $database->execute( - 'INSERT INTO image_tag_categories VALUES (?, ?, ?, ?)', - array("character", "Character", "Characters", "#66BB66") - ); - } - } + if ($number_of_db_rows == 0) { + $database->execute( + 'INSERT INTO image_tag_categories VALUES (:category, :single, :multiple, :color)', + ["category"=>"artist", "single"=>"Artist", "multiple"=>"Artists", "color"=>"#BB6666"] + ); + $database->execute( + 'INSERT INTO image_tag_categories VALUES (:category, :single, :multiple, :color)', + ["category"=>"series", "single"=>"Series", "multiple"=>"Series", "color"=>"#AA00AA"] + ); + $database->execute( + 'INSERT INTO image_tag_categories VALUES (:category, :single, :multiple, :color)', + ["category"=>"character", "single"=>"Character", "multiple"=>"Characters", "color"=>"#66BB66"] + ); + } + } - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; - if($event->page_matches("tags/categories")) { - if($user->is_admin()) { - $this->page_update(); - $this->show_tag_categories($page); - } - } - } + if ($event->page_matches("tags/categories")) { + if ($user->can(Permissions::EDIT_TAG_CATEGORIES)) { + $this->page_update(); + $this->show_tag_categories($page); + } + } + } - public function onSearchTermParse(SearchTermParseEvent $event) { - $matches = array(); + public function onSearchTermParse(SearchTermParseEvent $event) + { + if (is_null($event->term)) { + return; + } - if(preg_match("/^(.+)tags([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])([0-9]+)$/i", $event->term, $matches)) { - global $database; - $type = $matches[1]; - $cmp = ltrim($matches[2], ":") ?: "="; - $count = $matches[3]; + $matches = []; + if (preg_match("/^(.+)tags([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])([0-9]+)$/i", $event->term, $matches)) { + global $database; + $type = strtolower($matches[1]); + $cmp = ltrim($matches[2], ":") ?: "="; + $count = $matches[3]; - $types = $database->get_col('SELECT category FROM image_tag_categories'); - if(in_array($type, $types)) { - $event->add_querylet( - new Querylet("EXISTS ( + $types = $database->get_col( + 'SELECT LOWER(category) FROM image_tag_categories' + ); + if (in_array($type, $types)) { + $event->add_querylet( + new Querylet("EXISTS ( SELECT 1 FROM image_tags it LEFT JOIN tags t ON it.tag_id = t.id WHERE images.id = it.image_id GROUP BY image_id - HAVING SUM(CASE WHEN t.tag LIKE '$type:%' THEN 1 ELSE 0 END) $cmp $count - )")); - } - } - } + HAVING SUM(CASE WHEN LOWER(t.tag) LIKE LOWER('$type:%') THEN 1 ELSE 0 END) $cmp $count + )") + ); + } + } + } - public function getDict() { - global $database; + public function onHelpPageBuilding(HelpPageBuildingEvent $event) + { + if ($event->key===HelpPages::SEARCH) { + $block = new Block(); + $block->header = "Tag Categories"; + $block->body = $this->theme->get_help_html(); + $event->add_block($block); + } + } - $tc_dict = $database->get_all('SELECT * FROM image_tag_categories;'); + public function getDict() + { + global $database; + return $database->get_all('SELECT * FROM image_tag_categories;'); + } - return $tc_dict; - } + public function getKeyedDict($key_with = 'category') + { + $tc_dict = $this->getDict(); + $tc_keyed_dict = []; - public function getKeyedDict($key_with = 'category') { - $tc_dict = $this->getDict(); - $tc_keyed_dict = array(); + foreach ($tc_dict as $row) { + $key = $row[$key_with]; + $tc_keyed_dict[$key] = $row; + } - foreach ($tc_dict as $row) { - $key = $row[$key_with]; - $tc_keyed_dict[$key] = $row; - } + return $tc_keyed_dict; + } - return $tc_keyed_dict; - } + public function page_update() + { + global $user, $database; - public function page_update() { - global $user, $database; + if (!$user->can(Permissions::EDIT_TAG_CATEGORIES)) { + return false; + } - if(!$user->is_admin()) { - return false; - } + if (!isset($_POST['tc_status']) and + !isset($_POST['tc_category']) and + !isset($_POST['tc_display_singular']) and + !isset($_POST['tc_display_multiple']) and + !isset($_POST['tc_color'])) { + return false; + } - if(!isset($_POST['tc_status']) and - !isset($_POST['tc_category']) and - !isset($_POST['tc_display_singular']) and - !isset($_POST['tc_display_multiple']) and - !isset($_POST['tc_color'])) { - return false; - } + $is_success = null; - if($_POST['tc_status'] == 'edit') { - $is_success = $database->execute('UPDATE image_tag_categories + if ($_POST['tc_status'] == 'edit') { + $is_success = $database->execute( + 'UPDATE image_tag_categories SET display_singular=:display_singular, display_multiple=:display_multiple, color=:color WHERE category=:category', - array( - 'category' => $_POST['tc_category'], - 'display_singular' => $_POST['tc_display_singular'], - 'display_multiple' => $_POST['tc_display_multiple'], - 'color' => $_POST['tc_color'], - )); - } - else if($_POST['tc_status'] == 'new') { - $is_success = $database->execute('INSERT INTO image_tag_categories + [ + 'category' => $_POST['tc_category'], + 'display_singular' => $_POST['tc_display_singular'], + 'display_multiple' => $_POST['tc_display_multiple'], + 'color' => $_POST['tc_color'], + ] + ); + } elseif ($_POST['tc_status'] == 'new') { + $is_success = $database->execute( + 'INSERT INTO image_tag_categories VALUES (:category, :display_singular, :display_multiple, :color)', - array( - 'category' => $_POST['tc_category'], - 'display_singular' => $_POST['tc_display_singular'], - 'display_multiple' => $_POST['tc_display_multiple'], - 'color' => $_POST['tc_color'], - )); - } - else if($_POST['tc_status'] == 'delete') { - $is_success = $database->execute('DELETE FROM image_tag_categories + [ + 'category' => $_POST['tc_category'], + 'display_singular' => $_POST['tc_display_singular'], + 'display_multiple' => $_POST['tc_display_multiple'], + 'color' => $_POST['tc_color'], + ] + ); + } elseif ($_POST['tc_status'] == 'delete') { + $is_success = $database->execute( + 'DELETE FROM image_tag_categories WHERE category=:category', - array( - 'category' => $_POST['tc_category'] - )); - } + [ + 'category' => $_POST['tc_category'] + ] + ); + } - return $is_success; - } + return $is_success; + } - public function show_tag_categories($page) { - $this->theme->show_tag_categories($page, $this->getDict()); - } + public function show_tag_categories($page) + { + $this->theme->show_tag_categories($page, $this->getDict()); + } } - - diff --git a/ext/tag_categories/theme.php b/ext/tag_categories/theme.php index cb98b7e3..f862198a 100644 --- a/ext/tag_categories/theme.php +++ b/ext/tag_categories/theme.php @@ -1,10 +1,9 @@ - - +
    @@ -99,4 +98,20 @@ class TagCategoriesTheme extends Themelet { // add html to stuffs $page->add_block(new Block("Editing", $html, "main", 10)); } + + public function get_help_html() + { + return '

    Search for images containing a certain number of tags with the specified tag category.

    +
    +
    persontags=1
    +

    Returns images with exactly 1 tag with the tag category "person".

    +
    +
    +
    cattags>0
    +

    Returns images with 1 or more tags with the tag category "cat".

    +
    +

    Can use <, <=, >, >=, or =.

    +

    Category name is not case sensitive, category must exist for search to work.

    + '; + } } diff --git a/ext/tag_edit/info.php b/ext/tag_edit/info.php new file mode 100644 index 00000000..c9b0f6d9 --- /dev/null +++ b/ext/tag_edit/info.php @@ -0,0 +1,48 @@ + +
  • source=(*, none) eg -- using this metatag will ignore anything set in the \"Source\" box +
      +
    • source=http://example.com -- set source to http://example.com +
    • source=none -- set source to NULL +
    + +

    Metatags can be followed by \":\" rather than \"=\" if you prefer. +
    I.E: \"source:http://example.com\", \"source=http://example.com\" etc. +

    Some tagging metatags provided by extensions: +

      +
    • Numeric Score +
        +
      • vote=(up, down, remove) -- vote, or remove your vote on an image +
      +
    • Pools +
        +
      • pool=(PoolID, PoolTitle, lastcreated) -- add post to pool (if exists) +
      • pool=(PoolID, PoolTitle, lastcreated):(PoolOrder) -- add post to pool (if exists) with set pool order +
          +
        • pool=50 -- add post to pool with ID of 50 +
        • pool=10:25 -- add post to pool with ID of 10 and with order 25 +
        • pool=This_is_a_Pool -- add post to pool with a title of \"This is a Pool\" +
        • pool=lastcreated -- add post to the last pool the user created +
        +
      +
    • Post Relationships +
        +
      • parent=(parentID, none) -- set parent ID of current image +
      • child=(childID) -- set parent ID of child image to current image ID +
      +
    "; +} diff --git a/ext/tag_edit/main.php b/ext/tag_edit/main.php index 2f270fd0..df87e9a2 100644 --- a/ext/tag_edit/main.php +++ b/ext/tag_edit/main.php @@ -1,44 +1,4 @@ - - *
  • source=(*, none) eg -- using this metatag will ignore anything set in the "Source" box - *
      - *
    • source=http://example.com -- set source to http://example.com - *
    • source=none -- set source to NULL - *
    - * - *

    Metatags can be followed by ":" rather than "=" if you prefer. - *
    I.E: "source:http://example.com", "source=http://example.com" etc. - *

    Some tagging metatags provided by extensions: - *

      - *
    • Numeric Score - *
        - *
      • vote=(up, down, remove) -- vote, or remove your vote on an image - *
      - *
    • Pools - *
        - *
      • pool=(PoolID, PoolTitle, lastcreated) -- add post to pool (if exists) - *
      • pool=(PoolID, PoolTitle, lastcreated):(PoolOrder) -- add post to pool (if exists) with set pool order - *
          - *
        • pool=50 -- add post to pool with ID of 50 - *
        • pool=10:25 -- add post to pool with ID of 10 and with order 25 - *
        • pool=This_is_a_Pool -- add post to pool with a title of "This is a Pool" - *
        • pool=lastcreated -- add post to the last pool the user created - *
        - *
      - *
    • Post Relationships - *
        - *
      • parent=(parentID, none) -- set parent ID of current image - *
      • child=(childID) -- set parent ID of child image to current image ID - *
      - *
    - */ +image = $image; - $this->owner = $owner; - } + public function __construct(Image $image, User $owner) + { + parent::__construct(); + $this->image = $image; + $this->owner = $owner; + } } -/* - * SourceSetEvent: - * $image_id - * $source - * +class SourceSetEvent extends Event +{ + /** @var Image */ + public $image; + /** @var string */ + public $source; + + public function __construct(Image $image, string $source=null) + { + parent::__construct(); + $this->image = $image; + $this->source = $source; + } +} + + +class TagSetEvent extends Event +{ + /** @var Image */ + public $image; + public $tags; + public $metatags; + + /** + * #param string[] $tags + */ + public function __construct(Image $image, array $tags) + { + parent::__construct(); + $this->image = $image; + + $this->tags = []; + $this->metatags = []; + + foreach ($tags as $tag) { + if ((strpos($tag, ':') === false) && (strpos($tag, '=') === false)) { + //Tag doesn't contain : or =, meaning it can't possibly be a metatag. + //This should help speed wise, as it avoids running every single tag through a bunch of preg_match instead. + array_push($this->tags, $tag); + continue; + } + + $ttpe = new TagTermCheckEvent($tag); + send_event($ttpe); + + //seperate tags from metatags + if (!$ttpe->metatag) { + array_push($this->tags, $tag); + } else { + array_push($this->metatags, $tag); + } + } + } +} + +class LockSetEvent extends Event +{ + /** @var Image */ + public $image; + /** @var bool */ + public $locked; + + public function __construct(Image $image, bool $locked) + { + parent::__construct(); + $this->image = $image; + $this->locked = $locked; + } +} + +/** + * Check whether or not a tag is a meta-tag */ -class SourceSetEvent extends Event { - /** @var \Image */ - public $image; - /** @var string */ - public $source; +class TagTermCheckEvent extends Event +{ + public $term = null; //tag + /** @var bool */ + public $metatag = false; - /** - * @param Image $image - * @param string $source - */ - public function __construct(Image $image, $source) { - $this->image = $image; - $this->source = $source; - } + public function __construct(string $term) + { + parent::__construct(); + $this->term = $term; + } } - -/* - * TagSetEvent: - * $image_id - * $tags - * +/** + * If a tag is a meta-tag, parse it */ -class TagSetEvent extends Event { - /** @var \Image */ - public $image; - public $tags; - public $metatags; +class TagTermParseEvent extends Event +{ + public $term = null; + public $image_id = null; - /** - * @param Image $image - * @param string[] $tags - */ - public function __construct(Image $image, array $tags) { - $this->image = $image; - - $this->tags = array(); - $this->metatags = array(); - - foreach($tags as $tag) { - if((strpos($tag, ':') === FALSE) && (strpos($tag, '=') === FALSE)) { - //Tag doesn't contain : or =, meaning it can't possibly be a metatag. - //This should help speed wise, as it avoids running every single tag through a bunch of preg_match instead. - array_push($this->tags, $tag); - continue; - } - - $ttpe = new TagTermParseEvent($tag, $this->image->id, FALSE); //Only check for metatags, don't parse. Parsing is done after set_tags. - send_event($ttpe); - - //seperate tags from metatags - if(!$ttpe->is_metatag()) { - array_push($this->tags, $tag); - }else{ - array_push($this->metatags, $tag); - } - } - } + public function __construct(string $term, int $image_id) + { + parent::__construct(); + $this->term = $term; + $this->image_id = $image_id; + } } -class LockSetEvent extends Event { - /** @var \Image */ - public $image; - /** @var bool */ - public $locked; +class TagEdit extends Extension +{ + /** @var TagEditTheme */ + protected $theme; - /** - * @param Image $image - * @param bool $locked - */ - public function __construct(Image $image, $locked) { - assert('is_bool($locked)'); + public function onPageRequest(PageRequestEvent $event) + { + global $user, $page; + if ($event->page_matches("tag_edit")) { + if ($event->get_arg(0) == "replace") { + if ($user->can(Permissions::MASS_TAG_EDIT) && isset($_POST['search']) && isset($_POST['replace'])) { + $search = $_POST['search']; + $replace = $_POST['replace']; + $this->mass_tag_edit($search, $replace); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("admin")); + } + } + if ($event->get_arg(0) == "mass_source_set") { + if ($user->can(Permissions::MASS_TAG_EDIT) && isset($_POST['tags']) && isset($_POST['source'])) { + $this->mass_source_edit($_POST['tags'], $_POST['source']); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/list")); + } + } + } + } - $this->image = $image; - $this->locked = $locked; - } + // public function onPostListBuilding(PostListBuildingEvent $event) + // { + // global $user; + // if ($user->can(UserAbilities::BULK_EDIT_IMAGE_SOURCE) && !empty($event->search_terms)) { + // $event->add_control($this->theme->mss_html(Tag::implode($event->search_terms))); + // } + // } + + public function onImageInfoSet(ImageInfoSetEvent $event) + { + global $user; + if ($user->can(Permissions::EDIT_IMAGE_OWNER) && isset($_POST['tag_edit__owner'])) { + $owner = User::by_name($_POST['tag_edit__owner']); + if ($owner instanceof User) { + send_event(new OwnerSetEvent($event->image, $owner)); + } else { + throw new NullUserException("Error: No user with that name was found."); + } + } + if ($user->can(Permissions::EDIT_IMAGE_TAG) && isset($_POST['tag_edit__tags'])) { + send_event(new TagSetEvent($event->image, Tag::explode($_POST['tag_edit__tags']))); + } + if ($user->can(Permissions::EDIT_IMAGE_SOURCE) && isset($_POST['tag_edit__source'])) { + if (isset($_POST['tag_edit__tags']) ? !preg_match('/source[=|:]/', $_POST["tag_edit__tags"]) : true) { + send_event(new SourceSetEvent($event->image, $_POST['tag_edit__source'])); + } + } + if ($user->can(Permissions::EDIT_IMAGE_LOCK)) { + $locked = isset($_POST['tag_edit__locked']) && $_POST['tag_edit__locked']=="on"; + send_event(new LockSetEvent($event->image, $locked)); + } + } + + public function onOwnerSet(OwnerSetEvent $event) + { + global $user; + if ($user->can(Permissions::EDIT_IMAGE_OWNER) && (!$event->image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK))) { + $event->image->set_owner($event->owner); + } + } + + public function onTagSet(TagSetEvent $event) + { + global $user; + if ($user->can(Permissions::EDIT_IMAGE_TAG) && (!$event->image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK))) { + $event->image->set_tags($event->tags); + } + foreach ($event->metatags as $tag) { + send_event(new TagTermParseEvent($tag, $event->image->id)); + } + } + + public function onSourceSet(SourceSetEvent $event) + { + global $user; + if ($user->can(Permissions::EDIT_IMAGE_SOURCE) && (!$event->image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK))) { + $event->image->set_source($event->source); + } + } + + public function onLockSet(LockSetEvent $event) + { + global $user; + if ($user->can(Permissions::EDIT_IMAGE_LOCK)) { + $event->image->set_locked($event->locked); + } + } + + public function onImageDeletion(ImageDeletionEvent $event) + { + $event->image->delete_tags_from_image(); + } + + public function onAdminBuilding(AdminBuildingEvent $event) + { + $this->theme->display_mass_editor(); + } + + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + if ($event->parent=="tags") { + $event->add_nav_link("tags_help", new Link('ext_doc/tag_edit'), "Help"); + } + } + + /** + * When an alias is added, oldtag becomes inaccessible. + */ + public function onAddAlias(AddAliasEvent $event) + { + $this->mass_tag_edit($event->oldtag, $event->newtag); + } + + public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) + { + $event->add_part($this->theme->get_user_editor_html($event->image), 39); + $event->add_part($this->theme->get_tag_editor_html($event->image), 40); + $event->add_part($this->theme->get_source_editor_html($event->image), 41); + $event->add_part($this->theme->get_lock_editor_html($event->image), 42); + } + + public function onTagTermCheck(TagTermCheckEvent $event) + { + if (preg_match("/^source[=|:](.*)$/i", $event->term)) { + $event->metatag = true; + } + } + + public function onTagTermParse(TagTermParseEvent $event) + { + if (preg_match("/^source[=|:](.*)$/i", $event->term, $matches)) { + $source = ($matches[1] !== "none" ? $matches[1] : null); + send_event(new SourceSetEvent(Image::by_id($event->image_id), $source)); + } + } + + public function onParseLinkTemplate(ParseLinkTemplateEvent $event) + { + $tags = $event->image->get_tag_list(); + $tags = str_replace("/", "", $tags); + $tags = preg_replace("/^\.+/", "", $tags); + $event->replace('$tags', $tags); + } + + private function mass_tag_edit(string $search, string $replace) + { + global $database; + + $search_set = Tag::explode(strtolower($search), false); + $replace_set = Tag::explode(strtolower($replace), false); + + log_info("tag_edit", "Mass editing tags: '$search' -> '$replace'"); + + if (count($search_set) == 1 && count($replace_set) == 1) { + $images = Image::find_images(0, 10, $replace_set); + if (count($images) == 0) { + log_info("tag_edit", "No images found with target tag, doing in-place rename"); + $database->execute( + "DELETE FROM tags WHERE tag=:replace", + ["replace" => $replace_set[0]] + ); + $database->execute( + "UPDATE tags SET tag=:replace WHERE tag=:search", + ["replace" => $replace_set[0], "search" => $search_set[0]] + ); + return; + } + } + + $last_id = -1; + while (true) { + // make sure we don't look at the same images twice. + // search returns high-ids first, so we want to look + // at images with lower IDs than the previous. + $search_forward = $search_set; + $search_forward[] = "order=id_desc"; //Default order can be changed, so make sure we order high > low ID + if ($last_id >= 0) { + $search_forward[] = "id<$last_id"; + } + + $images = Image::find_images(0, 100, $search_forward); + if (count($images) == 0) { + break; + } + + foreach ($images as $image) { + // remove the search'ed tags + $before = array_map('strtolower', $image->get_tag_array()); + $after = []; + foreach ($before as $tag) { + if (!in_array($tag, $search_set)) { + $after[] = $tag; + } + } + + // add the replace'd tags + foreach ($replace_set as $tag) { + $after[] = $tag; + } + + // replace'd tag may already exist in tag set, so remove dupes to avoid integrity constraint violations. + $after = array_unique($after); + + $image->set_tags($after); + + $last_id = $image->id; + } + } + } + + private function mass_source_edit(string $tags, string $source) + { + $tags = Tag::explode($tags); + + $last_id = -1; + while (true) { + // make sure we don't look at the same images twice. + // search returns high-ids first, so we want to look + // at images with lower IDs than the previous. + $search_forward = $tags; + if ($last_id >= 0) { + $search_forward[] = "id<$last_id"; + } + + $images = Image::find_images(0, 100, $search_forward); + if (count($images) == 0) { + break; + } + + foreach ($images as $image) { + $image->set_source($source); + $last_id = $image->id; + } + } + } } - -/* - * TagTermParseEvent: - * Signal that a tag term needs parsing - */ -class TagTermParseEvent extends Event { - public $term = NULL; //tag - public $id = NULL; //image_id - /** @var bool */ - public $metatag = FALSE; - /** @var bool */ - public $parse = TRUE; //marks the tag to be parsed, and not just checked if valid metatag - - /** - * @param string $term - * @param int $id - * @param bool $parse - */ - public function __construct($term, $id, $parse) { - assert('is_string($term)'); - assert('is_int($id)'); - assert('is_bool($parse)'); - - $this->term = $term; - $this->id = $id; - $this->parse = $parse; - } - - /** - * @return bool - */ - public function is_metatag() { - return $this->metatag; - } -} - -class TagEdit extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $user, $page; - if($event->page_matches("tag_edit")) { - if($event->get_arg(0) == "replace") { - if($user->can("mass_tag_edit") && isset($_POST['search']) && isset($_POST['replace'])) { - $search = $_POST['search']; - $replace = $_POST['replace']; - $this->mass_tag_edit($search, $replace); - $page->set_mode("redirect"); - $page->set_redirect(make_link("admin")); - } - } - if($event->get_arg(0) == "mass_source_set") { - if($user->can("mass_tag_edit") && isset($_POST['tags']) && isset($_POST['source'])) { - $this->mass_source_edit($_POST['tags'], $_POST['source']); - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/list")); - } - } - } - } - - public function onPostListBuilding(PostListBuildingEvent $event) { - global $user; - if($user->can("bulk_edit_image_source") && !empty($event->search_terms)) { - $event->add_control($this->theme->mss_html(implode(" ", $event->search_terms))); - } - } - - public function onImageInfoSet(ImageInfoSetEvent $event) { - global $user; - if($user->can("edit_image_owner") && isset($_POST['tag_edit__owner'])) { - $owner = User::by_name($_POST['tag_edit__owner']); - if ($owner instanceof User) { - send_event(new OwnerSetEvent($event->image, $owner)); - } else { - throw new NullUserException("Error: No user with that name was found."); - } - } - if($this->can_tag($event->image) && isset($_POST['tag_edit__tags'])) { - send_event(new TagSetEvent($event->image, Tag::explode($_POST['tag_edit__tags']))); - } - if($this->can_source($event->image) && isset($_POST['tag_edit__source'])) { - if(isset($_POST['tag_edit__tags']) ? !preg_match('/source[=|:]/', $_POST["tag_edit__tags"]) : TRUE){ - send_event(new SourceSetEvent($event->image, $_POST['tag_edit__source'])); - } - } - if($user->can("edit_image_lock")) { - $locked = isset($_POST['tag_edit__locked']) && $_POST['tag_edit__locked']=="on"; - send_event(new LockSetEvent($event->image, $locked)); - } - } - - public function onOwnerSet(OwnerSetEvent $event) { - global $user; - if($user->can("edit_image_owner") && (!$event->image->is_locked() || $user->can("edit_image_lock"))) { - $event->image->set_owner($event->owner); - } - } - - public function onTagSet(TagSetEvent $event) { - global $user; - if($user->can("edit_image_tag") && (!$event->image->is_locked() || $user->can("edit_image_lock"))) { - $event->image->set_tags($event->tags); - } - $event->image->parse_metatags($event->metatags, $event->image->id); - } - - public function onSourceSet(SourceSetEvent $event) { - global $user; - if($user->can("edit_image_source") && (!$event->image->is_locked() || $user->can("edit_image_lock"))) { - $event->image->set_source($event->source); - } - } - - public function onLockSet(LockSetEvent $event) { - global $user; - if($user->can("edit_image_lock")) { - $event->image->set_locked($event->locked); - } - } - - public function onImageDeletion(ImageDeletionEvent $event) { - $event->image->delete_tags_from_image(); - } - - public function onAdminBuilding(AdminBuildingEvent $event) { - $this->theme->display_mass_editor(); - } - - /** - * When an alias is added, oldtag becomes inaccessible. - * @param AddAliasEvent $event - */ - public function onAddAlias(AddAliasEvent $event) { - $this->mass_tag_edit($event->oldtag, $event->newtag); - } - - public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) { - $event->add_part($this->theme->get_user_editor_html($event->image), 39); - $event->add_part($this->theme->get_tag_editor_html($event->image), 40); - $event->add_part($this->theme->get_source_editor_html($event->image), 41); - $event->add_part($this->theme->get_lock_editor_html($event->image), 42); - } - - public function onTagTermParse(TagTermParseEvent $event) { - $matches = array(); - - if(preg_match("/^source[=|:](.*)$/i", $event->term, $matches) && $event->parse) { - $source = ($matches[1] !== "none" ? $matches[1] : null); - send_event(new SourceSetEvent(Image::by_id($event->id), $source)); - } - - if(!empty($matches)) $event->metatag = true; - } - - /** - * @param Image $image - * @return bool - */ - private function can_tag(Image $image) { - global $user; - return ($user->can("edit_image_tag") || !$image->is_locked()); - } - - /** - * @param Image $image - * @return bool - */ - private function can_source(Image $image) { - global $user; - return ($user->can("edit_image_source") || !$image->is_locked()); - } - - /** - * @param string $search - * @param string $replace - */ - private function mass_tag_edit($search, $replace) { - global $database; - - $search_set = Tag::explode(strtolower($search), false); - $replace_set = Tag::explode(strtolower($replace), false); - - log_info("tag_edit", "Mass editing tags: '$search' -> '$replace'"); - - if(count($search_set) == 1 && count($replace_set) == 1) { - $images = Image::find_images(0, 10, $replace_set); - if(count($images) == 0) { - log_info("tag_edit", "No images found with target tag, doing in-place rename"); - $database->execute("DELETE FROM tags WHERE tag=:replace", - array("replace" => $replace_set[0])); - $database->execute("UPDATE tags SET tag=:replace WHERE tag=:search", - array("replace" => $replace_set[0], "search" => $search_set[0])); - return; - } - } - - $last_id = -1; - while(true) { - // make sure we don't look at the same images twice. - // search returns high-ids first, so we want to look - // at images with lower IDs than the previous. - $search_forward = $search_set; - $search_forward[] = "order=id_desc"; //Default order can be changed, so make sure we order high > low ID - if($last_id >= 0){ - $search_forward[] = "id<$last_id"; - } - - $images = Image::find_images(0, 100, $search_forward); - if(count($images) == 0) break; - - foreach($images as $image) { - // remove the search'ed tags - $before = array_map('strtolower', $image->get_tag_array()); - $after = array(); - foreach($before as $tag) { - if(!in_array($tag, $search_set)) { - $after[] = $tag; - } - } - - // add the replace'd tags - foreach($replace_set as $tag) { - $after[] = $tag; - } - - // replace'd tag may already exist in tag set, so remove dupes to avoid integrity constraint violations. - $after = array_unique($after); - - $image->set_tags($after); - - $last_id = $image->id; - } - } - } - - /** - * @param string $tags - * @param string $source - */ - private function mass_source_edit($tags, $source) { - assert('is_string($tags)'); - assert('is_string($source)'); - - $tags = Tag::explode($tags); - - $last_id = -1; - while(true) { - // make sure we don't look at the same images twice. - // search returns high-ids first, so we want to look - // at images with lower IDs than the previous. - $search_forward = $tags; - if($last_id >= 0) $search_forward[] = "id<$last_id"; - - $images = Image::find_images(0, 100, $search_forward); - if(count($images) == 0) break; - - foreach($images as $image) { - $image->set_source($source); - $last_id = $image->id; - } - } - } -} - diff --git a/ext/tag_edit/test.php b/ext/tag_edit/test.php index 8099a702..31112a6f 100644 --- a/ext/tag_edit/test.php +++ b/ext/tag_edit/test.php @@ -1,84 +1,54 @@ -log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - $this->get_page("post/view/$image_id"); - $this->assert_title("Image $image_id: pbx"); +log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + $image = Image::by_id($image_id); - $this->markTestIncomplete(); + // Original + $this->get_page("post/view/$image_id"); + $this->assert_title("Image $image_id: pbx"); - $this->set_field("tag_edit__tags", "new"); - $this->click("Set"); - $this->assert_title("Image $image_id: new"); - $this->set_field("tag_edit__tags", ""); - $this->click("Set"); - $this->assert_title("Image $image_id: tagme"); - $this->log_out(); + // Modified + send_event(new TagSetEvent($image, ["new"])); + $this->get_page("post/view/$image_id"); + $this->assert_title("Image $image_id: new"); + } - $this->log_in_as_admin(); - $this->delete_image($image_id); - $this->log_out(); - } + public function testInvalidChange() + { + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + $image = Image::by_id($image_id); - public function testTagEdit_tooLong() { - $this->log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", str_repeat("a", 500)); - $this->get_page("post/view/$image_id"); - $this->assert_title("Image $image_id: tagme"); - } + try { + send_event(new TagSetEvent($image, [])); + $this->assertTrue(false); + } catch (SCoreException $e) { + $this->assertEquals("Tried to set zero tags", $e->error); + } + } - public function testSourceEdit() { - $this->log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - $this->get_page("post/view/$image_id"); - $this->assert_title("Image $image_id: pbx"); + public function testTagEdit_tooLong() + { + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", str_repeat("a", 500)); + $this->get_page("post/view/$image_id"); + $this->assert_title("Image $image_id: tagme"); + } - $this->markTestIncomplete(); + public function testSourceEdit() + { + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + $image = Image::by_id($image_id); - $this->set_field("tag_edit__source", "example.com"); - $this->click("Set"); - $this->click("example.com"); - $this->assert_title("Example Domain"); - $this->back(); + send_event(new SourceSetEvent($image, "example.com")); + send_event(new SourceSetEvent($image, "http://example.com")); - $this->set_field("tag_edit__source", "http://example.com"); - $this->click("Set"); - $this->click("example.com"); - $this->assert_title("Example Domain"); - $this->back(); - - $this->log_out(); - - $this->log_in_as_admin(); - $this->delete_image($image_id); - $this->log_out(); - } - - /* - * FIXME: Mass Tagger seems to be broken, and this test case always fails. - */ - public function testMassEdit() { - $this->markTestIncomplete(); - - $this->log_in_as_admin(); - - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - $this->get_page("post/view/$image_id"); - $this->assert_title("Image $image_id: pbx"); - - $this->get_page("admin"); - $this->assert_text("Mass Tag Edit"); - $this->set_field("search", "pbx"); - $this->set_field("replace", "pox"); - $this->click("Replace"); - - $this->get_page("post/view/$image_id"); - $this->assert_title("Image $image_id: pox"); - - $this->delete_image($image_id); - - $this->log_out(); - } + $this->get_page("post/view/$image_id"); + $this->assert_text("example.com"); + } } - diff --git a/ext/tag_edit/theme.php b/ext/tag_edit/theme.php index 498cfcd1..6ea9bec4 100644 --- a/ext/tag_edit/theme.php +++ b/ext/tag_edit/theme.php @@ -1,13 +1,15 @@ - Search @@ -16,38 +18,40 @@ class TagEditTheme extends Themelet { "; - $page->add_block(new Block("Mass Tag Edit", $html)); - } + $page->add_block(new Block("Mass Tag Edit", $html)); + } - public function mss_html($terms) { - $h_terms = html_escape($terms); - $html = make_form(make_link("tag_edit/mass_source_set"), "POST") . " + public function mss_html($terms): string + { + $h_terms = html_escape($terms); + $html = make_form(make_link("tag_edit/mass_source_set"), "POST") . " "; - return $html; - } + return $html; + } - public function get_tag_editor_html(Image $image) { - global $user; + public function get_tag_editor_html(Image $image): string + { + global $user; - $tag_links = array(); - foreach($image->get_tag_array() as $tag) { - $h_tag = html_escape($tag); - $u_tag = url_escape($tag); - $h_link = make_link("post/list/$u_tag/1"); - $tag_links[] = "$h_tag"; - } - $h_tag_links = implode(" ", $tag_links); - $h_tags = html_escape($image->get_tag_list()); + $tag_links = []; + foreach ($image->get_tag_array() as $tag) { + $h_tag = html_escape($tag); + $u_tag = url_escape($tag); + $h_link = make_link("post/list/$u_tag/1"); + $tag_links[] = "$h_tag"; + } + $h_tag_links = Tag::implode($tag_links); + $h_tags = html_escape($image->get_tag_list()); - return " + return " Tags - ".($user->can("edit_image_tag") ? " + ".($user->can(Permissions::EDIT_IMAGE_TAG) ? " $h_tag_links " : " @@ -56,19 +60,20 @@ class TagEditTheme extends Themelet { "; - } + } - public function get_user_editor_html(Image $image) { - global $user; - $h_owner = html_escape($image->get_owner()->name); - $h_av = $image->get_owner()->get_avatar_html(); - $h_date = autodate($image->posted); - $h_ip = $user->can("view_ip") ? " (".show_ip($image->owner_ip, "Image posted {$image->posted}").")" : ""; - return " + public function get_user_editor_html(Image $image): string + { + global $user; + $h_owner = html_escape($image->get_owner()->name); + $h_av = $image->get_owner()->get_avatar_html(); + $h_date = autodate($image->posted); + $h_ip = $user->can(Permissions::VIEW_IP) ? " (".show_ip($image->owner_ip, "Image posted {$image->posted}").")" : ""; + return " Uploader - ".($user->can("edit_image_owner") ? " + ".($user->can(Permissions::EDIT_IMAGE_OWNER) ? " $h_owner$h_ip, $h_date " : " @@ -78,18 +83,19 @@ class TagEditTheme extends Themelet { $h_av "; - } + } - public function get_source_editor_html(Image $image) { - global $user; - $h_source = html_escape($image->get_source()); - $f_source = $this->format_source($image->get_source()); - $style = "overflow: hidden; white-space: nowrap; max-width: 350px; text-overflow: ellipsis;"; - return " + public function get_source_editor_html(Image $image): string + { + global $user; + $h_source = html_escape($image->get_source()); + $f_source = $this->format_source($image->get_source()); + $style = "overflow: hidden; white-space: nowrap; max-width: 350px; text-overflow: ellipsis;"; + return " Source - ".($user->can("edit_image_source") ? " + ".($user->can(Permissions::EDIT_IMAGE_SOURCE) ? "
    $f_source
    " : " @@ -98,37 +104,35 @@ class TagEditTheme extends Themelet { "; - } + } - /** - * @param string $source - * @return string - */ - protected function format_source(/*string*/ $source) { - if(!empty($source)) { - if(!startsWith($source, "http://") && !startsWith($source, "https://")) { - $source = "http://" . $source; - } - $proto_domain = explode("://", $source); - $h_source = html_escape($proto_domain[1]); - $u_source = html_escape($source); - if(endsWith($h_source, "/")) { - $h_source = substr($h_source, 0, -1); - } - return "$h_source"; - } - return "Unknown"; - } + protected function format_source(string $source=null): string + { + if (!empty($source)) { + if (!startsWith($source, "http://") && !startsWith($source, "https://")) { + $source = "http://" . $source; + } + $proto_domain = explode("://", $source); + $h_source = html_escape($proto_domain[1]); + $u_source = html_escape($source); + if (endsWith($h_source, "/")) { + $h_source = substr($h_source, 0, -1); + } + return "$h_source"; + } + return "Unknown"; + } - public function get_lock_editor_html(Image $image) { - global $user; - $b_locked = $image->is_locked() ? "Yes (Only admins may edit these details)" : "No"; - $h_locked = $image->is_locked() ? " checked" : ""; - return " + public function get_lock_editor_html(Image $image): string + { + global $user; + $b_locked = $image->is_locked() ? "Yes (Only admins may edit these details)" : "No"; + $h_locked = $image->is_locked() ? " checked" : ""; + return " Locked - ".($user->can("edit_image_lock") ? " + ".($user->can(Permissions::EDIT_IMAGE_LOCK) ? " $b_locked " : " @@ -137,6 +141,5 @@ class TagEditTheme extends Themelet { "; - } + } } - diff --git a/ext/tag_editcloud/info.php b/ext/tag_editcloud/info.php new file mode 100644 index 00000000..faea8b08 --- /dev/null +++ b/ext/tag_editcloud/info.php @@ -0,0 +1,11 @@ +get_bool("tageditcloud_disable") && $this->can_tag($event->image)) { - $html = $this->build_tag_map($event->image); - if(!is_null($html)) { - $event->add_part($html, 40); - } - } - } + if (!$config->get_bool("tageditcloud_disable") && $this->can_tag($event->image)) { + $html = $this->build_tag_map($event->image); + if (!is_null($html)) { + $event->add_part($html, 40); + } + } + } - public function onInitExt(InitExtEvent $event) { - global $config; - $config->set_default_bool("tageditcloud_disable", false); - $config->set_default_bool("tageditcloud_usedfirst", true); - $config->set_default_string("tageditcloud_sort", 'a'); - $config->set_default_int("tageditcloud_minusage", 2); - $config->set_default_int("tageditcloud_defcount", 40); - $config->set_default_int("tageditcloud_maxcount", 4096); - $config->set_default_string("tageditcloud_ignoretags", 'tagme'); - } + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_bool("tageditcloud_disable", false); + $config->set_default_bool("tageditcloud_usedfirst", true); + $config->set_default_string("tageditcloud_sort", 'a'); + $config->set_default_int("tageditcloud_minusage", 2); + $config->set_default_int("tageditcloud_defcount", 40); + $config->set_default_int("tageditcloud_maxcount", 4096); + $config->set_default_string("tageditcloud_ignoretags", 'tagme'); + } - public function onSetupBuilding(SetupBuildingEvent $event) { - $sort_by = array('Alphabetical'=>'a','Popularity'=>'p','Relevance'=>'r'); + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sort_by = ['Alphabetical'=>'a','Popularity'=>'p','Relevance'=>'r']; - $sb = new SetupBlock("Tag Edit Cloud"); - $sb->add_bool_option("tageditcloud_disable", "Disable Tag Selection Cloud: "); - $sb->add_choice_option("tageditcloud_sort", $sort_by, "
    Sort the tags by:"); - $sb->add_bool_option("tageditcloud_usedfirst","
    Always show used tags first: "); - $sb->add_label("
    Alpha sort:
    Only show tags used at least "); - $sb->add_int_option("tageditcloud_minusage"); - $sb->add_label(" times.
    Popularity/Relevance sort:
    Show "); - $sb->add_int_option("tageditcloud_defcount"); - $sb->add_label(" tags by default.
    Show a maximum of "); - $sb->add_int_option("tageditcloud_maxcount"); - $sb->add_label(" tags."); - $sb->add_label("
    Relevance sort:
    Ignore tags (space separated): "); - $sb->add_text_option("tageditcloud_ignoretags"); + $sb = new SetupBlock("Tag Edit Cloud"); + $sb->add_bool_option("tageditcloud_disable", "Disable Tag Selection Cloud: "); + $sb->add_choice_option("tageditcloud_sort", $sort_by, "
    Sort the tags by:"); + $sb->add_bool_option("tageditcloud_usedfirst", "
    Always show used tags first: "); + $sb->add_label("
    Alpha sort:
    Only show tags used at least "); + $sb->add_int_option("tageditcloud_minusage"); + $sb->add_label(" times.
    Popularity/Relevance sort:
    Show "); + $sb->add_int_option("tageditcloud_defcount"); + $sb->add_label(" tags by default.
    Show a maximum of "); + $sb->add_int_option("tageditcloud_maxcount"); + $sb->add_label(" tags."); + $sb->add_label("
    Relevance sort:
    Ignore tags (space separated): "); + $sb->add_text_option("tageditcloud_ignoretags"); - $event->panel->add_block($sb); - } + $event->panel->add_block($sb); + } - /** - * @param Image $image - * @return string - */ - private function build_tag_map(Image $image) { - global $database, $config; + private function build_tag_map(Image $image): ?string + { + global $database, $config; - $html = ""; - $cloud = ""; - $precloud = ""; - $postcloud = ""; + $html = ""; + $cloud = ""; + $precloud = ""; + $postcloud = ""; - $sort_method = $config->get_string("tageditcloud_sort"); - $tags_min = $config->get_int("tageditcloud_minusage"); - $used_first = $config->get_bool("tageditcloud_usedfirst"); - $max_count = $config->get_int("tageditcloud_maxcount"); - $def_count = $config->get_int("tageditcloud_defcount"); + $sort_method = $config->get_string("tageditcloud_sort"); + $tags_min = $config->get_int("tageditcloud_minusage"); + $used_first = $config->get_bool("tageditcloud_usedfirst"); + $max_count = $config->get_int("tageditcloud_maxcount"); + $def_count = $config->get_int("tageditcloud_defcount"); - $ignore_tags = Tag::explode($config->get_string("tageditcloud_ignoretags")); + $ignore_tags = Tag::explode($config->get_string("tageditcloud_ignoretags")); - if(ext_is_live("TagCategories")) { - $categories = $database->get_all("SELECT category, color FROM image_tag_categories"); - $cat_color = array(); - foreach($categories as $row) { - $cat_color[$row['category']] = $row['color']; - } - } + $cat_color = []; + if (Extension::is_enabled(TagCategoriesInfo::KEY)) { + $categories = $database->get_all("SELECT category, color FROM image_tag_categories"); + foreach ($categories as $row) { + $cat_color[$row['category']] = $row['color']; + } + } - switch($sort_method) { - case 'r': - $relevant_tags = array_diff($image->get_tag_array(),$ignore_tags); - if(count($relevant_tags) == 0) { - return null; - } - $relevant_tags = implode(",",array_map(array($database,"escape"),$relevant_tags)); - $tag_data = $database->get_all(" + switch ($sort_method) { + case 'r': + $relevant_tags = array_diff($image->get_tag_array(), $ignore_tags); + if (count($relevant_tags) == 0) { + return null; + } + $tag_data = $database->get_all( + " SELECT t2.tag AS tag, COUNT(image_id) AS count, FLOOR(LN(LN(COUNT(image_id) - :tag_min1 + 1)+1)*150)/200 AS scaled FROM image_tags it1 JOIN image_tags it2 USING(image_id) JOIN tags t1 ON it1.tag_id = t1.id JOIN tags t2 ON it2.tag_id = t2.id - WHERE t1.count >= :tag_min2 AND t1.tag IN($relevant_tags) + WHERE t1.count >= :tag_min2 AND t1.tag IN(:relevant_tags) GROUP BY t2.tag ORDER BY count DESC LIMIT :limit", - array("tag_min1" => $tags_min, "tag_min2" => $tags_min, "limit" => $max_count)); - break; - case 'a': - case 'p': - default: - $order_by = $sort_method == 'a' ? "tag" : "count DESC"; - $tag_data = $database->get_all(" + ["tag_min1" => $tags_min, "tag_min2" => $tags_min, "limit" => $max_count, "relevant_tags"=>$relevant_tags] + ); + break; + case 'a': + case 'p': + default: + $order_by = $sort_method == 'a' ? "tag" : "count DESC"; + $tag_data = $database->get_all( + " SELECT tag, FLOOR(LN(LN(count - :tag_min1 + 1)+1)*150)/200 AS scaled, count FROM tags WHERE count >= :tag_min2 ORDER BY $order_by LIMIT :limit", - array("tag_min1" => $tags_min, "tag_min2" => $tags_min, "limit" => $max_count)); - break; - } + ["tag_min1" => $tags_min, "tag_min2" => $tags_min, "limit" => $max_count] + ); + break; + } - $counter = 1; - foreach($tag_data as $row) { - $full_tag = $row['tag']; + $counter = 1; + foreach ($tag_data as $row) { + $full_tag = $row['tag']; - if(ext_is_live("TagCategories")){ - $tc = explode(':',$row['tag']); - if(isset($tc[1]) && isset($cat_color[$tc[0]])){ - $h_tag = html_escape($tc[1]); - $color = '; color:'.$cat_color[$tc[0]]; - } else { - $h_tag = html_escape($row['tag']); - $color = ''; - } - } else { - $h_tag = html_escape($row['tag']); - $color = ''; - } + if (Extension::is_enabled(TagCategoriesInfo::KEY)) { + $tc = explode(':', $row['tag']); + if (isset($tc[1]) && isset($cat_color[$tc[0]])) { + $h_tag = html_escape($tc[1]); + $color = '; color:'.$cat_color[$tc[0]]; + } else { + $h_tag = html_escape($row['tag']); + $color = ''; + } + } else { + $h_tag = html_escape($row['tag']); + $color = ''; + } - $size = sprintf("%.2f", max($row['scaled'],0.5)); - $js = html_escape('tageditcloud_toggle_tag(this,'.json_encode($full_tag).')'); //Ugly, but it works + $size = sprintf("%.2f", max($row['scaled'], 0.5)); + $js = html_escape('tageditcloud_toggle_tag(this,'.json_encode($full_tag).')'); //Ugly, but it works - if(array_search($row['tag'],$image->get_tag_array()) !== FALSE) { - if($used_first) { - $precloud .= " {$h_tag} \n"; - continue; - } else { - $entry = " {$h_tag} \n"; - } - } else { - $entry = " {$h_tag} \n"; - } + if (array_search($row['tag'], $image->get_tag_array()) !== false) { + if ($used_first) { + $precloud .= " {$h_tag} \n"; + continue; + } else { + $entry = " {$h_tag} \n"; + } + } else { + $entry = " {$h_tag} \n"; + } - if($counter++ <= $def_count) { - $cloud .= $entry; - } else { - $postcloud .= $entry; - } - } + if ($counter++ <= $def_count) { + $cloud .= $entry; + } else { + $postcloud .= $entry; + } + } - if($precloud != '') { - $html .= "
    {$precloud}
    "; - } + if ($precloud != '') { + $html .= "
    {$precloud}
    "; + } - if($postcloud != '') { - $postcloud = ""; - } + if ($postcloud != '') { + $postcloud = ""; + } - $html .= "
    {$cloud}{$postcloud}
    "; + $html .= "
    {$cloud}{$postcloud}
    "; - if($sort_method != 'a' && $counter > $def_count) { - $rem = $counter - $def_count; - $html .= "
    [show {$rem} more tags]"; - } + if ($sort_method != 'a' && $counter > $def_count) { + $rem = $counter - $def_count; + $html .= "
    [show {$rem} more tags]"; + } - return "
    {$html}
    "; // FIXME: stupidasallhell - } + return "
    {$html}
    "; // FIXME: stupidasallhell + } - /** - * @param Image $image - * @return bool - */ - private function can_tag(Image $image) { - global $user; - return ($user->can("edit_image_tag") && (!$image->is_locked() || $user->can("edit_image_lock"))); - } + private function can_tag(Image $image): bool + { + global $user; + return ($user->can(Permissions::EDIT_IMAGE_TAG) && (!$image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK))); + } } - diff --git a/ext/tag_history/info.php b/ext/tag_history/info.php new file mode 100644 index 00000000..68d8cab3 --- /dev/null +++ b/ext/tag_history/info.php @@ -0,0 +1,11 @@ +"bzchan@animemahou.com","jgen"=>"jgen.tech@gmail.com"]; + public $description = "Keep a record of tag changes, and allows you to revert changes."; +} diff --git a/ext/tag_history/main.php b/ext/tag_history/main.php index 19dd7fea..e0468119 100644 --- a/ext/tag_history/main.php +++ b/ext/tag_history/main.php @@ -1,409 +1,404 @@ -, modified by jgen - * Description: Keep a record of tag changes, and allows you to revert changes. - */ +set_default_int("history_limit", -1); + // in before tags are actually set, so that "get current tags" works + public function get_priority(): int + { + return 40; + } - // shimmie is being installed so call install to create the table. - if($config->get_int("ext_tag_history_version") < 3) { - $this->install(); - } - } + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_int("history_limit", -1); + } - public function onAdminBuilding(AdminBuildingEvent $event) { - $this->theme->display_admin_block(); - } + public function onAdminBuilding(AdminBuildingEvent $event) + { + $this->theme->display_admin_block(); + } - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; - if($event->page_matches("tag_history/revert")) { - // this is a request to revert to a previous version of the tags - if($user->can("edit_image_tag")) { - if(isset($_POST['revert'])) { - $this->process_revert_request($_POST['revert']); - } - } - } - else if($event->page_matches("tag_history/bulk_revert")) { - if($user->can("bulk_edit_image_tag") && $user->check_auth_token()) { - $this->process_bulk_revert_request(); - } - } - else if($event->page_matches("tag_history/all")) { - $page_id = int_escape($event->get_arg(0)); - $this->theme->display_global_page($page, $this->get_global_tag_history($page_id), $page_id); - } - else if($event->page_matches("tag_history") && $event->count_args() == 1) { - // must be an attempt to view a tag history - $image_id = int_escape($event->get_arg(0)); - $this->theme->display_history_page($page, $image_id, $this->get_tag_history_from_id($image_id)); - } - } - - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { - $event->add_part(" + if ($event->page_matches("tag_history/revert")) { + // this is a request to revert to a previous version of the tags + if ($user->can(Permissions::EDIT_IMAGE_TAG)) { + if (isset($_POST['revert'])) { + $this->process_revert_request((int)$_POST['revert']); + } + } + } elseif ($event->page_matches("tag_history/bulk_revert")) { + if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG) && $user->check_auth_token()) { + $this->process_bulk_revert_request(); + } + } elseif ($event->page_matches("tag_history/all")) { + $page_id = int_escape($event->get_arg(0)); + $this->theme->display_global_page($page, $this->get_global_tag_history($page_id), $page_id); + } elseif ($event->page_matches("tag_history") && $event->count_args() == 1) { + // must be an attempt to view a tag history + $image_id = int_escape($event->get_arg(0)); + $this->theme->display_history_page($page, $image_id, $this->get_tag_history_from_id($image_id)); + } + } + + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) + { + $event->add_part("
    ", 20); - } + } - /* - // disk space is cheaper than manually rebuilding history, - // so let's default to -1 and the user can go advanced if - // they /really/ want to - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Tag History"); - $sb->add_label("Limit to "); - $sb->add_int_option("history_limit"); - $sb->add_label(" entires per image"); - $sb->add_label("
    (-1 for unlimited)"); - $event->panel->add_block($sb); - } - */ + /* + // disk space is cheaper than manually rebuilding history, + // so let's default to -1 and the user can go advanced if + // they /really/ want to + public function onSetupBuilding(SetupBuildingEvent $event) { + $sb = new SetupBlock("Tag History"); + $sb->add_label("Limit to "); + $sb->add_int_option("history_limit"); + $sb->add_label(" entires per image"); + $sb->add_label("
    (-1 for unlimited)"); + $event->panel->add_block($sb); + } + */ - public function onTagSet(TagSetEvent $event) { - $this->add_tag_history($event->image, $event->tags); - } + public function onTagSet(TagSetEvent $event) + { + $this->add_tag_history($event->image, $event->tags); + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("bulk_edit_image_tag")) { - $event->add_link("Tag Changes", make_link("tag_history/all/1")); - } - } - - protected function install() { - global $database, $config; + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) { + $event->add_nav_link("tag_history", new Link('tag_history/all/1'), "Tag Changes", NavLink::is_active(["tag_history"])); + } + } + } - if($config->get_int("ext_tag_history_version") < 1) { - $database->create_table("tag_histories", " + + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) { + $event->add_link("Tag Changes", make_link("tag_history/all/1")); + } + } + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $database; + + if ($this->get_version("ext_tag_history_version") < 1) { + $database->create_table("tag_histories", " id SCORE_AIPK, image_id INTEGER NOT NULL, user_id INTEGER NOT NULL, user_ip SCORE_INET NOT NULL, tags TEXT NOT NULL, - date_set SCORE_DATETIME NOT NULL, + date_set TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE "); - $database->execute("CREATE INDEX tag_histories_image_id_idx ON tag_histories(image_id)", array()); - $config->set_int("ext_tag_history_version", 3); - } - - if($config->get_int("ext_tag_history_version") == 1) { - $database->Execute("ALTER TABLE tag_histories ADD COLUMN user_id INTEGER NOT NULL"); - $database->Execute($database->scoreql_to_sql("ALTER TABLE tag_histories ADD COLUMN date_set SCORE_DATETIME NOT NULL")); - $config->set_int("ext_tag_history_version", 2); - } + $database->execute("CREATE INDEX tag_histories_image_id_idx ON tag_histories(image_id)", []); + $this->set_version("ext_tag_history_version", 3); + } - if($config->get_int("ext_tag_history_version") == 2) { - $database->Execute("ALTER TABLE tag_histories ADD COLUMN user_ip CHAR(15) NOT NULL"); - $config->set_int("ext_tag_history_version", 3); - } - } + if ($this->get_version("ext_tag_history_version") == 1) { + $database->Execute("ALTER TABLE tag_histories ADD COLUMN user_id INTEGER NOT NULL"); + $database->Execute("ALTER TABLE tag_histories ADD COLUMN date_set TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP"); + $this->set_version("ext_tag_history_version", 2); + } - /** - * This function is called when a revert request is received. - * - * @param int $revert_id - * @throws ImageDoesNotExist - */ - private function process_revert_request($revert_id) { - global $page; + if ($this->get_version("ext_tag_history_version") == 2) { + $database->Execute("ALTER TABLE tag_histories ADD COLUMN user_ip CHAR(15) NOT NULL"); + $this->set_version("ext_tag_history_version", 3); + } + } - $revert_id = int_escape($revert_id); + /** + * This function is called when a revert request is received. + */ + private function process_revert_request(int $revert_id) + { + global $page; - // check for the nothing case - if($revert_id < 1) { - $page->set_mode("redirect"); - $page->set_redirect(make_link()); - return; - } - - // lets get this revert id assuming it exists - $result = $this->get_tag_history_from_revert($revert_id); - - if(empty($result)) { - // there is no history entry with that id so either the image was deleted - // while the user was viewing the history, someone is playing with form - // variables or we have messed up in code somewhere. - /* FIXME: calling die() is probably not a good idea, we should throw an Exception */ - die("Error: No tag history with specified id was found."); - } - - // lets get the values out of the result - $stored_image_id = int_escape($result['image_id']); - $stored_tags = $result['tags']; + // check for the nothing case + if ($revert_id < 1) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link()); + return; + } - $image = Image::by_id($stored_image_id); - if ( ! $image instanceof Image) { - throw new ImageDoesNotExist("Error: cannot find any image with the ID = ". $stored_image_id); - } + // lets get this revert id assuming it exists + $result = $this->get_tag_history_from_revert($revert_id); - log_debug("tag_history", 'Reverting tags of Image #'.$stored_image_id.' to ['.$stored_tags.']'); - // all should be ok so we can revert by firing the SetUserTags event. - send_event(new TagSetEvent($image, Tag::explode($stored_tags))); - - // all should be done now so redirect the user back to the image - $page->set_mode("redirect"); - $page->set_redirect(make_link('post/view/'.$stored_image_id)); - } + if (empty($result)) { + // there is no history entry with that id so either the image was deleted + // while the user was viewing the history, someone is playing with form + // variables or we have messed up in code somewhere. + /* FIXME: calling die() is probably not a good idea, we should throw an Exception */ + die("Error: No tag history with specified id was found."); + } - protected function process_bulk_revert_request() { - if (isset($_POST['revert_name']) && !empty($_POST['revert_name'])) { - $revert_name = $_POST['revert_name']; - } - else { - $revert_name = null; - } + // lets get the values out of the result + $stored_image_id = (int)$result['image_id']; + $stored_tags = $result['tags']; - if (isset($_POST['revert_ip']) && !empty($_POST['revert_ip'])) { - $revert_ip = filter_var($_POST['revert_ip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE); - - if ($revert_ip === false) { - // invalid ip given. - $this->theme->display_admin_block('Invalid IP'); - return; - } - } - else { - $revert_ip = null; - } - - if (isset($_POST['revert_date']) && !empty($_POST['revert_date'])) { - if (isValidDate($_POST['revert_date']) ){ - $revert_date = addslashes($_POST['revert_date']); // addslashes is really unnecessary since we just checked if valid, but better safe. - } - else { - $this->theme->display_admin_block('Invalid Date'); - return; - } - } - else { - $revert_date = null; - } - - set_time_limit(0); // reverting changes can take a long time, disable php's timelimit if possible. - - // Call the revert function. - $this->process_revert_all_changes($revert_name, $revert_ip, $revert_date); - // output results - $this->theme->display_revert_ip_results(); - } + $image = Image::by_id($stored_image_id); + if (! $image instanceof Image) { + throw new ImageDoesNotExist("Error: cannot find any image with the ID = ". $stored_image_id); + } - /** - * @param int $revert_id - * @return mixed|null - */ - public function get_tag_history_from_revert(/*int*/ $revert_id) { - global $database; - $row = $database->get_row(" + log_debug("tag_history", 'Reverting tags of Image #'.$stored_image_id.' to ['.$stored_tags.']'); + // all should be ok so we can revert by firing the SetUserTags event. + send_event(new TagSetEvent($image, Tag::explode($stored_tags))); + + // all should be done now so redirect the user back to the image + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link('post/view/'.$stored_image_id)); + } + + protected function process_bulk_revert_request() + { + if (isset($_POST['revert_name']) && !empty($_POST['revert_name'])) { + $revert_name = $_POST['revert_name']; + } else { + $revert_name = null; + } + + if (isset($_POST['revert_ip']) && !empty($_POST['revert_ip'])) { + $revert_ip = filter_var($_POST['revert_ip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE); + + if ($revert_ip === false) { + // invalid ip given. + $this->theme->display_admin_block('Invalid IP'); + return; + } + } else { + $revert_ip = null; + } + + if (isset($_POST['revert_date']) && !empty($_POST['revert_date'])) { + if (isValidDate($_POST['revert_date'])) { + $revert_date = addslashes($_POST['revert_date']); // addslashes is really unnecessary since we just checked if valid, but better safe. + } else { + $this->theme->display_admin_block('Invalid Date'); + return; + } + } else { + $revert_date = null; + } + + set_time_limit(0); // reverting changes can take a long time, disable php's timelimit if possible. + + // Call the revert function. + $this->process_revert_all_changes($revert_name, $revert_ip, $revert_date); + // output results + $this->theme->display_revert_ip_results(); + } + + public function get_tag_history_from_revert(int $revert_id): ?array + { + global $database; + $row = $database->get_row(" SELECT tag_histories.*, users.name FROM tag_histories JOIN users ON tag_histories.user_id = users.id - WHERE tag_histories.id = ?", array($revert_id)); - return ($row ? $row : null); - } + WHERE tag_histories.id = :id", ["id"=>$revert_id]); + return ($row ? $row : null); + } - /** - * @param int $image_id - * @return array - */ - public function get_tag_history_from_id(/*int*/ $image_id) { - global $database; - $row = $database->get_all(" + public function get_tag_history_from_id(int $image_id): array + { + global $database; + return $database->get_all( + " SELECT tag_histories.*, users.name FROM tag_histories JOIN users ON tag_histories.user_id = users.id - WHERE image_id = ? + WHERE image_id = :id ORDER BY tag_histories.id DESC", - array($image_id)); - return ($row ? $row : array()); - } + ["id"=>$image_id] + ); + } - /** - * @param int $page_id - * @return array - */ - public function get_global_tag_history($page_id) { - global $database; - $row = $database->get_all(" + public function get_global_tag_history(int $page_id): array + { + global $database; + return $database->get_all(" SELECT tag_histories.*, users.name FROM tag_histories JOIN users ON tag_histories.user_id = users.id ORDER BY tag_histories.id DESC LIMIT 100 OFFSET :offset - ", array("offset" => ($page_id-1)*100)); - return ($row ? $row : array()); - } - - /** - * This function attempts to revert all changes by a given IP within an (optional) timeframe. - * - * @param string $name - * @param string $ip - * @param string $date - */ - public function process_revert_all_changes($name, $ip, $date) { - global $database; - - $select_code = array(); - $select_args = array(); + ", ["offset" => ($page_id-1)*100]); + } - if(!is_null($name)) { - $duser = User::by_name($name); - if(is_null($duser)) { - $this->theme->add_status($name, "user not found"); - return; - } - else { - $select_code[] = 'user_id = ?'; - $select_args[] = $duser->id; - } - } + /** + * This function attempts to revert all changes by a given IP within an (optional) timeframe. + */ + public function process_revert_all_changes(?string $name, ?string $ip, ?string $date) + { + global $database; - if(!is_null($date)) { - $select_code[] = 'date_set >= ?'; - $select_args[] = $date; - } + $select_code = []; + $select_args = []; - if(!is_null($ip)) { - $select_code[] = 'user_ip = ?'; - $select_args[] = $ip; - } + if (!is_null($name)) { + $duser = User::by_name($name); + if (is_null($duser)) { + $this->theme->add_status($name, "user not found"); + return; + } else { + $select_code[] = 'user_id = :user_id'; + $select_args['user_id'] = $duser->id; + } + } - if(count($select_code) == 0) { - log_error("tag_history", "Tried to mass revert without any conditions"); - return; - } + if (!is_null($ip)) { + $select_code[] = 'user_ip = :user_ip'; + $select_args['user_ip'] = $ip; + } - log_info("tag_history", 'Attempting to revert edits where '.implode(" and ", $select_code)." (".implode(" / ", $select_args).")"); - - // Get all the images that the given IP has changed tags on (within the timeframe) that were last edited by the given IP - $result = $database->get_col(' + if (!is_null($date)) { + $select_code[] = 'date_set >= :date_set'; + $select_args['date_set'] = $date; + } + + if (count($select_code) == 0) { + log_error("tag_history", "Tried to mass revert without any conditions"); + return; + } + + log_info("tag_history", 'Attempting to revert edits where '.implode(" and ", $select_code)." (".implode(" / ", $select_args).")"); + + // Get all the images that the given IP has changed tags on (within the timeframe) that were last edited by the given IP + $result = $database->get_col(' SELECT t1.image_id FROM tag_histories t1 LEFT JOIN tag_histories t2 ON (t1.image_id = t2.image_id AND t1.date_set < t2.date_set) WHERE t2.image_id IS NULL - AND t1.image_id IN ( select image_id from tag_histories where '.implode(" AND ", $select_code).') + AND t1.image_id IN ( select image_id from tag_histories where '.implode(" AND ", $select_code).') ORDER BY t1.image_id ', $select_args); - - foreach($result as $image_id) { - // Get the first tag history that was done before the given IP edit - $row = $database->get_row(' + + foreach ($result as $image_id) { + // Get the first tag history that was done before the given IP edit + $row = $database->get_row(' SELECT id, tags FROM tag_histories WHERE image_id='.$image_id.' AND NOT ('.implode(" AND ", $select_code).') ORDER BY date_set DESC LIMIT 1 ', $select_args); - - if (empty($row)) { - // we can not revert this image based on the date restriction. - // Output a message perhaps? - } - else { - $revert_id = $row['id']; - $result = $this->get_tag_history_from_revert($revert_id); - - if(empty($result)) { - // there is no history entry with that id so either the image was deleted - // while the user was viewing the history, or something messed up - /* calling die() is probably not a good idea, we should throw an Exception */ - die('Error: No tag history with specified id ('.$revert_id.') was found in the database.'."\n\n". - 'Perhaps the image was deleted while processing this request.'); - } - - // lets get the values out of the result - $stored_result_id = int_escape($result['id']); - $stored_image_id = int_escape($result['image_id']); - $stored_tags = $result['tags']; - $image = Image::by_id($stored_image_id); - if ( ! $image instanceof Image) { - continue; - //throw new ImageDoesNotExist("Error: cannot find any image with the ID = ". $stored_image_id); - } + if (!empty($row)) { + $revert_id = (int)$row['id']; + $result = $this->get_tag_history_from_revert($revert_id); - log_debug("tag_history", 'Reverting tags of Image #'.$stored_image_id.' to ['.$stored_tags.']'); - // all should be ok so we can revert by firing the SetTags event. - send_event(new TagSetEvent($image, Tag::explode($stored_tags))); - $this->theme->add_status('Reverted Change','Reverted Image #'.$image_id.' to Tag History #'.$stored_result_id.' ('.$row['tags'].')'); - } - } + if (empty($result)) { + // there is no history entry with that id so either the image was deleted + // while the user was viewing the history, or something messed up + /* calling die() is probably not a good idea, we should throw an Exception */ + die('Error: No tag history with specified id ('.$revert_id.') was found in the database.'."\n\n". + 'Perhaps the image was deleted while processing this request.'); + } - log_info("tag_history", 'Reverted '.count($result).' edits.'); - } + // lets get the values out of the result + $stored_result_id = (int)$result['id']; + $stored_image_id = (int)$result['image_id']; + $stored_tags = $result['tags']; - /** - * This function is called just before an images tag are changed. - * - * @param Image $image - * @param string[] $tags - */ - private function add_tag_history(Image $image, $tags) { - global $database, $config, $user; - assert('is_array($tags)'); + $image = Image::by_id($stored_image_id); + if (! $image instanceof Image) { + continue; + //throw new ImageDoesNotExist("Error: cannot find any image with the ID = ". $stored_image_id); + } - $new_tags = Tag::implode($tags); - $old_tags = $image->get_tag_list(); - - if($new_tags == $old_tags) { return; } - - if(empty($old_tags)) { - /* no old tags, so we are probably adding the image for the first time */ - log_debug("tag_history", "adding new tag history: [$new_tags]", false, array("image_id" => $image->id)); - } - else { - log_debug("tag_history", "adding tag history: [$old_tags] -> [$new_tags]", false, array("image_id" => $image->id)); - } - - $allowed = $config->get_int("history_limit"); - if($allowed == 0) { return; } - - // if the image has no history, make one with the old tags - $entries = $database->get_one("SELECT COUNT(*) FROM tag_histories WHERE image_id = ?", array($image->id)); - if($entries == 0 && !empty($old_tags)) { - $database->execute(" + log_debug("tag_history", 'Reverting tags of Image #'.$stored_image_id.' to ['.$stored_tags.']'); + // all should be ok so we can revert by firing the SetTags event. + send_event(new TagSetEvent($image, Tag::explode($stored_tags))); + $this->theme->add_status('Reverted Change', 'Reverted Image #'.$image_id.' to Tag History #'.$stored_result_id.' ('.$row['tags'].')'); + } + } + + log_info("tag_history", 'Reverted '.count($result).' edits.'); + } + + /** + * This function is called just before an images tag are changed. + * + * #param string[] $tags + */ + private function add_tag_history(Image $image, array $tags) + { + global $database, $config, $user; + + $new_tags = Tag::implode($tags); + $old_tags = $image->get_tag_list(); + + if ($new_tags == $old_tags) { + return; + } + + if (empty($old_tags)) { + /* no old tags, so we are probably adding the image for the first time */ + log_debug("tag_history", "adding new tag history: [$new_tags]"); + } else { + log_debug("tag_history", "adding tag history: [$old_tags] -> [$new_tags]"); + } + + $allowed = $config->get_int("history_limit"); + if ($allowed == 0) { + return; + } + + // if the image has no history, make one with the old tags + $entries = $database->get_one("SELECT COUNT(*) FROM tag_histories WHERE image_id = :id", ["id"=>$image->id]); + if ($entries == 0 && !empty($old_tags)) { + $database->execute( + " INSERT INTO tag_histories(image_id, tags, user_id, user_ip, date_set) - VALUES (?, ?, ?, ?, now())", - array($image->id, $old_tags, $config->get_int('anon_id'), '127.0.0.1')); - $entries++; - } + VALUES (:image_id, :tags, :user_id, :user_ip, now())", + ["image_id"=>$image->id, "tags"=>$old_tags, "user_id"=>$config->get_int('anon_id'), "user_ip"=>'127.0.0.1'] + ); + $entries++; + } - // add a history entry - $database->execute(" + // add a history entry + $database->execute( + " INSERT INTO tag_histories(image_id, tags, user_id, user_ip, date_set) - VALUES (?, ?, ?, ?, now())", - array($image->id, $new_tags, $user->id, $_SERVER['REMOTE_ADDR'])); - $entries++; - - // if needed remove oldest one - if($allowed == -1) { return; } - if($entries > $allowed) { - // TODO: Make these queries better - /* - MySQL does NOT allow you to modify the same table which you use in the SELECT part. - Which means that these will probably have to stay as TWO separate queries... - - http://dev.mysql.com/doc/refman/5.1/en/subquery-restrictions.html - http://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause - */ - $min_id = $database->get_one("SELECT MIN(id) FROM tag_histories WHERE image_id = ?", array($image->id)); - $database->execute("DELETE FROM tag_histories WHERE id = ?", array($min_id)); - } - } + VALUES (:image_id, :tags, :user_id, :user_ip, now())", + ["image_id"=>$image->id, "tags"=>$new_tags, "user_id"=>$user->id, "user_ip"=>$_SERVER['REMOTE_ADDR']] + ); + $entries++; + + // if needed remove oldest one + if ($allowed == -1) { + return; + } + if ($entries > $allowed) { + // TODO: Make these queries better + /* + MySQL does NOT allow you to modify the same table which you use in the SELECT part. + Which means that these will probably have to stay as TWO separate queries... + + http://dev.mysql.com/doc/refman/5.1/en/subquery-restrictions.html + http://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause + */ + $min_id = $database->get_one("SELECT MIN(id) FROM tag_histories WHERE image_id = :image_id", ["image_id"=>$image->id]); + $database->execute("DELETE FROM tag_histories WHERE id = :id", ["id"=>$min_id]); + } + } } - diff --git a/ext/tag_history/test.php b/ext/tag_history/test.php index 4914be06..fc441aff 100644 --- a/ext/tag_history/test.php +++ b/ext/tag_history/test.php @@ -1,24 +1,28 @@ -log_in_as_admin(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - $this->get_page("post/view/$image_id"); - $this->assert_title("Image $image_id: pbx"); +log_in_as_admin(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "old_tag"); + $image = Image::by_id($image_id); - $this->markTestIncomplete(); + // Original + $this->get_page("post/view/$image_id"); + $this->assert_title("Image $image_id: old_tag"); - // FIXME - $this->set_field("tag_edit__tags", "new"); - $this->click("Set"); - $this->assert_title("Image $image_id: new"); - $this->click("View Tag History"); - $this->assert_text("new (Set by demo"); - $this->click("Revert To"); - $this->assert_title("Image $image_id: pbx"); + // Modified + send_event(new TagSetEvent($image, ["new_tag"])); - $this->get_page("tag_history/all/1"); - $this->assert_title("Global Tag History"); - } + // FIXME + // $this->click("View Tag History"); + // $this->assert_text("new (Set by demo"); + // $this->click("Revert To"); + // $this->get_page("post/view/$image_id"); + // $this->assert_title("Image $image_id: pbx"); + + $this->get_page("tag_history/all/1"); + $this->assert_title("Global Tag History"); + $this->assert_text("new_tag"); + } } - diff --git a/ext/tag_history/theme.php b/ext/tag_history/theme.php index 7e6bb78c..f27c705e 100644 --- a/ext/tag_history/theme.php +++ b/ext/tag_history/theme.php @@ -1,47 +1,38 @@ -, modified by jgen - */ + ".make_form(make_link("tag_history/revert"))."
      "; - $history_list = ""; - $n = 0; - foreach($history as $fields) - { - $n++; - $current_id = $fields['id']; - $current_tags = html_escape($fields['tags']); - $name = $fields['name']; - $date_set = autodate($fields['date_set']); - $h_ip = $user->can("view_ip") ? " ".show_ip($fields['user_ip'], "Tagging Image #$image_id as '$current_tags'") : ""; - $setter = "".html_escape($name)."$h_ip"; + $history_list = ""; + $n = 0; + foreach ($history as $fields) { + $n++; + $current_id = $fields['id']; + $current_tags = html_escape($fields['tags']); + $name = $fields['name']; + $date_set = autodate($fields['date_set']); + $h_ip = $user->can(Permissions::VIEW_IP) ? " ".show_ip($fields['user_ip'], "Tagging Image #$image_id as '$current_tags'") : ""; + $setter = "".html_escape($name)."$h_ip"; - $selected = ($n == 2) ? " checked" : ""; + $selected = ($n == 2) ? " checked" : ""; - $current_tags = Tag::explode($current_tags); - $taglinks = array(); - foreach($current_tags as $tag){ - $taglinks[] = "".$tag.""; - } - $current_tags = implode(' ', $taglinks); + $current_tags = Tag::explode($current_tags); + $taglinks = []; + foreach ($current_tags as $tag) { + $taglinks[] = "".$tag.""; + } + $current_tags = implode(' ', $taglinks); - $history_list .= " + $history_list .= "
    • "; - } + } - $end_string = " + $end_string = "
    "; - $history_html = $start_string . $history_list . $end_string; + $history_html = $start_string . $history_list . $end_string; - $page->set_title('Image '.$image_id.' Tag History'); - $page->set_heading('Tag History: '.$image_id); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Tag History", $history_html, "main", 10)); - } + $page->set_title('Image '.$image_id.' Tag History'); + $page->set_heading('Tag History: '.$image_id); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Tag History", $history_html, "main", 10)); + } - /** - * @param Page $page - * @param array $history - * @param int $page_number - */ - public function display_global_page(Page $page, /*array*/ $history, /*int*/ $page_number) { - $start_string = " + public function display_global_page(Page $page, array $history, int $page_number) + { + $start_string = "
    ".make_form(make_link("tag_history/revert"))."
      "; - $end_string = " + $end_string = "
    "; - global $user; - $history_list = ""; - foreach($history as $fields) - { - $current_id = $fields['id']; - $image_id = $fields['image_id']; - $current_tags = html_escape($fields['tags']); - $name = $fields['name']; - $h_ip = $user->can("view_ip") ? " ".show_ip($fields['user_ip'], "Tagging Image #$image_id as '$current_tags'") : ""; - $setter = "".html_escape($name)."$h_ip"; + global $user; + $history_list = ""; + foreach ($history as $fields) { + $current_id = $fields['id']; + $image_id = $fields['image_id']; + $current_tags = html_escape($fields['tags']); + $name = $fields['name']; + $h_ip = $user->can(Permissions::VIEW_IP) ? " ".show_ip($fields['user_ip'], "Tagging Image #$image_id as '$current_tags'") : ""; + $setter = "".html_escape($name)."$h_ip"; - $history_list .= ' + $history_list .= '
  • '.$image_id.': '.$current_tags.' (Set by '.$setter.')
  • '; - } + } - $history_html = $start_string . $history_list . $end_string; - $page->set_title("Global Tag History"); - $page->set_heading("Global Tag History"); - $page->add_block(new Block("Tag History", $history_html, "main", 10)); + $history_html = $start_string . $history_list . $end_string; + $page->set_title("Global Tag History"); + $page->set_heading("Global Tag History"); + $page->add_block(new Block("Tag History", $history_html, "main", 10)); - $h_prev = ($page_number <= 1) ? "Prev" : - 'Prev'; - $h_index = "Index"; - $h_next = 'Next'; + $h_prev = ($page_number <= 1) ? "Prev" : + 'Prev'; + $h_index = "Index"; + $h_next = 'Next'; - $nav = $h_prev.' | '.$h_index.' | '.$h_next; - $page->add_block(new Block("Navigation", $nav, "left")); - } + $nav = $h_prev.' | '.$h_index.' | '.$h_next; + $page->add_block(new Block("Navigation", $nav, "left")); + } - /** - * Add a section to the admin page. - * - * @param string $validation_msg - */ - public function display_admin_block(/*string*/ $validation_msg='') { - global $page; - - if (!empty($validation_msg)) { - $validation_msg = '
    '. $validation_msg .''; - } - - $html = ' - Revert tag changes/edit by a specific IP address or username. -
    You can restrict the time frame to revert these edits as well. -
    (Date format: 2011-10-23) + /** + * Add a section to the admin page. + */ + public function display_admin_block(string $validation_msg='') + { + global $page; + + if (!empty($validation_msg)) { + $validation_msg = '
    '. $validation_msg .''; + } + + $html = ' + Revert tag changes by a specific IP address or username, optionally limited to recent changes. '.$validation_msg.'

    '.make_form(make_link("tag_history/bulk_revert"), 'POST')." - +
    Username
    IP Address
    Date range
    Since
    "; - $page->add_block(new Block("Mass Tag Revert", $html)); - } - - /* - * Show a standard page for results to be put into - */ - public function display_revert_ip_results() { - global $page; - $html = implode($this->messages, "\n"); - $page->add_block(new Block("Bulk Revert Results", $html)); - } + $page->add_block(new Block("Mass Tag Revert", $html)); + } - /** - * @param string $title - * @param string $body - */ - public function add_status(/*string*/ $title, /*string*/ $body) { - $this->messages[] = '

    '. $title .'
    '. $body .'

    '; - } + /* + * Show a standard page for results to be put into + */ + public function display_revert_ip_results() + { + global $page; + $html = implode($this->messages, "\n"); + $page->add_block(new Block("Bulk Revert Results", $html)); + } + + public function add_status(string $title, string $body) + { + $this->messages[] = '

    '. $title .'
    '. $body .'

    '; + } } - diff --git a/ext/tag_list/config.php b/ext/tag_list/config.php new file mode 100644 index 00000000..8a80cdbd --- /dev/null +++ b/ext/tag_list/config.php @@ -0,0 +1,31 @@ + TagListConfig::TYPE_TAGS, + "Show related" => TagListConfig::TYPE_RELATED + ]; + + public const SORT_ALPHABETICAL = "alphabetical"; + public const SORT_TAG_COUNT = "tagcount"; + + public const SORT_CHOICES = [ + "Tag Count" => TagListConfig::SORT_TAG_COUNT, + "Alphabetical" => TagListConfig::SORT_ALPHABETICAL + ]; +} diff --git a/ext/tag_list/info.php b/ext/tag_list/info.php new file mode 100644 index 00000000..6dca0dd8 --- /dev/null +++ b/ext/tag_list/info.php @@ -0,0 +1,13 @@ + - * Link: http://code.shishnet.org/shimmie2/ - * Description: Show the tags in various ways - */ +set_default_int("tag_list_length", 15); - $config->set_default_int("popular_tag_list_length", 15); - $config->set_default_int("tags_min", 3); - $config->set_default_string("info_link", 'http://en.wikipedia.org/wiki/$tag'); - $config->set_default_string("tag_list_image_type", 'related'); - $config->set_default_string("tag_list_related_sort", 'alphabetical'); - $config->set_default_string("tag_list_popular_sort", 'tagcount'); - $config->set_default_bool("tag_list_pages", false); - } +require_once "config.php"; - public function onPageRequest(PageRequestEvent $event) { - global $page, $database; +class TagList extends Extension +{ + /** @var TagListTheme */ + protected $theme; - if($event->page_matches("tags")) { - $this->theme->set_navigation($this->build_navigation()); - switch($event->get_arg(0)) { - default: - case 'map': - $this->theme->set_heading("Tag Map"); - $this->theme->set_tag_list($this->build_tag_map()); - break; - case 'alphabetic': - $this->theme->set_heading("Alphabetic Tag List"); - $this->theme->set_tag_list($this->build_tag_alphabetic()); - break; - case 'popularity': - $this->theme->set_heading("Tag List by Popularity"); - $this->theme->set_tag_list($this->build_tag_popularity()); - break; - case 'categories': - $this->theme->set_heading("Popular Categories"); - $this->theme->set_tag_list($this->build_tag_list()); - break; - } - $this->theme->display_page($page); - } - else if($event->page_matches("api/internal/tag_list/complete")) { - if(!isset($_GET["s"]) || $_GET["s"] == "" || $_GET["s"] == "_") return; + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_int(TagListConfig::LENGTH, 15); + $config->set_default_int(TagListConfig::POPULAR_TAG_LIST_LENGTH, 15); + $config->set_default_int(TagListConfig::TAGS_MIN, 3); + $config->set_default_string(TagListConfig::INFO_LINK, 'http://en.wikipedia.org/wiki/$tag'); + $config->set_default_string(TagListConfig::OMIT_TAGS, 'tagme*'); + $config->set_default_string(TagListConfig::IMAGE_TYPE, TagListConfig::TYPE_RELATED); + $config->set_default_string(TagListConfig::RELATED_SORT, TagListConfig::SORT_ALPHABETICAL); + $config->set_default_string(TagListConfig::POPULAR_SORT, TagListConfig::SORT_TAG_COUNT); + $config->set_default_bool(TagListConfig::PAGES, false); + } - //$limit = 0; - $cache_key = "autocomplete-" . strtolower($_GET["s"]); - $limitSQL = ""; - $SQLarr = array("search"=>$_GET["s"]."%"); - if(isset($_GET["limit"]) && $_GET["limit"] !== 0){ - $limitSQL = "LIMIT :limit"; - $SQLarr['limit'] = $_GET["limit"]; - $cache_key .= "-" . $_GET["limit"]; - } + public function onPageRequest(PageRequestEvent $event) + { + global $cache, $page, $database; - $res = null; - $database->cache->get($cache_key); - if(!$res) { - $res = $database->get_col($database->scoreql_to_sql(" + if ($event->page_matches("tags")) { + $this->theme->set_navigation($this->build_navigation()); + if ($event->count_args() == 0) { + $sub = "map"; + } else { + $sub = $event->get_arg(0); + } + switch ($sub) { + default: + case 'map': + $this->theme->set_heading("Tag Map"); + $this->theme->set_tag_list($this->build_tag_map()); + break; + case 'alphabetic': + $this->theme->set_heading("Alphabetic Tag List"); + $this->theme->set_tag_list($this->build_tag_alphabetic()); + break; + case 'popularity': + $this->theme->set_heading("Tag List by Popularity"); + $this->theme->set_tag_list($this->build_tag_popularity()); + break; + } + $this->theme->display_page($page); + } elseif ($event->page_matches("api/internal/tag_list/complete")) { + if (!isset($_GET["s"]) || $_GET["s"] == "" || $_GET["s"] == "_") { + return; + } + + //$limit = 0; + $cache_key = "autocomplete-" . strtolower($_GET["s"]); + $limitSQL = ""; + $SQLarr = ["search"=>$_GET["s"]."%"]; + if (isset($_GET["limit"]) && $_GET["limit"] !== 0) { + $limitSQL = "LIMIT :limit"; + $SQLarr['limit'] = $_GET["limit"]; + $cache_key .= "-" . $_GET["limit"]; + } + + $res = null; + $cache->get($cache_key); + if (!$res) { + $res = $database->get_col(" SELECT tag FROM tags - WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:search) + WHERE LOWER(tag) LIKE LOWER(:search) AND count > 0 $limitSQL - "), $SQLarr); - $database->cache->set($cache_key, $res, 600); - } + ", $SQLarr); + $cache->set($cache_key, $res, 600); + } - $page->set_mode("data"); - $page->set_type("text/plain"); - $page->set_data(implode("\n", $res)); - } - } + $page->set_mode(PageMode::DATA); + $page->set_type("text/plain"); + $page->set_data(implode("\n", $res)); + } + } - public function onPostListBuilding(PostListBuildingEvent $event) { - global $config, $page; - if($config->get_int('tag_list_length') > 0) { - if(!empty($event->search_terms)) { - $this->add_refine_block($page, $event->search_terms); - } - else { - $this->add_popular_block($page); - } - } - } + public function onPostListBuilding(PostListBuildingEvent $event) + { + global $config, $page; + if ($config->get_int(TagListConfig::LENGTH) > 0) { + if (!empty($event->search_terms)) { + $this->add_refine_block($page, $event->search_terms); + } else { + $this->add_popular_block($page); + } + } + } - public function onDisplayingImage(DisplayingImageEvent $event) { - global $config, $page; - if($config->get_int('tag_list_length') > 0) { - if($config->get_string('tag_list_image_type') == 'related') { - $this->add_related_block($page, $event->image); - } - else { - if(class_exists("TagCategories") and $config->get_bool('tag_categories_split_on_view')) { - $this->add_split_tags_block($page, $event->image); - } - else { - $this->add_tags_block($page, $event->image); - } - } - } - } + public function onPageNavBuilding(PageNavBuildingEvent $event) + { + $event->add_nav_link("tags", new Link('tags/map'), "Tags"); + } - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Tag Map Options"); - $sb->add_int_option("tags_min", "Only show tags used at least "); $sb->add_label(" times"); - $sb->add_bool_option("tag_list_pages", "
    Paged tag lists: "); - $event->panel->add_block($sb); + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + if ($event->parent=="tags") { + $event->add_nav_link("tags_map", new Link('tags/map'), "Map"); + $event->add_nav_link("tags_alphabetic", new Link('tags/alphabetic'), "Alphabetic"); + $event->add_nav_link("tags_popularity", new Link('tags/popularity'), "Popularity"); + } + } - $sb = new SetupBlock("Popular / Related Tag List"); - $sb->add_int_option("tag_list_length", "Show top "); $sb->add_label(" related tags"); - $sb->add_int_option("popular_tag_list_length", "
    Show top "); $sb->add_label(" popular tags"); - $sb->add_text_option("info_link", "
    Tag info link: "); - $sb->add_choice_option("tag_list_image_type", array( - "Image's tags only" => "tags", - "Show related" => "related" - ), "
    Image tag list: "); - $sb->add_choice_option("tag_list_related_sort", array( - "Tag Count" => "tagcount", - "Alphabetical" => "alphabetical" - ), "
    Sort related list by: "); - $sb->add_choice_option("tag_list_popular_sort", array( - "Tag Count" => "tagcount", - "Alphabetical" => "alphabetical" - ), "
    Sort popular list by: "); - $sb->add_bool_option("tag_list_numbers", "
    Show tag counts: "); - $event->panel->add_block($sb); - } -// }}} -// misc {{{ - /** - * @param string $tag - * @return string - */ - private function tag_link(/*string*/ $tag) { - $u_tag = url_escape($tag); - return make_link("post/list/$u_tag/1"); - } + public function onDisplayingImage(DisplayingImageEvent $event) + { + global $config, $page; + if ($config->get_int(TagListConfig::LENGTH) > 0) { + if ($config->get_string(TagListConfig::IMAGE_TYPE) == TagListConfig::TYPE_RELATED) { + $this->add_related_block($page, $event->image); + } else { + if (class_exists("TagCategories") and $config->get_bool(TagCategoriesConfig::SPLIT_ON_VIEW)) { + $this->add_split_tags_block($page, $event->image); + } else { + $this->add_tags_block($page, $event->image); + } + } + } + } - /** - * Get the minimum number of times a tag needs to be used - * in order to be considered in the tag list. - * - * @return int - */ - private function get_tags_min() { - if(isset($_GET['mincount'])) { - return int_escape($_GET['mincount']); - } - else { - global $config; - return $config->get_int('tags_min'); // get the default. - } - } + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Tag Map Options"); + $sb->add_int_option(TagListConfig::TAGS_MIN, "Only show tags used at least "); + $sb->add_label(" times"); + $sb->add_bool_option(TagListConfig::PAGES, "
    Paged tag lists: "); + $event->panel->add_block($sb); - /** - * @return string - */ - private function get_starts_with() { - global $config; - if(isset($_GET['starts_with'])) { - return $_GET['starts_with'] . "%"; - } - else { - if($config->get_bool("tag_list_pages")) { - return "a%"; - } - else { - return "%"; - } - } - } + $sb = new SetupBlock("Popular / Related Tag List"); + $sb->add_int_option(TagListConfig::LENGTH, "Show top "); + $sb->add_label(" related tags"); + $sb->add_int_option(TagListConfig::POPULAR_TAG_LIST_LENGTH, "
    Show top "); + $sb->add_label(" popular tags"); + $sb->start_table(); + $sb->add_text_option(TagListConfig::INFO_LINK, "Tag info link", true); + $sb->add_text_option(TagListConfig::OMIT_TAGS, "Omit tags", true); + $sb->add_choice_option( + TagListConfig::IMAGE_TYPE, + TagListConfig::TYPE_CHOICES, + "Image tag list", + true + ); + $sb->add_choice_option( + TagListConfig::RELATED_SORT, + TagListConfig::SORT_CHOICES, + "Sort related list by", + true + ); + $sb->add_choice_option( + TagListConfig::POPULAR_SORT, + TagListConfig::SORT_CHOICES, + "Sort popular list by", + true + ); + $sb->add_bool_option("tag_list_numbers", "Show tag counts", true); + $sb->end_table(); + $event->panel->add_block($sb); + } - /** - * @return string - */ - private function build_az() { - global $database; + /** + * Get the minimum number of times a tag needs to be used + * in order to be considered in the tag list. + */ + private function get_tags_min(): int + { + if (isset($_GET['mincount'])) { + return int_escape($_GET['mincount']); + } else { + global $config; + return $config->get_int(TagListConfig::TAGS_MIN); // get the default. + } + } - $tags_min = $this->get_tags_min(); + private static function get_omitted_tags(): array + { + global $cache, $config, $database; + $tags_config = $config->get_string(TagListConfig::OMIT_TAGS); - $tag_data = $database->get_col($database->scoreql_to_sql(" + $results = $cache->get("tag_list_omitted_tags:".$tags_config); + + if ($results==null) { + $tags = explode(" ", $tags_config); + + if (empty($tags)) { + return []; + } + + $where = []; + $args = []; + $i = 0; + foreach ($tags as $tag) { + $i++; + $arg = "tag$i"; + $args[$arg] = Tag::sqlify($tag); + if (strpos($tag, '*') === false + && strpos($tag, '?') === false) { + $where[] = " tag = :$arg "; + } else { + $where[] = " tag LIKE :$arg "; + } + } + + $results = $database->get_col("SELECT id FROM tags WHERE " . implode(" OR ", $where), $args); + + $cache->set("tag_list_omitted_tags:" . $tags_config, $results, 600); + } + return $results; + } + + private function get_starts_with(): string + { + global $config; + if (isset($_GET['starts_with'])) { + return $_GET['starts_with'] . "%"; + } else { + if ($config->get_bool(TagListConfig::PAGES)) { + return "a%"; + } else { + return "%"; + } + } + } + + private function build_az(): string + { + global $database; + + $tags_min = $this->get_tags_min(); + + $tag_data = $database->get_col(" SELECT DISTINCT - SCORE_STRNORM(substr(tag, 1, 1)) + LOWER(substr(tag, 1, 1)) FROM tags WHERE count >= :tags_min - ORDER BY SCORE_STRNORM(substr(tag, 1, 1)) - "), array("tags_min"=>$tags_min)); + ORDER BY LOWER(substr(tag, 1, 1)) + ", ["tags_min"=>$tags_min]); - $html = ""; - foreach($tag_data as $a) { - $html .= " $a"; - } - $html .= "\n


    "; + $html = ""; + foreach ($tag_data as $a) { + $html .= " $a"; + } + $html .= "\n


    "; - return $html; - } -// }}} -// maps {{{ + return $html; + } - /** - * @return string - */ - private function build_navigation() { - $h_index = "Index"; - $h_map = "Map"; - $h_alphabetic = "Alphabetic"; - $h_popularity = "Popularity"; - $h_cats = "Categories"; - $h_all = "Show All"; - return "$h_index
     
    $h_map
    $h_alphabetic
    $h_popularity
    $h_cats
     
    $h_all"; - } + private function build_navigation(): string + { + $h_index = "Index"; + $h_map = "Map"; + $h_alphabetic = "Alphabetic"; + $h_popularity = "Popularity"; + $h_all = "Show All"; + return "$h_index
     
    $h_map
    $h_alphabetic
    $h_popularity
     
    $h_all"; + } - /** - * @return string - */ - private function build_tag_map() { - global $config, $database; + private function build_tag_map(): string + { + global $config, $database; - $tags_min = $this->get_tags_min(); - $starts_with = $this->get_starts_with(); - - // check if we have a cached version - $cache_key = data_path("cache/tag_cloud-" . md5("tc" . $tags_min . $starts_with) . ".html"); - if(file_exists($cache_key)) {return file_get_contents($cache_key);} + $tags_min = $this->get_tags_min(); + $starts_with = $this->get_starts_with(); - // SHIT: PDO/pgsql has problems using the same named param twice -_-;; - $tag_data = $database->get_all($database->scoreql_to_sql(" + // check if we have a cached version + $cache_key = warehouse_path("cache/tag_cloud", md5("tc" . $tags_min . $starts_with)); + if (file_exists($cache_key)) { + return file_get_contents($cache_key); + } + + // SHIT: PDO/pgsql has problems using the same named param twice -_-;; + $tag_data = $database->get_all(" SELECT tag, FLOOR(LOG(2.7, LOG(2.7, count - :tags_min2 + 1)+1)*1.5*100)/100 AS scaled FROM tags WHERE count >= :tags_min - AND tag SCORE_ILIKE :starts_with - ORDER BY SCORE_STRNORM(tag) - "), array("tags_min"=>$tags_min, "tags_min2"=>$tags_min, "starts_with"=>$starts_with)); + AND LOWER(tag) LIKE LOWER(:starts_with) + ORDER BY LOWER(tag) + ", ["tags_min"=>$tags_min, "tags_min2"=>$tags_min, "starts_with"=>$starts_with]); - $html = ""; - if($config->get_bool("tag_list_pages")) $html .= $this->build_az(); - foreach($tag_data as $row) { - $h_tag = html_escape($row['tag']); - $size = sprintf("%.2f", (float)$row['scaled']); - $link = $this->tag_link($row['tag']); - if($size<0.5) $size = 0.5; - $h_tag_no_underscores = str_replace("_", " ", $h_tag); - $html .= " $h_tag_no_underscores \n"; - } + $html = ""; + if ($config->get_bool(TagListConfig::PAGES)) { + $html .= $this->build_az(); + } + foreach ($tag_data as $row) { + $h_tag = html_escape($row['tag']); + $size = sprintf("%.2f", (float)$row['scaled']); + $link = $this->theme->tag_link($row['tag']); + if ($size<0.5) { + $size = 0.5; + } + $h_tag_no_underscores = str_replace("_", " ", $h_tag); + $html .= " $h_tag_no_underscores \n"; + } - if(SPEED_HAX) {file_put_contents($cache_key, $html);} + if (SPEED_HAX) { + file_put_contents($cache_key, $html); + } - return $html; - } + return $html; + } - /** - * @return string - */ - private function build_tag_alphabetic() { - global $config, $database; + private function build_tag_alphabetic(): string + { + global $config, $database; - $tags_min = $this->get_tags_min(); - $starts_with = $this->get_starts_with(); - - // check if we have a cached version - $cache_key = data_path("cache/tag_alpha-" . md5("ta" . $tags_min . $starts_with) . ".html"); - if(file_exists($cache_key)) {return file_get_contents($cache_key);} + $tags_min = $this->get_tags_min(); + $starts_with = $this->get_starts_with(); - $tag_data = $database->get_pairs($database->scoreql_to_sql(" + // check if we have a cached version + $cache_key = warehouse_path("cache/tag_alpha", md5("ta" . $tags_min . $starts_with)); + if (file_exists($cache_key)) { + return file_get_contents($cache_key); + } + + $tag_data = $database->get_pairs(" SELECT tag, count FROM tags WHERE count >= :tags_min - AND tag SCORE_ILIKE :starts_with - ORDER BY SCORE_STRNORM(tag) - "), array("tags_min"=>$tags_min, "starts_with"=>$starts_with)); + AND LOWER(tag) LIKE LOWER(:starts_with) + ORDER BY LOWER(tag) + ", ["tags_min"=>$tags_min, "starts_with"=>$starts_with]); - $html = ""; - if($config->get_bool("tag_list_pages")) $html .= $this->build_az(); - - /* - strtolower() vs. mb_strtolower() - ( See http://www.php.net/manual/en/function.mb-strtolower.php for more info ) - - PHP5's strtolower function does not support Unicode (UTF-8) properly, so - you have to use another function, mb_strtolower, to handle UTF-8 strings. - - What's worse is that mb_strtolower is horribly SLOW. - - It would probably be better to have a config option for the Tag List that - would allow you to specify if there are UTF-8 tags. - - */ - mb_internal_encoding('UTF-8'); - - $lastLetter = ""; - # postres utf8 string sort ignores punctuation, so we get "aza, a-zb, azc" - # which breaks down into "az, a-, az" :( - ksort($tag_data, SORT_STRING | SORT_FLAG_CASE); - foreach($tag_data as $tag => $count) { - if($lastLetter != mb_strtolower(substr($tag, 0, count($starts_with)+1))) { - $lastLetter = mb_strtolower(substr($tag, 0, count($starts_with)+1)); - $h_lastLetter = html_escape($lastLetter); - $html .= "

    $h_lastLetter
    "; - } - $link = $this->tag_link($tag); - $h_tag = html_escape($tag); - $html .= "$h_tag ($count)\n"; - } + $html = ""; + if ($config->get_bool(TagListConfig::PAGES)) { + $html .= $this->build_az(); + } - if(SPEED_HAX) {file_put_contents($cache_key, $html);} + /* + strtolower() vs. mb_strtolower() + ( See http://www.php.net/manual/en/function.mb-strtolower.php for more info ) - return $html; - } + PHP5's strtolower function does not support Unicode (UTF-8) properly, so + you have to use another function, mb_strtolower, to handle UTF-8 strings. - /** - * @return string - */ - private function build_tag_popularity() { - global $database; + What's worse is that mb_strtolower is horribly SLOW. - $tags_min = $this->get_tags_min(); - - // Make sure that the value of $tags_min is at least 1. - // Otherwise the database will complain if you try to do: LOG(0) - if ($tags_min < 1){ $tags_min = 1; } - - // check if we have a cached version - $cache_key = data_path("cache/tag_popul-" . md5("tp" . $tags_min) . ".html"); - if(file_exists($cache_key)) {return file_get_contents($cache_key);} + It would probably be better to have a config option for the Tag List that + would allow you to specify if there are UTF-8 tags. - $tag_data = $database->get_all(" + */ + mb_internal_encoding('UTF-8'); + + $lastLetter = ""; + # postres utf8 string sort ignores punctuation, so we get "aza, a-zb, azc" + # which breaks down into "az, a-, az" :( + ksort($tag_data, SORT_STRING | SORT_FLAG_CASE); + foreach ($tag_data as $tag => $count) { + // In PHP, $array["10"] sets the array key as int(10), not string("10")... + $tag = (string)$tag; + if ($lastLetter != mb_strtolower(substr($tag, 0, strlen($starts_with)+1))) { + $lastLetter = mb_strtolower(substr($tag, 0, strlen($starts_with)+1)); + $h_lastLetter = html_escape($lastLetter); + $html .= "

    $h_lastLetter
    "; + } + $link = $this->theme->tag_link($tag); + $h_tag = html_escape($tag); + $html .= "$h_tag ($count)\n"; + } + + if (SPEED_HAX) { + file_put_contents($cache_key, $html); + } + + return $html; + } + + private function build_tag_popularity(): string + { + global $database; + + $tags_min = $this->get_tags_min(); + + // Make sure that the value of $tags_min is at least 1. + // Otherwise the database will complain if you try to do: LOG(0) + if ($tags_min < 1) { + $tags_min = 1; + } + + // check if we have a cached version + $cache_key = warehouse_path("cache/tag_popul", md5("tp" . $tags_min)); + if (file_exists($cache_key)) { + return file_get_contents($cache_key); + } + + $tag_data = $database->get_all(" SELECT tag, count, FLOOR(LOG(count)) AS scaled FROM tags WHERE count >= :tags_min ORDER BY count DESC, tag ASC - ", array("tags_min"=>$tags_min)); + ", ["tags_min"=>$tags_min]); - $html = "Results grouped by log10(n)"; - $lastLog = ""; - foreach($tag_data as $row) { - $h_tag = html_escape($row['tag']); - $count = $row['count']; - $scaled = $row['scaled']; - if($lastLog != $scaled) { - $lastLog = $scaled; - $html .= "

    $lastLog
    "; - } - $link = $this->tag_link($row['tag']); - $html .= "$h_tag ($count)\n"; - } + $html = "Results grouped by log10(n)"; + $lastLog = ""; + foreach ($tag_data as $row) { + $h_tag = html_escape($row['tag']); + $count = $row['count']; + $scaled = $row['scaled']; + if ($lastLog != $scaled) { + $lastLog = $scaled; + $html .= "

    $lastLog
    "; + } + $link = $this->theme->tag_link($row['tag']); + $html .= "$h_tag ($count)\n"; + } - if(SPEED_HAX) {file_put_contents($cache_key, $html);} + if (SPEED_HAX) { + file_put_contents($cache_key, $html); + } - return $html; - } + return $html; + } - /** - * @return string - */ - private function build_tag_list() { - global $database; + private function add_related_block(Page $page, Image $image): void + { + global $database, $config; - //$tags_min = $this->get_tags_min(); - $tag_data = $database->get_all("SELECT tag,count FROM tags ORDER BY count DESC, tag ASC LIMIT 9"); + $omitted_tags = self::get_omitted_tags(); + $starting_tags = $database->get_col("SELECT tag_id FROM image_tags WHERE image_id = :image_id", ["image_id" => $image->id]); - $html = ""; - $n = 0; - foreach($tag_data as $row) { - if($n%3==0) $html .= ""; - $h_tag = html_escape($row['tag']); - $link = $this->tag_link($row['tag']); - $image = Image::by_random(array($row['tag'])); - if(is_null($image)) continue; // one of the popular tags has no images - $thumb = $image->get_thumb_link(); - $tsize = get_thumbnail_size($image->width, $image->height); - $html .= "\n"; - if($n%3==2) $html .= ""; - $n++; - } - $html .= "

    $h_tag
    "; + $starting_tags = array_diff($starting_tags, $omitted_tags); - return $html; - } -// }}} -// blocks {{{ - /** - * @param Page $page - * @param Image $image - */ - private function add_related_block(Page $page, Image $image) { - global $database, $config; + if (count($starting_tags) === 0) { + // No valid starting tags, so can't look anything up + return; + } - $query = " - SELECT t3.tag AS tag, t3.count AS calc_count, it3.tag_id - FROM - image_tags AS it1, - image_tags AS it2, - image_tags AS it3, - tags AS t1, - tags AS t3 - WHERE - it1.image_id=:image_id - AND it1.tag_id=it2.tag_id - AND it2.image_id=it3.image_id - AND t1.tag != 'tagme' - AND t3.tag != 'tagme' - AND t1.id = it1.tag_id - AND t3.id = it3.tag_id - GROUP BY it3.tag_id, t3.tag, t3.count - ORDER BY calc_count DESC + $query = "SELECT tags.* FROM tags INNER JOIN ( + SELECT it2.tag_id + FROM image_tags AS it1 + INNER JOIN image_tags AS it2 ON it1.image_id=it2.image_id + AND it2.tag_id NOT IN (".implode(",", array_merge($omitted_tags, $starting_tags)).") + WHERE + it1.tag_id IN (".implode(",", $starting_tags).") + GROUP BY it2.tag_id + ) A ON A.tag_id = tags.id + ORDER BY count DESC LIMIT :tag_list_length "; - $args = array("image_id"=>$image->id, "tag_list_length"=>$config->get_int('tag_list_length')); - $tags = $database->get_all($query, $args); - if(count($tags) > 0) { - $this->theme->display_related_block($page, $tags); - } - } + $args = ["tag_list_length" => $config->get_int(TagListConfig::LENGTH)]; - /** - * @param Page $page - * @param Image $image - */ - private function add_split_tags_block(Page $page, Image $image) { - global $database; + $tags = $database->get_all($query, $args); + if (count($tags) > 0) { + $this->theme->display_related_block($page, $tags); + } + } - $query = " - SELECT tags.tag, tags.count as calc_count + private function add_split_tags_block(Page $page, Image $image) + { + global $database; + + $query = " + SELECT tags.tag, tags.count FROM tags, image_tags WHERE tags.id = image_tags.tag_id AND image_tags.image_id = :image_id - ORDER BY calc_count DESC + ORDER BY tags.count DESC "; - $args = array("image_id"=>$image->id); + $args = ["image_id"=>$image->id]; - $tags = $database->get_all($query, $args); - if(count($tags) > 0) { - $this->theme->display_split_related_block($page, $tags); - } - } + $tags = $database->get_all($query, $args); + if (count($tags) > 0) { + $this->theme->display_split_related_block($page, $tags); + } + } - /** - * @param Page $page - * @param Image $image - */ - private function add_tags_block(Page $page, Image $image) { - global $database; + private function add_tags_block(Page $page, Image $image) + { + global $database; - $query = " - SELECT tags.tag, tags.count as calc_count + $query = " + SELECT tags.tag, tags.count FROM tags, image_tags WHERE tags.id = image_tags.tag_id AND image_tags.image_id = :image_id - ORDER BY calc_count DESC + ORDER BY tags.count DESC "; - $args = array("image_id"=>$image->id); + $args = ["image_id"=>$image->id]; - $tags = $database->get_all($query, $args); - if(count($tags) > 0) { - $this->theme->display_related_block($page, $tags); - } - } + $tags = $database->get_all($query, $args); + if (count($tags) > 0) { + $this->theme->display_related_block($page, $tags); + } + } - /** - * @param Page $page - */ - private function add_popular_block(Page $page) { - global $database, $config; + private function add_popular_block(Page $page) + { + global $cache, $database, $config; - $tags = $database->cache->get("popular_tags"); - if(empty($tags)) { - $query = " - SELECT tag, count as calc_count - FROM tags - WHERE count > 0 - ORDER BY count DESC - LIMIT :popular_tag_list_length - "; - $args = array("popular_tag_list_length"=>$config->get_int('popular_tag_list_length')); + $tags = $cache->get("popular_tags"); + if (empty($tags)) { + $omitted_tags = self::get_omitted_tags(); - $tags = $database->get_all($query, $args); - $database->cache->set("popular_tags", $tags, 600); - } - if(count($tags) > 0) { - $this->theme->display_popular_block($page, $tags); - } - } + if (empty($omitted_tags)) { + $query = " + SELECT tag, count + FROM tags + WHERE count > 0 + ORDER BY count DESC + LIMIT :popular_tag_list_length + "; + } else { + $query = " + SELECT tag, count + FROM tags + WHERE count > 0 + AND id NOT IN (".(implode(",", $omitted_tags)).") + ORDER BY count DESC + LIMIT :popular_tag_list_length + "; + } - /** - * @param Page $page - * @param string[] $search - */ - private function add_refine_block(Page $page, $search) { - global $database, $config; - assert('is_array($search)'); + $args = ["popular_tag_list_length"=>$config->get_int(TagListConfig::POPULAR_TAG_LIST_LENGTH)]; - $wild_tags = $search; - $str_search = Tag::implode($search); - $related_tags = $database->cache->get("related_tags:$str_search"); + $tags = $database->get_all($query, $args); - if(empty($related_tags)) { - // $search_tags = array(); + $cache->set("popular_tags", $tags, 600); + } + if (count($tags) > 0) { + $this->theme->display_popular_block($page, $tags); + } + } - $tag_id_array = array(); - $tags_ok = true; - foreach($wild_tags as $tag) { - $tag = str_replace("*", "%", $tag); - $tag = str_replace("?", "_", $tag); - $tag_ids = $database->get_col("SELECT id FROM tags WHERE tag LIKE :tag", array("tag"=>$tag)); - // $search_tags = array_merge($search_tags, - // $database->get_col("SELECT tag FROM tags WHERE tag LIKE :tag", array("tag"=>$tag))); - $tag_id_array = array_merge($tag_id_array, $tag_ids); - $tags_ok = count($tag_ids) > 0; - if(!$tags_ok) break; - } - $tag_id_list = join(', ', $tag_id_array); + /** + * #param string[] $search + */ + private function add_refine_block(Page $page, array $search) + { + global $config; - if($tags_ok) { - $query = " - SELECT t2.tag AS tag, COUNT(it2.image_id) AS calc_count - FROM - image_tags AS it1, - image_tags AS it2, - tags AS t1, - tags AS t2 + if (count($search) > 5) { + return; + } + + $wild_tags = $search; + + $related_tags = self::get_related_tags($search, $config->get_int(TagListConfig::LENGTH)); + + if (!empty($related_tags)) { + $this->theme->display_refine_block($page, $related_tags, $wild_tags); + } + } + + public static function get_related_tags(array $search, int $limit): array + { + global $cache, $database; + + + $wild_tags = $search; + $str_search = Tag::implode($search); + $related_tags = $cache->get("related_tags:$str_search"); + + if (empty($related_tags)) { + // $search_tags = array(); + + $starting_tags = []; + $tags_ok = true; + foreach ($wild_tags as $tag) { + if ($tag[0] == "-" || strpos($tag, "tagme")===0) { + continue; + } + $tag = Tag::sqlify($tag); + $tag_ids = $database->get_col("SELECT id FROM tags WHERE tag LIKE :tag AND count < 25000", ["tag" => $tag]); + // $search_tags = array_merge($search_tags, + // $database->get_col("SELECT tag FROM tags WHERE tag LIKE :tag", array("tag"=>$tag))); + $starting_tags = array_merge($starting_tags, $tag_ids); + $tags_ok = count($tag_ids) > 0; + if (!$tags_ok) { + break; + } + } + + if (count($starting_tags) > 5 || count($starting_tags) === 0) { + return []; + } + + $omitted_tags = self::get_omitted_tags(); + + $starting_tags = array_diff($starting_tags, $omitted_tags); + + if (count($starting_tags) === 0) { + // No valid starting tags, so can't look anything up + return []; + } + + if ($tags_ok) { + $query = "SELECT t.tag, A.calc_count AS count FROM tags t INNER JOIN ( + SELECT it2.tag_id, COUNT(it2.image_id) AS calc_count + FROM image_tags AS it1 -- Got other images with the same tags + INNER JOIN image_tags AS it2 ON it1.image_id=it2.image_id + -- And filter out unwanted tags + AND it2.tag_id NOT IN (".implode(",", array_merge($omitted_tags, $starting_tags)).") WHERE - t1.id IN($tag_id_list) - AND it1.image_id=it2.image_id - AND it1.tag_id = t1.id - AND it2.tag_id = t2.id - GROUP BY t2.tag - ORDER BY calc_count + it1.tag_id IN (".implode(",", $starting_tags).") + GROUP BY it2.tag_id) A ON A.tag_id = t.id + ORDER BY A.calc_count DESC LIMIT :limit "; - $args = array("limit"=>$config->get_int('tag_list_length')); + $args = ["limit" => $limit]; - $related_tags = $database->get_all($query, $args); - $database->cache->set("related_tags:$str_search", $related_tags, 60*60); - } - } - - if(!empty($related_tags)) { - $this->theme->display_refine_block($page, $related_tags, $wild_tags); - } - } -// }}} + $related_tags = $database->get_all($query, $args); + $cache->set("related_tags:$str_search", $related_tags, 60 * 60); + } + } + if ($related_tags === false) { + return []; + } else { + return $related_tags; + } + } } - diff --git a/ext/tag_list/test.php b/ext/tag_list/test.php index 7fe82e72..3278c491 100644 --- a/ext/tag_list/test.php +++ b/ext/tag_list/test.php @@ -1,36 +1,39 @@ -get_page('tags/map'); - $this->assert_title('Tag List'); + public function testTagList() + { + $this->get_page('tags/map'); + $this->assert_title('Tag List'); - $this->get_page('tags/alphabetic'); - $this->assert_title('Tag List'); + $this->get_page('tags/alphabetic'); + $this->assert_title('Tag List'); - $this->get_page('tags/popularity'); - $this->assert_title('Tag List'); + $this->get_page('tags/popularity'); + $this->assert_title('Tag List'); - $this->get_page('tags/categories'); - $this->assert_title('Tag List'); + $this->get_page('tags/categories'); + $this->assert_title('Tag List'); - # FIXME: test that these show the right stuff - } + # FIXME: test that these show the right stuff + } - public function testMinCount() { - foreach($this->pages as $page) { - $this->get_page("tags/$page?mincount=999999"); - $this->assert_title("Tag List"); + public function testMinCount() + { + foreach ($this->pages as $page) { + $this->get_page("tags/$page?mincount=999999"); + $this->assert_title("Tag List"); - $this->get_page("tags/$page?mincount=1"); - $this->assert_title("Tag List"); + $this->get_page("tags/$page?mincount=1"); + $this->assert_title("Tag List"); - $this->get_page("tags/$page?mincount=0"); - $this->assert_title("Tag List"); + $this->get_page("tags/$page?mincount=0"); + $this->assert_title("Tag List"); - $this->get_page("tags/$page?mincount=-1"); - $this->assert_title("Tag List"); - } - } + $this->get_page("tags/$page?mincount=-1"); + $this->assert_title("Tag List"); + } + } } diff --git a/ext/tag_list/theme.php b/ext/tag_list/theme.php index 5ca82bde..6f771871 100644 --- a/ext/tag_list/theme.php +++ b/ext/tag_list/theme.php @@ -1,320 +1,310 @@ -heading = $text; - } + public function set_heading(string $text) + { + $this->heading = $text; + } - /** - * @param string|string[] $list - */ - public function set_tag_list($list) { - $this->list = $list; - } + public function set_tag_list(string $list) + { + $this->list = $list; + } - public function set_navigation($nav) { - $this->navigation = $nav; - } + public function set_navigation(string $nav) + { + $this->navigation = $nav; + } - public function display_page(Page $page) { - $page->set_title("Tag List"); - $page->set_heading($this->heading); - $page->add_block(new Block("Tags", $this->list)); - $page->add_block(new Block("Navigation", $this->navigation, "left", 0)); - } + public function display_page(Page $page) + { + $page->set_title("Tag List"); + $page->set_heading($this->heading); + $page->add_block(new Block("Tags", $this->list)); + $page->add_block(new Block("Navigation", $this->navigation, "left", 0)); + } - // ======================================================================= + // ======================================================================= - protected function get_tag_list_preamble() { - global $config; + protected function get_tag_list_preamble() + { + global $config; - $tag_info_link_is_visible = !is_null($config->get_string('info_link')); - $tag_count_is_visible = $config->get_bool("tag_list_numbers"); + $tag_info_link_is_visible = !is_null($config->get_string(TagListConfig::INFO_LINK)); + $tag_count_is_visible = $config->get_bool("tag_list_numbers"); - return ' + return ' ' . - ($tag_info_link_is_visible ? '' : '') . - ('') . - ($tag_count_is_visible ? '' : '') . ' + ($tag_info_link_is_visible ? '' : '') . + ('') . + ($tag_count_is_visible ? '' : '') . ' ' . - ($tag_info_link_is_visible ? '' : '') . - ('') . - ($tag_count_is_visible ? '' : '') . ' + ($tag_info_link_is_visible ? '' : '') . + ('') . + ($tag_count_is_visible ? '' : '') . ' '; - } + } - /* - * $tag_infos = array( - * array('tag' => $tag, 'count' => $number_of_uses), - * ... - * ) - */ - public function display_split_related_block(Page $page, $tag_infos) { - global $config; + /* + * $tag_infos = array( + * array('tag' => $tag, 'count' => $number_of_uses), + * ... + * ) + */ + public function display_split_related_block(Page $page, $tag_infos) + { + global $config; - if($config->get_string('tag_list_related_sort') == 'alphabetical') asort($tag_infos); + if ($config->get_string(TagListConfig::RELATED_SORT) == TagListConfig::SORT_ALPHABETICAL) { + asort($tag_infos); + } - if(class_exists('TagCategories')) { - $this->tagcategories = new TagCategories; - $tag_category_dict = $this->tagcategories->getKeyedDict(); - } - else { - $tag_category_dict = array(); - } - $tag_categories_html = array(); - $tag_categories_count = array(); + if (class_exists('TagCategories')) { + $this->tagcategories = new TagCategories; + $tag_category_dict = $this->tagcategories->getKeyedDict(); + } else { + $tag_category_dict = []; + } + $tag_categories_html = []; + $tag_categories_count = []; - foreach($tag_infos as $row) { - $split = self::return_tag($row, $tag_category_dict); - $category = $split[0]; - $tag_html = $split[1]; - if(!isset($tag_categories_html[$category])) { - $tag_categories_html[$category] = $this->get_tag_list_preamble(); - } - $tag_categories_html[$category] .= "$tag_html"; + foreach ($tag_infos as $row) { + $split = self::return_tag($row, $tag_category_dict); + $category = $split[0]; + $tag_html = $split[1]; + if (!isset($tag_categories_html[$category])) { + $tag_categories_html[$category] = $this->get_tag_list_preamble(); + } + $tag_categories_html[$category] .= "$tag_html"; - if(!isset($tag_categories_count[$category])) { - $tag_categories_count[$category] = 0; - } - $tag_categories_count[$category] += 1; - } + if (!isset($tag_categories_count[$category])) { + $tag_categories_count[$category] = 0; + } + $tag_categories_count[$category] += 1; + } - foreach(array_keys($tag_categories_html) as $category) { - $tag_categories_html[$category] .= '
    Tag#Tag#
    '; - } + foreach (array_keys($tag_categories_html) as $category) { + $tag_categories_html[$category] .= ''; + } - asort($tag_categories_html); - if(isset($tag_categories_html[' '])) $main_html = $tag_categories_html[' ']; else $main_html = null; - unset($tag_categories_html[' ']); + asort($tag_categories_html); + if (isset($tag_categories_html[' '])) { + $main_html = $tag_categories_html[' ']; + } else { + $main_html = null; + } + unset($tag_categories_html[' ']); - foreach(array_keys($tag_categories_html) as $category) { - if($tag_categories_count[$category] < 2) { - $category_display_name = html_escape($tag_category_dict[$category]['display_singular']); - } - else{ - $category_display_name = html_escape($tag_category_dict[$category]['display_multiple']); - } - $page->add_block(new Block($category_display_name, $tag_categories_html[$category], "left", 9)); - } + foreach (array_keys($tag_categories_html) as $category) { + if ($tag_categories_count[$category] < 2) { + $category_display_name = html_escape($tag_category_dict[$category]['display_singular']); + } else { + $category_display_name = html_escape($tag_category_dict[$category]['display_multiple']); + } + $page->add_block(new Block($category_display_name, $tag_categories_html[$category], "left", 9)); + } - if($config->get_string('tag_list_image_type')=="tags") { - $page->add_block(new Block("Tags", $main_html, "left", 10)); - } - else { - $page->add_block(new Block("Related Tags", $main_html, "left", 10)); - } - } + if ($main_html != null) { + if ($config->get_string(TagListConfig::IMAGE_TYPE)==TagListConfig::TYPE_TAGS) { + $page->add_block(new Block("Tags", $main_html, "left", 10)); + } else { + $page->add_block(new Block("Related Tags", $main_html, "left", 10)); + } + } + } - /* - * $tag_infos = array( - * array('tag' => $tag, 'count' => $number_of_uses), - * ... - * ) - */ - private function get_tag_list_html($tag_infos, $sort) { - if($sort == 'alphabetical') asort($tag_infos); + /* + * $tag_infos = array( + * array('tag' => $tag, 'count' => $number_of_uses), + * ... + * ) + */ + private function get_tag_list_html($tag_infos, $sort) + { + if ($sort == TagListConfig::SORT_ALPHABETICAL) { + asort($tag_infos); + } - if(class_exists('TagCategories')) { - $this->tagcategories = new TagCategories; - $tag_category_dict = $this->tagcategories->getKeyedDict(); - } - else { - $tag_category_dict = array(); - } - $main_html = $this->get_tag_list_preamble(); + if (class_exists('TagCategories')) { + $this->tagcategories = new TagCategories; + $tag_category_dict = $this->tagcategories->getKeyedDict(); + } else { + $tag_category_dict = []; + } + $main_html = $this->get_tag_list_preamble(); - foreach($tag_infos as $row) { - $split = $this->return_tag($row, $tag_category_dict); - //$category = $split[0]; - $tag_html = $split[1]; - $main_html .= "$tag_html"; - } + foreach ($tag_infos as $row) { + $split = $this->return_tag($row, $tag_category_dict); + //$category = $split[0]; + $tag_html = $split[1]; + $main_html .= "$tag_html"; + } - $main_html .= ''; + $main_html .= ''; - return $main_html; - } + return $main_html; + } - /* - * $tag_infos = array( - * array('tag' => $tag, 'count' => $number_of_uses), - * ... - * ) - */ - public function display_related_block(Page $page, $tag_infos) { - global $config; + /* + * $tag_infos = array( + * array('tag' => $tag, 'count' => $number_of_uses), + * ... + * ) + */ + public function display_related_block(Page $page, $tag_infos) + { + global $config; - $main_html = $this->get_tag_list_html( - $tag_infos, $config->get_string('tag_list_related_sort')); + $main_html = $this->get_tag_list_html( + $tag_infos, + $config->get_string(TagListConfig::RELATED_SORT) + ); - if($config->get_string('tag_list_image_type')=="tags") { - $page->add_block(new Block("Tags", $main_html, "left", 10)); - } - else { - $page->add_block(new Block("Related Tags", $main_html, "left", 10)); - } - } + if ($config->get_string(TagListConfig::IMAGE_TYPE)==TagListConfig::TYPE_TAGS) { + $page->add_block(new Block("Tags", $main_html, "left", 10)); + } else { + $page->add_block(new Block("Related Tags", $main_html, "left", 10)); + } + } - /* - * $tag_infos = array( - * array('tag' => $tag, 'count' => $number_of_uses), - * ... - * ) - */ - public function display_popular_block(Page $page, $tag_infos) { - global $config; + /* + * $tag_infos = array( + * array('tag' => $tag, 'count' => $number_of_uses), + * ... + * ) + */ + public function display_popular_block(Page $page, $tag_infos) + { + global $config; - $main_html = $this->get_tag_list_html( - $tag_infos, $config->get_string('tag_list_popular_sort')); - $main_html .= " 
    Full List\n"; + $main_html = $this->get_tag_list_html( + $tag_infos, + $config->get_string(TagListConfig::POPULAR_SORT) + ); + $main_html .= " 
    Full List\n"; - $page->add_block(new Block("Popular Tags", $main_html, "left", 60)); - } + $page->add_block(new Block("Popular Tags", $main_html, "left", 60)); + } - /* - * $tag_infos = array( - * array('tag' => $tag), - * ... - * ) - * $search = the current array of tags being searched for - */ - public function display_refine_block(Page $page, $tag_infos, $search) { - global $config; + /* + * $tag_infos = array( + * array('tag' => $tag), + * ... + * ) + * $search = the current array of tags being searched for + */ + public function display_refine_block(Page $page, $tag_infos, $search) + { + global $config; - $main_html = $this->get_tag_list_html( - $tag_infos, $config->get_string('tag_list_popular_sort')); - $main_html .= " 
    Full List\n"; + $main_html = $this->get_tag_list_html( + $tag_infos, + $config->get_string(TagListConfig::POPULAR_SORT) + ); + $main_html .= " 
    Full List\n"; - $page->add_block(new Block("refine Search", $main_html, "left", 60)); - } + $page->add_block(new Block("refine Search", $main_html, "left", 60)); + } - public function return_tag($row, $tag_category_dict) { - global $config; + public function return_tag($row, $tag_category_dict) + { + global $config; - $display_html = ''; - $tag = $row['tag']; - $h_tag = html_escape($tag); - - $tag_category_css = ''; - $tag_category_style = ''; - $h_tag_split = explode(':', html_escape($tag), 2); - $category = ' '; + $display_html = ''; + $tag = $row['tag']; + $h_tag = html_escape($tag); - // we found a tag, see if it's valid! - if((count($h_tag_split) > 1) and array_key_exists($h_tag_split[0], $tag_category_dict)) { - $category = $h_tag_split[0]; - $h_tag = $h_tag_split[1]; - $tag_category_css .= ' tag_category_'.$category; - $tag_category_style .= 'style="color:'.html_escape($tag_category_dict[$category]['color']).';" '; - } + $tag_category_css = ''; + $tag_category_style = ''; + $h_tag_split = explode(':', html_escape($tag), 2); + $category = ' '; - $h_tag_no_underscores = str_replace("_", " ", $h_tag); - $count = $row['calc_count']; - // if($n++) $display_html .= "\n
    "; - if(!is_null($config->get_string('info_link'))) { - $link = html_escape(str_replace('$tag', url_escape($tag), $config->get_string('info_link'))); - $display_html .= ' ?'; - } - $link = $this->tag_link($row['tag']); - $display_html .= ' '.$h_tag_no_underscores.''; + // we found a tag, see if it's valid! + if ((count($h_tag_split) > 1) and array_key_exists($h_tag_split[0], $tag_category_dict)) { + $category = $h_tag_split[0]; + $h_tag = $h_tag_split[1]; + $tag_category_css .= ' tag_category_'.$category; + $tag_category_style .= 'style="color:'.html_escape($tag_category_dict[$category]['color']).';" '; + } - if($config->get_bool("tag_list_numbers")) { - $display_html .= " $count"; - } + $h_tag_no_underscores = str_replace("_", " ", $h_tag); + $count = $row['count']; + // if($n++) $display_html .= "\n
    "; + if (!is_null($config->get_string(TagListConfig::INFO_LINK))) { + $link = html_escape(str_replace('$tag', url_escape($tag), $config->get_string(TagListConfig::INFO_LINK))); + $display_html .= ' ?'; + } + $link = $this->tag_link($row['tag']); + $display_html .= ' '.$h_tag_no_underscores.''; - return array($category, $display_html); - } + if ($config->get_bool("tag_list_numbers")) { + $display_html .= " $count"; + } - /** - * @param string $tag - * @param string[] $tags - * @return string - */ - protected function ars(/*string*/ $tag, /*array(string)*/ $tags) { - assert(is_array($tags)); + return [$category, $display_html]; + } - // FIXME: a better fix would be to make sure the inputs are correct - $tag = strtolower($tag); - $tags = array_map("strtolower", $tags); - $html = ""; - $html .= " ("; - $html .= $this->get_add_link($tags, $tag); - $html .= $this->get_remove_link($tags, $tag); - $html .= $this->get_subtract_link($tags, $tag); - $html .= ")"; - return $html; - } + protected function ars(string $tag, array $tags): string + { + // FIXME: a better fix would be to make sure the inputs are correct + $tag = strtolower($tag); + $tags = array_map("strtolower", $tags); + $html = ""; + $html .= " ("; + $html .= $this->get_add_link($tags, $tag); + $html .= $this->get_remove_link($tags, $tag); + $html .= $this->get_subtract_link($tags, $tag); + $html .= ")"; + return $html; + } - /** - * @param array $tags - * @param string $tag - * @return string - */ - protected function get_remove_link($tags, $tag) { - if(!in_array($tag, $tags) && !in_array("-$tag", $tags)) { - return ""; - } - else { - $tags = array_remove($tags, $tag); - $tags = array_remove($tags, "-$tag"); - return "R"; - } - } + protected function get_remove_link(array $tags, string $tag): string + { + if (!in_array($tag, $tags) && !in_array("-$tag", $tags)) { + return ""; + } else { + $tags = array_diff($tags, [$tag, "-$tag"]); + return "R"; + } + } - /** - * @param array $tags - * @param string $tag - * @return string - */ - protected function get_add_link($tags, $tag) { - if(in_array($tag, $tags)) { - return ""; - } - else { - $tags = array_remove($tags, "-$tag"); - $tags = array_add($tags, $tag); - return "A"; - } - } + protected function get_add_link(array $tags, string $tag): string + { + if (in_array($tag, $tags)) { + return ""; + } else { + $tags = array_diff($tags, ["-$tag"]) + [$tag]; + return "A"; + } + } - /** - * @param array $tags - * @param string $tag - * @return string - */ - protected function get_subtract_link($tags, $tag) { - if(in_array("-$tag", $tags)) { - return ""; - } - else { - $tags = array_remove($tags, $tag); - $tags = array_add($tags, "-$tag"); - return "S"; - } - } + protected function get_subtract_link(array $tags, string $tag): string + { + if (in_array("-$tag", $tags)) { + return ""; + } else { + $tags = array_diff($tags, [$tag]) + ["-$tag"]; + return "S"; + } + } - /** - * @param string $tag - * @return string - */ - protected function tag_link($tag) { - $u_tag = url_escape($tag); - return make_link("post/list/$u_tag/1"); - } + public function tag_link(string $tag): string + { + $u_tag = url_escape(Tag::caret($tag)); + return make_link("post/list/$u_tag/1"); + } } diff --git a/ext/tagger/info.php b/ext/tagger/info.php new file mode 100644 index 00000000..3eedce9d --- /dev/null +++ b/ext/tagger/info.php @@ -0,0 +1,12 @@ +"artanis.00@gmail.com"]; + public $dependencies = [TaggerXMLInfo::KEY]; + public $description = "Advanced Tagging v2"; +} diff --git a/ext/tagger/main.php b/ext/tagger/main.php index 42d9e01a..645d3a19 100644 --- a/ext/tagger/main.php +++ b/ext/tagger/main.php @@ -1,150 +1,28 @@ - - * Do not remove this notice. - */ +can("edit_image_tag") && ($event->image->is_locked() || $user->can("edit_image_lock"))) { - $this->theme->build_tagger($page,$event); - } - } + public function onDisplayingImage(DisplayingImageEvent $event) + { + global $page, $user; - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Tagger"); - $sb->add_int_option("ext_tagger_search_delay", "Delay queries by "); - $sb->add_label(" milliseconds."); - $sb->add_label("
    Limit queries returning more than "); - $sb->add_int_option("ext_tagger_tag_max"); - $sb->add_label(" tags to "); - $sb->add_int_option("ext_tagger_limit"); - $event->panel->add_block($sb); - } + if ($user->can(Permissions::EDIT_IMAGE_TAG) && ($event->image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK))) { + $this->theme->build_tagger($page, $event); + } + } + + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Tagger"); + $sb->add_int_option("ext_tagger_search_delay", "Delay queries by "); + $sb->add_label(" milliseconds."); + $sb->add_label("
    Limit queries returning more than "); + $sb->add_int_option("ext_tagger_tag_max"); + $sb->add_label(" tags to "); + $sb->add_int_option("ext_tagger_limit"); + $event->panel->add_block($sb); + } } - -// Tagger AJAX back-end -class TaggerXML extends Extension { - public function get_priority() {return 10;} - - public function onPageRequest(PageRequestEvent $event) { - if($event->page_matches("tagger/tags")) { - global $page; - - //$match_tags = null; - //$image_tags = null; - $tags=null; - if (isset($_GET['s'])) { // tagger/tags[/...]?s=$string - // return matching tags in XML form - $tags = $this->match_tag_list($_GET['s']); - } else if($event->get_arg(0)) { // tagger/tags/$int - // return arg[1] AS image_id's tag list in XML form - $tags = $this->image_tag_list($event->get_arg(0)); - } - - $xml = "\n". - "". - $tags. - ""; - - $page->set_mode("data"); - $page->set_type("text/xml"); - $page->set_data($xml); - } - } - - /** @param string $s */ - private function match_tag_list ($s) { - global $database, $config; - - $max_rows = $config->get_int("ext_tagger_tag_max",30); - $limit_rows = $config->get_int("ext_tagger_limit",30); - - $values = array(); - - // Match - $p = strlen($s) == 1? " ":"\_"; - $sq = "%".$p.sql_escape($s)."%"; - $match = "concat(?,tag) LIKE ?"; - array_push($values,$p,$sq); - // Exclude -// $exclude = $event->get_arg(1)? "AND NOT IN ".$this->image_tags($event->get_arg(1)) : null; - - // Hidden Tags - $hidden = $config->get_string('ext-tagger_show-hidden','N')=='N' ? - "AND substring(tag,1,1) != '.'" : null; - - $q_where = "WHERE {$match} {$hidden} AND count > 0"; - - // FROM based on return count - $count = $this->count($q_where,$values); - if ($count > $max_rows) { - $q_from = "FROM (SELECT * FROM `tags` {$q_where} ". - "ORDER BY count DESC LIMIT 0, {$limit_rows}) AS `c_tags`"; - $q_where = null; - $count = array("max"=>$count); - } else { - $q_from = "FROM `tags`"; - $count = null; - } - - $tags = $database->Execute(" - SELECT * - {$q_from} - {$q_where} - ORDER BY tag", - $values); - - return $this->list_to_xml($tags,"search",$s,$count); - } - - /** @param int $image_id */ - private function image_tag_list ($image_id) { - global $database; - $tags = $database->Execute(" - SELECT tags.* - FROM image_tags JOIN tags ON image_tags.tag_id = tags.id - WHERE image_id=? ORDER BY tag", array($image_id)); - return $this->list_to_xml($tags,"image",$image_id); - } - - /** - * @param PDOStatement $tags - * @param string $type - * @param string $query - * @param array $misc - */ - private function list_to_xml ($tags,$type,$query,$misc=null) { - $r = $tags->_numOfRows; - - $s_misc = ""; - if(!is_null($misc)) - foreach($misc as $attr => $val) $s_misc .= " ".$attr."=\"".$val."\""; - - $result = ""; - foreach($tags as $tag) { - $result .= $this->tag_to_xml($tag); - } - return $result.""; - } - - private function tag_to_xml ($tag) { - return - "". - html_escape($tag['tag']). - ""; - } - - private function count($query,$values) { - global $database; - return $database->Execute( - "SELECT COUNT(*) FROM `tags` $query",$values)->fields['COUNT(*)']; - } -} - diff --git a/ext/tagger/script.js b/ext/tagger/script.js index 49ad07c3..05206611 100644 --- a/ext/tagger/script.js +++ b/ext/tagger/script.js @@ -6,6 +6,10 @@ * Do not remove this notice. * \* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +function byId(id) { + return document.getElementById(id); +} + var Tagger = { initialize : function (image_id) { // object navigation @@ -53,7 +57,7 @@ var Tagger = { } } else if (text) { // create - var t_alert = document.createElement("div"); + t_alert = document.createElement("div"); t_alert.setAttribute("id",id); t_alert.appendChild(document.createTextNode(text)); this.editor.statusbar.appendChild(t_alert); diff --git a/ext/tagger/style.css b/ext/tagger/style.css index 40c79065..ba9f8407 100644 --- a/ext/tagger/style.css +++ b/ext/tagger/style.css @@ -27,11 +27,10 @@ #tagger_toolbar, #tagger_body { padding:2px 2px 0 2px; border-style:solid; - border-width: 0px 2px 0px 2px; + border-width: 0 2px 0 2px; } #tagger_body { max-height:175px; - overflow:auto; overflow-x:hidden; overflow-y:auto; } @@ -48,7 +47,7 @@ #tagger_body div { padding-top:2px; margin-top:2px; - border-top:1px solid; + border-top:1px solid; } /* Tagger Styling @@ -70,7 +69,7 @@ display: block; } #tagger_parent tag { - font-size:1.25em; + font-size:1.25em; display:block; } diff --git a/ext/tagger/theme.php b/ext/tagger/theme.php index 5b446382..a8fa9407 100644 --- a/ext/tagger/theme.php +++ b/ext/tagger/theme.php @@ -1,68 +1,75 @@ -) * * Do not remove this notice. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -class taggerTheme extends Themelet { - public function build_tagger (Page $page, $event) { - // Initialization code - $base_href = get_base_href(); - // TODO: AJAX test and fallback. +class TaggerTheme extends Themelet +{ + public function build_tagger(Page $page, DisplayingImageEvent $event) + { + // Initialization code + $base_href = get_base_href(); + // TODO: AJAX test and fallback. - $page->add_html_header(""); - $page->add_block(new Block(null, - ""); + $page->add_block(new Block( + null, + "","main",1000)); + ", + "main", + 1000 + )); - // Tagger block - $page->add_block( new Block( - null, - $this->html($event->get_image()), - "main")); - } - private function html(Image $image) { - global $config; - $i_image_id = int_escape($image->id); - $h_source = html_escape($image->source); - $h_query = isset($_GET['search'])? $h_query= "search=".url_escape($_GET['search']) : ""; + // Tagger block + $page->add_block(new Block( + null, + (string)$this->html($event->get_image()), + "main" + )); + } + private function html(Image $image) + { + global $config; + $h_query = isset($_GET['search'])? $h_query= "search=".url_escape($_GET['search']) : ""; - $delay = $config->get_string("ext_tagger_search_delay","250"); + $delay = $config->get_string("ext_tagger_search_delay", "250"); - $url_form = make_link("tag_edit/set"); - - // TODO: option for initial Tagger window placement. - $html = <<< EOD -

    -EOD; - return $html; - } + // TODO: option for initial Tagger window placement. + return DIV( + ["id"=>"tagger_parent", "style"=>"display:none; top:25px; right:25px;"], + DIV(["id"=>"tagger_titlebar"], "Tagger"), + DIV( + ["id"=>"tagger_toolbar"], + INPUT(["type"=>"text", "value"=>"", "id"=>"tagger_filter", "onkeyup"=>"Tagger.tag.search(this.value, $delay);"]), + INPUT(["type"=>"button", "value">"Add", "onclick"=>"Tagger.tag.create(byId('tagger_filter').value);"]), + FORM( + ["action"=>make_link("tag_edit/set"), "method"=>"POST", "onsubmit"=>"Tagger.tag.submit();"], + INPUT(["type"=>"hidden", "name"=>"image_id", "value"=>$image->id, "id"=>"image_id"]), + INPUT(["type"=>"hidden", "name"=>"query", "value"=>$h_query, "id"=>""]), + INPUT(["type"=>"hidden", "name"=>"source", "value"=>$image->source, "id"=>""]), + INPUT(["type"=>"hidden", "name"=>"tags", "value"=>"", "id"=>"tagger_tags"]), + INPUT(["type"=>"", "value"=>"Set"]), + ), + # UL(["id"=>"tagger_p-menu"]), + # BR(["style"=>"clear:both;"]), + ), + DIV( + ["id"=>"tagger_body"], + DIV(["id"=>"tagger_p-search", "name"=>"Searched Tags"]), + DIV(["id"=>"tagger_p-applied", "name"=>"Applied Tags"]), + ), + DIV( + ["id"=>"tagger_statusbar"], + ), + ); + } } - diff --git a/ext/tagger_xml/info.php b/ext/tagger_xml/info.php new file mode 100644 index 00000000..7d2edd1f --- /dev/null +++ b/ext/tagger_xml/info.php @@ -0,0 +1,12 @@ +"artanis.00@gmail.com"]; + public $visibility = self::VISIBLE_HIDDEN; + public $description = "Advanced Tagging v2 AJAX backend"; +} diff --git a/ext/tagger_xml/main.php b/ext/tagger_xml/main.php new file mode 100644 index 00000000..8a8165dd --- /dev/null +++ b/ext/tagger_xml/main.php @@ -0,0 +1,132 @@ +page_matches("tagger/tags")) { + global $page; + + //$match_tags = null; + //$image_tags = null; + $tags=null; + if (isset($_GET['s'])) { // tagger/tags[/...]?s=$string + // return matching tags in XML form + $tags = $this->match_tag_list($_GET['s']); + } elseif ($event->get_arg(0)) { // tagger/tags/$int + // return arg[1] AS image_id's tag list in XML form + $tags = $this->image_tag_list(int_escape($event->get_arg(0))); + } + + $xml = "\n". + "". + $tags. + ""; + + $page->set_mode(PageMode::DATA); + $page->set_type("text/xml"); + $page->set_data($xml); + } + } + + private function match_tag_list(string $s) + { + global $database, $config; + + $max_rows = $config->get_int("ext_tagger_tag_max", 30); + $limit_rows = $config->get_int("ext_tagger_limit", 30); + + $p = strlen($s) == 1 ? " " : "\_"; + $values = [ + 'p' => $p, + 'sq' => "%".$p.$s."%" + ]; + + // Match + $match = "concat(:p, tag) LIKE :sq"; + // Exclude + // $exclude = $event->get_arg(1)? "AND NOT IN ".$this->image_tags($event->get_arg(1)) : null; + + // Hidden Tags + $hidden = $config->get_string('ext-tagger_show-hidden', 'N')=='N' ? + "AND substring(tag,1,1) != '.'" : null; + + $q_where = "WHERE {$match} {$hidden} AND count > 0"; + + // FROM based on return count + $count = $this->count($q_where, $values); + if ($count > $max_rows) { + $q_from = "FROM (SELECT * FROM `tags` {$q_where} ". + "ORDER BY count DESC LIMIT {$limit_rows} OFFSET 0) AS `c_tags`"; + $q_where = null; + $count = ["max"=>$count]; + } else { + $q_from = "FROM `tags`"; + $count = null; + } + + $tags = $database->Execute( + " + SELECT * + {$q_from} + {$q_where} + ORDER BY tag", + $values + ); + + return $this->list_to_xml($tags, "search", $s, $count); + } + + private function image_tag_list(int $image_id) + { + global $database; + $tags = $database->Execute(" + SELECT tags.* + FROM image_tags JOIN tags ON image_tags.tag_id = tags.id + WHERE image_id=:image_id ORDER BY tag", ['image_id'=>$image_id]); + return $this->list_to_xml($tags, "image", (string)$image_id); + } + + private function list_to_xml(PDOStatement $tags, string $type, string $query, ?array$misc=null): string + { + $r = $tags->_numOfRows; + + $s_misc = ""; + if (!is_null($misc)) { + foreach ($misc as $attr => $val) { + $s_misc .= " ".$attr."=\"".$val."\""; + } + } + + $result = ""; + foreach ($tags as $tag) { + $result .= $this->tag_to_xml($tag); + } + return $result.""; + } + + private function tag_to_xml(PDORow $tag): string + { + return + "". + html_escape($tag['tag']). + ""; + } + + private function count(string $query, $values) + { + global $database; + return $database->Execute( + "SELECT COUNT(*) FROM `tags` $query", + $values + )->fields['COUNT(*)']; + } +} diff --git a/ext/tips/info.php b/ext/tips/info.php new file mode 100644 index 00000000..c5b85708 --- /dev/null +++ b/ext/tips/info.php @@ -0,0 +1,14 @@ +"mail@seinkraft.info"]; + public $license = "GPLv2"; + public $description = "Show a random line of text in the subheader space"; + public $documentation = "Formatting is done with HTML"; + public $db_support = [DatabaseDriver::MYSQL, DatabaseDriver::SQLITE]; // rand() ? +} diff --git a/ext/tips/main.php b/ext/tips/main.php index 079e53fb..fe78a05a 100644 --- a/ext/tips/main.php +++ b/ext/tips/main.php @@ -1,162 +1,189 @@ - - * License: GPLv2 - * Description: Show a random line of text in the subheader space - * Documentation: - * Formatting is done with HTML - */ +enable = $enable; + $this->image = $image; + $this->text = $text; + } +} - public function onInitExt(InitExtEvent $event) { - global $config, $database; +class DeleteTipEvent extends Event +{ + public $tip_id; + public function __construct(int $tip_id) + { + parent::__construct(); + $this->tip_id = $tip_id; + } +} - if ($config->get_int("ext_tips_version") < 1){ - $database->create_table("tips", " +class Tips extends Extension +{ + /** @var TipsTheme */ + protected $theme; + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $database; + + if ($this->get_version("ext_tips_version") < 1) { + $database->create_table("tips", " id SCORE_AIPK, enable SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N, image TEXT NOT NULL, text TEXT NOT NULL, "); - $database->execute(" + $database->execute( + " INSERT INTO tips (enable, image, text) - VALUES (?, ?, ?)", - array("Y", "coins.png", "Do you like this extension? Please support us for developing new ones. Donate through paypal.")); + VALUES (:enable, :image, :text)", + ["enable"=>"Y", "image"=>"coins.png", "text"=>"Do you like this extension? Please support us for developing new ones. Donate through paypal."] + ); - $config->set_int("ext_tips_version", 1); - log_info("tips", "extension installed"); - } - } + $this->set_version("ext_tips_version", 1); + } + } - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; - $this->getTip(); + $this->getTip(); - if($event->page_matches("tips") && $user->is_admin()) { - switch($event->get_arg(0)) { - case "list": - $this->manageTips(); - $this->getAll(); - break; - case "save": - if($user->check_auth_token()) { - $this->saveTip(); - $page->set_mode("redirect"); - $page->set_redirect(make_link("tips/list")); - } - break; - case "status": - // FIXME: HTTP GET CSRF - $tipID = int_escape($event->get_arg(1)); - $this->setStatus($tipID); - $page->set_mode("redirect"); - $page->set_redirect(make_link("tips/list")); - break; - case "delete": - // FIXME: HTTP GET CSRF - $tipID = int_escape($event->get_arg(1)); - $this->deleteTip($tipID); - $page->set_mode("redirect"); - $page->set_redirect(make_link("tips/list")); - break; - } - } - } + if ($event->page_matches("tips") && $user->can(Permissions::TIPS_ADMIN)) { + switch ($event->get_arg(0)) { + case "list": + $this->manageTips(); + $this->getAll(); + break; + case "save": + if ($user->check_auth_token()) { + send_event(new CreateTipEvent(isset($_POST["enable"]), $_POST["image"], $_POST["text"])); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("tips/list")); + } + break; + case "status": + // FIXME: HTTP GET CSRF + $tipID = int_escape($event->get_arg(1)); + $this->setStatus($tipID); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("tips/list")); + break; + case "delete": + // FIXME: HTTP GET CSRF + $tipID = int_escape($event->get_arg(1)); + send_event(new DeleteTipEvent($tipID)); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("tips/list")); + break; + } + } + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->is_admin()) { - $event->add_link("Tips Editor", make_link("tips/list")); - } - } + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::TIPS_ADMIN)) { + $event->add_nav_link("tips", new Link('tips/list'), "Tips Editor"); + } + } + } - private function manageTips() { - $data_href = get_base_href(); - $url = $data_href."/ext/tips/images/"; + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::TIPS_ADMIN)) { + $event->add_link("Tips Editor", make_link("tips/list")); + } + } - $dirPath = dir('./ext/tips/images'); - $images = array(); - while(($file = $dirPath->read()) !== false) { - if($file[0] != ".") { - $images[] = trim($file); - } - } - $dirPath->close(); - sort($images); + private function manageTips() + { + $data_href = get_base_href(); + $url = $data_href."/ext/tips/images/"; - $this->theme->manageTips($url, $images); - } + $dirPath = dir('./ext/tips/images'); + $images = []; + while (($file = $dirPath->read()) !== false) { + if ($file[0] != ".") { + $images[] = trim($file); + } + } + $dirPath->close(); + sort($images); - private function saveTip() { - global $database; + $this->theme->manageTips($url, $images); + } - $enable = isset($_POST["enable"]) ? "Y" : "N"; - $image = html_escape($_POST["image"]); - $text = $_POST["text"]; - - $database->execute(" + public function onCreateTip(CreateTipEvent $event) + { + global $database; + $database->execute( + " INSERT INTO tips (enable, image, text) - VALUES (?, ?, ?)", - array($enable, $image, $text)); + VALUES (:enable, :image, :text)", + ["enable"=>$event->enable ? "Y" : "N", "image"=>$event->image, "text"=>$event->text] + ); + } - } + private function getTip() + { + global $database; - private function getTip() { - global $database; + $data_href = get_base_href(); + $url = $data_href."/ext/tips/images/"; - $data_href = get_base_href(); - $url = $data_href."/ext/tips/images/"; + $tip = $database->get_row("SELECT * ". + "FROM tips ". + "WHERE enable = 'Y' ". + "ORDER BY RAND() ". + "LIMIT 1"); - $tip = $database->get_row("SELECT * ". - "FROM tips ". - "WHERE enable = 'Y' ". - "ORDER BY RAND() ". - "LIMIT 1"); + if ($tip) { + $this->theme->showTip($url, $tip); + } + } - if($tip) { - $this->theme->showTip($url, $tip); - } - } + private function getAll() + { + global $database; - private function getAll() { - global $database; + $data_href = get_base_href(); + $url = $data_href."/ext/tips/images/"; - $data_href = get_base_href(); - $url = $data_href."/ext/tips/images/"; + $tips = $database->get_all("SELECT * FROM tips ORDER BY id ASC"); - $tips = $database->get_all("SELECT * FROM tips ORDER BY id ASC"); + $this->theme->showAll($url, $tips); + } - $this->theme->showAll($url, $tips); - } + private function setStatus(int $tipID) + { + global $database; - /** - * @param int $tipID - */ - private function setStatus($tipID) { - global $database; + $tip = $database->get_row("SELECT * FROM tips WHERE id = :id ", ["id"=>$tipID]); - $tip = $database->get_row("SELECT * FROM tips WHERE id = ? ", array(int_escape($tipID))); + if (bool_escape($tip['enable'])) { + $enable = "N"; + } else { + $enable = "Y"; + } - if (bool_escape($tip['enable'])) { - $enable = "N"; - } else { - $enable = "Y"; - } + $database->execute("UPDATE tips SET enable = :enable WHERE id = :id", ["enable"=>$enable, "id"=>$tipID]); + } - $database->execute("UPDATE tips SET enable = ? WHERE id = ?", array ($enable, int_escape($tipID))); - } - - /** - * @param int $tipID - */ - private function deleteTip($tipID) { - global $database; - $database->execute("DELETE FROM tips WHERE id = ?", array(int_escape($tipID))); - } + public function onDeleteTip(DeleteTipEvent $event) + { + global $database; + $database->execute("DELETE FROM tips WHERE id = :id", ["id"=>$event->tip_id]); + } } - diff --git a/ext/tips/test.php b/ext/tips/test.php index e95fb5d8..1feaceb5 100644 --- a/ext/tips/test.php +++ b/ext/tips/test.php @@ -1,85 +1,65 @@ -execute("DELETE FROM tips"); + } - $this->log_in_as_admin(); - $this->get_page("tips/list"); + public function testImageless() + { + global $database; + $this->log_in_as_admin(); - $this->markTestIncomplete(); + $this->get_page("tips/list"); + $this->assert_title("Tips List"); - // get rid of the default data if it's there - if(strpos($raw, "Delete")) { - $this->click("Delete"); - } - $this->log_out(); - } + send_event(new CreateTipEvent(true, "", "an imageless tip")); + $this->get_page("post/list"); + $this->assert_text("an imageless tip"); - public function testImageless() { - $this->log_in_as_admin(); + $tip_id = (int)$database->get_one("SELECT id FROM tips"); + send_event(new DeleteTipEvent($tip_id)); + $this->get_page("post/list"); + $this->assert_no_text("an imageless tip"); + } - $this->get_page("tips/list"); - $this->assert_title("Tips List"); + public function testImaged() + { + global $database; + $this->log_in_as_admin(); - $this->markTestIncomplete(); + $this->get_page("tips/list"); + $this->assert_title("Tips List"); - $this->set_field("image", ""); - $this->set_field("text", "an imageless tip"); - $this->click("Submit"); - $this->assert_title("Tips List"); + send_event(new CreateTipEvent(true, "coins.png", "an imageless tip")); + $this->get_page("post/list"); + $this->assert_text("an imageless tip"); - $this->get_page("post/list"); - $this->assert_text("an imageless tip"); + $tip_id = (int)$database->get_one("SELECT id FROM tips"); + send_event(new DeleteTipEvent($tip_id)); + $this->get_page("post/list"); + $this->assert_no_text("an imageless tip"); + } - $this->get_page("tips/list"); - $this->click("Delete"); + public function testDisabled() + { + global $database; + $this->log_in_as_admin(); - $this->log_out(); - } + $this->get_page("tips/list"); + $this->assert_title("Tips List"); - public function testImaged() { - $this->log_in_as_admin(); + send_event(new CreateTipEvent(false, "", "an imageless tip")); + $this->get_page("post/list"); + $this->assert_no_text("an imageless tip"); - $this->get_page("tips/list"); - $this->assert_title("Tips List"); - - $this->markTestIncomplete(); - - $this->set_field("image", "coins.png"); - $this->set_field("text", "an imaged tip"); - $this->click("Submit"); - $this->assert_title("Tips List"); - - $this->get_page("post/list"); - $this->assert_text("an imaged tip"); - - $this->get_page("tips/list"); - $this->click("Delete"); - - $this->log_out(); - } - - public function testDisabled() { - $this->log_in_as_admin(); - - $this->get_page("tips/list"); - $this->assert_title("Tips List"); - - $this->markTestIncomplete(); - - $this->set_field("image", "coins.png"); - $this->set_field("text", "an imaged tip"); - $this->click("Submit"); - $this->click("Yes"); - $this->assert_title("Tips List"); - - $this->get_page("post/list"); - $this->assert_no_text("an imaged tip"); - - $this->get_page("tips/list"); - $this->click("Delete"); - - $this->log_out(); - } + $tip_id = (int)$database->get_one("SELECT id FROM tips"); + send_event(new DeleteTipEvent($tip_id)); + $this->get_page("post/list"); + $this->assert_no_text("an imageless tip"); + } } - diff --git a/ext/tips/theme.php b/ext/tips/theme.php index d724ca7c..7c49a9d9 100644 --- a/ext/tips/theme.php +++ b/ext/tips/theme.php @@ -1,16 +1,18 @@ -"; +"; - foreach($images as $image){ - $select .= "\n"; - } + foreach ($images as $image) { + $select .= "\n"; + } - $select .= ""; + $select .= ""; - $html = " + $html = " ".make_form(make_link("tips/save"))." @@ -32,64 +34,65 @@ class TipsTheme extends Themelet { "; - $page->set_title("Tips List"); - $page->set_heading("Tips List"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Add Tip", $html, "main", 10)); - } + $page->set_title("Tips List"); + $page->set_heading("Tips List"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Add Tip", $html, "main", 10)); + } - public function showTip($url, $tip) { - global $page; + public function showTip($url, $tip) + { + global $page; - $img = ""; - if(!empty($tip['image'])) { - $img = " "; - } - $html = "
    ".$img.$tip['text']."
    "; - $page->add_block(new Block(null, $html, "subheading", 10)); - } + $img = ""; + if (!empty($tip['image'])) { + $img = " "; + } + $html = "
    ".$img.html_escape($tip['text'])."
    "; + $page->add_block(new Block(null, $html, "subheading", 10)); + } - public function showAll($url, $tips){ - global $user, $page; + public function showAll($url, $tips) + { + global $user, $page; - $html = "
    ". - "". - "". - "". - "". - ""; + $html = "
    IDEnabledImageText
    ". + "". + "". + "". + "". + ""; - if($user->is_admin()){ - $html .= ""; - } + if ($user->can(Permissions::TIPS_ADMIN)) { + $html .= ""; + } - $html .= ""; + $html .= ""; - foreach ($tips as $tip) { - $tip_enable = ($tip['enable'] == "Y") ? "Yes" : "No"; - $set_link = "".$tip_enable.""; + foreach ($tips as $tip) { + $tip_enable = ($tip['enable'] == "Y") ? "Yes" : "No"; + $set_link = "".$tip_enable.""; - $html .= "". - "". - "". - ( - empty($tip['image']) ? - "" : - "" - ). - ""; + $html .= "". + "". + "". + ( + empty($tip['image']) ? + "" : + "" + ). + ""; - $del_link = "Delete"; + $del_link = "Delete"; - if($user->is_admin()){ - $html .= ""; - } + if ($user->can(Permissions::TIPS_ADMIN)) { + $html .= ""; + } - $html .= ""; - } - $html .= "
    IDEnabledImageTextActionAction
    ".$tip['id']."".$set_link."".$tip['text']."
    ".$tip['id']."".$set_link."".$tip['text']."".$del_link."".$del_link."
    "; + $html .= ""; + } + $html .= ""; - $page->add_block(new Block("All Tips", $html, "main", 20)); - } + $page->add_block(new Block("All Tips", $html, "main", 20)); + } } - diff --git a/ext/transcode/config.php b/ext/transcode/config.php new file mode 100644 index 00000000..f5855027 --- /dev/null +++ b/ext/transcode/config.php @@ -0,0 +1,11 @@ +"matthew@darkholme.net"]; + public $license = self::LICENSE_WTFPL; + public $description = "Allows admins to automatically and manually transcode images."; + public $documentation = +"Can transcode on-demand and automatically on upload. Config screen allows choosing an output format for each of the supported input formats. +Supports GD and ImageMagick. Both support bmp, gif, jpg, png, and webp as inputs, and jpg, png, and lossy webp as outputs. +ImageMagick additionally supports tiff and psd inputs, and webp lossless output. +If and image is unable to be transcoded for any reason, the upload will continue unaffected."; +} diff --git a/ext/transcode/main.php b/ext/transcode/main.php new file mode 100644 index 00000000..44062aaf --- /dev/null +++ b/ext/transcode/main.php @@ -0,0 +1,407 @@ + "bmp", + "GIF" => "gif", + "ICO" => "ico", + "JPG" => "jpg", + "PNG" => "png", + "PSD" => "psd", + "TIFF" => "tiff", + "WEBP" => "webp", + ]; + + const OUTPUT_FORMATS = [ + "" => "", + "JPEG (lossy)" => "jpg", + "PNG (lossless)" => "png", + "WEBP (lossy)" => Media::WEBP_LOSSY, + "WEBP (lossless)" => Media::WEBP_LOSSLESS, + ]; + + /** + * Needs to be after upload, but before the processing extensions + */ + public function get_priority(): int + { + return 45; + } + + + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_bool(TranscodeConfig::ENABLED, true); + $config->set_default_bool(TranscodeConfig::UPLOAD, false); + $config->set_default_string(TranscodeConfig::ENGINE, MediaEngine::GD); + $config->set_default_int(TranscodeConfig::QUALITY, 80); + + foreach (array_values(self::INPUT_FORMATS) as $format) { + $config->set_default_string(TranscodeConfig::UPLOAD_PREFIX.$format, ""); + } + } + + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) + { + global $user, $config; + + if ($user->can(Permissions::EDIT_FILES)) { + $engine = $config->get_string(TranscodeConfig::ENGINE); + if ($this->can_convert_format($engine, $event->image->ext, $event->image->lossless)) { + $options = $this->get_supported_output_formats($engine, $event->image->ext, $event->image->lossless??false); + $event->add_part($this->theme->get_transcode_html($event->image, $options)); + } + } + } + + public function onSetupBuilding(SetupBuildingEvent $event) + { + global $config; + + $engine = $config->get_string(TranscodeConfig::ENGINE); + + + $sb = new SetupBlock("Image Transcode"); + $sb->start_table(); + $sb->add_bool_option(TranscodeConfig::ENABLED, "Allow transcoding images: ", true); + $sb->add_bool_option(TranscodeConfig::UPLOAD, "Transcode on upload: ", true); + $sb->add_choice_option(TranscodeConfig::ENGINE, Media::IMAGE_MEDIA_ENGINES, "Engine", true); + foreach (self::INPUT_FORMATS as $display=>$format) { + if (in_array($format, MediaEngine::INPUT_SUPPORT[$engine])) { + $outputs = $this->get_supported_output_formats($engine, $format); + $sb->add_choice_option(TranscodeConfig::UPLOAD_PREFIX.$format, $outputs, "$display", true); + } + } + $sb->add_int_option(TranscodeConfig::QUALITY, "Lossy format quality: "); + $sb->end_table(); + $event->panel->add_block($sb); + } + + public function onDataUpload(DataUploadEvent $event) + { + global $config; + + if ($config->get_bool(TranscodeConfig::UPLOAD) == true) { + $ext = strtolower($event->type); + + $ext = Media::normalize_format($ext); + + if ($event->type=="gif"&&Media::is_animated_gif($event->tmpname)) { + return; + } + + if (in_array($ext, array_values(self::INPUT_FORMATS))) { + $target_format = $config->get_string(TranscodeConfig::UPLOAD_PREFIX.$ext); + if (empty($target_format)) { + return; + } + try { + $new_image = $this->transcode_image($event->tmpname, $ext, $target_format); + $event->set_type(Media::determine_ext($target_format)); + $event->set_tmpname($new_image); + } catch (Exception $e) { + log_error("transcode", "Error while performing upload transcode: ".$e->getMessage()); + // We don't want to interfere with the upload process, + // so if something goes wrong the untranscoded image jsut continues + } + } + } + } + + + + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; + + if ($event->page_matches("transcode") && $user->can(Permissions::EDIT_FILES)) { + if ($event->count_args() >= 1) { + $image_id = int_escape($event->get_arg(0)); + } elseif (isset($_POST['image_id'])) { + $image_id = int_escape($_POST['image_id']); + } else { + throw new ImageTranscodeException("Can not resize Image: No valid Image ID given."); + } + $image_obj = Image::by_id($image_id); + if (is_null($image_obj)) { + $this->theme->display_error(404, "Image not found", "No image in the database has the ID #$image_id"); + } else { + if (isset($_POST['transcode_format'])) { + try { + $this->transcode_and_replace_image($image_obj, $_POST['transcode_format']); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$image_id)); + } catch (ImageTranscodeException $e) { + $this->theme->display_transcode_error($page, "Error Transcoding", $e->getMessage()); + } + } + } + } + } + + + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) + { + global $user, $config; + + $engine = $config->get_string(TranscodeConfig::ENGINE); + + if ($user->can(Permissions::EDIT_FILES)) { + $event->add_action(self::ACTION_BULK_TRANSCODE, "Transcode", null, "", $this->theme->get_transcode_picker_html($this->get_supported_output_formats($engine))); + } + } + + public function onBulkAction(BulkActionEvent $event) + { + global $user, $database, $page; + + switch ($event->action) { + case self::ACTION_BULK_TRANSCODE: + if (!isset($_POST['transcode_format'])) { + return; + } + if ($user->can(Permissions::EDIT_FILES)) { + $format = $_POST['transcode_format']; + $total = 0; + $size_difference = 0; + foreach ($event->items as $image) { + try { + $database->beginTransaction(); + + $before_size = $image->filesize; + + $new_image = $this->transcode_and_replace_image($image, $format); + // If a subsequent transcode fails, the database needs to have everything about the previous + // transcodes recorded already, otherwise the image entries will be stuck pointing to + // missing image files + $database->commit(); + $total++; + $size_difference += ($before_size - $new_image->filesize); + } catch (Exception $e) { + log_error("transcode", "Error while bulk transcode on item {$image->id} to $format: ".$e->getMessage()); + try { + $database->rollback(); + } catch (Exception $e) { + // is this safe? o.o + } + } + } + if ($size_difference>0) { + $page->flash("Transcoded $total items, reduced size by ".human_filesize($size_difference)); + } elseif ($size_difference<0) { + $page->flash("Transcoded $total items, increased size by ".human_filesize(-1*$size_difference)); + } else { + $page->flash("Transcoded $total items, no size difference"); + } + } + break; + } + } + + + private function can_convert_format($engine, $format, ?bool $lossless = null): bool + { + return Media::is_input_supported($engine, $format, $lossless); + } + + + private function get_supported_output_formats($engine, ?String $omit_format = null, ?bool $lossless = null): array + { + if ($omit_format!=null) { + $omit_format = Media::normalize_format($omit_format, $lossless); + } + $output = []; + + + foreach (self::OUTPUT_FORMATS as $key=>$value) { + if ($value=="") { + $output[$key] = $value; + continue; + } + if (Media::is_output_supported($engine, $value) + &&(empty($omit_format)||$omit_format!=$value)) { + $output[$key] = $value; + } + } + return $output; + } + + + + private function transcode_and_replace_image(Image $image_obj, String $target_format): Image + { + $original_file = warehouse_path(Image::IMAGE_DIR, $image_obj->hash); + + $tmp_filename = $this->transcode_image($original_file, $image_obj->ext, $target_format); + + $new_image = new Image(); + $new_image->hash = md5_file($tmp_filename); + $new_image->filesize = filesize($tmp_filename); + $new_image->filename = $image_obj->filename; + $new_image->width = $image_obj->width; + $new_image->height = $image_obj->height; + $new_image->ext = Media::determine_ext($target_format); + + /* Move the new image into the main storage location */ + $target = warehouse_path(Image::IMAGE_DIR, $new_image->hash); + if (!@copy($tmp_filename, $target)) { + throw new ImageTranscodeException("Failed to copy new image file from temporary location ({$tmp_filename}) to archive ($target)"); + } + + /* Remove temporary file */ + @unlink($tmp_filename); + + send_event(new ImageReplaceEvent($image_obj->id, $new_image)); + + return $new_image; + } + + + private function transcode_image(String $source_name, String $source_format, string $target_format): string + { + global $config; + + if ($source_format==$target_format) { + throw new ImageTranscodeException("Source and target formats are the same: ".$source_format); + } + + $engine = $config->get_string("transcode_engine"); + + + + if (!$this->can_convert_format($engine, $source_format)) { + throw new ImageTranscodeException("Engine $engine does not support input format $source_format"); + } + if (!in_array($target_format, MediaEngine::OUTPUT_SUPPORT[$engine])) { + throw new ImageTranscodeException("Engine $engine does not support output format $target_format"); + } + + switch ($engine) { + case "gd": + return $this->transcode_image_gd($source_name, $source_format, $target_format); + case "convert": + return $this->transcode_image_convert($source_name, $source_format, $target_format); + default: + throw new ImageTranscodeException("No engine specified"); + } + } + + private function transcode_image_gd(String $source_name, String $source_format, string $target_format): string + { + global $config; + + $q = $config->get_int("transcode_quality"); + + $tmp_name = tempnam("/tmp", "shimmie_transcode"); + + $image = imagecreatefromstring(file_get_contents($source_name)); + try { + $result = false; + switch ($target_format) { + case "webp": + case Media::WEBP_LOSSY: + $result = imagewebp($image, $tmp_name, $q); + break; + case "png": + $result = imagepng($image, $tmp_name, 9); + break; + case "jpg": + // In case of alpha channels + $width = imagesx($image); + $height = imagesy($image); + $new_image = imagecreatetruecolor($width, $height); + if ($new_image===false) { + throw new ImageTranscodeException("Could not create image with dimensions $width x $height"); + } + try { + $black = imagecolorallocate($new_image, 0, 0, 0); + if ($black===false) { + throw new ImageTranscodeException("Could not allocate background color"); + } + if (imagefilledrectangle($new_image, 0, 0, $width, $height, $black)===false) { + throw new ImageTranscodeException("Could not fill background color"); + } + if (imagecopy($new_image, $image, 0, 0, 0, 0, $width, $height)===false) { + throw new ImageTranscodeException("Could not copy source image to new image"); + } + $result = imagejpeg($new_image, $tmp_name, $q); + } finally { + imagedestroy($new_image); + } + break; + } + } finally { + imagedestroy($image); + } + if ($result===false) { + throw new ImageTranscodeException("Error while transcoding ".$source_name." to ".$target_format); + } + return $tmp_name; + } + + private function transcode_image_convert(String $source_name, String $source_format, string $target_format): string + { + global $config; + + $q = $config->get_int("transcode_quality"); + $convert = $config->get_string(MediaConfig::CONVERT_PATH); + + if ($convert==null||$convert=="") { + throw new ImageTranscodeException("ImageMagick path not configured"); + } + $ext = Media::determine_ext($target_format); + + $args = " -flatten "; + $bg = "none"; + switch ($target_format) { + case Media::WEBP_LOSSLESS: + $args .= '-define webp:lossless=true'; + break; + case Media::WEBP_LOSSY: + $args .= ''; + break; + case "png": + $args .= '-define png:compression-level=9'; + break; + default: + $bg = "black"; + break; + } + $tmp_name = tempnam("/tmp", "shimmie_transcode"); + + $source_type = ""; + switch ($source_format) { + case "ico": + $source_type = "ico:"; + } + + $format = '"%s" %s -quality %u -background %s %s"%s" %s:"%s" 2>&1'; + $cmd = sprintf($format, $convert, $args, $q, $bg, $source_type, $source_name, $ext, $tmp_name); + $cmd = str_replace("\"convert\"", "convert", $cmd); // quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27 + exec($cmd, $output, $ret); + + log_debug('transcode', "Transcoding with command `$cmd`, returns $ret"); + + if ($ret!==0) { + throw new ImageTranscodeException("Transcoding failed with command ".$cmd.", returning ".implode("\r\n", $output)); + } + + return $tmp_name; + } +} diff --git a/ext/transcode/script.js b/ext/transcode/script.js new file mode 100644 index 00000000..6f78ac33 --- /dev/null +++ b/ext/transcode/script.js @@ -0,0 +1,11 @@ +function transcodeSubmit(e) { + var format = document.getElementById('transcode_format').value; + if(format!="webp-lossless" && format != "png") { + var lossless = document.getElementById('image_lossless'); + if(lossless!=null && lossless.value=='1') { + return confirm('You are about to transcode from a lossless format to a lossy format. Lossless formats compress with no quality loss, but converting to a lossy format always results in quality loss, and it will lose more quality every time it is done again on the same image. Are you sure you want to perform this transcode?'); + } else { + return confirm('Converting to a lossy format always results in quality loss, and it will lose more quality every time it is done again on the same image. Are you sure you want to perform this transcode?'); + } + } +} \ No newline at end of file diff --git a/ext/transcode/theme.php b/ext/transcode/theme.php new file mode 100644 index 00000000..e6230a64 --- /dev/null +++ b/ext/transcode/theme.php @@ -0,0 +1,45 @@ +id}"), + 'POST', + false, + "", + "return transcodeSubmit()" + )." + + + ".$this->get_transcode_picker_html($options)." +
    + + "; + + return $html; + } + + public function get_transcode_picker_html(array $options) + { + $html = ""; + } + + public function display_transcode_error(Page $page, string $title, string $message) + { + $page->set_title("Transcode Image"); + $page->set_heading("Transcode Image"); + $page->add_block(new NavBlock()); + $page->add_block(new Block($title, $message)); + } +} diff --git a/ext/trash/info.php b/ext/trash/info.php new file mode 100644 index 00000000..2223562b --- /dev/null +++ b/ext/trash/info.php @@ -0,0 +1,12 @@ +"matthew@darkholme.net"]; + public $license = self::LICENSE_WTFPL; + public $description = "Provides \"Trash\" or \"Recycle Bin\"-type functionality, storing delete images for later recovery"; +} diff --git a/ext/trash/main.php b/ext/trash/main.php new file mode 100644 index 00000000..afeccb3e --- /dev/null +++ b/ext/trash/main.php @@ -0,0 +1,172 @@ +page_matches("trash_restore") && $user->can(Permissions::VIEW_TRASH)) { + // Try to get the image ID + if ($event->count_args() >= 1) { + $image_id = int_escape($event->get_arg(0)); + } elseif (isset($_POST['image_id'])) { + $image_id = $_POST['image_id']; + } else { + throw new SCoreException("Can not restore image: No valid Image ID given."); + } + + self::set_trash($image_id, false); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$image_id)); + } + } + + public function onDisplayingImage(DisplayingImageEvent $event) + { + global $user, $page; + + if ($event->image->trash===true && !$user->can(Permissions::VIEW_TRASH)) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/list")); + } + } + + public function onImageDeletion(ImageDeletionEvent $event) + { + if ($event->force!==true && $event->image->trash!==true) { + self::set_trash($event->image->id, true); + $event->stop_processing = true; + } + } + + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent=="posts") { + if ($user->can(Permissions::VIEW_TRASH)) { + $event->add_nav_link("posts_trash", new Link('/post/list/in%3Atrash/1'), "Trash", null, 60); + } + } + } + + const SEARCH_REGEXP = "/^in:trash$/"; + public function onSearchTermParse(SearchTermParseEvent $event) + { + global $user, $database; + + $matches = []; + + if (is_null($event->term) && $this->no_trash_query($event->context)) { + $event->add_querylet(new Querylet($database->scoreql_to_sql("trash = SCORE_BOOL_N "))); + } + + if (is_null($event->term)) { + return; + } + if (preg_match(self::SEARCH_REGEXP, strtolower($event->term), $matches)) { + if ($user->can(Permissions::VIEW_TRASH)) { + $event->add_querylet(new Querylet($database->scoreql_to_sql("trash = SCORE_BOOL_Y "))); + } + } + } + + public function onHelpPageBuilding(HelpPageBuildingEvent $event) + { + global $user; + if ($event->key===HelpPages::SEARCH) { + if ($user->can(Permissions::VIEW_TRASH)) { + $block = new Block(); + $block->header = "Trash"; + $block->body = $this->theme->get_help_html(); + $event->add_block($block); + } + } + } + + private function no_trash_query(array $context): bool + { + foreach ($context as $term) { + if (preg_match(self::SEARCH_REGEXP, $term)) { + return false; + } + } + return true; + } + + public static function set_trash($image_id, $trash) + { + global $database; + + $database->execute( + "UPDATE images SET trash = :trash WHERE id = :id", + ["trash"=>$database->scoresql_value_prepare($trash),"id"=>$image_id] + ); + } + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) + { + global $user; + if ($event->image->trash===true && $user->can(Permissions::VIEW_TRASH)) { + $event->add_part($this->theme->get_image_admin_html($event->image->id)); + } + } + + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) + { + global $user; + + if ($user->can(Permissions::VIEW_TRASH)&&in_array("in:trash", $event->search_terms)) { + $event->add_action("bulk_trash_restore", "(U)ndelete", "u"); + } + } + + public function onBulkAction(BulkActionEvent $event) + { + global $page, $user; + + switch ($event->action) { + case "bulk_trash_restore": + if ($user->can(Permissions::VIEW_TRASH)) { + $total = 0; + foreach ($event->items as $image) { + self::set_trash($image->id, false); + $total++; + } + $page->flash("Restored $total items from trash"); + } + break; + } + } + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $database; + + if ($this->get_version(TrashConfig::VERSION) < 1) { + $database->Execute($database->scoreql_to_sql( + "ALTER TABLE images ADD COLUMN trash SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N" + )); + $database->Execute("CREATE INDEX images_trash_idx ON images(trash)"); + $this->set_version(TrashConfig::VERSION, 1); + } + } +} diff --git a/ext/trash/theme.php b/ext/trash/theme.php new file mode 100644 index 00000000..21733295 --- /dev/null +++ b/ext/trash/theme.php @@ -0,0 +1,25 @@ +'hidden', "name"=>'image_id', "value"=>$image_id]), + INPUT(["type"=>'submit', "value"=>'Restore From Trash']), + ); + } + + + public function get_help_html() + { + return '

    Search for images in the trash.

    +
    +
    in:trash
    +

    Returns images that are in the trash.

    +
    + '; + } +} diff --git a/ext/update/info.php b/ext/update/info.php new file mode 100644 index 00000000..90c7b6ed --- /dev/null +++ b/ext/update/info.php @@ -0,0 +1,13 @@ +"dakutree@codeanimu.net"]; + public $license = self::LICENSE_GPLV2; + public $description = "Shimmie updater! (Requires admin panel extension & transload engine (cURL/fopen/Wget))"; +} diff --git a/ext/update/main.php b/ext/update/main.php index e1a5c11f..15ea9434 100644 --- a/ext/update/main.php +++ b/ext/update/main.php @@ -1,117 +1,120 @@ - - * Link: http://www.codeanimu.net - * License: GPLv2 - * Description: Shimmie updater! (Requires admin panel extension & transload engine (cURL/fopen/Wget)) - */ -class Update extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config; - $config->set_default_string("update_guserrepo", "shish/shimmie2"); - $config->set_default_string("commit_hash", "unknown"); - $config->set_default_string("update_time", "01/01/1970"); - } +add_text_option("update_guserrepo", "User/Repo: "); - $event->panel->add_block($sb); - } +class Update extends Extension +{ + /** @var UpdateTheme */ + protected $theme; - public function onAdminBuilding(AdminBuildingEvent $event) { - global $config; - if($config->get_string('transload_engine') !== "none"){ - $this->theme->display_admin_block(); - } - } + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_string("update_guserrepo", "shish/shimmie2"); + $config->set_default_string("commit_hash", "unknown"); + $config->set_default_string("update_time", "01/01/1970"); + } - public function onPageRequest(PageRequestEvent $event) { - global $user, $page; - if($user->is_admin() && isset($_GET['sha'])){ - if($event->page_matches("update/download")){ - $ok = $this->download_shimmie(); + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Update"); + $sb->add_text_option("update_guserrepo", "User/Repo: "); + $event->panel->add_block($sb); + } - $page->set_mode("redirect"); - if($ok) $page->set_redirect(make_link("update/update", "sha=".$_GET['sha'])); - else $page->set_redirect(make_link("admin")); //TODO: Show error? - }elseif($event->page_matches("update/update")){ - $ok = $this->update_shimmie(); + public function onAdminBuilding(AdminBuildingEvent $event) + { + global $config; + if ($config->get_string('transload_engine') !== "none") { + $this->theme->display_admin_block(); + } + } - $page->set_mode("redirect"); - if($ok) $page->set_redirect(make_link("admin")); //TODO: Show success? - else $page->set_redirect(make_link("admin")); //TODO: Show error? - } - } - } + public function onPageRequest(PageRequestEvent $event) + { + global $user, $page; + if ($user->can(Permissions::EDIT_FILES) && isset($_GET['sha'])) { + if ($event->page_matches("update/download")) { + $ok = $this->download_shimmie(); - /** - * @return bool - */ - private function download_shimmie() { - global $config; + $page->set_mode(PageMode::REDIRECT); + if ($ok) { + $page->set_redirect(make_link("update/update", "sha=".$_GET['sha'])); + } else { + $page->set_redirect(make_link("admin")); + } //TODO: Show error? + } elseif ($event->page_matches("update/update")) { + $ok = $this->update_shimmie(); - $commitSHA = $_GET['sha']; - $g_userrepo = $config->get_string('update_guserrepo'); + $page->set_mode(PageMode::REDIRECT); + if ($ok) { + $page->set_redirect(make_link("admin")); + } //TODO: Show success? + else { + $page->set_redirect(make_link("admin")); + } //TODO: Show error? + } + } + } - $url = "https://codeload.github.com/".$g_userrepo."/zip/".$commitSHA; - $filename = "./data/update_{$commitSHA}.zip"; + private function download_shimmie(): bool + { + global $config; - log_info("update", "Attempting to download Shimmie commit: ".$commitSHA); - if($headers = transload($url, $filename)){ - if(($headers['Content-Type'] !== "application/zip") || ((int) $headers['Content-Length'] !== filesize($filename))){ - unlink("./data/update_{$commitSHA}.zip"); - log_warning("update", "Download failed: not zip / not same size as remote file."); - return false; - } + $commitSHA = $_GET['sha']; + $g_userrepo = $config->get_string('update_guserrepo'); - return true; - } + $url = "https://codeload.github.com/".$g_userrepo."/zip/".$commitSHA; + $filename = "./data/update_{$commitSHA}.zip"; - log_warning("update", "Download failed to download."); - return false; - } + log_info("update", "Attempting to download Shimmie commit: ".$commitSHA); + if ($headers = transload($url, $filename)) { + if (($headers['Content-Type'] !== "application/zip") || ((int) $headers['Content-Length'] !== filesize($filename))) { + unlink("./data/update_{$commitSHA}.zip"); + log_warning("update", "Download failed: not zip / not same size as remote file."); + return false; + } - /** - * @return bool - */ - private function update_shimmie() { - global $config; + return true; + } - $commitSHA = $_GET['sha']; + log_warning("update", "Download failed to download."); + return false; + } - log_info("update", "Download succeeded. Attempting to update Shimmie."); - $config->set_bool("in_upgrade", TRUE); - $ok = FALSE; + private function update_shimmie(): bool + { + global $config; - /** TODO: Backup all folders (except /data, /images, /thumbs) before attempting this? - Either that or point to https://github.com/shish/shimmie2/blob/master/README.txt -> Upgrade from 2.3.X **/ + $commitSHA = $_GET['sha']; - $zip = new ZipArchive; - if ($zip->open("./data/update_$commitSHA.zip") === TRUE) { - for($i = 1; $i < $zip->numFiles; $i++) { - $filename = $zip->getNameIndex($i); + log_info("update", "Download succeeded. Attempting to update Shimmie."); + $ok = false; - if(substr($filename, -1) !== "/"){ - copy("zip://".dirname(dirname(__DIR__)).'/'."./data/update_$commitSHA.zip"."#".$filename, substr($filename, 50)); - } - } - $ok = TRUE; //TODO: Do proper checking to see if everything copied properly - }else{ log_warning("update", "Update failed to open ZIP."); } + /** TODO: Backup all folders (except /data, /images, /thumbs) before attempting this? + Either that or point to https://github.com/shish/shimmie2/blob/master/README.txt -> Upgrade from 2.3.X **/ - $zip->close(); - unlink("./data/update_$commitSHA.zip"); - $config->set_bool("in_upgrade", FALSE); + $zip = new ZipArchive; + if ($zip->open("./data/update_$commitSHA.zip") === true) { + for ($i = 1; $i < $zip->numFiles; $i++) { + $filename = $zip->getNameIndex($i); - if($ok){ - $config->set_string("commit_hash", $commitSHA); - $config->set_string("update_time", date('d-m-Y')); - log_info("update", "Update succeeded?"); - } + if (substr($filename, -1) !== "/") { + copy("zip://".dirname(dirname(__DIR__)).'/'."./data/update_$commitSHA.zip"."#".$filename, substr($filename, 50)); + } + } + $ok = true; //TODO: Do proper checking to see if everything copied properly + } else { + log_warning("update", "Update failed to open ZIP."); + } - return $ok; - } + $zip->close(); + unlink("./data/update_$commitSHA.zip"); + + if ($ok) { + $config->set_string("commit_hash", $commitSHA); + $config->set_string("update_time", date('d-m-Y')); + log_info("update", "Update succeeded?"); + } + + return $ok; + } } - - diff --git a/ext/update/script.js b/ext/update/script.js index c2719786..424702d2 100644 --- a/ext/update/script.js +++ b/ext/update/script.js @@ -1,6 +1,6 @@ /*jshint bitwise:true, curly:true, forin:false, noarg:true, noempty:true, nonew:true, undef:true, strict:false, browser:true, jquery:true */ -$(function() { +document.addEventListener('DOMContentLoaded', () => { if($('#updatecheck').length !== 0){ $.getJSON('https://api.github.com/repos/shish/shimmie2/commits', function(data){ var c = data[0]; diff --git a/ext/update/theme.php b/ext/update/theme.php index e3dffb6a..35cc3f52 100644 --- a/ext/update/theme.php +++ b/ext/update/theme.php @@ -1,14 +1,15 @@ -Current Commit: ".$config->get_string('commit_hash')." | (".$config->get_string('update_time').")". - "
    Latest Commit: Loading...". - "
    "; - //TODO: Show warning before use. - $page->add_block(new Block("Software Update", $html, "main", 75)); - } + $html = "". + "Current Commit: ".$config->get_string('commit_hash')." | (".$config->get_string('update_time').")". + "
    Latest Commit: Loading...". + "
    "; + //TODO: Show warning before use. + $page->add_block(new Block("Software Update", $html, "main", 75)); + } } - diff --git a/ext/upgrade/info.php b/ext/upgrade/info.php new file mode 100644 index 00000000..43d56abb --- /dev/null +++ b/ext/upgrade/info.php @@ -0,0 +1,14 @@ + - * Link: http://code.shishnet.org/shimmie2/ - * Description: Keeps things happy behind the scenes - * Visibility: admin - */ +cmd == "help") { + print "\tdb-upgrade\n"; + print "\t\tRun DB schema updates, if automatic updates are disabled\n\n"; + } + if ($event->cmd == "db-upgrade") { + print("Running DB Upgrade\n"); + global $database; + $database->set_timeout(300000); // These updates can take a little bit + send_event(new DatabaseUpgradeEvent()); + } + } - if($config->get_bool("in_upgrade")) return; + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $config, $database; - if(!is_numeric($config->get_string("db_version"))) { - $config->set_int("db_version", 2); - } + if ($config->get_int("db_version") < 1) { + $this->set_version("db_version", 2); + } - if($config->get_int("db_version") < 6) { - // cry :S - } + // v7 is convert to innodb with adodb + // now done again as v9 with PDO - // v7 is convert to innodb with adodb - // now done again as v9 with PDO + if ($this->get_version("db_version") < 8) { + $database->execute($database->scoreql_to_sql( + "ALTER TABLE images ADD COLUMN locked SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N" + )); - if($config->get_int("db_version") < 8) { - $config->set_bool("in_upgrade", true); - $config->set_int("db_version", 8); + $this->set_version("db_version", 8); + } - $database->execute($database->scoreql_to_sql( - "ALTER TABLE images ADD COLUMN locked SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N" - )); + if ($this->get_version("db_version") < 9) { + if ($database->get_driver_name() == DatabaseDriver::MYSQL) { + $tables = $database->get_col("SHOW TABLES"); + foreach ($tables as $table) { + log_info("upgrade", "converting $table to innodb"); + $database->execute("ALTER TABLE $table ENGINE=INNODB"); + } + } - log_info("upgrade", "Database at version 8"); - $config->set_bool("in_upgrade", false); - } + $this->set_version("db_version", 9); + } - if($config->get_int("db_version") < 9) { - $config->set_bool("in_upgrade", true); - $config->set_int("db_version", 9); + if ($this->get_version("db_version") < 10) { + log_info("upgrade", "Adding foreign keys to images"); + $database->Execute("ALTER TABLE images ADD FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT"); - if($database->get_driver_name() == 'mysql') { - $tables = $database->get_col("SHOW TABLES"); - foreach($tables as $table) { - log_info("upgrade", "converting $table to innodb"); - $database->execute("ALTER TABLE $table ENGINE=INNODB"); - } - } + $this->set_version("db_version", 10); + } - log_info("upgrade", "Database at version 9"); - $config->set_bool("in_upgrade", false); - } + if ($this->get_version("db_version") < 11) { + log_info("upgrade", "Converting user flags to classes"); + $database->execute("ALTER TABLE users ADD COLUMN class VARCHAR(32) NOT NULL default :user", ["user" => "user"]); + $database->execute("UPDATE users SET class = :name WHERE id=:id", ["name"=>"anonymous", "id"=>$config->get_int('anon_id')]); + $database->execute("UPDATE users SET class = :name WHERE admin=:admin", ["name"=>"admin", "admin"=>'Y']); - if($config->get_int("db_version") < 10) { - $config->set_bool("in_upgrade", true); - $config->set_int("db_version", 10); + $this->set_version("db_version", 11); + } - log_info("upgrade", "Adding foreign keys to images"); - $database->Execute("ALTER TABLE images ADD FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT"); - - log_info("upgrade", "Database at version 10"); - $config->set_bool("in_upgrade", false); - } + if ($this->get_version("db_version") < 12) { + if ($database->get_driver_name() == DatabaseDriver::PGSQL) { + log_info("upgrade", "Changing ext column to VARCHAR"); + $database->execute("ALTER TABLE images ALTER COLUMN ext SET DATA TYPE VARCHAR(4)"); + } - if($config->get_int("db_version") < 11) { - $config->set_bool("in_upgrade", true); - $config->set_int("db_version", 11); + log_info("upgrade", "Lowering case of all exts"); + $database->execute("UPDATE images SET ext = LOWER(ext)"); - log_info("upgrade", "Converting user flags to classes"); - $database->execute("ALTER TABLE users ADD COLUMN class VARCHAR(32) NOT NULL default :user", array("user" => "user")); - $database->execute("UPDATE users SET class = :name WHERE id=:id", array("name"=>"anonymous", "id"=>$config->get_int('anon_id'))); - $database->execute("UPDATE users SET class = :name WHERE admin=:admin", array("name"=>"admin", "admin"=>'Y')); + $this->set_version("db_version", 12); + } - log_info("upgrade", "Database at version 11"); - $config->set_bool("in_upgrade", false); - } + if ($this->get_version("db_version") < 13) { + log_info("upgrade", "Changing password column to VARCHAR(250)"); + if ($database->get_driver_name() == DatabaseDriver::PGSQL) { + $database->execute("ALTER TABLE users ALTER COLUMN pass SET DATA TYPE VARCHAR(250)"); + } elseif ($database->get_driver_name() == DatabaseDriver::MYSQL) { + $database->execute("ALTER TABLE users CHANGE pass pass VARCHAR(250)"); + } - if($config->get_int("db_version") < 12) { - $config->set_bool("in_upgrade", true); - $config->set_int("db_version", 12); + $this->set_version("db_version", 13); + } - if($database->get_driver_name() == 'pgsql') { - log_info("upgrade", "Changing ext column to VARCHAR"); - $database->execute("ALTER TABLE images ALTER COLUMN ext SET DATA TYPE VARCHAR(4)"); - } + if ($this->get_version("db_version") < 14) { + log_info("upgrade", "Changing tag column to VARCHAR(255)"); + if ($database->get_driver_name() == DatabaseDriver::PGSQL) { + $database->execute('ALTER TABLE tags ALTER COLUMN tag SET DATA TYPE VARCHAR(255)'); + $database->execute('ALTER TABLE aliases ALTER COLUMN oldtag SET DATA TYPE VARCHAR(255)'); + $database->execute('ALTER TABLE aliases ALTER COLUMN newtag SET DATA TYPE VARCHAR(255)'); + } elseif ($database->get_driver_name() == DatabaseDriver::MYSQL) { + $database->execute('ALTER TABLE tags MODIFY COLUMN tag VARCHAR(255) NOT NULL'); + $database->execute('ALTER TABLE aliases MODIFY COLUMN oldtag VARCHAR(255) NOT NULL'); + $database->execute('ALTER TABLE aliases MODIFY COLUMN newtag VARCHAR(255) NOT NULL'); + } - log_info("upgrade", "Lowering case of all exts"); - $database->execute("UPDATE images SET ext = LOWER(ext)"); + $this->set_version("db_version", 14); + } - log_info("upgrade", "Database at version 12"); - $config->set_bool("in_upgrade", false); - } + if ($this->get_version("db_version") < 15) { + log_info("upgrade", "Adding lower indexes for postgresql use"); + if ($database->get_driver_name() == DatabaseDriver::PGSQL) { + $database->execute('CREATE INDEX tags_lower_tag_idx ON tags ((lower(tag)))'); + $database->execute('CREATE INDEX users_lower_name_idx ON users ((lower(name)))'); + } - if($config->get_int("db_version") < 13) { - $config->set_bool("in_upgrade", true); - $config->set_int("db_version", 13); + $this->set_version("db_version", 15); + } - log_info("upgrade", "Changing password column to VARCHAR(250)"); - if($database->get_driver_name() == 'pgsql') { - $database->execute("ALTER TABLE users ALTER COLUMN pass SET DATA TYPE VARCHAR(250)"); - } - else if($database->get_driver_name() == 'mysql') { - $database->execute("ALTER TABLE users CHANGE pass pass VARCHAR(250)"); - } + if ($this->get_version("db_version") < 16) { + log_info("upgrade", "Adding tag_id, image_id index to image_tags"); + $database->execute('CREATE UNIQUE INDEX image_tags_tag_id_image_id_idx ON image_tags(tag_id,image_id) '); - log_info("upgrade", "Database at version 13"); - $config->set_bool("in_upgrade", false); - } + log_info("upgrade", "Changing filename column to VARCHAR(255)"); + if ($database->get_driver_name() == DatabaseDriver::PGSQL) { + $database->execute('ALTER TABLE images ALTER COLUMN filename SET DATA TYPE VARCHAR(255)'); + // Postgresql creates a unique index for unique columns, not just a constraint, + // so we don't need two indexes on the same column + $database->execute('DROP INDEX IF EXISTS images_hash_idx'); + $database->execute('DROP INDEX IF EXISTS users_name_idx'); + } elseif ($database->get_driver_name() == DatabaseDriver::MYSQL) { + $database->execute('ALTER TABLE images MODIFY COLUMN filename VARCHAR(255) NOT NULL'); + } + // SQLite doesn't support altering existing columns? This seems like a problem? - if($config->get_int("db_version") < 14) { - $config->set_bool("in_upgrade", true); - $config->set_int("db_version", 14); + $this->set_version("db_version", 16); + } - log_info("upgrade", "Changing tag column to VARCHAR(255)"); - if($database->get_driver_name() == 'pgsql') { - $database->execute('ALTER TABLE tags ALTER COLUMN tag SET DATA TYPE VARCHAR(255)'); - $database->execute('ALTER TABLE aliases ALTER COLUMN oldtag SET DATA TYPE VARCHAR(255)'); - $database->execute('ALTER TABLE aliases ALTER COLUMN newtag SET DATA TYPE VARCHAR(255)'); - } - else if($database->get_driver_name() == 'mysql') { - $database->execute('ALTER TABLE tags MODIFY COLUMN tag VARCHAR(255) NOT NULL'); - $database->execute('ALTER TABLE aliases MODIFY COLUMN oldtag VARCHAR(255) NOT NULL'); - $database->execute('ALTER TABLE aliases MODIFY COLUMN newtag VARCHAR(255) NOT NULL'); - } + if ($this->get_version("db_version") < 17) { + log_info("upgrade", "Adding media information columns to images table"); + $database->execute($database->scoreql_to_sql( + "ALTER TABLE images ADD COLUMN lossless SCORE_BOOL NULL" + )); + $database->execute($database->scoreql_to_sql( + "ALTER TABLE images ADD COLUMN video SCORE_BOOL NULL" + )); + $database->execute($database->scoreql_to_sql( + "ALTER TABLE images ADD COLUMN audio SCORE_BOOL NULL" + )); + $database->execute("ALTER TABLE images ADD COLUMN length INTEGER NULL "); - log_info("upgrade", "Database at version 14"); - $config->set_bool("in_upgrade", false); - } - } + log_info("upgrade", "Setting indexes for media columns"); + switch ($database->get_driver_name()) { + case DatabaseDriver::PGSQL: + case DatabaseDriver::SQLITE: + $database->execute('CREATE INDEX images_video_idx ON images(video) WHERE video IS NOT NULL'); + $database->execute('CREATE INDEX images_audio_idx ON images(audio) WHERE audio IS NOT NULL'); + $database->execute('CREATE INDEX images_length_idx ON images(length) WHERE length IS NOT NULL'); + break; + default: + $database->execute('CREATE INDEX images_video_idx ON images(video)'); + $database->execute('CREATE INDEX images_audio_idx ON images(audio)'); + $database->execute('CREATE INDEX images_length_idx ON images(length)'); + break; + } - /** @return int */ - public function get_priority() {return 5;} + $database->set_timeout(300000); // These updates can take a little bit + + log_info("upgrade", "Setting index for ext column"); + $database->execute('CREATE INDEX images_ext_idx ON images(ext)'); + + $this->set_version("db_version", 17); + } + + if ($this->get_version("db_version") < 18) { + log_info("upgrade", "Setting predictable media values for known file types"); + if ($database->transaction) { + // Each of these commands could hit a lot of data, combining + // them into one big transaction would not be a good idea. + $database->commit(); + } + $database->execute($database->scoreql_to_sql("UPDATE images SET lossless = SCORE_BOOL_Y, video = SCORE_BOOL_Y WHERE ext IN ('swf')")); + $database->execute($database->scoreql_to_sql("UPDATE images SET lossless = SCORE_BOOL_N, video = SCORE_BOOL_N, audio = SCORE_BOOL_Y WHERE ext IN ('mp3')")); + $database->execute($database->scoreql_to_sql("UPDATE images SET lossless = SCORE_BOOL_N, video = SCORE_BOOL_N, audio = SCORE_BOOL_N WHERE ext IN ('jpg','jpeg')")); + $database->execute($database->scoreql_to_sql("UPDATE images SET lossless = SCORE_BOOL_Y, video = SCORE_BOOL_N, audio = SCORE_BOOL_N WHERE ext IN ('ico','ani','cur','png','svg')")); + $database->execute($database->scoreql_to_sql("UPDATE images SET lossless = SCORE_BOOL_Y, audio = SCORE_BOOL_N WHERE ext IN ('gif')")); + $database->execute($database->scoreql_to_sql("UPDATE images SET audio = SCORE_BOOL_N WHERE ext IN ('webp')")); + $database->execute($database->scoreql_to_sql("UPDATE images SET lossless = SCORE_BOOL_N, video = SCORE_BOOL_Y WHERE ext IN ('flv','mp4','m4v','ogv','webm')")); + $this->set_version("db_version", 18); + } + } + + public function get_priority(): int + { + return 5; + } } - diff --git a/ext/upload/bookmarklet.js b/ext/upload/bookmarklet.js index c03e06a6..29c2945b 100644 --- a/ext/upload/bookmarklet.js +++ b/ext/upload/bookmarklet.js @@ -131,18 +131,17 @@ if(document.getElementById("image-container") !== null) { if(supext.search(furl.match("[a-zA-Z0-9]+$")[0]) !== -1){ history.pushState(history.state, document.title, location.href); - var href = ste + furl + + location.href = ste + furl + "&tags=" + encodeURIComponent(tag) + "&rating=" + encodeURIComponent(rating) + "&source=" + encodeURIComponent(source); - location.href = href; } else{ alert(notsup); } } -/* +/* * Shimmie * * One problem with shimmie is each theme does not show the same info @@ -184,4 +183,4 @@ else if(document.getElementsByTagName("title")[0].innerHTML.search("Image [0-9.- alert(notsup); } } -} \ No newline at end of file +} diff --git a/ext/upload/info.php b/ext/upload/info.php new file mode 100644 index 00000000..5cd8501b --- /dev/null +++ b/ext/upload/info.php @@ -0,0 +1,13 @@ + - * Link: http://code.shishnet.org/shimmie2/ - * Description: Allows people to upload files to the website - */ +tmpname = $tmpname; + assert(file_exists($tmpname)); + assert(is_string($metadata["filename"])); + assert(is_array($metadata["tags"])); + assert(is_string($metadata["source"]) || is_null($metadata["source"])); - $this->metadata = $metadata; - $this->metadata['hash'] = md5_file($tmpname); - $this->metadata['size'] = filesize($tmpname); + // DB limits to 64 char filenames + $metadata['filename'] = substr($metadata['filename'], 0, 63); - // useful for most file handlers, so pull directly into fields - $this->hash = $this->metadata['hash']; - $this->type = strtolower($metadata['extension']); - } + $this->metadata = $metadata; + + $this->set_tmpname($tmpname); + + if ($config->get_bool("upload_use_mime")) { + $this->set_type(get_extension(getMimeType($tmpname))); + } else { + if (array_key_exists('extension', $metadata) && !empty($metadata['extension'])) { + $this->type = strtolower($metadata['extension']); + } else { + throw new UploadException("Could not determine extension for file " . $metadata["filename"]); + } + } + } + + public function set_type(String $type) + { + $this->type = strtolower($type); + $this->metadata["extension"] = $this->type; + } + + public function set_tmpname(String $tmpname) + { + $this->tmpname = $tmpname; + $this->metadata['hash'] = md5_file($tmpname); + $this->metadata['size'] = filesize($tmpname); + // useful for most file handlers, so pull directly into fields + $this->hash = $this->metadata['hash']; + } } -class UploadException extends SCoreException {} +class UploadException extends SCoreException +{ +} /** * Main upload class. * All files that are uploaded to the site are handled through this class. * This also includes transloaded files as well. */ -class Upload extends Extension { - /** @var bool */ - public $is_full; +class Upload extends Extension +{ + /** @var UploadTheme */ + protected $theme; - /** - * Early, so it can stop the DataUploadEvent before any data handlers see it. - * - * @return int - */ - public function get_priority() {return 40;} + /** @var bool */ + public $is_full; - public function onInitExt(InitExtEvent $event) { - global $config; - $config->set_default_int('upload_count', 3); - $config->set_default_int('upload_size', '1MB'); - $config->set_default_int('upload_min_free_space', '100MB'); - $config->set_default_bool('upload_tlsource', TRUE); + /** + * Early, so it can stop the DataUploadEvent before any data handlers see it. + */ + public function get_priority(): int + { + return 40; + } - $this->is_full = false; + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_int('upload_count', 3); + $config->set_default_int('upload_size', parse_shorthand_int('1MB')); + $config->set_default_int('upload_min_free_space', parse_shorthand_int('100MB')); + $config->set_default_bool('upload_tlsource', true); + $config->set_default_bool('upload_use_mime', false); - $min_free_space = $config->get_int("upload_min_free_space"); - if($min_free_space > 0) { - // SHIT: fucking PHP "security" measures -_-;;; - $free_num = @disk_free_space(realpath("./images/")); - if($free_num !== FALSE) { - $this->is_full = $free_num < $min_free_space; - } - } - } + $this->is_full = false; - public function onPostListBuilding(PostListBuildingEvent $event) { - global $user, $page; - if($user->can("create_image")) { - if($this->is_full) { - $this->theme->display_full($page); - } - else { - $this->theme->display_block($page); - } - } - } + $min_free_space = $config->get_int("upload_min_free_space"); + if ($min_free_space > 0) { + // SHIT: fucking PHP "security" measures -_-;;; + $img_path = realpath("./images/"); + if ($img_path) { + $free_num = @disk_free_space($img_path); + if ($free_num !== false) { + $this->is_full = $free_num < $min_free_space; + } + } + } + } - public function onSetupBuilding(SetupBuildingEvent $event) { - $tes = array(); - $tes["Disabled"] = "none"; - if(function_exists("curl_init")) { - $tes["cURL"] = "curl"; - } - $tes["fopen"] = "fopen"; - $tes["WGet"] = "wget"; + public function onSetupBuilding(SetupBuildingEvent $event) + { + $tes = []; + $tes["Disabled"] = "none"; + if (function_exists("curl_init")) { + $tes["cURL"] = "curl"; + } + $tes["fopen"] = "fopen"; + $tes["WGet"] = "wget"; - $sb = new SetupBlock("Upload"); - $sb->position = 10; - // Output the limits from PHP so the user has an idea of what they can set. - $sb->add_int_option("upload_count", "Max uploads: "); - $sb->add_label("PHP Limit = ".ini_get('max_file_uploads').""); - $sb->add_shorthand_int_option("upload_size", "
    Max size per file: "); - $sb->add_label("PHP Limit = ".ini_get('upload_max_filesize').""); - $sb->add_choice_option("transload_engine", $tes, "
    Transload: "); - $sb->add_bool_option("upload_tlsource", "
    Use transloaded URL as source if none is provided: "); - $event->panel->add_block($sb); - } + $sb = new SetupBlock("Upload"); + $sb->position = 10; + // Output the limits from PHP so the user has an idea of what they can set. + $sb->add_int_option("upload_count", "Max uploads: "); + $sb->add_label("PHP Limit = " . ini_get('max_file_uploads') . ""); + $sb->add_shorthand_int_option("upload_size", "
    Max size per file: "); + $sb->add_label("PHP Limit = " . ini_get('upload_max_filesize') . ""); + $sb->add_choice_option("transload_engine", $tes, "
    Transload: "); + $sb->add_bool_option("upload_tlsource", "
    Use transloaded URL as source if none is provided: "); + $sb->add_bool_option("upload_use_mime", "
    Use mime type to determine file types: "); + $event->panel->add_block($sb); + } - public function onDataUpload(DataUploadEvent $event) { - global $config; - if($this->is_full) { - throw new UploadException("Upload failed; disk nearly full"); - } - if(filesize($event->tmpname) > $config->get_int('upload_size')) { - $size = to_shorthand_int(filesize($event->tmpname)); - $limit = to_shorthand_int($config->get_int('upload_size')); - throw new UploadException("File too large ($size > $limit)"); - } - } - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; + public function onPageNavBuilding(PageNavBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::CREATE_IMAGE)) { + $event->add_nav_link("upload", new Link('upload'), "Upload"); + } + } - if($event->page_matches("upload/replace")) { - // check if the user is an administrator and can upload files. - if(!$user->can("replace_image")) { - $this->theme->display_permission_denied(); - } - else { - if($this->is_full) { - throw new UploadException("Can not replace Image: disk nearly full"); - } - // 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 UploadException("Can not replace Image: No valid Image ID given."); - } + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + if ($event->parent=="upload") { + if (class_exists("Wiki")) { + $event->add_nav_link("upload_guidelines", new Link('wiki/upload_guidelines'), "Guidelines"); + } + } + } - $image_old = Image::by_id($image_id); - if(is_null($image_old)) { - $this->theme->display_error(404, "Image not found", "No image in the database has the ID #$image_id"); - } + public function onDataUpload(DataUploadEvent $event) + { + global $config; + if ($this->is_full) { + throw new UploadException("Upload failed; disk nearly full"); + } + if (filesize($event->tmpname) > $config->get_int('upload_size')) { + $size = to_shorthand_int(filesize($event->tmpname)); + $limit = to_shorthand_int($config->get_int('upload_size')); + throw new UploadException("File too large ($size > $limit)"); + } + } - if(count($_FILES) + count($_POST) > 0) { - if(count($_FILES) > 1) { - throw new UploadException("Can not upload more than one image for replacing."); - } - - $source = isset($_POST['source']) ? $_POST['source'] : null; - $tags = array(); // Tags aren't changed when replacing. Set to empty to stop PHP warnings. - - $ok = false; - if(count($_FILES)) { - foreach($_FILES as $file) { - $ok = $this->try_upload($file, $tags, $source, $image_id); - break; // leave the foreach loop. - } - } - else { - foreach($_POST as $name => $value) { - if(substr($name, 0, 3) == "url" && strlen($value) > 0) { - $ok = $this->try_transload($value, $tags, $source, $image_id); - break; // leave the foreach loop. - } - } - } - $this->theme->display_upload_status($page, $ok); - } - else if(!empty($_GET['url'])) { - $url = $_GET['url']; - $tags = isset($_GET['tags']) ? Tag::explode($_GET['tags']) : 'tagme'; - $source = isset($_GET['source']) ? $_GET['source'] : $url; - $ok = $this->try_transload($url, $tags, $source, $image_id); - $this->theme->display_upload_status($page, $ok); - } - else { - $this->theme->display_replace_page($page, $image_id); - } - } - } - else if($event->page_matches("upload")) { - if(!$user->can("create_image")) { - $this->theme->display_permission_denied(); - } - else { - /* Regular Upload Image */ - if(count($_FILES) + count($_POST) > 0) { - $ok = true; - foreach($_FILES as $name => $file) { - $tags = $this->tags_for_upload_slot(int_escape(substr($name, 4))); - $source = isset($_POST['source']) ? $_POST['source'] : null; - $ok = $ok & $this->try_upload($file, $tags, $source); - } - foreach($_POST as $name => $value) { - if(substr($name, 0, 3) == "url" && strlen($value) > 0) { - $tags = $this->tags_for_upload_slot(int_escape(substr($name, 3))); - $source = isset($_POST['source']) ? $_POST['source'] : $value; - $ok = $ok & $this->try_transload($value, $tags, $source); - } - } + public function onPageRequest(PageRequestEvent $event) + { + global $cache, $page, $user; - $this->theme->display_upload_status($page, $ok); - } - else if(!empty($_GET['url'])) { - $url = $_GET['url']; - $source = isset($_GET['source']) ? $_GET['source'] : $url; - $tags = array('tagme'); - if(!empty($_GET['tags']) && $_GET['tags'] != "null") { - $tags = Tag::explode($_GET['tags']); - } - - $ok = $this->try_transload($url, $tags, $source); - $this->theme->display_upload_status($page, $ok); - } - else { - if ($this->is_full) { - $this->theme->display_full($page); - } else { - $this->theme->display_page($page); - } - } - } - } - } + if ($user->can(Permissions::CREATE_IMAGE)) { + if ($this->is_full) { + $this->theme->display_full($page); + } else { + $this->theme->display_block($page); + } + } - /** - * @param int $id - * @return string[] - */ - private function tags_for_upload_slot($id) { - $post_tags = isset($_POST["tags"]) ? $_POST["tags"] : ""; + if ($event->page_matches("upload/replace")) { + // check if the user is an administrator and can upload files. + if (!$user->can(Permissions::REPLACE_IMAGE)) { + $this->theme->display_permission_denied(); + } else { + if ($this->is_full) { + throw new UploadException("Can not replace Image: disk nearly full"); + } + // Try to get the image ID + if ($event->count_args() >= 1) { + $image_id = int_escape($event->get_arg(0)); + } elseif (isset($_POST['image_id'])) { + $image_id = $_POST['image_id']; + } else { + throw new UploadException("Can not replace Image: No valid Image ID given."); + } - if(isset($_POST["tags$id"])) { - # merge then explode, not explode then merge - else - # one of the merges may create a surplus "tagme" - $tags = Tag::explode($post_tags . " " . $_POST["tags$id"]); - } - else { - $tags = Tag::explode($post_tags); - } - return $tags; - } + $image_old = Image::by_id($image_id); + if (is_null($image_old)) { + $this->theme->display_error(404, "Image not found", "No image in the database has the ID #$image_id"); + } -// do things {{{ + if (count($_FILES) + count($_POST) > 0) { + if (count($_FILES) > 1) { + throw new UploadException("Can not upload more than one image for replacing."); + } - /** - * Returns a descriptive error message for the specified PHP error code. - * - * This is a helper function based on the one from the online PHP Documentation - * which is licensed under Creative Commons Attribution 3.0 License - * - * TODO: Make these messages user/admin editable - * - * @param int $error_code PHP error code - * @return string - */ - private function upload_error_message($error_code) { - switch ($error_code) { - case UPLOAD_ERR_INI_SIZE: - return 'The uploaded file exceeds the upload_max_filesize directive in php.ini'; - case UPLOAD_ERR_FORM_SIZE: - return 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'; - case UPLOAD_ERR_PARTIAL: - return 'The uploaded file was only partially uploaded'; - case UPLOAD_ERR_NO_FILE: - return 'No file was uploaded'; - case UPLOAD_ERR_NO_TMP_DIR: - return 'Missing a temporary folder'; - case UPLOAD_ERR_CANT_WRITE: - return 'Failed to write file to disk'; - case UPLOAD_ERR_EXTENSION: - return 'File upload stopped by extension'; - default: - return 'Unknown upload error'; - } - } + $source = isset($_POST['source']) ? $_POST['source'] : null; + $tags = []; // Tags aren't changed when replacing. Set to empty to stop PHP warnings. - /** - * Handle an upload. - * @param string[] $file - * @param string[] $tags - * @param string|null $source - * @param int $replace - * @return bool TRUE on upload successful. - */ - private function try_upload($file, $tags, $source, $replace=-1) { - global $page; - assert('is_array($file)'); - assert('is_array($tags)'); - assert('is_string($source) || is_null($source)'); - assert('is_int($replace)'); + $ok = false; + if (count($_FILES)) { + foreach ($_FILES as $file) { + $ok = $this->try_upload($file, $tags, $source, $image_id); + break; // leave the foreach loop. + } + } else { + foreach ($_POST as $name => $value) { + if (substr($name, 0, 3) == "url" && strlen($value) > 0) { + $ok = $this->try_transload($value, $tags, $source, $image_id); + break; // leave the foreach loop. + } + } + } + $cache->delete("thumb-block:{$image_id}"); + $this->theme->display_upload_status($page, $ok); + } elseif (!empty($_GET['url'])) { + $url = $_GET['url']; + $tags = isset($_GET['tags']) ? Tag::explode($_GET['tags']) : 'tagme'; + $source = isset($_GET['source']) ? $_GET['source'] : $url; + $ok = $this->try_transload($url, $tags, $source, $image_id); + $cache->delete("thumb-block:{$image_id}"); + $this->theme->display_upload_status($page, $ok); + } else { + $this->theme->display_replace_page($page, $image_id); + } + } + } elseif ($event->page_matches("upload")) { + if (!$user->can(Permissions::CREATE_IMAGE)) { + $this->theme->display_permission_denied(); + } else { + /* Regular Upload Image */ + if (count($_FILES) + count($_POST) > 0) { + $ok = true; + foreach ($_FILES as $name => $file) { + $tags = $this->tags_for_upload_slot(int_escape(substr($name, 4))); + $source = isset($_POST['source']) ? $_POST['source'] : null; + $ok = $this->try_upload($file, $tags, $source) && $ok; + } + foreach ($_POST as $name => $value) { + if (substr($name, 0, 3) == "url" && strlen($value) > 0) { + $tags = $this->tags_for_upload_slot(int_escape(substr($name, 3))); + $source = isset($_POST['source']) ? $_POST['source'] : $value; + $ok = $this->try_transload($value, $tags, $source) && $ok; + } + } - if(empty($source)) $source = null; + $this->theme->display_upload_status($page, $ok); + } elseif (!empty($_GET['url'])) { + $url = $_GET['url']; + $source = isset($_GET['source']) ? $_GET['source'] : $url; + $tags = ['tagme']; + if (!empty($_GET['tags']) && $_GET['tags'] != "null") { + $tags = Tag::explode($_GET['tags']); + } - $ok = true; + $ok = $this->try_transload($url, $tags, $source); + $this->theme->display_upload_status($page, $ok); + } else { + if ($this->is_full) { + $this->theme->display_full($page); + } else { + $this->theme->display_page($page); + } + } + } + } + } - // blank file boxes cause empty uploads, no need for error message - if (!empty($file['name'])) { - try { - // check if the upload was successful - if ($file['error'] !== UPLOAD_ERR_OK) { - throw new UploadException($this->upload_error_message($file['error'])); - } - - $pathinfo = pathinfo($file['name']); - $metadata = array(); - $metadata['filename'] = $pathinfo['basename']; - $metadata['extension'] = $pathinfo['extension']; - $metadata['tags'] = $tags; - $metadata['source'] = $source; - - /* check if we have been given an image ID to replace */ - if ($replace >= 0) { - $metadata['replace'] = $replace; - } - - $event = new DataUploadEvent($file['tmp_name'], $metadata); - send_event($event); - if($event->image_id == -1) { - throw new UploadException("File type not recognised"); - } - $page->add_http_header("X-Shimmie-Image-ID: ".int_escape($event->image_id)); - } - catch(UploadException $ex) { - $this->theme->display_upload_error($page, "Error with ".html_escape($file['name']), - $ex->getMessage()); - $ok = false; - } - } + private function tags_for_upload_slot(int $id): array + { + $post_tags = isset($_POST["tags"]) ? $_POST["tags"] : ""; - return $ok; - } + if (isset($_POST["tags$id"])) { + # merge then explode, not explode then merge - else + # one of the merges may create a surplus "tagme" + $tags = Tag::explode($post_tags . " " . $_POST["tags$id"]); + } else { + $tags = Tag::explode($post_tags); + } + return $tags; + } - /** - * Handle an transload. - * - * @param string $url - * @param string[] $tags - * @param string|null $source - * @param int $replace - * @return bool Returns TRUE on transload successful. - */ - private function try_transload($url, $tags, $source, $replace=-1) { - global $page, $config, $user; - assert('is_string($url)'); - assert('is_array($tags)'); - assert('is_string($source) || is_null($source)'); - assert('is_int($replace)'); + /** + * Returns a descriptive error message for the specified PHP error code. + * + * This is a helper function based on the one from the online PHP Documentation + * which is licensed under Creative Commons Attribution 3.0 License + * + * TODO: Make these messages user/admin editable + */ + private function upload_error_message(int $error_code): string + { + switch ($error_code) { + case UPLOAD_ERR_INI_SIZE: + return 'The uploaded file exceeds the upload_max_filesize directive in php.ini'; + case UPLOAD_ERR_FORM_SIZE: + return 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'; + case UPLOAD_ERR_PARTIAL: + return 'The uploaded file was only partially uploaded'; + case UPLOAD_ERR_NO_FILE: + return 'No file was uploaded'; + case UPLOAD_ERR_NO_TMP_DIR: + return 'Missing a temporary folder'; + case UPLOAD_ERR_CANT_WRITE: + return 'Failed to write file to disk'; + case UPLOAD_ERR_EXTENSION: + return 'File upload stopped by extension'; + default: + return 'Unknown upload error'; + } + } - $ok = true; + /** + * Handle an upload. + * #param string[] $file + * #param string[] $tags + */ + private function try_upload(array $file, array $tags, ?string $source = null, int $replace = -1): bool + { + global $page; - // Checks if user is admin > check if you want locked. - if($user->can("edit_image_lock") && !empty($_GET['locked'])){ - $locked = bool_escape($_GET['locked']); - } - - // Checks if url contains rating, also checks if the rating extension is enabled. - if($config->get_string("transload_engine", "none") != "none" && ext_is_live("Ratings") && !empty($_GET['rating'])) { - // Rating event will validate that this is s/q/e/u - $rating = strtolower($_GET['rating']); - $rating = $rating[0]; - }else{ - $rating = ""; - } + if (empty($source)) { + $source = null; + } - $tmp_filename = tempnam(ini_get('upload_tmp_dir'), "shimmie_transload"); + $ok = true; - // transload() returns Array or Bool, depending on the transload_engine. - $headers = transload($url, $tmp_filename); - - $s_filename = is_array($headers) ? findHeader($headers, 'Content-Disposition') : null; - $h_filename = ($s_filename ? preg_replace('/^.*filename="([^ ]+)"/i', '$1', $s_filename) : null); - $filename = $h_filename ?: basename($url); + // blank file boxes cause empty uploads, no need for error message + if (!empty($file['name'])) { + try { + // check if the upload was successful + if ($file['error'] !== UPLOAD_ERR_OK) { + throw new UploadException($this->upload_error_message($file['error'])); + } - if(!$headers) { - $this->theme->display_upload_error($page, "Error with ".html_escape($filename), - "Error reading from ".html_escape($url)); - return false; - } + $pathinfo = pathinfo($file['name']); + $metadata = []; + $metadata['filename'] = $pathinfo['basename']; + if (array_key_exists('extension', $pathinfo)) { + $metadata['extension'] = $pathinfo['extension']; + } + $metadata['tags'] = $tags; + $metadata['source'] = $source; - if(filesize($tmp_filename) == 0) { - $this->theme->display_upload_error($page, "Error with ".html_escape($filename), - "No data found -- perhaps the site has hotlink protection?"); - $ok = false; - }else{ - $pathinfo = pathinfo($url); - $metadata = array(); - $metadata['filename'] = $filename; - $metadata['tags'] = $tags; - $metadata['source'] = (($url == $source) && !$config->get_bool('upload_tlsource') ? "" : $source); - - $ext = false; - if (is_array($headers)) { - $ext = getExtension(findHeader($headers, 'Content-Type')); - } - if ($ext === false) { - $ext = $pathinfo['extension']; - } - $metadata['extension'] = $ext; - - /* check for locked > adds to metadata if it has */ - if(!empty($locked)){ - $metadata['locked'] = $locked ? "on" : ""; - } + /* check if we have been given an image ID to replace */ + if ($replace >= 0) { + $metadata['replace'] = $replace; + } - /* check for rating > adds to metadata if it has */ - if(!empty($rating)){ - $metadata['rating'] = $rating; - } - - /* check if we have been given an image ID to replace */ - if ($replace >= 0) { - $metadata['replace'] = $replace; - } - - $event = new DataUploadEvent($tmp_filename, $metadata); - try { - send_event($event); - } - catch(UploadException $ex) { - $this->theme->display_upload_error($page, "Error with ".html_escape($url), - $ex->getMessage()); - $ok = false; - } - } + $event = new DataUploadEvent($file['tmp_name'], $metadata); + send_event($event); + if ($event->image_id == -1) { + throw new UploadException("File type not supported: " . $metadata['extension']); + } + $page->add_http_header("X-Shimmie-Image-ID: " . $event->image_id); + } catch (UploadException $ex) { + $this->theme->display_upload_error( + $page, + "Error with " . html_escape($file['name']), + $ex->getMessage() + ); + $ok = false; + } + } - unlink($tmp_filename); + return $ok; + } - return $ok; - } -// }}} + private function try_transload(string $url, array $tags, string $source = null, int $replace = -1): bool + { + global $page, $config, $user; + + $ok = true; + + // Checks if user is admin > check if you want locked. + if ($user->can(Permissions::EDIT_IMAGE_LOCK) && !empty($_GET['locked'])) { + $locked = bool_escape($_GET['locked']); + } + + // Checks if url contains rating, also checks if the rating extension is enabled. + if ($config->get_string("transload_engine", "none") != "none" && Extension::is_enabled(RatingsInfo::KEY) && !empty($_GET['rating'])) { + // Rating event will validate that this is s/q/e/u + $rating = strtolower($_GET['rating']); + $rating = $rating[0]; + } else { + $rating = ""; + } + + $tmp_filename = tempnam(ini_get('upload_tmp_dir'), "shimmie_transload"); + + // transload() returns Array or Bool, depending on the transload_engine. + $headers = transload($url, $tmp_filename); + + $s_filename = is_array($headers) ? findHeader($headers, 'Content-Disposition') : null; + $h_filename = ($s_filename ? preg_replace('/^.*filename="([^ ]+)"/i', '$1', $s_filename) : null); + $filename = $h_filename ?: basename($url); + + if (!$headers) { + $this->theme->display_upload_error( + $page, + "Error with " . html_escape($filename), + "Error reading from " . html_escape($url) + ); + return false; + } + + if (filesize($tmp_filename) == 0) { + $this->theme->display_upload_error( + $page, + "Error with " . html_escape($filename), + "No data found -- perhaps the site has hotlink protection?" + ); + $ok = false; + } else { + $pathinfo = pathinfo($url); + $metadata = []; + $metadata['filename'] = $filename; + $metadata['tags'] = $tags; + $metadata['source'] = (($url == $source) && !$config->get_bool('upload_tlsource') ? "" : $source); + + $ext = false; + if (is_array($headers)) { + $ext = get_extension(findHeader($headers, 'Content-Type')); + } + if ($ext === false) { + $ext = $pathinfo['extension']; + } + $metadata['extension'] = $ext; + + /* check for locked > adds to metadata if it has */ + if (!empty($locked)) { + $metadata['locked'] = $locked ? "on" : ""; + } + + /* check for rating > adds to metadata if it has */ + if (!empty($rating)) { + $metadata['rating'] = $rating; + } + + /* check if we have been given an image ID to replace */ + if ($replace >= 0) { + $metadata['replace'] = $replace; + } + + try { + $event = new DataUploadEvent($tmp_filename, $metadata); + send_event($event); + if ($event->image_id == -1) { + throw new UploadException("File type not supported: " . $metadata['extension']); + } + } catch (UploadException $ex) { + $this->theme->display_upload_error( + $page, + "Error with " . html_escape($url), + $ex->getMessage() + ); + $ok = false; + } + } + + unlink($tmp_filename); + + return $ok; + } } - diff --git a/ext/upload/test.php b/ext/upload/test.php index b1044202..af57c960 100644 --- a/ext/upload/test.php +++ b/ext/upload/test.php @@ -1,47 +1,48 @@ -log_in_as_user(); +log_in_as_user(); - $this->get_page("upload"); - $this->assert_title("Upload"); - } + $this->get_page("upload"); + $this->assert_title("Upload"); + } - public function testUpload() { - $this->log_in_as_user(); - $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - } + public function testUpload() + { + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + $this->assertGreaterThan(0, $image_id); + } - public function testRejectDupe() { - $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + public function testRejectDupe() + { + $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - try { - $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - } - catch(UploadException $e) { - $this->assertContains("already has hash", $e->getMessage()); - } - } + try { + $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + } catch (UploadException $e) { + $this->assertStringContainsString("already has hash", $e->getMessage()); + } + } - public function testRejectUnknownFiletype() { - try { - $this->post_image("index.php", "test"); - } - catch(UploadException $e) { - $this->assertContains("Invalid or corrupted file", $e->getMessage()); - } - } + public function testRejectUnknownFiletype() + { + $image_id = $this->post_image("index.php", "test"); + $this->assertEquals(-1, $image_id); // no file handler claimed this + } - public function testRejectHuge() { - $this->markTestIncomplete(); - - // FIXME: huge.dat is rejected for other reasons; manual testing shows that this works - file_put_contents("huge.dat", file_get_contents("tests/pbx_screenshot.jpg") . str_repeat("U", 1024*1024*3)); - $this->post_image("index.php", "test"); - $this->assert_response(200); - $this->assert_title("Upload Status"); - $this->assert_text("File too large"); - unlink("huge.dat"); - } + public function testRejectHuge() + { + // FIXME: huge.dat is rejected for other reasons; manual testing shows that this works + file_put_contents("data/huge.jpg", file_get_contents("tests/pbx_screenshot.jpg") . str_repeat("U", 1024*1024*3)); + try { + $this->post_image("data/huge.jpg", "test"); + $this->assertTrue(false, "Uploading huge.jpg didn't fail..."); + } catch (UploadException $e) { + $this->assertEquals("File too large (3.0MB > 1.0MB)", $e->error); + } + unlink("data/huge.jpg"); + } } - diff --git a/ext/upload/theme.php b/ext/upload/theme.php index 3dba5c72..f8e74712 100644 --- a/ext/upload/theme.php +++ b/ext/upload/theme.php @@ -1,53 +1,56 @@ -add_block(new Block("Upload", $this->build_upload_block(), "left", 20)); - } +class UploadTheme extends Themelet +{ + public function display_block(Page $page) + { + $page->add_block(new Block("Upload", $this->build_upload_block(), "left", 20)); + } - public function display_full(Page $page) { - $page->add_block(new Block("Upload", "Disk nearly full, uploads disabled", "left", 20)); - } + public function display_full(Page $page) + { + $page->add_block(new Block("Upload", "Disk nearly full, uploads disabled", "left", 20)); + } - public function display_page(Page $page) { - global $config, $page; + public function display_page(Page $page) + { + global $config, $page; - $tl_enabled = ($config->get_string("transload_engine", "none") != "none"); - $max_size = $config->get_int('upload_size'); - $max_kb = to_shorthand_int($max_size); - $upload_list = $this->h_upload_list_1(); - $html = " - ".make_form(make_link("upload"), "POST", $multipart=True, 'file_upload')." + $tl_enabled = ($config->get_string("transload_engine", "none") != "none"); + $max_size = $config->get_int('upload_size'); + $max_kb = to_shorthand_int($max_size); + $upload_list = $this->h_upload_list_1(); + $html = " + ".make_form(make_link("upload"), "POST", $multipart=true, 'file_upload')." + + $upload_list - -
    Common Tags
    Common Source
    Tags
    Source
    (Max file size is $max_kb) "; - - $page->set_title("Upload"); - $page->set_heading("Upload"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Upload", $html, "main", 20)); - if($tl_enabled) { - $page->add_block(new Block("Bookmarklets", $this->h_bookmarklets(), "left", 20)); - } - } - /** - * @return string - */ - protected function h_upload_list_1() { - global $config; - $upload_list = ""; - $upload_count = $config->get_int('upload_count'); - $tl_enabled = ($config->get_string("transload_engine", "none") != "none"); + $page->set_title("Upload"); + $page->set_heading("Upload"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Upload", $html, "main", 20)); + if ($tl_enabled) { + $page->add_block(new Block("Bookmarklets", $this->h_bookmarklets(), "left", 20)); + } + } - if($tl_enabled) { - $upload_list .= " + protected function h_upload_list_1(): string + { + global $config; + $upload_list = ""; + $upload_count = $config->get_int('upload_count'); + $tl_enabled = ($config->get_string("transload_engine", "none") != "none"); + $accept = $this->get_accept(); + + if ($tl_enabled) { + $upload_list .= " Files URLs @@ -55,151 +58,48 @@ class UploadTheme extends Themelet { "; - for($i=0; $i<$upload_count; $i++) { - $upload_list .= " + for ($i=0; $i<$upload_count; $i++) { + $upload_list .= " - + "; - } - } - else { - $upload_list .= " + } + } else { + $upload_list .= " Files Image-Specific Tags "; - for($i=0; $i<$upload_count; $i++) { - $upload_list .= " + for ($i=0; $i<$upload_count; $i++) { + $upload_list .= " - + "; - } - } + } + } - return $upload_list; - } + return $upload_list; + } - /** - * @return string - */ - protected function h_upload_List_2() { - global $config; + protected function h_bookmarklets(): string + { + global $config; + $link = make_http(make_link("upload")); + $main_page = make_http(make_link()); + $title = $config->get_string(SetupConfig::TITLE); + $max_size = $config->get_int('upload_size'); + $max_kb = to_shorthand_int($max_size); + $delimiter = $config->get_bool('nice_urls') ? '?' : '&'; + $html = ''; - $tl_enabled = ($config->get_string("transload_engine", "none") != "none"); - // Uploader 2.0! - $upload_list = ""; - $upload_count = $config->get_int('upload_count'); - - for($i=0; $i<$upload_count; $i++) { - $a = $i+1; - $s = $i-1; - - if($i != 0) { - $upload_list .=""; - }else{ - $upload_list .= ""; - } - - $upload_list .= ""; - - if($i == 0) { - $js = 'javascript:$(function() { - $("#row'.$a.'").show(); - $("#hide'.$i.'").hide(); - $("#hide'.$a.'").show();});'; - - $upload_list .= " -
    - - -
    - "; - } else { - $js = 'javascript:$(function() { - $("#row'.$i.'").hide(); - $("#hide'.$i.'").hide(); - $("#hide'.$s.'").show(); - $("#data'.$i.'").val(""); - $("#url'.$i.'").val(""); - });'; - - $upload_list .=" -
    - - "; - - if($a == $upload_count){ - $upload_list .=""; - } - else{ - $js1 = 'javascript:$(function() { - $("#row'.$a.'").show(); - $("#hide'.$i.'").hide(); - $("#hide'.$a.'").show(); });'; - - $upload_list .= - "". - ""; - } - $upload_list .= "
    "; - } - $upload_list .= ""; - - $js2 = 'javascript:$(function() { - $("#url'.$i.'").hide(); - $("#url'.$i.'").val(""); - $("#data'.$i.'").show(); });'; - - $upload_list .= " -
    File
    "; - - if($tl_enabled) { - $js = 'javascript:$(function() { - $("#data'.$i.'").hide(); - $("#data'.$i.'").val(""); - $("#url'.$i.'").show(); });'; - - $upload_list .= - " URL
    - - - - "; - } else { - $upload_list .= " - - "; - } - - $upload_list .= " - - "; - } - - return $upload_list; - } - - /** - * @return string - */ - protected function h_bookmarklets() { - global $config; - $link = make_http(make_link("upload")); - $main_page = make_http(make_link()); - $title = $config->get_string('title'); - $max_size = $config->get_int('upload_size'); - $max_kb = to_shorthand_int($max_size); - $delimiter = $config->get_bool('nice_urls') ? '?' : '&'; - $html = ''; - - $js='javascript:( + $js='javascript:( function() { if(typeof window=="undefined" || !window.location || window.location.href=="about:blank") { window.location = "'. $main_page .'"; @@ -219,19 +119,29 @@ class UploadTheme extends Themelet { } } )();'; - $html .= 'Upload to '.$title.''; - $html .= ' (Drag & drop onto your bookmarks toolbar, then click when looking at an image)'; + $html .= 'Upload to '.$title.''; + $html .= ' (Drag & drop onto your bookmarks toolbar, then click when looking at an image)'; - // Bookmarklet checks if shimmie supports ext. If not, won't upload to site/shows alert saying not supported. - $supported_ext = "jpg jpeg gif png"; - if(class_exists("FlashFileHandler")){$supported_ext .= " swf";} - if(class_exists("ICOFileHandler")){$supported_ext .= " ico ani cur";} - if(class_exists("MP3FileHandler")){$supported_ext .= " mp3";} - if(class_exists("SVGFileHandler")){$supported_ext .= " svg";} - if(class_exists("VideoFileHandler")){$supported_ext .= " flv mp4 ogv webm m4v";} - $title = "Booru to " . $config->get_string('title'); - // CA=0: Ask to use current or new tags | CA=1: Always use current tags | CA=2: Always use new tags - $html .= '

    get_string(SetupConfig::TITLE); + // CA=0: Ask to use current or new tags | CA=1: Always use current tags | CA=2: Always use new tags + $html .= '

    '. $title . ' (Click when looking at an image page. Works on sites running Shimmie / Danbooru / Gelbooru. (This also grabs the tags / rating / source!))'; - return $html; - } + return $html; + } - /** - * Only allows 1 file to be uploaded - for replacing another image file. - * - * @param Page $page - * @param int $image_id - */ - public function display_replace_page(Page $page, /*int*/ $image_id) { - global $config, $page; - $tl_enabled = ($config->get_string("transload_engine", "none") != "none"); + /** + * Only allows 1 file to be uploaded - for replacing another image file. + */ + public function display_replace_page(Page $page, int $image_id) + { + global $config, $page; + $tl_enabled = ($config->get_string("transload_engine", "none") != "none"); + $accept = $this->get_accept(); - $upload_list = " - - File - - "; - if($tl_enabled) { - $upload_list .=" - or URL - + $upload_list = " + + File + + + "; + if ($tl_enabled) { + $upload_list .=" + + or URL + + "; - } - $upload_list .= ""; + } - $max_size = $config->get_int('upload_size'); - $max_kb = to_shorthand_int($max_size); - - $image = Image::by_id($image_id); - $thumbnail = $this->build_thumb_html($image); - - $html = " + $max_size = $config->get_int('upload_size'); + $max_kb = to_shorthand_int($max_size); + + $image = Image::by_id($image_id); + $thumbnail = $this->build_thumb_html($image); + + $html = "

    Replacing Image ID ".$image_id."
    Please note: You will have to refresh the image page, or empty your browser cache.

    " - .$thumbnail."
    " - .make_form(make_link("upload/replace/".$image_id), "POST", $multipart=True)." + .$thumbnail."
    " + .make_form(make_link("upload/replace/".$image_id), "POST", $multipart=true)." $upload_list @@ -285,57 +196,54 @@ class UploadTheme extends Themelet { (Max file size is $max_kb) "; - $page->set_title("Replace Image"); - $page->set_heading("Replace Image"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Upload Replacement Image", $html, "main", 20)); - } + $page->set_title("Replace Image"); + $page->set_heading("Replace Image"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Upload Replacement Image", $html, "main", 20)); + } - /** - * @param Page $page - * @param bool $ok - */ - public function display_upload_status(Page $page, /*bool*/ $ok) { - if($ok) { - $page->set_mode("redirect"); - $page->set_redirect(make_link()); - } - else { - $page->set_title("Upload Status"); - $page->set_heading("Upload Status"); - $page->add_block(new NavBlock()); - } - } + public function display_upload_status(Page $page, bool $ok) + { + if ($ok) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link()); + } else { + $page->set_title("Upload Status"); + $page->set_heading("Upload Status"); + $page->add_block(new NavBlock()); + } + } - /** - * @param Page $page - * @param string $title - * @param string $message - */ - public function display_upload_error(Page $page, /*string*/ $title, /*string*/ $message) { - $page->add_block(new Block($title, $message)); - } + public function display_upload_error(Page $page, string $title, string $message) + { + // this message has intentional HTML in it... + $message = strpos($message, "already has hash") ? $message : html_escape($message); + $page->add_block(new Block($title, $message)); + } - /** - * @return string - */ - protected function build_upload_block() { - global $config; + protected function build_upload_block(): string + { + global $config; - $upload_list = ""; - $upload_count = $config->get_int('upload_count'); - - for($i=0; $i<$upload_count; $i++) { - if($i == 0) $style = ""; // "style='display:visible'"; - else $style = "style='display:none'"; - $upload_list .= "\n"; - } - $max_size = $config->get_int('upload_size'); - $max_kb = to_shorthand_int($max_size); - // - return " + $upload_list = ""; + $upload_count = $config->get_int('upload_count'); + $accept = $this->get_accept(); + + for ($i=0; $i<$upload_count; $i++) { + if ($i == 0) { + $style = ""; + } // "style='display:visible'"; + else { + $style = "style='display:none'"; + } + $upload_list .= "\n"; + } + $max_size = $config->get_int('upload_size'); + $max_kb = to_shorthand_int($max_size); + // + return "
    - ".make_form(make_link("upload"), "POST", $multipart=True)." + ".make_form(make_link("upload"), "POST", $multipart=true)." $upload_list @@ -344,6 +252,10 @@ class UploadTheme extends Themelet {
    "; - } -} + } + protected function get_accept() + { + return join(",", DataHandlerExtension::get_all_supported_exts()); + } +} diff --git a/ext/user/events.php b/ext/user/events.php new file mode 100644 index 00000000..c9171df6 --- /dev/null +++ b/ext/user/events.php @@ -0,0 +1,88 @@ +parts[$position])) { + $position++; + } + $this->parts[$position] = ["name" => $name, "link" => $link]; + } +} + +class UserOptionsBuildingEvent extends Event +{ + /** @var array */ + public $parts = []; + + public function add_html(string $html) + { + $this->parts[] = $html; + } +} + +class UserPageBuildingEvent extends Event +{ + /** @var User */ + public $display_user; + /** @var array */ + public $stats = []; + + public function __construct(User $display_user) + { + parent::__construct(); + $this->display_user = $display_user; + } + + public function add_stats(string $html, int $position=50) + { + while (isset($this->stats[$position])) { + $position++; + } + $this->stats[$position] = $html; + } +} + +class UserCreationEvent extends Event +{ + /** @var string */ + public $username; + /** @var string */ + public $password; + /** @var string */ + public $email; + + public function __construct(string $name, string $pass, string $email) + { + parent::__construct(); + $this->username = $name; + $this->password = $pass; + $this->email = $email; + } +} + +class UserLoginEvent extends Event +{ + public $user; + public function __construct(User $user) + { + parent::__construct(); + $this->user = $user; + } +} + +class UserDeletionEvent extends Event +{ + /** @var int */ + public $id; + + public function __construct(int $id) + { + parent::__construct(); + $this->id = $id; + } +} diff --git a/ext/user/info.php b/ext/user/info.php new file mode 100644 index 00000000..2e29dc15 --- /dev/null +++ b/ext/user/info.php @@ -0,0 +1,12 @@ +parts[$position])) $position++; - $this->parts[$position] = array("name" => $name, "link" => $link); - } +use function MicroHTML\A; +use MicroCRUD\ActionColumn; +use MicroCRUD\EnumColumn; +use MicroCRUD\IntegerColumn; +use MicroCRUD\TextColumn; +use MicroCRUD\DateColumn; +use MicroCRUD\Table; + +class UserNameColumn extends TextColumn +{ + public function display(array $row) + { + return A(["href"=>make_link("user/{$row[$this->name]}")], $row[$this->name]); + } } -class UserPageBuildingEvent extends Event { - /** @var \User */ - public $display_user; - /** @var array */ - public $stats = array(); +class UserActionColumn extends ActionColumn +{ + public function __construct() + { + parent::__construct("id", "User Links"); + $this->sortable = false; + } - /** - * @param User $display_user - */ - public function __construct(User $display_user) { - $this->display_user = $display_user; - } - - /** - * @param string $html - * @param int $position - */ - public function add_stats($html, $position=50) { - while(isset($this->stats[$position])) { $position++; } - $this->stats[$position] = $html; - } + public function display(array $row) + { + return A(["href"=>make_link("post/list/user={$row['name']}/1")], "Posts"); + } } -class UserCreationEvent extends Event { - /** @var string */ - public $username; - /** @var string */ - public $password; - /** @var string */ - public $email; - - /** - * @param string $name - * @param string $pass - * @param string $email - */ - public function __construct($name, $pass, $email) { - $this->username = $name; - $this->password = $pass; - $this->email = $email; - } +class UserTable extends Table +{ + public function __construct(\FFSPHP\PDO $db) + { + global $_shm_user_classes; + $classes = []; + foreach ($_shm_user_classes as $cls) { + $classes[$cls->name] = $cls->name; + } + ksort($classes); + parent::__construct($db); + $this->table = "users"; + $this->base_query = "SELECT * FROM users"; + $this->size = 100; + $this->limit = 1000000; + $this->set_columns([ + new IntegerColumn("id", "ID"), + new UserNameColumn("name", "Name"), + new EnumColumn("class", "Class", $classes), + // Added later, for admins only + // new TextColumn("email", "Email"), + new DateColumn("joindate", "Join Date"), + new UserActionColumn(), + ]); + $this->order_by = ["id DESC"]; + $this->table_attrs = ["class" => "zebra"]; + } } -class UserDeletionEvent extends Event { - /** @var int */ - public $id; - - /** - * @param int $id - */ - public function __construct($id) { - $this->id = $id; - } +class UserCreationException extends SCoreException +{ } -class UserCreationException extends SCoreException {} +class NullUserException extends SCoreException +{ +} -class NullUserException extends SCoreException {} +class UserPage extends Extension +{ + /** @var UserPageTheme $theme */ + public $theme; -class UserPage extends Extension { - /** @var UserPageTheme $theme */ - public $theme; + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_bool("login_signup_enabled", true); + $config->set_default_int("login_memory", 365); + $config->set_default_string("avatar_host", "none"); + $config->set_default_int("avatar_gravatar_size", 80); + $config->set_default_string("avatar_gravatar_default", ""); + $config->set_default_string("avatar_gravatar_rating", "g"); + $config->set_default_bool("login_tac_bbcode", true); + } - public function onInitExt(InitExtEvent $event) { - global $config; - $config->set_default_bool("login_signup_enabled", true); - $config->set_default_int("login_memory", 365); - $config->set_default_string("avatar_host", "none"); - $config->set_default_int("avatar_gravatar_size", 80); - $config->set_default_string("avatar_gravatar_default", ""); - $config->set_default_string("avatar_gravatar_rating", "g"); - $config->set_default_bool("login_tac_bbcode", true); - } + public function onUserLogin(UserLoginEvent $event) + { + global $user; + $user = $event->user; + } - public function onPageRequest(PageRequestEvent $event) { - global $config, $database, $page, $user; + public function onPageRequest(PageRequestEvent $event) + { + global $config, $database, $page, $user; - $this->show_user_info(); + $this->show_user_info(); - if($event->page_matches("user_admin")) { - if($event->get_arg(0) == "login") { - if(isset($_POST['user']) && isset($_POST['pass'])) { - $this->page_login($_POST['user'], $_POST['pass']); - } - else { - $this->theme->display_login_page($page); - } - } - else if($event->get_arg(0) == "recover") { - $this->page_recover($_POST['username']); - } - else if($event->get_arg(0) == "create") { - $this->page_create(); - } - else if($event->get_arg(0) == "list") { - $offset = 0; - $limit = 50; + if ($event->page_matches("user_admin")) { + if ($event->get_arg(0) == "login") { + if (isset($_POST['user']) && isset($_POST['pass'])) { + $this->page_login($_POST['user'], $_POST['pass']); + } else { + $this->theme->display_login_page($page); + } + } elseif ($event->get_arg(0) == "recover") { + $this->page_recover($_POST['username']); + } elseif ($event->get_arg(0) == "create") { + $this->page_create(); + } elseif ($event->get_arg(0) == "list") { + $t = new UserTable($database->raw_db()); + $t->token = $user->get_auth_token(); + $t->inputs = $_GET; + if ($user->can(Permissions::DELETE_USER)) { + $col = new TextColumn("email", "Email"); + // $t->columns[] = $col; + array_splice($t->columns, 2, 0, [$col]); + } + $this->theme->display_user_list($page, $t->table($t->query()), $t->paginator()); + } elseif ($event->get_arg(0) == "logout") { + $this->page_logout(); + } - $q = "SELECT * FROM users WHERE 1=1"; - $a = array("offset"=>$offset, "limit"=>$limit); + if (!$user->check_auth_token()) { + return; + } elseif ($event->get_arg(0) == "change_name") { + $input = validate_input([ + 'id' => 'user_id,exists', + 'name' => 'user_name', + ]); + $duser = User::by_id($input['id']); + $this->change_name_wrapper($duser, $input['name']); + } elseif ($event->get_arg(0) == "change_pass") { + $input = validate_input([ + 'id' => 'user_id,exists', + 'pass1' => 'password', + 'pass2' => 'password', + ]); + $duser = User::by_id($input['id']); + $this->change_password_wrapper($duser, $input['pass1'], $input['pass2']); + } elseif ($event->get_arg(0) == "change_email") { + $input = validate_input([ + 'id' => 'user_id,exists', + 'address' => 'email', + ]); + $duser = User::by_id($input['id']); + $this->change_email_wrapper($duser, $input['address']); + } elseif ($event->get_arg(0) == "change_class") { + $input = validate_input([ + 'id' => 'user_id,exists', + 'class' => 'user_class', + ]); + $duser = User::by_id($input['id']); + $this->change_class_wrapper($duser, $input['class']); + } elseif ($event->get_arg(0) == "delete_user") { + $this->delete_user($page, isset($_POST["with_images"]), isset($_POST["with_comments"])); + } + } - if(@$_GET['username']) { - $q .= " AND SCORE_STRNORM(name) LIKE SCORE_STRNORM(:name)"; - $a["name"] = '%' . $_GET['username'] . '%'; - } + if ($event->page_matches("user")) { + $display_user = ($event->count_args() == 0) ? $user : User::by_name($event->get_arg(0)); + if ($event->count_args() == 0 && $user->is_anonymous()) { + $this->theme->display_error( + 401, + "Not Logged In", + "You aren't logged in. First do that, then you can see your stats." + ); + } elseif (!is_null($display_user) && ($display_user->id != $config->get_int("anon_id"))) { + $e = new UserPageBuildingEvent($display_user); + send_event($e); + $this->display_stats($e); + } else { + $this->theme->display_error( + 404, + "No Such User", + "If you typed the ID by hand, try again; if you came from a link on this ". + "site, it might be bug report time..." + ); + } + } + } - if($user->can('delete_user') && @$_GET['email']) { - $q .= " AND SCORE_STRNORM(name) LIKE SCORE_STRNORM(:email)"; - $a["email"] = '%' . $_GET['email'] . '%'; - } + public function onUserPageBuilding(UserPageBuildingEvent $event) + { + global $user, $config; - if(@$_GET['class']) { - $q .= " AND class LIKE :class"; - $a["class"] = $_GET['class']; - } + $h_join_date = autodate($event->display_user->join_date); + if ($event->display_user->can(Permissions::HELLBANNED)) { + $h_class = $event->display_user->class->parent->name; + } else { + $h_class = $event->display_user->class->name; + } - $q .= " LIMIT :limit OFFSET :offset"; + $event->add_stats("Joined: $h_join_date", 10); + if ($user->name == $event->display_user->name) { + $event->add_stats("Current IP: {$_SERVER['REMOTE_ADDR']}", 80); + } + $event->add_stats("Class: $h_class", 90); - $rows = $database->get_all($database->scoreql_to_sql($q), $a); - $users = array_map("_new_user", $rows); - $this->theme->display_user_list($page, $users, $user); - } - else if($event->get_arg(0) == "logout") { - $this->page_logout(); - } + $av = $event->display_user->get_avatar_html(); + if ($av) { + $event->add_stats($av, 0); + } elseif (( + $config->get_string("avatar_host") == "gravatar" + ) && + ($user->id == $event->display_user->id) + ) { + $event->add_stats( + "No avatar? This gallery uses Gravatar for avatar hosting, use the". + "
    same email address here and there to have your avatar synced
    ", + 0 + ); + } + } - if(!$user->check_auth_token()) { - return; - } + public function onPageNavBuilding(PageNavBuildingEvent $event) + { + global $user; + if ($user->is_anonymous()) { + $event->add_nav_link("user", new Link('user_admin/login'), "Account", null, 10); + } else { + $event->add_nav_link("user", new Link('user'), "Account", null, 10); + } + } - else if($event->get_arg(0) == "change_name") { - $input = validate_input(array( - 'id' => 'user_id,exists', - 'name' => 'user_name', - )); - $duser = User::by_id($input['id']); - $this->change_name_wrapper($duser, $input['name']); - } - else if($event->get_arg(0) == "change_pass") { - $input = validate_input(array( - 'id' => 'user_id,exists', - 'pass1' => 'password', - 'pass2' => 'password', - )); - $duser = User::by_id($input['id']); - $this->change_password_wrapper($duser, $input['pass1'], $input['pass2']); - } - else if($event->get_arg(0) == "change_email") { - $input = validate_input(array( - 'id' => 'user_id,exists', - 'address' => 'email', - )); - $duser = User::by_id($input['id']); - $this->change_email_wrapper($duser, $input['address']); - } - else if($event->get_arg(0) == "change_class") { - $input = validate_input(array( - 'id' => 'user_id,exists', - 'class' => 'user_class', - )); - $duser = User::by_id($input['id']); - $this->change_class_wrapper($duser, $input['class']); - } - else if($event->get_arg(0) == "delete_user") { - $this->delete_user($page, isset($_POST["with_images"]), isset($_POST["with_comments"])); - } - } + private function display_stats(UserPageBuildingEvent $event) + { + global $user, $page, $config; - if($event->page_matches("user")) { - $display_user = ($event->count_args() == 0) ? $user : User::by_name($event->get_arg(0)); - if($event->count_args() == 0 && $user->is_anonymous()) { - $this->theme->display_error(401, "Not Logged In", - "You aren't logged in. First do that, then you can see your stats."); - } - else if(!is_null($display_user) && ($display_user->id != $config->get_int("anon_id"))) { - $e = new UserPageBuildingEvent($display_user); - send_event($e); - $this->display_stats($e); - } - else { - $this->theme->display_error(404, "No Such User", - "If you typed the ID by hand, try again; if you came from a link on this ". - "site, it might be bug report time..."); - } - } - } + ksort($event->stats); + $this->theme->display_user_page($event->display_user, $event->stats); - /** - * @param UserPageBuildingEvent $event - */ - public function onUserPageBuilding(UserPageBuildingEvent $event) { - global $user, $config; + if (!$user->is_anonymous()) { + if ($user->id == $event->display_user->id || $user->can("edit_user_info")) { + $uobe = new UserOptionsBuildingEvent(); + send_event($uobe); - $h_join_date = autodate($event->display_user->join_date); - if($event->display_user->can("hellbanned")) { - $h_class = $event->display_user->class->parent->name; - } - else { - $h_class = $event->display_user->class->name; - } + $page->add_block(new Block("Options", $this->theme->build_options($event->display_user, $uobe), "main", 60)); + } + } - $event->add_stats("Joined: $h_join_date", 10); - $event->add_stats("Class: $h_class", 90); + if ($user->id == $event->display_user->id) { + $ubbe = new UserBlockBuildingEvent(); + send_event($ubbe); + ksort($ubbe->parts); + $this->theme->display_user_links($page, $user, $ubbe->parts); + } + if ( + ($user->can(Permissions::VIEW_IP) || ($user->is_logged_in() && $user->id == $event->display_user->id)) && # admin or self-user + ($event->display_user->id != $config->get_int('anon_id')) # don't show anon's IP list, it is le huge + ) { + $this->theme->display_ip_list( + $page, + $this->count_upload_ips($event->display_user), + $this->count_comment_ips($event->display_user), + $this->count_log_ips($event->display_user) + ); + } + } - $av = $event->display_user->get_avatar_html(); - if($av) { - $event->add_stats($av, 0); - } - else if(( - $config->get_string("avatar_host") == "gravatar") && - ($user->id == $event->display_user->id) - ) { - $event->add_stats( - "No avatar? This gallery uses Gravatar for avatar hosting, use the". - "
    same email address here and there to have your avatar synced
    ", - 0 - ); - } - } + public function onSetupBuilding(SetupBuildingEvent $event) + { + global $config; - /** - * @param UserPageBuildingEvent $event - */ - private function display_stats(UserPageBuildingEvent $event) { - global $user, $page, $config; + $hosts = [ + "None" => "none", + "Gravatar" => "gravatar" + ]; - ksort($event->stats); - $this->theme->display_user_page($event->display_user, $event->stats); - if($user->id == $event->display_user->id) { - $ubbe = new UserBlockBuildingEvent(); - send_event($ubbe); - ksort($ubbe->parts); - $this->theme->display_user_links($page, $user, $ubbe->parts); - } - if( - ($user->can("view_ip") || ($user->is_logged_in() && $user->id == $event->display_user->id)) && # admin or self-user - ($event->display_user->id != $config->get_int('anon_id')) # don't show anon's IP list, it is le huge - ) { - $this->theme->display_ip_list( - $page, - $this->count_upload_ips($event->display_user), - $this->count_comment_ips($event->display_user)); - } - } + $sb = new SetupBlock("User Options"); + $sb->add_bool_option("login_signup_enabled", "Allow new signups: "); + $sb->add_longtext_option("login_tac", "
    Terms & Conditions:
    "); + $sb->add_choice_option("avatar_host", $hosts, "
    Avatars: "); - /** - * @param SetupBuildingEvent $event - */ - public function onSetupBuilding(SetupBuildingEvent $event) { - global $config; + if ($config->get_string("avatar_host") == "gravatar") { + $sb->add_label("
     
    Gravatar Options"); + $sb->add_choice_option( + "avatar_gravatar_type", + [ + 'Default'=>'default', + 'Wavatar'=>'wavatar', + 'Monster ID'=>'monsterid', + 'Identicon'=>'identicon' + ], + "
    Type: " + ); + $sb->add_choice_option( + "avatar_gravatar_rating", + ['G'=>'g', 'PG'=>'pg', 'R'=>'r', 'X'=>'x'], + "
    Rating: " + ); + } - $hosts = array( - "None" => "none", - "Gravatar" => "gravatar" - ); + $sb->add_choice_option( + "user_loginshowprofile", + [ + "return to previous page" => 0, // 0 is default + "send to user profile" => 1], + "
    When user logs in/out" + ); + $event->panel->add_block($sb); + } - $sb = new SetupBlock("User Options"); - $sb->add_bool_option("login_signup_enabled", "Allow new signups: "); - $sb->add_longtext_option("login_tac", "
    Terms & Conditions:
    "); - $sb->add_choice_option("avatar_host", $hosts, "
    Avatars: "); + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::EDIT_USER_PASSWORD)) { + $event->add_nav_link("user_admin", new Link('user_admin/list'), "User List", NavLink::is_active(["user_admin"])); + } + } + } - if($config->get_string("avatar_host") == "gravatar") { - $sb->add_label("
     
    Gravatar Options"); - $sb->add_choice_option("avatar_gravatar_type", - array( - 'Default'=>'default', - 'Wavatar'=>'wavatar', - 'Monster ID'=>'monsterid', - 'Identicon'=>'identicon' - ), - "
    Type: "); - $sb->add_choice_option("avatar_gravatar_rating", - array('G'=>'g', 'PG'=>'pg', 'R'=>'r', 'X'=>'x'), - "
    Rating: "); - } + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + $event->add_link("My Profile", make_link("user")); + if ($user->can(Permissions::EDIT_USER_PASSWORD)) { + $event->add_link("User List", make_link("user_admin/list"), 98); + } + $event->add_link("Log Out", make_link("user_admin/logout"), 99); + } - $sb->add_choice_option("user_loginshowprofile", array( - "return to previous page" => 0, // 0 is default - "send to user profile" => 1), - "
    When user logs in/out"); - $event->panel->add_block($sb); - } + public function onUserCreation(UserCreationEvent $event) + { + $this->check_user_creation($event); + $this->create_user($event); + } - /** - * @param UserBlockBuildingEvent $event - */ - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - $event->add_link("My Profile", make_link("user")); - if($user->can("edit_user_class")) { - $event->add_link("User List", make_link("user_admin/list"), 98); - } - $event->add_link("Log Out", make_link("user_admin/logout"), 99); - } + public function onSearchTermParse(SearchTermParseEvent $event) + { + global $user; - /** - * @param UserCreationEvent $event - */ - public function onUserCreation(UserCreationEvent $event) { - $this->check_user_creation($event); - $this->create_user($event); - } + if (is_null($event->term)) { + return; + } - /** - * @param SearchTermParseEvent $event - */ - public function onSearchTermParse(SearchTermParseEvent $event) { - global $user; + $matches = []; + if (preg_match("/^(?:poster|user)[=|:](.*)$/i", $event->term, $matches)) { + $user_id = User::name_to_id($matches[1]); + $event->add_querylet(new Querylet("images.owner_id = $user_id")); + } elseif (preg_match("/^(?:poster|user)_id[=|:]([0-9]+)$/i", $event->term, $matches)) { + $user_id = int_escape($matches[1]); + $event->add_querylet(new Querylet("images.owner_id = $user_id")); + } elseif ($user->can(Permissions::VIEW_IP) && preg_match("/^(?:poster|user)_ip[=|:]([0-9\.]+)$/i", $event->term, $matches)) { + $user_ip = $matches[1]; // FIXME: ip_escape? + $event->add_querylet(new Querylet("images.owner_ip = '$user_ip'")); + } + } - $matches = array(); - if(preg_match("/^(?:poster|user)[=|:](.*)$/i", $event->term, $matches)) { - $duser = User::by_name($matches[1]); - if(!is_null($duser)) { - $user_id = $duser->id; - } - else { - $user_id = -1; - } - $event->add_querylet(new Querylet("images.owner_id = $user_id")); - } - else if(preg_match("/^(?:poster|user)_id[=|:]([0-9]+)$/i", $event->term, $matches)) { - $user_id = int_escape($matches[1]); - $event->add_querylet(new Querylet("images.owner_id = $user_id")); - } - else if($user->can("view_ip") && preg_match("/^(?:poster|user)_ip[=|:]([0-9\.]+)$/i", $event->term, $matches)) { - $user_ip = $matches[1]; // FIXME: ip_escape? - $event->add_querylet(new Querylet("images.owner_ip = '$user_ip'")); - } - } - - private function show_user_info() { - global $user, $page; - // user info is shown on all pages - if ($user->is_anonymous()) { - $this->theme->display_login_block($page); - } else { - $ubbe = new UserBlockBuildingEvent(); - send_event($ubbe); - ksort($ubbe->parts); - $this->theme->display_user_block($page, $user, $ubbe->parts); - } - } -// }}} -// Things done *with* the user {{{ - private function page_login($name, $pass) { - global $config, $user, $page; + public function onHelpPageBuilding(HelpPageBuildingEvent $event) + { + if ($event->key===HelpPages::SEARCH) { + $block = new Block(); + $block->header = "Users"; + $block->body = $this->theme->get_help_html(); + $event->add_block($block); + } + } - if(empty($name) || empty($pass)) { - $this->theme->display_error(400, "Error", "Username or password left blank"); - return; - } + private function show_user_info() + { + global $user, $page; + // user info is shown on all pages + if ($user->is_anonymous()) { + $this->theme->display_login_block($page); + } else { + $ubbe = new UserBlockBuildingEvent(); + send_event($ubbe); + ksort($ubbe->parts); + $this->theme->display_user_block($page, $user, $ubbe->parts); + } + } - $duser = User::by_name_and_pass($name, $pass); - if(!is_null($duser)) { - $user = $duser; - $this->set_login_cookie($duser->name, $pass); - log_info("user", "{$user->class->name} logged in"); - $page->set_mode("redirect"); + private function page_login($name, $pass) + { + global $config, $page; - // Try returning to previous page - if ($config->get_int("user_loginshowprofile",0) == 0 && - isset($_SERVER['HTTP_REFERER']) && - strstr($_SERVER['HTTP_REFERER'], "post/")) - { - $page->set_redirect($_SERVER['HTTP_REFERER']); - } else { - $page->set_redirect(make_link("user")); - } - } - else { - log_warning("user", "Failed to log in as ".html_escape($name)); - $this->theme->display_error(401, "Error", "No user with those details was found"); - } - } + if (empty($name) || empty($pass)) { + $this->theme->display_error(400, "Error", "Username or password left blank"); + return; + } - private function page_logout() { - global $page, $config; - $page->add_cookie("session", "", time() + 60 * 60 * 24 * $config->get_int('login_memory'), "/"); - if (CACHE_HTTP || SPEED_HAX) { - # to keep as few versions of content as possible, - # make cookies all-or-nothing - $page->add_cookie("user", "", time() + 60 * 60 * 24 * $config->get_int('login_memory'), "/"); - } - log_info("user", "Logged out"); - $page->set_mode("redirect"); + $duser = User::by_name_and_pass($name, $pass); + if (!is_null($duser)) { + send_event(new UserLoginEvent($duser)); + $this->set_login_cookie($duser->name, $pass); + $page->set_mode(PageMode::REDIRECT); - // Try forwarding to same page on logout unless user comes from registration page - if ($config->get_int("user_loginshowprofile", 0) == 0 && - isset($_SERVER['HTTP_REFERER']) && - strstr($_SERVER['HTTP_REFERER'], "post/") - ) { - $page->set_redirect($_SERVER['HTTP_REFERER']); - } else { - $page->set_redirect(make_link()); - } - } + // Try returning to previous page + if ($config->get_int("user_loginshowprofile", 0) == 0 && + isset($_SERVER['HTTP_REFERER']) && + strstr($_SERVER['HTTP_REFERER'], "post/")) { + $page->set_redirect($_SERVER['HTTP_REFERER']); + } else { + $page->set_redirect(make_link("user")); + } + } else { + $this->theme->display_error(401, "Error", "No user with those details was found"); + } + } - /** - * @param string $username - */ - private function page_recover($username) { - $user = User::by_name($username); - if (is_null($user)) { - $this->theme->display_error(404, "Error", "There's no user with that name"); - } else if (is_null($user->email)) { - $this->theme->display_error(400, "Error", "That user has no registered email address"); - } else { - // send email - } - } + private function page_logout() + { + global $page, $config; + $page->add_cookie("session", "", time() + 60 * 60 * 24 * $config->get_int('login_memory'), "/"); + if (SPEED_HAX) { + # to keep as few versions of content as possible, + # make cookies all-or-nothing + $page->add_cookie("user", "", time() + 60 * 60 * 24 * $config->get_int('login_memory'), "/"); + } + log_info("user", "Logged out"); + $page->set_mode(PageMode::REDIRECT); - private function page_create() { - global $config, $page; - if (!$config->get_bool("login_signup_enabled")) { - $this->theme->display_signups_disabled($page); - } else if (!isset($_POST['name'])) { - $this->theme->display_signup_page($page); - } else if ($_POST['pass1'] != $_POST['pass2']) { - $this->theme->display_error(400, "Password Mismatch", "Passwords don't match"); - } else { - try { - if (!captcha_check()) { - throw new UserCreationException("Error in captcha"); - } + // Try forwarding to same page on logout unless user comes from registration page + if ($config->get_int("user_loginshowprofile", 0) == 0 && + isset($_SERVER['HTTP_REFERER']) && + strstr($_SERVER['HTTP_REFERER'], "post/") + ) { + $page->set_redirect($_SERVER['HTTP_REFERER']); + } else { + $page->set_redirect(make_link()); + } + } - $uce = new UserCreationEvent($_POST['name'], $_POST['pass1'], $_POST['email']); - send_event($uce); - $this->set_login_cookie($uce->username, $uce->password); - $page->set_mode("redirect"); - $page->set_redirect(make_link("user")); - } catch (UserCreationException $ex) { - $this->theme->display_error(400, "User Creation Error", $ex->getMessage()); - } - } - } + private function page_recover(string $username) + { + $my_user = User::by_name($username); + if (is_null($my_user)) { + $this->theme->display_error(404, "Error", "There's no user with that name"); + } elseif (is_null($my_user->email)) { + $this->theme->display_error(400, "Error", "That user has no registered email address"); + } else { + throw new SCoreException("Email sending not implemented"); + } + } - /** - * @param UserCreationEvent $event - * @throws UserCreationException - */ - private function check_user_creation(UserCreationEvent $event) { - $name = $event->username; - //$pass = $event->password; - //$email = $event->email; + private function page_create() + { + global $config, $page, $user; + if (!$user->can(Permissions::CREATE_USER)) { + $this->theme->display_error(403, "Account creation blocked", "Account creation is currently disabled"); + return; + } - if(strlen($name) < 1) { - throw new UserCreationException("Username must be at least 1 character"); - } - else if(!preg_match('/^[a-zA-Z0-9-_]+$/', $name)) { - throw new UserCreationException( - "Username contains invalid characters. Allowed characters are ". - "letters, numbers, dash, and underscore"); - } - else if(User::by_name($name)) { - throw new UserCreationException("That username is already taken"); - } - } + if (!$config->get_bool("login_signup_enabled")) { + $this->theme->display_signups_disabled($page); + } elseif (!isset($_POST['name'])) { + $this->theme->display_signup_page($page); + } elseif ($_POST['pass1'] != $_POST['pass2']) { + $this->theme->display_error(400, "Password Mismatch", "Passwords don't match"); + } else { + try { + if (!captcha_check()) { + throw new UserCreationException("Error in captcha"); + } - private function create_user(UserCreationEvent $event) { - global $database, $user; + $uce = new UserCreationEvent($_POST['name'], $_POST['pass1'], $_POST['email']); + send_event($uce); + $this->set_login_cookie($uce->username, $uce->password); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("user")); + } catch (UserCreationException $ex) { + $this->theme->display_error(400, "User Creation Error", $ex->getMessage()); + } + } + } - $email = (!empty($event->email)) ? $event->email : null; + private function check_user_creation(UserCreationEvent $event) + { + $name = $event->username; + //$pass = $event->password; + //$email = $event->email; - // if there are currently no admins, the new user should be one - $need_admin = ($database->get_one("SELECT COUNT(*) FROM users WHERE class='admin'") == 0); - $class = $need_admin ? 'admin' : 'user'; + if (strlen($name) < 1) { + throw new UserCreationException("Username must be at least 1 character"); + } elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $name)) { + throw new UserCreationException( + "Username contains invalid characters. Allowed characters are ". + "letters, numbers, dash, and underscore" + ); + } elseif (User::by_name($name)) { + throw new UserCreationException("That username is already taken"); + } + } - $database->Execute( - "INSERT INTO users (name, pass, joindate, email, class) VALUES (:username, :hash, now(), :email, :class)", - array("username"=>$event->username, "hash"=>'', "email"=>$email, "class"=>$class)); - $uid = $database->get_last_insert_id('users_id_seq'); - $user = User::by_name($event->username); - $user->set_password($event->password); - log_info("user", "Created User #$uid ({$event->username})"); - } + private function create_user(UserCreationEvent $event) + { + global $database, $user; - /** - * @param string $name - * @param string $pass - */ - private function set_login_cookie(/*string*/ $name, /*string*/ $pass) { - global $config, $page; + $email = (!empty($event->email)) ? $event->email : null; - $addr = get_session_ip($config); - $hash = User::by_name($name)->passhash; + // if there are currently no admins, the new user should be one + $need_admin = ($database->get_one("SELECT COUNT(*) FROM users WHERE class='admin'") == 0); + $class = $need_admin ? 'admin' : 'user'; - $page->add_cookie("user", $name, - time()+60*60*24*365, '/'); - $page->add_cookie("session", md5($hash.$addr), - time()+60*60*24*$config->get_int('login_memory'), '/'); - } -//}}} -// Things done *to* the user {{{ - /** - * @param User $a - * @param User $b - * @return bool - */ - private function user_can_edit_user(User $a, User $b) { - if($a->is_anonymous()) { - $this->theme->display_error(401, "Error", "You aren't logged in"); - return false; - } + $database->Execute( + "INSERT INTO users (name, pass, joindate, email, class) VALUES (:username, :hash, now(), :email, :class)", + ["username"=>$event->username, "hash"=>'', "email"=>$email, "class"=>$class] + ); + $uid = $database->get_last_insert_id('users_id_seq'); + $user = User::by_name($event->username); + $user->set_password($event->password); + send_event(new UserLoginEvent($user)); - if( - ($a->name == $b->name) || - ($b->can("protected") && $a->class->name == "admin") || - (!$b->can("protected") && $a->can("edit_user_info")) - ) { - return true; - } - else { - $this->theme->display_error(401, "Error", "You need to be an admin to change other people's details"); - return false; - } - } + log_info("user", "Created User #$uid ({$event->username})"); + } - private function redirect_to_user(User $duser) { - global $page, $user; + private function set_login_cookie(string $name, string $pass) + { + global $config, $page; - if($user->id == $duser->id) { - $page->set_mode("redirect"); - $page->set_redirect(make_link("user")); - } - else { - $page->set_mode("redirect"); - $page->set_redirect(make_link("user/{$duser->name}")); - } - } + $addr = get_session_ip($config); + $hash = User::by_name($name)->passhash; - private function change_name_wrapper(User $duser, $name) { - global $user; + $page->add_cookie( + "user", + $name, + time()+60*60*24*365, + '/' + ); + $page->add_cookie( + "session", + md5($hash.$addr), + time()+60*60*24*$config->get_int('login_memory'), + '/' + ); + } - if($user->can('edit_user_name') && $this->user_can_edit_user($user, $duser)) { - $duser->set_name($name); - flash_message("Username changed"); - // TODO: set login cookie if user changed themselves - $this->redirect_to_user($duser); - } - else { - $this->theme->display_error(400, "Error", "Permission denied"); - } - } + private function user_can_edit_user(User $a, User $b): bool + { + if ($a->is_anonymous()) { + $this->theme->display_error(401, "Error", "You aren't logged in"); + return false; + } - /** - * @param User $duser - * @param string $pass1 - * @param string $pass2 - */ - private function change_password_wrapper(User $duser, $pass1, $pass2) { - global $user; + if ( + ($a->name == $b->name) || + ($b->can(Permissions::PROTECTED) && $a->class->name == "admin") || + (!$b->can(Permissions::PROTECTED) && $a->can(Permissions::EDIT_USER_INFO)) + ) { + return true; + } else { + $this->theme->display_error(401, "Error", "You need to be an admin to change other people's details"); + return false; + } + } - if($this->user_can_edit_user($user, $duser)) { - if($pass1 != $pass2) { - $this->theme->display_error(400, "Error", "Passwords don't match"); - } - else { - // FIXME: send_event() - $duser->set_password($pass1); + private function redirect_to_user(User $duser) + { + global $page, $user; - if($duser->id == $user->id) { - $this->set_login_cookie($duser->name, $pass1); - } + if ($user->id == $duser->id) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("user")); + } else { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("user/{$duser->name}")); + } + } - flash_message("Password changed"); - $this->redirect_to_user($duser); - } - } - } + private function change_name_wrapper(User $duser, $name) + { + global $page, $user; - /** - * @param User $duser - * @param string $address - */ - private function change_email_wrapper(User $duser, /*string(email)*/ $address) { - global $user; + if ($user->can(Permissions::EDIT_USER_NAME) && $this->user_can_edit_user($user, $duser)) { + $duser->set_name($name); + $page->flash("Username changed"); + // TODO: set login cookie if user changed themselves + $this->redirect_to_user($duser); + } else { + $this->theme->display_error(400, "Error", "Permission denied"); + } + } - if($this->user_can_edit_user($user, $duser)) { - $duser->set_email($address); + private function change_password_wrapper(User $duser, string $pass1, string $pass2) + { + global $page, $user; - flash_message("Email changed"); - $this->redirect_to_user($duser); - } - } + if ($this->user_can_edit_user($user, $duser)) { + if ($pass1 != $pass2) { + $this->theme->display_error(400, "Error", "Passwords don't match"); + } else { + // FIXME: send_event() + $duser->set_password($pass1); - /** - * @param User $duser - * @param string $class - * @throws NullUserException - */ - private function change_class_wrapper(User $duser, /*string(class)*/ $class) { - global $user; + if ($duser->id == $user->id) { + $this->set_login_cookie($duser->name, $pass1); + } - if($user->class->name == "admin") { - $duser->set_class($class); - flash_message("Class changed"); - $this->redirect_to_user($duser); - } - } -// }}} -// ips {{{ - /** - * @param User $duser - * @return array - */ - private function count_upload_ips(User $duser) { - global $database; - $rows = $database->get_pairs(" + $page->flash("Password changed"); + $this->redirect_to_user($duser); + } + } + } + + private function change_email_wrapper(User $duser, string $address) + { + global $page, $user; + + if ($this->user_can_edit_user($user, $duser)) { + $duser->set_email($address); + + $page->flash("Email changed"); + $this->redirect_to_user($duser); + } + } + + private function change_class_wrapper(User $duser, string $class) + { + global $page, $user; + + if ($user->class->name == "admin") { + $duser->set_class($class); + $page->flash("Class changed"); + $this->redirect_to_user($duser); + } + } + + private function count_upload_ips(User $duser): array + { + global $database; + return $database->get_pairs(" SELECT owner_ip, - COUNT(images.id) AS count, - MAX(posted) AS most_recent + COUNT(images.id) AS count FROM images WHERE owner_id=:id GROUP BY owner_ip - ORDER BY most_recent DESC", array("id"=>$duser->id)); - return $rows; - } + ORDER BY max(posted) DESC", ["id"=>$duser->id]); + } - /** - * @param User $duser - * @return array - */ - private function count_comment_ips(User $duser) { - global $database; - $rows = $database->get_pairs(" + private function count_comment_ips(User $duser): array + { + global $database; + return $database->get_pairs(" SELECT owner_ip, - COUNT(comments.id) AS count, - MAX(posted) AS most_recent + COUNT(comments.id) AS count FROM comments WHERE owner_id=:id GROUP BY owner_ip - ORDER BY most_recent DESC", array("id"=>$duser->id)); - return $rows; - } + ORDER BY max(posted) DESC", ["id"=>$duser->id]); + } - /** - * @param Page $page - * @param bool $with_images - * @param bool $with_comments - */ - private function delete_user(Page $page, /*boolean*/ $with_images=false, /*boolean*/ $with_comments=false) { - global $user, $config, $database; - - $page->set_title("Error"); - $page->set_heading("Error"); - $page->add_block(new NavBlock()); - - if (!$user->can("delete_user")) { - $page->add_block(new Block("Not Admin", "Only admins can delete accounts")); - } - else if(!isset($_POST['id']) || !is_numeric($_POST['id'])) { - $page->add_block(new Block("No ID Specified", - "You need to specify the account number to edit")); - } - else { - log_warning("user", "Deleting user #{$_POST['id']}"); + private function count_log_ips(User $duser): array + { + if (!class_exists('LogDatabase')) { + return []; + } + global $database; + return $database->get_pairs(" + SELECT + address, + COUNT(id) AS count + FROM score_log + WHERE username=:username + GROUP BY address + ORDER BY MAX(date_sent) DESC", ["username"=>$duser->name]); + } - if($with_images) { - log_warning("user", "Deleting user #{$_POST['id']}'s uploads"); - $rows = $database->get_all("SELECT * FROM images WHERE owner_id = :owner_id", array("owner_id" => $_POST['id'])); - foreach ($rows as $key => $value) { - $image = Image::by_id($value['id']); - if($image) { - send_event(new ImageDeletionEvent($image)); - } - } - } - else { - $database->Execute( - "UPDATE images SET owner_id = :new_owner_id WHERE owner_id = :old_owner_id", - array("new_owner_id" => $config->get_int('anon_id'), "old_owner_id" => $_POST['id']) - ); - } + private function delete_user(Page $page, bool $with_images=false, bool $with_comments=false) + { + global $user, $config, $database; - if($with_comments) { - log_warning("user", "Deleting user #{$_POST['id']}'s comments"); - $database->execute("DELETE FROM comments WHERE owner_id = :owner_id", array("owner_id" => $_POST['id'])); - } - else { - $database->Execute( - "UPDATE comments SET owner_id = :new_owner_id WHERE owner_id = :old_owner_id", - array("new_owner_id" => $config->get_int('anon_id'), "old_owner_id" => $_POST['id']) - ); - } + $page->set_title("Error"); + $page->set_heading("Error"); + $page->add_block(new NavBlock()); - send_event(new UserDeletionEvent($_POST['id'])); + if (!$user->can(Permissions::DELETE_USER)) { + $page->add_block(new Block("Not Admin", "Only admins can delete accounts")); + } elseif (!isset($_POST['id']) || !is_numeric($_POST['id'])) { + $page->add_block(new Block( + "No ID Specified", + "You need to specify the account number to edit" + )); + } else { + log_warning("user", "Deleting user #{$_POST['id']}"); - $database->execute( - "DELETE FROM users WHERE id = :id", - array("id" => $_POST['id']) - ); - - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/list")); - } - } -// }}} + if ($with_images) { + log_warning("user", "Deleting user #{$_POST['id']}'s uploads"); + $rows = $database->get_all("SELECT * FROM images WHERE owner_id = :owner_id", ["owner_id" => $_POST['id']]); + foreach ($rows as $key => $value) { + $image = Image::by_id($value['id']); + if ($image) { + send_event(new ImageDeletionEvent($image)); + } + } + } else { + $database->Execute( + "UPDATE images SET owner_id = :new_owner_id WHERE owner_id = :old_owner_id", + ["new_owner_id" => $config->get_int('anon_id'), "old_owner_id" => $_POST['id']] + ); + } + + if ($with_comments) { + log_warning("user", "Deleting user #{$_POST['id']}'s comments"); + $database->execute("DELETE FROM comments WHERE owner_id = :owner_id", ["owner_id" => $_POST['id']]); + } else { + $database->Execute( + "UPDATE comments SET owner_id = :new_owner_id WHERE owner_id = :old_owner_id", + ["new_owner_id" => $config->get_int('anon_id'), "old_owner_id" => $_POST['id']] + ); + } + + send_event(new UserDeletionEvent((int)$_POST['id'])); + + $database->execute( + "DELETE FROM users WHERE id = :id", + ["id" => $_POST['id']] + ); + + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/list")); + } + } } - diff --git a/ext/user/test.php b/ext/user/test.php index 6e72ebc3..d548c0fe 100644 --- a/ext/user/test.php +++ b/ext/user/test.php @@ -1,41 +1,43 @@ -get_page('user'); - $this->assert_title("Not Logged In"); - $this->assert_no_text("Options"); - $this->assert_no_text("More Options"); +get_page('user'); + $this->assert_title("Not Logged In"); + $this->assert_no_text("Options"); + $this->assert_no_text("More Options"); - $this->get_page('user/demo'); - $this->assert_title("demo's Page"); - $this->assert_text("Joined:"); + $this->get_page('user/demo'); + $this->assert_title("demo's Page"); + $this->assert_text("Joined:"); - $this->get_page('user/MauMau'); - $this->assert_title("No Such User"); + $this->get_page('user/MauMau'); + $this->assert_title("No Such User"); - $this->log_in_as_user(); - // should be on the user page - $this->get_page('user/test'); - $this->assert_title("test's Page"); - $this->assert_text("Options"); - // FIXME: check class - //$this->assert_no_text("Admin:"); - $this->log_out(); + $this->log_in_as_user(); + // should be on the user page + $this->get_page('user/test'); + $this->assert_title("test's Page"); + $this->assert_text("Options"); + // FIXME: check class + //$this->assert_no_text("Admin:"); + $this->log_out(); - $this->log_in_as_admin(); - // should be on the user page - $this->get_page('user/demo'); - $this->assert_title("demo's Page"); - $this->assert_text("Options"); - // FIXME: check class - //$this->assert_text("Admin:"); - $this->log_out(); + $this->log_in_as_admin(); + // should be on the user page + $this->get_page('user/demo'); + $this->assert_title("demo's Page"); + $this->assert_text("Options"); + // FIXME: check class + //$this->assert_text("Admin:"); + $this->log_out(); - # FIXME: test user creation - # FIXME: test adminifying - # FIXME: test password reset + # FIXME: test user creation + # FIXME: test adminifying + # FIXME: test password reset - $this->get_page('user_admin/list'); - $this->assert_text("demo"); - } + $this->get_page('user_admin/list'); + $this->assert_text("demo"); + } } diff --git a/ext/user/theme.php b/ext/user/theme.php index 6f16a86c..2fa232bd 100644 --- a/ext/user/theme.php +++ b/ext/user/theme.php @@ -1,294 +1,305 @@ -set_title("Login"); - $page->set_heading("Login"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Login There", - "There should be a login box to the left")); - } +class UserPageTheme extends Themelet +{ + public function display_login_page(Page $page) + { + $page->set_title("Login"); + $page->set_heading("Login"); + $page->add_block(new NavBlock()); + $page->add_block(new Block( + "Login There", + "There should be a login box to the left" + )); + } - /** - * @param Page $page - * @param User[] $users - * @param User $user - */ - public function display_user_list(Page $page, $users, User $user) { - $page->set_title("User List"); - $page->set_heading("User List"); - $page->add_block(new NavBlock()); + public function display_user_list(Page $page, $table, $paginator) + { + $page->set_title("User List"); + $page->set_heading("User List"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Users", $table . $paginator)); + } - $html = "
    "; + public function display_user_links(Page $page, User $user, $parts) + { + # $page->add_block(new Block("User Links", join(", ", $parts), "main", 10)); + } - $html .= ""; - $html .= ""; - if($user->can('delete_user')) - $html .= ""; - $html .= ""; - $html .= ""; - $html .= ""; + public function display_user_block(Page $page, User $user, $parts) + { + $html = emptyHTML('Logged in as ', $user->name); + foreach ($parts as $part) { + $html->appendChild(BR()); + $html->appendChild(A(["href"=>$part["link"]], $part["name"])); + } + $page->add_block(new Block("User Links", (string)$html, "left", 90)); + } - $h_username = html_escape(@$_GET['username']); - $h_email = html_escape(@$_GET['email']); - $h_class = html_escape(@$_GET['class']); + public function display_signup_page(Page $page) + { + global $config; + $tac = $config->get_string("login_tac", ""); - $html .= "" . make_form("user_admin/list", "GET"); - $html .= ""; - if($user->can('delete_user')) - $html .= ""; - $html .= ""; - $html .= ""; - $html .= ""; + if ($config->get_bool("login_tac_bbcode")) { + $tfe = new TextFormattingEvent($tac); + send_event($tfe); + $tac = $tfe->formatted; + } - foreach($users as $duser) { - $h_name = html_escape($duser->name); - $h_email = html_escape($duser->email); - $h_class = html_escape($duser->class->name); - $u_link = make_link("user/" . url_escape($duser->name)); - $u_posts = make_link("post/list/user_id=" . url_escape($duser->id) . "/1"); + $form = SHM_SIMPLE_FORM( + "user_admin/create", + TABLE( + ["class"=>"form"], + TBODY( + TR( + TH("Name"), + TD(INPUT(["type"=>'text', "name"=>'name', "required"=>true])) + ), + TR( + TH("Password"), + TD(INPUT(["type"=>'password', "name"=>'pass1', "required"=>true])) + ), + TR( + TH(rawHTML("Repeat Password")), + TD(INPUT(["type"=>'password', "name"=>'pass2', "required"=>true])) + ), + TR( + TH(rawHTML("Email (Optional)")), + TD(INPUT(["type"=>'email', "name"=>'email'])) + ), + TR( + TD(["colspan"=>"2"], rawHTML(captcha_get_html())) + ), + ), + TFOOT( + TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>"Create Account"]))) + ) + ) + ); - $html .= ""; - $html .= ""; - if($user->can('delete_user')) - $html .= ""; - $html .= ""; - $html .= ""; - $html .= ""; - } + $html = emptyHTML( + $tac ? P(rawHTML($tac)) : null, + $form + ); - $html .= "
    NameEmailClassAction
    $h_name$h_email$h_classShow Posts
    "; + $page->set_title("Create Account"); + $page->set_heading("Create Account"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Signup", (string)$html)); + } - $page->add_block(new Block("Users", $html)); - } + public function display_signups_disabled(Page $page) + { + $page->set_title("Signups Disabled"); + $page->set_heading("Signups Disabled"); + $page->add_block(new NavBlock()); + $page->add_block(new Block( + "Signups Disabled", + "The board admin has disabled the ability to create new accounts~" + )); + } - public function display_user_links(Page $page, User $user, $parts) { - # $page->add_block(new Block("User Links", join(", ", $parts), "main", 10)); - } + public function display_login_block(Page $page) + { + global $config, $user; + $form = SHM_SIMPLE_FORM( + "user_admin/login", + TABLE( + ["style"=>"width: 100%", "class"=>"form"], + TBODY( + TR( + TH(LABEL(["for"=>"user"], "Name")), + TD(INPUT(["id"=>"user", "type"=>"text", "name"=>"user", "autocomplete"=>"username"])) + ), + TR( + TH(LABEL(["for"=>"pass"], "Password")), + TD(INPUT(["id"=>"pass", "type"=>"password", "name"=>"pass", "autocomplete"=>"current-password"])) + ) + ), + TFOOT( + TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>"Log In"]))) + ) + ) + ); - public function display_user_block(Page $page, User $user, $parts) { - $h_name = html_escape($user->name); - $html = 'Logged in as '.$h_name; - foreach($parts as $part) { - $html .= '
    '.$part["name"].''; - } - $page->add_block(new Block("User Links", $html, "left", 90)); - } + $html = emptyHTML(); + $html->appendChild($form); + if ($config->get_bool("login_signup_enabled") && $user->can(Permissions::CREATE_USER)) { + $html->appendChild(SMALL(A(["href"=>make_link("user_admin/create")], "Create Account"))); + } - public function display_signup_page(Page $page) { - global $config; - $tac = $config->get_string("login_tac", ""); + $page->add_block(new Block("Login", (string)$html, "left", 90)); + } - if($config->get_bool("login_tac_bbcode")) { - $tfe = new TextFormattingEvent($tac); - send_event($tfe); - $tac = $tfe->formatted; - } + private function _ip_list(string $name, array $ips) + { + $td = TD("$name: "); + $n = 0; + foreach ($ips as $ip => $count) { + $td->appendChild(BR()); + $td->appendChild("$ip ($count)"); + if (++$n >= 20) { + $td->appendChild(BR()); + $td->appendChild("..."); + break; + } + } + return $td; + } - if(empty($tac)) {$html = "";} - else {$html = '

    '.$tac.'

    ';} + public function display_ip_list(Page $page, array $uploads, array $comments, array $events) + { + $html = TABLE( + ["id"=>"ip-history"], + TR( + $this->_ip_list("Uploaded from", $uploads), + $this->_ip_list("Commented from", $comments), + $this->_ip_list("Logged Events", $events) + ), + TR( + TD(["colspan"=>"3"], "(Most recent at top)") + ) + ); - $h_reca = "".captcha_get_html().""; + $page->add_block(new Block("IPs", (string)$html, "main", 70)); + } - $html .= ' - '.make_form(make_link("user_admin/create"))." - - - - - - - $h_reca - - - - -
    Name
    Password
    Repeat Password
    Email (Optional)
    - - "; + public function display_user_page(User $duser, $stats) + { + global $page; + assert(is_array($stats)); + $stats[] = 'User ID: '.$duser->id; - $page->set_title("Create Account"); - $page->set_heading("Create Account"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Signup", $html)); - } + $page->set_title(html_escape($duser->name)."'s Page"); + $page->set_heading(html_escape($duser->name)."'s Page"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Stats", join("
    ", $stats), "main", 10)); + } - public function display_signups_disabled(Page $page) { - $page->set_title("Signups Disabled"); - $page->set_heading("Signups Disabled"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Signups Disabled", - "The board admin has disabled the ability to create new accounts~")); - } + public function build_options(User $duser, UserOptionsBuildingEvent $event): string + { + global $config, $user; + $html = emptyHTML(); - public function display_login_block(Page $page) { - global $config; - $html = ' - '.make_form(make_link("user_admin/login"))." - - - - - - - - - - - - - - -
    - - "; - if($config->get_bool("login_signup_enabled")) { - $html .= "Create Account"; - } - $page->add_block(new Block("Login", $html, "left", 90)); - } + // just a fool-admin protection so they dont mess around with anon users. + if ($duser->id != $config->get_int('anon_id')) { + if ($user->can(Permissions::EDIT_USER_NAME)) { + $html->appendChild(SHM_USER_FORM( + $duser, + "user_admin/change_name", + "Change Name", + TBODY(TR( + TH("New name"), + TD(INPUT(["type"=>'text', "name"=>'name', "value"=>$duser->name])) + )), + "Set" + )); + } - /** - * @param Page $page - * @param array $uploads - * @param array $comments - */ - public function display_ip_list(Page $page, $uploads, $comments) { - $html = ""; - $html .= ""; - $html .= "
    Uploaded from: "; - $n = 0; - foreach($uploads as $ip => $count) { - $html .= '
    '.$ip.' ('.$count.')'; - if(++$n >= 20) { - $html .= "
    ..."; - break; - } - } + $html->appendChild(SHM_USER_FORM( + $duser, + "user_admin/change_pass", + "Change Password", + TBODY( + TR( + TH("Password"), + TD(INPUT(["type"=>'password', "name"=>'pass1', "autocomplete"=>'new-password'])) + ), + TR( + TH("Repeat Password"), + TD(INPUT(["type"=>'password', "name"=>'pass2', "autocomplete"=>'new-password'])) + ), + ), + "Set" + )); - $html .= "
    Commented from:"; - $n = 0; - foreach($comments as $ip => $count) { - $html .= '
    '.$ip.' ('.$count.')'; - if(++$n >= 20) { - $html .= "
    ..."; - break; - } - } + $html->appendChild(SHM_USER_FORM( + $duser, + "user_admin/change_email", + "Change Email", + TBODY(TR( + TH("Address"), + TD(INPUT(["type"=>'text', "name"=>'address', "value"=>$duser->email, "autocomplete"=>'email', "inputmode"=>'email'])) + )), + "Set" + )); - $html .= "
    (Most recent at top)
    "; + if ($user->can(Permissions::EDIT_USER_CLASS)) { + global $_shm_user_classes; + $select = SELECT(["name"=>"class"]); + foreach ($_shm_user_classes as $name => $values) { + $select->appendChild( + OPTION(["value"=>$name, "selected"=>$name == $duser->class->name], ucwords($name)) + ); + } + $html->appendChild(SHM_USER_FORM( + $duser, + "user_admin/change_class", + "Change Class", + TBODY(TR(TD($select))), + "Set" + )); + } - $page->add_block(new Block("IPs", $html, "main", 70)); - } + if ($user->can(Permissions::DELETE_USER)) { + $html->appendChild(SHM_USER_FORM( + $duser, + "user_admin/delete_user", + "Delete User", + TBODY( + TR(TD(LABEL(INPUT(["type"=>'checkbox', "name"=>'with_images']), "Delete images"))), + TR(TD(LABEL(INPUT(["type"=>'checkbox', "name"=>'with_comments']), "Delete comments"))), + ), + TFOOT( + TR(TD(INPUT(["type"=>'button', "class"=>'shm-unlocker', "data-unlock-sel"=>'.deluser', "value"=>'Unlock']))), + TR(TD(INPUT(["type"=>'submit', "class"=>'deluser', "value"=>'Delete User', "disabled"=>'true']))), + ) + )); + } - public function display_user_page(User $duser, $stats) { - global $page, $user; - assert(is_array($stats)); - $stats[] = 'User ID: '.$duser->id; + foreach ($event->parts as $part) { + $html .= $part; + } + } + return (string)$html; + } - $page->set_title(html_escape($duser->name)."'s Page"); - $page->set_heading(html_escape($duser->name)."'s Page"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Stats", join("
    ", $stats), "main", 10)); + public function get_help_html() + { + global $user; + $output = emptyHTML(P("Search for images posted by particular individuals.")); + $output->appendChild(SHM_COMMAND_EXAMPLE( + "poster=username", + 'Returns images posted by "username".' + )); + $output->appendChild(SHM_COMMAND_EXAMPLE( + "poster_id=123", + 'Returns images posted by user 123.' + )); - if(!$user->is_anonymous()) { - if($user->id == $duser->id || $user->can("edit_user_info")) { - $page->add_block(new Block("Options", $this->build_options($duser), "main", 60)); - } - } - } - - protected function build_options(User $duser) { - global $config, $user; - $html = ""; - if($duser->id != $config->get_int('anon_id')){ //justa fool-admin protection so they dont mess around with anon users. - - if($user->can('edit_user_name')) { - $html .= " -

    ".make_form(make_link("user_admin/change_name"))." - - - - - -
    Change Name
    New name
    - - "; - } - - $html .= " -

    ".make_form(make_link("user_admin/change_pass"))." - - - - - - - - - - - - -
    Change Password
    Password
    Repeat Password
    - - -

    ".make_form(make_link("user_admin/change_email"))." - - - - - -
    Change Email
    Address
    - - "; - - $i_user_id = int_escape($duser->id); - - if($user->can("edit_user_class")) { - global $_shm_user_classes; - $class_html = ""; - foreach($_shm_user_classes as $name => $values) { - $h_name = html_escape($name); - $h_title = html_escape(ucwords($name)); - $h_selected = ($name == $duser->class->name ? " selected" : ""); - $class_html .= "\n"; - } - $html .= " -

    ".make_form(make_link("user_admin/change_class"))." - - - - - -
    Change Class
    - - "; - } - - if($user->can("delete_user")) { - $html .= " -

    ".make_form(make_link("user_admin/delete_user"))." - - - - - - - - - - - - - -
    Delete User
    Delete images
    Delete comments
    - - "; - } - } - return $html; - } -// }}} + if ($user->can(Permissions::VIEW_IP)) { + $output->appendChild(SHM_COMMAND_EXAMPLE( + "poster_ip=127.0.0.1", + "Returns images posted from IP 127.0.0.1." + )); + } + return $output; + } } - diff --git a/ext/user_config/info.php b/ext/user_config/info.php new file mode 100644 index 00000000..c25317ab --- /dev/null +++ b/ext/user_config/info.php @@ -0,0 +1,14 @@ +"matthew@darkholme.net"]; + public $license = self::LICENSE_WTFPL; + public $description = "Provides system-wide support for user-specific settings"; + public $visibility = self::VISIBLE_ADMIN; + public $core = true; +} diff --git a/ext/user_config/main.php b/ext/user_config/main.php new file mode 100644 index 00000000..c243f6b7 --- /dev/null +++ b/ext/user_config/main.php @@ -0,0 +1,58 @@ +user = $user; + $this->user_config = $user_config; + } +} + +class UserConfig extends Extension +{ + private const VERSION = "ext_user_config_version"; + + public function onUserLogin(UserLoginEvent $event) + { + global $database, $user_config; + + $user_config = new DatabaseConfig($database, "user_config", "user_id", "{$event->user->id}"); + send_event(new InitUserConfigEvent($event->user, $user_config)); + } + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + { + global $database; + + if ($this->get_version(self::VERSION) < 1) { + $database->create_table("user_config", " + user_id INTEGER NOT NULL, + name VARCHAR(128) NOT NULL, + value TEXT, + PRIMARY KEY (user_id, name), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + "); + $database->execute("CREATE INDEX user_config_user_id_idx ON user_config(user_id)"); + + $this->set_version(self::VERSION, 1); + } + } + + + // This needs to happen before any other events, but after db upgrade + public function get_priority(): int + { + return 6; + } +} diff --git a/ext/varnish/info.php b/ext/varnish/info.php new file mode 100644 index 00000000..cf2255f0 --- /dev/null +++ b/ext/varnish/info.php @@ -0,0 +1,14 @@ + -* License: GPLv2 -* Visibility: admin -* Description: Sends PURGE requests when a /post/view is updated -*/ +curl_purge("post/view/{$event->image_id}"); - } + public function onCommentPosting(CommentPostingEvent $event) + { + $this->curl_purge("post/view/{$event->image_id}"); + } - public function onImageInfoSet(ImageInfoSetEvent $event) { - $this->curl_purge("post/view/{$event->image->id}"); - } + public function onImageInfoSet(ImageInfoSetEvent $event) + { + $this->curl_purge("post/view/{$event->image->id}"); + } - public function onImageDeletion(ImageDeletionEvent $event) { - $this->curl_purge("post/view/{$event->image->id}"); - } + public function onImageDeletion(ImageDeletionEvent $event) + { + $this->curl_purge("post/view/{$event->image->id}"); + } - /** - * @return int - */ - public function get_priority() {return 99;} + public function get_priority(): int + { + return 99; + } } diff --git a/ext/view/events/displaying_image_event.php b/ext/view/events/displaying_image_event.php new file mode 100644 index 00000000..92d5d4bf --- /dev/null +++ b/ext/view/events/displaying_image_event.php @@ -0,0 +1,27 @@ +image = $image; + } + + public function get_image(): Image + { + return $this->image; + } +} diff --git a/ext/view/events/image_admin_block_building_event.php b/ext/view/events/image_admin_block_building_event.php new file mode 100644 index 00000000..4b2e51da --- /dev/null +++ b/ext/view/events/image_admin_block_building_event.php @@ -0,0 +1,26 @@ +image = $image; + $this->user = $user; + } + + public function add_part(string $html, int $position=50) + { + while (isset($this->parts[$position])) { + $position++; + } + $this->parts[$position] = $html; + } +} diff --git a/ext/view/events/image_info_box_building_event.php b/ext/view/events/image_info_box_building_event.php new file mode 100644 index 00000000..feec79c6 --- /dev/null +++ b/ext/view/events/image_info_box_building_event.php @@ -0,0 +1,26 @@ +image = $image; + $this->user = $user; + } + + public function add_part(string $html, int $position=50) + { + while (isset($this->parts[$position])) { + $position++; + } + $this->parts[$position] = $html; + } +} diff --git a/ext/view/events/image_info_set_event.php b/ext/view/events/image_info_set_event.php new file mode 100644 index 00000000..a318b82f --- /dev/null +++ b/ext/view/events/image_info_set_event.php @@ -0,0 +1,13 @@ +image = $image; + } +} diff --git a/ext/view/info.php b/ext/view/info.php new file mode 100644 index 00000000..6425ad72 --- /dev/null +++ b/ext/view/info.php @@ -0,0 +1,13 @@ +image = $image; - } - /** - * @return Image - */ - public function get_image() { - return $this->image; - } +class ViewImage extends Extension +{ + /** @var ViewImageTheme */ + protected $theme; + + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; + + if ($event->page_matches("post/prev") || $event->page_matches("post/next")) { + $image_id = int_escape($event->get_arg(0)); + + if (isset($_GET['search'])) { + $search_terms = Tag::explode(Tag::decaret($_GET['search'])); + $query = "#search=".url_escape($_GET['search']); + } else { + $search_terms = []; + $query = null; + } + + $image = Image::by_id($image_id); + if (is_null($image)) { + $this->theme->display_error(404, "Image not found", "Image $image_id could not be found"); + return; + } + + if ($event->page_matches("post/next")) { + $image = $image->get_next($search_terms); + } else { + $image = $image->get_prev($search_terms); + } + + if (is_null($image)) { + $this->theme->display_error(404, "Image not found", "No more images"); + return; + } + + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/{$image->id}", $query)); + } elseif ($event->page_matches("post/view")) { + if (!is_numeric($event->get_arg(0))) { + // For some reason there exists some very broken mobile client + // who follows up every request to '/post/view/123' with + // '/post/view/12300000000000Image 123: tags' which spams the + // database log with 'integer out of range' + $this->theme->display_error(404, "Image not found", "Invalid image ID"); + return; + } + + $image_id = int_escape($event->get_arg(0)); + + $image = Image::by_id($image_id); + + if (!is_null($image)) { + send_event(new DisplayingImageEvent($image)); + $iabbe = new ImageAdminBlockBuildingEvent($image, $user); + send_event($iabbe); + ksort($iabbe->parts); + $this->theme->display_admin_block($page, $iabbe->parts); + } else { + $this->theme->display_error(404, "Image not found", "No image in the database has the ID #$image_id"); + } + } elseif ($event->page_matches("post/set")) { + if (!isset($_POST['image_id'])) { + return; + } + + $image_id = int_escape($_POST['image_id']); + $image = Image::by_id($image_id); + if (!$image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK)) { + send_event(new ImageInfoSetEvent($image)); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/$image_id", url_escape(@$_POST['query']))); + } else { + $this->theme->display_error(403, "Image Locked", "An admin has locked this image"); + } + } + } + + public function onDisplayingImage(DisplayingImageEvent $event) + { + global $user; + $iibbe = new ImageInfoBoxBuildingEvent($event->get_image(), $user); + send_event($iibbe); + ksort($iibbe->parts); + $this->theme->display_meta_headers($event->get_image()); + $this->theme->display_page($event->get_image(), $iibbe->parts); + } } - -class ImageInfoBoxBuildingEvent extends Event { - /** @var array */ - public $parts = array(); - /** @var \Image */ - public $image; - /** @var \User */ - public $user; - - /** - * @param Image $image - * @param User $user - */ - public function __construct(Image $image, User $user) { - $this->image = $image; - $this->user = $user; - } - - /** - * @param string $html - * @param int $position - */ - public function add_part($html, $position=50) { - while(isset($this->parts[$position])) $position++; - $this->parts[$position] = $html; - } -} - -class ImageInfoSetEvent extends Event { - /** @var \Image */ - public $image; - - /** - * @param Image $image - */ - public function __construct(Image $image) { - $this->image = $image; - } -} - -class ImageAdminBlockBuildingEvent extends Event { - /** @var string[] */ - public $parts = array(); - /** @var \Image|null */ - public $image = null; - /** @var null|\User */ - public $user = null; - - /** - * @param Image $image - * @param User $user - */ - public function __construct(Image $image, User $user) { - $this->image = $image; - $this->user = $user; - } - - /** - * @param string $html - * @param int $position - */ - public function add_part(/*string*/ $html, /*int*/ $position=50) { - while(isset($this->parts[$position])) $position++; - $this->parts[$position] = $html; - } -} - -class ViewImage extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; - - if($event->page_matches("post/prev") || $event->page_matches("post/next")) { - $image_id = int_escape($event->get_arg(0)); - - if(isset($_GET['search'])) { - $search_terms = explode(' ', $_GET['search']); - $query = "#search=".url_escape($_GET['search']); - } - else { - $search_terms = array(); - $query = null; - } - - $image = Image::by_id($image_id); - if(is_null($image)) { - $this->theme->display_error(404, "Image not found", "Image $image_id could not be found"); - return; - } - - if($event->page_matches("post/next")) { - $image = $image->get_next($search_terms); - } - else { - $image = $image->get_prev($search_terms); - } - - if(is_null($image)) { - $this->theme->display_error(404, "Image not found", "No more images"); - return; - } - - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/{$image->id}", $query)); - } - else if($event->page_matches("post/view")) { - $image_id = int_escape($event->get_arg(0)); - - $image = Image::by_id($image_id); - - if(!is_null($image)) { - send_event(new DisplayingImageEvent($image)); - $iabbe = new ImageAdminBlockBuildingEvent($image, $user); - send_event($iabbe); - ksort($iabbe->parts); - $this->theme->display_admin_block($page, $iabbe->parts); - } - else { - $this->theme->display_error(404, "Image not found", "No image in the database has the ID #$image_id"); - } - } - else if($event->page_matches("post/set")) { - if(!isset($_POST['image_id'])) return; - - $image_id = int_escape($_POST['image_id']); - - send_event(new ImageInfoSetEvent(Image::by_id($image_id))); - - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/$image_id", url_escape(@$_POST['query']))); - } - } - - public function onDisplayingImage(DisplayingImageEvent $event) { - global $user; - $iibbe = new ImageInfoBoxBuildingEvent($event->get_image(), $user); - send_event($iibbe); - ksort($iibbe->parts); - $this->theme->display_page($event->get_image(), $iibbe->parts); - } -} - diff --git a/ext/view/script.js b/ext/view/script.js new file mode 100644 index 00000000..3c966a11 --- /dev/null +++ b/ext/view/script.js @@ -0,0 +1,18 @@ +document.addEventListener('DOMContentLoaded', () => { + if(document.location.hash.length > 3) { + var query = document.location.hash.substring(1); + + $('LINK#prevlink').attr('href', function(i, attr) { + return attr + '?' + query; + }); + $('LINK#nextlink').attr('href', function(i, attr) { + return attr + '?' + query; + }); + $('A#prevlink').attr('href', function(i, attr) { + return attr + '?' + query; + }); + $('A#nextlink').attr('href', function(i, attr) { + return attr + '?' + query; + }); + } +}); diff --git a/ext/view/test.php b/ext/view/test.php index d4ae305c..3d6e0cc8 100644 --- a/ext/view/test.php +++ b/ext/view/test.php @@ -1,66 +1,61 @@ -log_in_as_user(); - $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test"); - $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "test2"); - $image_id_3 = $this->post_image("tests/favicon.png", "test"); - $idp1 = $image_id_3 + 1; + public function testViewPage() + { + $this->log_in_as_user(); + $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test"); - $this->get_page("post/view/$image_id_1"); - $this->assert_title("Image $image_id_1: test"); - } + $this->get_page("post/view/$image_id_1"); + $this->assert_title("Image $image_id_1: test"); + } - public function testPrevNext() { - $this->markTestIncomplete(); + public function testPrevNext() + { + $this->log_in_as_user(); + $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test"); + $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "test2"); + $image_id_3 = $this->post_image("tests/favicon.png", "test"); - $this->log_in_as_user(); - $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test"); - $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "test2"); - $image_id_3 = $this->post_image("tests/favicon.png", "test"); + // Front image: no next, has prev + $page = $this->get_page("post/next/$image_id_1"); + $this->assertEquals(404, $page->code); + $page = $this->get_page("post/prev/$image_id_1"); + $this->assertEquals("/test/post/view/$image_id_2", $page->redirect); - $this->click("Prev"); - $this->assert_title("Image $image_id_2: test2"); + // When searching, we skip the middle + $page = $this->get_page("post/prev/$image_id_1?search=test"); + $this->assertEquals("/test/post/view/$image_id_2", $page->redirect); - $this->click("Next"); - $this->assert_title("Image $image_id_1: test"); + // Middle image: has next and prev + $page = $this->get_page("post/next/$image_id_2"); + $this->assertEquals("/test/post/view/$image_id_1", $page->redirect); + $page = $this->get_page("post/prev/$image_id_2"); + $this->assertEquals("/test/post/view/$image_id_3", $page->redirect); - $this->click("Next"); - $this->assert_title("Image not found"); - } + // Last image has next, no prev + $page = $this->get_page("post/next/$image_id_3"); + $this->assertEquals("/test/post/view/$image_id_2", $page->redirect); + $page = $this->get_page("post/prev/$image_id_3"); + $this->assertEquals(404, $page->code); + } - public function testView404() { - $this->log_in_as_user(); - $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test"); - $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "test2"); - $image_id_3 = $this->post_image("tests/favicon.png", "test"); - $idp1 = $image_id_3 + 1; + public function testView404() + { + $this->log_in_as_user(); + $image_id_1 = $this->post_image("tests/favicon.png", "test"); + $idp1 = $image_id_1 + 1; - $this->get_page("post/view/$idp1"); - $this->assert_title('Image not found'); + $this->get_page("post/view/$idp1"); + $this->assert_title('Image not found'); - $this->get_page('post/view/-1'); - $this->assert_title('Image not found'); - } - - public function testNextSearchResult() { - $this->markTestIncomplete(); - - $this->log_in_as_user(); - $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test"); - $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "test2"); - $image_id_3 = $this->post_image("tests/favicon.png", "test"); - - // FIXME: this assumes Nice URLs. - # note: skips image #2 - $this->get_page("post/view/$image_id_1?search=test"); // FIXME: assumes niceurls - $this->click("Prev"); - $this->assert_title("Image $image_id_3: test"); - } + $this->get_page('post/view/-1'); + $this->assert_title('Image not found'); + } } - diff --git a/ext/view/theme.php b/ext/view/theme.php index 17b96694..d02338ae 100644 --- a/ext/view/theme.php +++ b/ext/view/theme.php @@ -1,54 +1,67 @@ -get_tag_list())); + $h_metatags = str_replace(" ", ", ", html_escape($image->get_tag_list())); + $page->add_html_header(""); + $page->add_html_header(""); + $page->add_html_header(""); + $page->add_html_header("get_thumb_link())."\">"); + $page->add_html_header("id}"))."\">"); + } - $page->set_title("Image {$image->id}: ".html_escape($image->get_tag_list())); - $page->add_html_header(""); - $page->add_html_header(""); - $page->add_html_header(""); - $page->add_html_header("get_thumb_link())."\">"); - $page->add_html_header("id}"))."\">"); - $page->set_heading(html_escape($image->get_tag_list())); - $page->add_block(new Block("Navigation", $this->build_navigation($image), "left", 0)); - $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "main", 20)); - //$page->add_block(new Block(null, $this->build_pin($image), "main", 11)); - } + /* + * Build a page showing $image and some info about it + */ + public function display_page(Image $image, $editor_parts) + { + global $page; + $page->set_title("Image {$image->id}: ".$image->get_tag_list()); + $page->set_heading(html_escape($image->get_tag_list())); + $page->add_block(new Block("Navigation", $this->build_navigation($image), "left", 0)); + $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "main", 20)); + //$page->add_block(new Block(null, $this->build_pin($image), "main", 11)); - public function display_admin_block(Page $page, $parts) { - if(count($parts) > 0) { - $page->add_block(new Block("Image Controls", join("
    ", $parts), "left", 50)); - } - } + $query = $this->get_query(); + $page->add_html_header(""); + $page->add_html_header(""); + } + public function display_admin_block(Page $page, $parts) + { + if (count($parts) > 0) { + $page->add_block(new Block("Image Controls", join("
    ", $parts), "left", 50)); + } + } - protected function build_pin(Image $image) { - if(isset($_GET['search'])) { - $query = "search=".url_escape($_GET['search']); - } - else { - $query = null; - } + protected function get_query() + { + if (isset($_GET['search'])) { + $query = "search=".url_escape(Tag::caret($_GET['search'])); + } else { + $query = null; + } + return $query; + } - $h_prev = "Prev"; - $h_index = "Index"; - $h_next = "Next"; + protected function build_pin(Image $image) + { + $query = $this->get_query(); + $h_prev = "Prev"; + $h_index = "Index"; + $h_next = "Next"; - return "$h_prev | $h_index | $h_next"; - } + return "$h_prev | $h_index | $h_next"; + } - /** - * @return string - */ - protected function build_navigation(Image $image) { - $h_pin = $this->build_pin($image); - $h_search = " + protected function build_navigation(Image $image): string + { + $h_pin = $this->build_pin($image); + $h_search = "

    @@ -56,37 +69,39 @@ class ViewImageTheme extends Themelet {
    "; - return "$h_pin
    $h_search"; - } + return "$h_pin
    $h_search"; + } - protected function build_info(Image $image, $editor_parts) { - global $user; + protected function build_info(Image $image, $editor_parts) + { + global $user; - if(count($editor_parts) == 0) return ($image->is_locked() ? "
    [Image Locked]" : ""); + if (count($editor_parts) == 0) { + return ($image->is_locked() ? "
    [Image Locked]" : ""); + } - $html = make_form(make_link("post/set"))." + $html = make_form(make_link("post/set"))." - +
    "; - foreach($editor_parts as $part) { - $html .= $part; - } - if( - (!$image->is_locked() || $user->can("edit_image_lock")) && - $user->can("edit_image_tag") - ) { - $html .= " + foreach ($editor_parts as $part) { + $html .= $part; + } + if ( + (!$image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK)) && + $user->can(Permissions::EDIT_IMAGE_TAG) + ) { + $html .= " "; - } - $html .= " + } + $html .= "
    "; - return $html; - } + return $html; + } } - diff --git a/ext/wiki/info.php b/ext/wiki/info.php new file mode 100644 index 00000000..e2f9adbc --- /dev/null +++ b/ext/wiki/info.php @@ -0,0 +1,14 @@ + - * License: GPLv2 - * Description: A simple wiki, for those who don't want the hugeness of mediawiki - * Documentation: - * Standard formatting APIs are used (This will be BBCode by default) - */ +user = $user; - $this->wikipage = $wikipage; - } + public function __construct(User $user, WikiPage $wikipage) + { + parent::__construct(); + $this->user = $user; + $this->wikipage = $wikipage; + } } -class WikiUpdateException extends SCoreException { +class WikiDeleteRevisionEvent extends Event +{ + public $title; + public $revision; + + public function __construct($title, $revision) + { + parent::__construct(); + $this->title = $title; + $this->revision = $revision; + } } -class WikiPage { - /** @var int|string */ - public $id; +class WikiDeletePageEvent extends Event +{ + public $title; - /** @var int */ - public $owner_id; - - /** @var string */ - public $owner_ip; - - /** @var string */ - public $date; - - /** @var string */ - public $title; - - /** @var int */ - public $revision; - - /** @var bool */ - public $locked; - - /** @var string */ - public $body; - - /** - * @param mixed $row - */ - public function __construct($row=null) { - //assert(!empty($row)); - - if (!is_null($row)) { - $this->id = $row['id']; - $this->owner_id = $row['owner_id']; - $this->owner_ip = $row['owner_ip']; - $this->date = $row['date']; - $this->title = $row['title']; - $this->revision = $row['revision']; - $this->locked = ($row['locked'] == 'Y'); - $this->body = $row['body']; - } - } - - /** - * @return null|User - */ - public function get_owner() { - return User::by_id($this->owner_id); - } - - /** - * @return bool - */ - public function is_locked() { - return $this->locked; - } + public function __construct($title) + { + parent::__construct(); + $this->title = $title; + } } -class Wiki extends Extension { - public function onInitExt(InitExtEvent $event) { - global $database, $config; +class WikiUpdateException extends SCoreException +{ +} - if($config->get_int("ext_wiki_version", 0) < 1) { - $database->create_table("wiki_pages", " +class WikiPage +{ + /** @var int|string */ + public $id; + + /** @var int */ + public $owner_id; + + /** @var string */ + public $owner_ip; + + /** @var string */ + public $date; + + /** @var string */ + public $title; + + /** @var int */ + public $revision; + + /** @var bool */ + public $locked; + + /** @var string */ + public $body; + + public function __construct(array $row=null) + { + //assert(!empty($row)); + + if (!is_null($row)) { + $this->id = (int)$row['id']; + $this->owner_id = (int)$row['owner_id']; + $this->owner_ip = $row['owner_ip']; + $this->date = $row['date']; + $this->title = $row['title']; + $this->revision = (int)$row['revision']; + $this->locked = ($row['locked'] == 'Y'); + $this->body = $row['body']; + } + } + + public function get_owner(): User + { + return User::by_id($this->owner_id); + } + + public function is_locked(): bool + { + return $this->locked; + } +} + +class Wiki extends Extension +{ + /** @var WikiTheme */ + protected $theme; + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $database; + + if ($this->get_version("ext_wiki_version") < 1) { + $database->create_table("wiki_pages", " id SCORE_AIPK, owner_id INTEGER NOT NULL, owner_ip SCORE_INET NOT NULL, - date SCORE_DATETIME DEFAULT NULL, + date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, title VARCHAR(255) NOT NULL, revision INTEGER NOT NULL DEFAULT 1, locked SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N, @@ -102,425 +118,430 @@ class Wiki extends Extension { UNIQUE (title, revision), FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT "); - $config->set_int("ext_wiki_version", 2); - } - if($config->get_int("ext_wiki_version") < 2) { - $database->Execute("ALTER TABLE wiki_pages ADD COLUMN + $this->set_version("ext_wiki_version", 2); + } + if ($this->get_version("ext_wiki_version") < 2) { + $database->Execute("ALTER TABLE wiki_pages ADD COLUMN locked ENUM('Y', 'N') DEFAULT 'N' NOT NULL AFTER REVISION"); - $config->set_int("ext_wiki_version", 2); - } - } + $this->set_version("ext_wiki_version", 2); + } + } - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; - if($event->page_matches("wiki")) { - if(is_null($event->get_arg(0)) || strlen(trim($event->get_arg(0))) === 0) { - $title = "Index"; - } - else { - $title = $event->get_arg(0); - } + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; + if ($event->page_matches("wiki")) { + if ($event->count_args() == 0 || strlen(trim($event->get_arg(0))) === 0) { + $title = "Index"; + } else { + $title = $event->get_arg(0); + } - $content = $this->get_page($title); - $this->theme->display_page($page, $content, $this->get_page("wiki:sidebar")); - } - else if($event->page_matches("wiki_admin/edit")) { - $content = $this->get_page($_POST['title']); - $this->theme->display_page_editor($page, $content); - } - else if($event->page_matches("wiki_admin/save")) { - $title = $_POST['title']; - $rev = int_escape($_POST['revision']); - $body = $_POST['body']; - $lock = $user->is_admin() && isset($_POST['lock']) && ($_POST['lock'] == "on"); + $content = $this->get_page($title); + $this->theme->display_page($page, $content, $this->get_page("wiki:sidebar")); + } elseif ($event->page_matches("wiki_admin/edit")) { + $content = $this->get_page($_POST['title']); + $this->theme->display_page_editor($page, $content); + } elseif ($event->page_matches("wiki_admin/save")) { + $title = $_POST['title']; + $rev = int_escape($_POST['revision']); + $body = $_POST['body']; + $lock = $user->can(Permissions::WIKI_ADMIN) && isset($_POST['lock']) && ($_POST['lock'] == "on"); - if($this->can_edit($user, $this->get_page($title))) { - $wikipage = $this->get_page($title); - $wikipage->revision = $rev; - $wikipage->body = $body; - $wikipage->locked = $lock; - try { - send_event(new WikiUpdateEvent($user, $wikipage)); + if ($this->can_edit($user, $this->get_page($title))) { + $wikipage = $this->get_page($title); + $wikipage->revision = $rev; + $wikipage->body = $body; + $wikipage->locked = $lock; + try { + send_event(new WikiUpdateEvent($user, $wikipage)); - $u_title = url_escape($title); - $page->set_mode("redirect"); - $page->set_redirect(make_link("wiki/$u_title")); - } - catch(WikiUpdateException $e) { - $original = $this->get_page($title); - // @ because arr_diff is full of warnings - $original->body = @$this->arr_diff( - explode("\n", $original->body), - explode("\n", $wikipage->body) - ); - $this->theme->display_page_editor($page, $original); - } - } - else { - $this->theme->display_permission_denied(); - } - } - else if($event->page_matches("wiki_admin/delete_revision")) { - if($user->is_admin()) { - global $database; - $database->Execute( - "DELETE FROM wiki_pages WHERE title=:title AND revision=:rev", - array("title"=>$_POST["title"], "rev"=>$_POST["revision"])); - $u_title = url_escape($_POST["title"]); - $page->set_mode("redirect"); - $page->set_redirect(make_link("wiki/$u_title")); - } - } - else if($event->page_matches("wiki_admin/delete_all")) { - if($user->is_admin()) { - global $database; - $database->Execute( - "DELETE FROM wiki_pages WHERE title=:title", - array("title"=>$_POST["title"])); - $u_title = url_escape($_POST["title"]); - $page->set_mode("redirect"); - $page->set_redirect(make_link("wiki/$u_title")); - } - } - } + $u_title = url_escape($title); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("wiki/$u_title")); + } catch (WikiUpdateException $e) { + $original = $this->get_page($title); + // @ because arr_diff is full of warnings + $original->body = @$this->arr_diff( + explode("\n", $original->body), + explode("\n", $wikipage->body) + ); + $this->theme->display_page_editor($page, $original); + } + } else { + $this->theme->display_permission_denied(); + } + } elseif ($event->page_matches("wiki_admin/delete_revision")) { + if ($user->can(Permissions::WIKI_ADMIN)) { + send_event(new WikiDeleteRevisionEvent($_POST["title"], $_POST["revision"])); + $u_title = url_escape($_POST["title"]); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("wiki/$u_title")); + } + } elseif ($event->page_matches("wiki_admin/delete_all")) { + if ($user->can(Permissions::WIKI_ADMIN)) { + send_event(new WikiDeletePageEvent($_POST["title"])); + $u_title = url_escape($_POST["title"]); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("wiki/$u_title")); + } + } + } - public function onWikiUpdate(WikiUpdateEvent $event) { - global $database; - $wpage = $event->wikipage; - try { - $database->Execute(" + + public function onPageNavBuilding(PageNavBuildingEvent $event) + { + $event->add_nav_link("wiki", new Link('wiki'), "Wiki"); + } + + + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + if ($event->parent=="wiki") { + $event->add_nav_link("wiki_rules", new Link('wiki/rules'), "Rules"); + $event->add_nav_link("wiki_help", new Link('ext_doc/wiki'), "Help"); + } + } + + public function onWikiUpdate(WikiUpdateEvent $event) + { + global $database; + $wpage = $event->wikipage; + try { + $database->Execute( + " INSERT INTO wiki_pages(owner_id, owner_ip, date, title, revision, locked, body) - VALUES (?, ?, now(), ?, ?, ?, ?)", array($event->user->id, $_SERVER['REMOTE_ADDR'], - $wpage->title, $wpage->revision, $wpage->locked?'Y':'N', $wpage->body)); - } - catch(Exception $e) { - throw new WikiUpdateException("Somebody else edited that page at the same time :-("); - } - } + VALUES (:owner_id, :owner_ip, now(), :title, :revision, :locked, :body)", + ["owner_id"=>$event->user->id, "owner_ip"=>$_SERVER['REMOTE_ADDR'], + "title"=>$wpage->title, "revision"=>$wpage->revision, "locked"=>$wpage->locked?'Y':'N', "body"=>$wpage->body] + ); + } catch (Exception $e) { + throw new WikiUpdateException("Somebody else edited that page at the same time :-("); + } + } - /** - * See if the given user is allowed to edit the given page. - * - * @param User $user - * @param WikiPage $page - * @return bool - */ - public static function can_edit(User $user, WikiPage $page) { - // admins can edit everything - if($user->is_admin()) return true; + public function onWikiDeleteRevision(WikiDeleteRevisionEvent $event) + { + global $database; + $database->Execute( + "DELETE FROM wiki_pages WHERE title=:title AND revision=:rev", + ["title"=>$event->title, "rev"=>$event->revision] + ); + } - // anon / user can't ever edit locked pages - if($page->is_locked()) return false; + public function onWikiDeletePage(WikiDeletePageEvent $event) + { + global $database; + $database->Execute( + "DELETE FROM wiki_pages WHERE title=:title", + ["title" => $event->title] + ); + } - // anon / user can edit if allowed by config - if($user->can("edit_wiki_page")) return true; + /** + * See if the given user is allowed to edit the given page. + */ + public static function can_edit(User $user, WikiPage $page): bool + { + // admins can edit everything + if ($user->can(Permissions::WIKI_ADMIN)) { + return true; + } - return false; - } + // anon / user can't ever edit locked pages + if ($page->is_locked()) { + return false; + } - /** - * @param string $title - * @param integer $revision - * @return WikiPage - */ - private function get_page($title, $revision=-1) { - global $database; - // first try and get the actual page - $row = $database->get_row($database->scoreql_to_sql(" + // anon / user can edit if allowed by config + if ($user->can(Permissions::EDIT_WIKI_PAGE)) { + return true; + } + + return false; + } + + public static function get_page(string $title, int $revision=-1): WikiPage + { + global $database; + // first try and get the actual page + $row = $database->get_row( + " SELECT * FROM wiki_pages - WHERE SCORE_STRNORM(title) LIKE SCORE_STRNORM(:title) - ORDER BY revision DESC"), - array("title"=>$title)); + WHERE LOWER(title) LIKE LOWER(:title) + ORDER BY revision DESC + ", + ["title"=>$title] + ); - // fall back to wiki:default - if(empty($row)) { - $row = $database->get_row(" - SELECT * - FROM wiki_pages - WHERE title LIKE :title - ORDER BY revision DESC", array("title"=>"wiki:default")); + // fall back to wiki:default + if (empty($row)) { + $row = $database->get_row(" + SELECT * + FROM wiki_pages + WHERE title LIKE :title + ORDER BY revision DESC + ", ["title"=>"wiki:default"]); - // fall further back to manual - if(empty($row)) { - $row = array( - "id" => -1, - "owner_ip" => "0.0.0.0", - "date" => "", - "revision" => 0, - "locked" => false, - "body" => "This is a default page for when a page is empty, ". - "it can be edited by editing [[wiki:default]].", - ); - } + // fall further back to manual + if (empty($row)) { + $row = [ + "id" => -1, + "owner_ip" => "0.0.0.0", + "date" => "", + "revision" => 0, + "locked" => false, + "body" => "This is a default page for when a page is empty, ". + "it can be edited by editing [[wiki:default]].", + ]; + } - // correct the default - global $config; - $row["title"] = $title; - $row["owner_id"] = $config->get_int("anon_id", 0); - } + // correct the default + global $config; + $row["title"] = $title; + $row["owner_id"] = $config->get_int("anon_id", 0); + } - assert(!empty($row)); + assert(!empty($row)); - return new WikiPage($row); - } + return new WikiPage($row); + } -// php-diff {{{ - /** - Diff implemented in pure php, written from scratch. - Copyright (C) 2003 Daniel Unterberger - - 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - http://www.gnu.org/licenses/gpl.html + /** + Diff implemented in pure php, written from scratch. + Copyright (C) 2003 Daniel Unterberger - About: - I searched a function to compare arrays and the array_diff() - was not specific enough. It ignores the order of the array-values. - So I reimplemented the diff-function which is found on unix-systems - but this you can use directly in your code and adopt for your needs. - Simply adopt the formatline-function. with the third-parameter of arr_diff() - you can hide matching lines. Hope someone has use for this. + 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. - Contact: d.u.diff@holomind.de - **/ + 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. - private function arr_diff( $f1 , $f2 , $show_equal = 0 ) - { + 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - $c1 = 0 ; # current line of left - $c2 = 0 ; # current line of right - $max1 = count( $f1 ) ; # maximal lines of left - $max2 = count( $f2 ) ; # maximal lines of right - $outcount = 0; # output counter - $hit1 = "" ; # hit in left - $hit2 = "" ; # hit in right - $stop = 0; - $out = ""; + http://www.gnu.org/licenses/gpl.html - while ( - $c1 < $max1 # have next line in left - and - $c2 < $max2 # have next line in right - and - ($stop++) < 1000 # don-t have more then 1000 ( loop-stopper ) - and - $outcount < 20 # output count is less then 20 - ) - { - /** - * is the trimmed line of the current left and current right line - * the same ? then this is a hit (no difference) - */ - if ( trim( $f1[$c1] ) == trim ( $f2[$c2]) ) - { - /** - * add to output-string, if "show_equal" is enabled - */ - $out .= ($show_equal==1) - ? formatline ( ($c1) , ($c2), "=", $f1[ $c1 ] ) - : "" ; - /** - * increase the out-putcounter, if "show_equal" is enabled - * this ist more for demonstration purpose - */ - if ( $show_equal == 1 ) - { - $outcount++ ; - } - - /** - * move the current-pointer in the left and right side - */ - $c1 ++; - $c2 ++; - } + About: + I searched a function to compare arrays and the array_diff() + was not specific enough. It ignores the order of the array-values. + So I reimplemented the diff-function which is found on unix-systems + but this you can use directly in your code and adopt for your needs. + Simply adopt the formatline-function. with the third-parameter of arr_diff() + you can hide matching lines. Hope someone has use for this. - /** - * the current lines are different so we search in parallel - * on each side for the next matching pair, we walk on both - * sided at the same time comparing with the current-lines - * this should be most probable to find the next matching pair - * we only search in a distance of 10 lines, because then it - * is not the same function most of the time. other algos - * would be very complicated, to detect 'real' block movements. - */ - else - { - - $b = "" ; - $s1 = 0 ; # search on left - $s2 = 0 ; # search on right - $found = 0 ; # flag, found a matching pair - $b1 = "" ; - $b2 = "" ; - $fstop = 0 ; # distance of maximum search + Contact: d.u.diff@holomind.de + **/ - #fast search in on both sides for next match. - while ( - $found == 0 # search until we find a pair - and - ( $c1 + $s1 <= $max1 ) # and we are inside of the left lines - and - ( $c2 + $s2 <= $max2 ) # and we are inside of the right lines - and - $fstop++ < 10 # and the distance is lower than 10 lines - ) - { + private function arr_diff($f1, $f2, $show_equal = 0) + { + $c1 = 0 ; # current line of left + $c2 = 0 ; # current line of right + $max1 = count($f1) ; # maximal lines of left + $max2 = count($f2) ; # maximal lines of right + $outcount = 0; # output counter + $hit1 = []; # hit in left + $hit2 = []; # hit in right + $stop = 0; + $out = ""; - /** - * test the left side for a hit - * - * comparing current line with the searching line on the left - * b1 is a buffer, which collects the line which not match, to - * show the differences later, if one line hits, this buffer will - * be used, else it will be discarded later - */ - #hit - if ( trim( $f1[$c1+$s1] ) == trim( $f2[$c2] ) ) - { - $found = 1 ; # set flag to stop further search - $s2 = 0 ; # reset right side search-pointer - $c2-- ; # move back the current right, so next loop hits - $b = $b1 ; # set b=output (b)uffer - } - #no hit: move on - else - { - /** - * prevent finding a line again, which would show wrong results - * - * add the current line to leftbuffer, if this will be the hit - */ - if ( $hit1[ ($c1 + $s1) . "_" . ($c2) ] != 1 ) - { - /** - * add current search-line to diffence-buffer - */ - $b1 .= $this->formatline( ($c1 + $s1) , ($c2), "-", $f1[ $c1+$s1 ] ); + while ( + $c1 < $max1 # have next line in left + and + $c2 < $max2 # have next line in right + and + ($stop++) < 1000 # don-t have more then 1000 ( loop-stopper ) + and + $outcount < 20 # output count is less then 20 + ) { + /** + * is the trimmed line of the current left and current right line + * the same ? then this is a hit (no difference) + */ + if (trim($f1[$c1]) == trim($f2[$c2])) { + /** + * add to output-string, if "show_equal" is enabled + */ + $out .= ($show_equal==1) + ? $this->formatline(($c1), ($c2), "=", $f1[ $c1 ]) + : "" ; + /** + * increase the out-putcounter, if "show_equal" is enabled + * this ist more for demonstration purpose + */ + if ($show_equal == 1) { + $outcount++ ; + } - /** - * mark this line as 'searched' to prevent doubles. - */ - $hit1[ ($c1 + $s1) . "_" . $c2 ] = 1 ; - } - } + /** + * move the current-pointer in the left and right side + */ + $c1 ++; + $c2 ++; + } + + /** + * the current lines are different so we search in parallel + * on each side for the next matching pair, we walk on both + * sided at the same time comparing with the current-lines + * this should be most probable to find the next matching pair + * we only search in a distance of 10 lines, because then it + * is not the same function most of the time. other algos + * would be very complicated, to detect 'real' block movements. + */ + else { + $b = "" ; + $s1 = 0 ; # search on left + $s2 = 0 ; # search on right + $found = 0 ; # flag, found a matching pair + $b1 = "" ; + $b2 = "" ; + $fstop = 0 ; # distance of maximum search + + #fast search in on both sides for next match. + while ( + $found == 0 # search until we find a pair + and + ($c1 + $s1 <= $max1) # and we are inside of the left lines + and + ($c2 + $s2 <= $max2) # and we are inside of the right lines + and + $fstop++ < 10 # and the distance is lower than 10 lines + ) { + + /** + * test the left side for a hit + * + * comparing current line with the searching line on the left + * b1 is a buffer, which collects the line which not match, to + * show the differences later, if one line hits, this buffer will + * be used, else it will be discarded later + */ + #hit + if (trim($f1[$c1+$s1]) == trim($f2[$c2])) { + $found = 1 ; # set flag to stop further search + $s2 = 0 ; # reset right side search-pointer + $c2-- ; # move back the current right, so next loop hits + $b = $b1 ; # set b=output (b)uffer + } + #no hit: move on + else { + /** + * prevent finding a line again, which would show wrong results + * + * add the current line to leftbuffer, if this will be the hit + */ + if ($hit1[ ($c1 + $s1) . "_" . ($c2) ] != 1) { + /** + * add current search-line to diffence-buffer + */ + $b1 .= $this->formatline(($c1 + $s1), ($c2), "-", $f1[ $c1+$s1 ]); + + /** + * mark this line as 'searched' to prevent doubles. + */ + $hit1[ ($c1 + $s1) . "_" . $c2 ] = 1 ; + } + } - /** - * test the right side for a hit - * - * comparing current line with the searching line on the right - */ - if ( trim ( $f1[$c1] ) == trim ( $f2[$c2+$s2]) ) - { - $found = 1 ; # flag to stop search - $s1 = 0 ; # reset pointer for search - $c1-- ; # move current line back, so we hit next loop - $b = $b2 ; # get the buffered difference - } - else - { - /** - * prevent to find line again - */ - if ( $hit2[ ($c1) . "_" . ( $c2 + $s2) ] != 1 ) - { - /** - * add current searchline to buffer - */ - $b2 .= $this->formatline ( ($c1) , ($c2 + $s2), "+", $f2[ $c2+$s2 ] ); + /** + * test the right side for a hit + * + * comparing current line with the searching line on the right + */ + if (trim($f1[$c1]) == trim($f2[$c2+$s2])) { + $found = 1 ; # flag to stop search + $s1 = 0 ; # reset pointer for search + $c1-- ; # move current line back, so we hit next loop + $b = $b2 ; # get the buffered difference + } else { + /** + * prevent to find line again + */ + if ($hit2[ ($c1) . "_" . ($c2 + $s2) ] != 1) { + /** + * add current searchline to buffer + */ + $b2 .= $this->formatline(($c1), ($c2 + $s2), "+", $f2[ $c2+$s2 ]); - /** - * mark current line to prevent double-hits - */ - $hit2[ ($c1) . "_" . ($c2 + $s2) ] = 1; - } + /** + * mark current line to prevent double-hits + */ + $hit2[ ($c1) . "_" . ($c2 + $s2) ] = 1; + } + } - } + /** + * search in bigger distance + * + * increase the search-pointers (satelites) and try again + */ + $s1++ ; # increase left search-pointer + $s2++ ; # increase right search-pointer + } - /** - * search in bigger distance - * - * increase the search-pointers (satelites) and try again - */ - $s1++ ; # increase left search-pointer - $s2++ ; # increase right search-pointer - } + /** + * add line as different on both arrays (no match found) + */ + if ($found == 0) { + $b .= $this->formatline(($c1), ($c2), "-", $f1[ $c1 ]); + $b .= $this->formatline(($c1), ($c2), "+", $f2[ $c2 ]); + } - /** - * add line as different on both arrays (no match found) - */ - if ( $found == 0 ) - { - $b .= $this->formatline ( ($c1) , ($c2), "-", $f1[ $c1 ] ); - $b .= $this->formatline ( ($c1) , ($c2), "+", $f2[ $c2 ] ); - } + /** + * add current buffer to outputstring + */ + $out .= $b; + $outcount++ ; #increase outcounter - /** - * add current buffer to outputstring - */ - $out .= $b; - $outcount++ ; #increase outcounter + $c1++ ; #move currentline forward + $c2++ ; #move currentline forward - $c1++ ; #move currentline forward - $c2++ ; #move currentline forward + /** + * comment the lines are tested quite fast, because + * the current line always moves forward + */ + } /*endif*/ + }/*endwhile*/ - /** - * comment the lines are tested quite fast, because - * the current line always moves forward - */ + return $out; + }/*end func*/ - } /*endif*/ + /** + * callback function to format the diffence-lines with your 'style' + */ + private function formatline(int $nr1, int $nr2, string $stat, &$value): string + { #change to $value if problems + if (trim($value) == "") { + return ""; + } - }/*endwhile*/ + switch ($stat) { + case "=": + // return $nr1. " : $nr2 : = ".htmlentities( $value ) ."
    "; + return "$value\n"; + break; - return $out; + case "+": + //return $nr1. " : $nr2 : + ".htmlentities( $value ) ."
    "; + return "+++ $value\n"; + break; - }/*end func*/ + case "-": + //return $nr1. " : $nr2 : - ".htmlentities( $value ) ."
    "; + return "--- $value\n"; + break; - /** - * callback function to format the diffence-lines with your 'style' - * @param integer $nr1 - * @param integer $nr2 - * @param string $stat - * @return string - */ - private function formatline( $nr1, $nr2, $stat, &$value ) { #change to $value if problems - if(trim($value) == "") { - return ""; - } - - switch($stat) { - case "=": - // return $nr1. " : $nr2 : = ".htmlentities( $value ) ."
    "; - return "$value\n"; - break; - - case "+": - //return $nr1. " : $nr2 : + ".htmlentities( $value ) ."
    "; - return "+++ $value\n"; - break; - - case "-": - //return $nr1. " : $nr2 : - ".htmlentities( $value ) ."
    "; - return "--- $value\n"; - break; - } - } -// }}} + default: + throw new RuntimeException("stat needs to be =, + or -"); + } + } } - diff --git a/ext/wiki/test.php b/ext/wiki/test.php index 8d6e9bb2..2773133f 100644 --- a/ext/wiki/test.php +++ b/ext/wiki/test.php @@ -1,122 +1,115 @@ -get_page("wiki"); - $this->assert_title("Index"); - $this->assert_text("This is a default page"); - } +get_page("wiki"); + $this->assert_title("Index"); + $this->assert_text("This is a default page"); + } - public function testAccess() { - $this->markTestIncomplete(); + public function testAccess() + { + global $config; + foreach (["anon", "user", "admin"] as $user) { + foreach ([false, true] as $allowed) { + // admin has no settings to set + if ($user != "admin") { + $config->set_bool("wiki_edit_$user", $allowed); + } - global $config; - foreach(array("anon", "user", "admin") as $user) { - foreach(array(false, true) as $allowed) { - // admin has no settings to set - if($user != "admin") { - $config->set_bool("wiki_edit_$user", $allowed); - } + if ($user == "user") { + $this->log_in_as_user(); + } + if ($user == "admin") { + $this->log_in_as_admin(); + } - if($user == "user") {$this->log_in_as_user();} - if($user == "admin") {$this->log_in_as_admin();} + $this->get_page("wiki/test"); + $this->assert_title("test"); + $this->assert_text("This is a default page"); - $this->get_page("wiki/test"); - $this->assert_title("test"); - $this->assert_text("This is a default page"); + if ($allowed || $user == "admin") { + $this->post_page("wiki_admin/edit", ["title"=>"test"]); + $this->assert_text("Editor"); + } + /* + // Everyone can see the editor + else { + $this->post_page("wiki_admin/edit", ["title"=>"test"]); + $this->assert_no_text("Editor"); + } + */ - if($allowed || $user == "admin") { - $this->get_page("wiki/test", array('edit'=>'on')); - $this->assert_text("Editor"); - } - else { - $this->get_page("wiki/test", array('edit'=>'on')); - $this->assert_no_text("Editor"); - } + if ($user == "user" || $user == "admin") { + $this->log_out(); + } + } + } + } - if($user == "user" || $user == "admin") { - $this->log_out(); - } - } - } - } + public function testDefault() + { + global $user; + $this->log_in_as_admin(); - public function testLock() { - $this->markTestIncomplete(); + // Check default page is default + $this->get_page("wiki/wiki:default"); + $this->assert_title("wiki:default"); + $this->assert_text("This is a default page"); - global $config; - $config->set_bool("wiki_edit_anon", true); - $config->set_bool("wiki_edit_user", false); + // Customise default page + $wikipage = Wiki::get_page("wiki:default"); + $wikipage->revision = 1; + $wikipage->body = "New Default Template"; + send_event(new WikiUpdateEvent($user, $wikipage)); - $this->log_in_as_admin(); + // Check that some random page is using the new default + $this->get_page("wiki/something"); + $this->assert_text("New Default Template"); - $this->get_page("wiki/test_locked"); - $this->assert_title("test_locked"); - $this->assert_text("This is a default page"); - $this->click("Edit"); - $this->set_field("body", "test_locked content"); - $this->set_field("lock", true); - $this->click("Save"); - $this->log_out(); + // Delete the custom default + send_event(new WikiDeletePageEvent("wiki:default")); - $this->log_in_as_user(); - $this->get_page("wiki/test_locked"); - $this->assert_title("test_locked"); - $this->assert_text("test_locked content"); - $this->assert_no_text("Edit"); - $this->log_out(); + // Check that the default page is back to normal + $this->get_page("wiki/wiki:default"); + $this->assert_title("wiki:default"); + $this->assert_text("This is a default page"); + } - $this->get_page("wiki/test_locked"); - $this->assert_title("test_locked"); - $this->assert_text("test_locked content"); - $this->assert_no_text("Edit"); + public function testRevisions() + { + global $user; + $this->log_in_as_admin(); - $this->log_in_as_admin(); - $this->get_page("wiki/test_locked"); - $this->click("Delete All"); - $this->log_out(); - } + $this->get_page("wiki/test"); + $this->assert_title("test"); + $this->assert_text("This is a default page"); - public function testDefault() { - $this->markTestIncomplete(); + $wikipage = Wiki::get_page("test"); + $wikipage->revision = $wikipage->revision + 1; + $wikipage->body = "Mooooo 1"; + send_event(new WikiUpdateEvent($user, $wikipage)); + $this->get_page("wiki/test"); + $this->assert_text("Mooooo 1"); + $this->assert_text("Revision 1"); - $this->log_in_as_admin(); - $this->get_page("wiki/wiki:default"); - $this->assert_title("wiki:default"); - $this->assert_text("This is a default page"); - $this->click("Edit"); - $this->set_field("body", "Empty page! Fill it!"); - $this->click("Save"); + $wikipage = Wiki::get_page("test"); + $wikipage->revision = $wikipage->revision + 1; + $wikipage->body = "Mooooo 2"; + send_event(new WikiUpdateEvent($user, $wikipage)); + $this->get_page("wiki/test"); + $this->assert_text("Mooooo 2"); + $this->assert_text("Revision 2"); - $this->get_page("wiki/something"); - $this->assert_text("Empty page! Fill it!"); + send_event(new WikiDeleteRevisionEvent("test", 2)); + $this->get_page("wiki/test"); + $this->assert_text("Mooooo 1"); + $this->assert_text("Revision 1"); - $this->get_page("wiki/wiki:default"); - $this->click("Delete All"); - $this->log_out(); - } - - public function testRevisions() { - $this->markTestIncomplete(); - - $this->log_in_as_admin(); - $this->get_page("wiki/test"); - $this->assert_title("test"); - $this->assert_text("This is a default page"); - $this->click("Edit"); - $this->set_field("body", "Mooooo 1"); - $this->click("Save"); - $this->assert_text("Mooooo 1"); - $this->assert_text("Revision 1"); - $this->click("Edit"); - $this->set_field("body", "Mooooo 2"); - $this->click("Save"); - $this->assert_text("Mooooo 2"); - $this->assert_text("Revision 2"); - $this->click("Delete This Version"); - $this->assert_text("Mooooo 1"); - $this->assert_text("Revision 1"); - $this->click("Delete All"); - $this->log_out(); - } + send_event(new WikiDeletePageEvent("test")); + $this->get_page("wiki/test"); + $this->assert_title("test"); + $this->assert_text("This is a default page"); + } } - diff --git a/ext/wiki/theme.php b/ext/wiki/theme.php index 662159fb..4167951c 100644 --- a/ext/wiki/theme.php +++ b/ext/wiki/theme.php @@ -1,56 +1,58 @@ -title and ->body - * @param WikiPage|null $nav_page A wiki page object with navigation, has ->body - */ - public function display_page(Page $page, WikiPage $wiki_page, $nav_page) { - global $user; +class WikiTheme extends Themelet +{ + /** + * Show a page. + * + * $wiki_page The wiki page, has ->title and ->body + * $nav_page A wiki page object with navigation, has ->body + */ + public function display_page(Page $page, WikiPage $wiki_page, ?WikiPage $nav_page=null) + { + global $user; - if(is_null($nav_page)) { - $nav_page = new WikiPage(); - $nav_page->body = ""; - } + if (is_null($nav_page)) { + $nav_page = new WikiPage(); + $nav_page->body = ""; + } - $tfe = new TextFormattingEvent($nav_page->body); - send_event($tfe); + $tfe = new TextFormattingEvent($nav_page->body); + send_event($tfe); - // only the admin can edit the sidebar - if($user->is_admin()) { - $tfe->formatted .= "

    (Edit)"; - } + // only the admin can edit the sidebar + if ($user->can(Permissions::WIKI_ADMIN)) { + $tfe->formatted .= "

    (Edit)"; + } - $page->set_title(html_escape($wiki_page->title)); - $page->set_heading(html_escape($wiki_page->title)); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Wiki Index", $tfe->formatted, "left", 20)); - $page->add_block(new Block(html_escape($wiki_page->title), $this->create_display_html($wiki_page))); - } + $page->set_title(html_escape($wiki_page->title)); + $page->set_heading(html_escape($wiki_page->title)); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Wiki Index", $tfe->formatted, "left", 20)); + $page->add_block(new Block(html_escape($wiki_page->title), $this->create_display_html($wiki_page))); + } - public function display_page_editor(Page $page, WikiPage $wiki_page) { - $page->set_title(html_escape($wiki_page->title)); - $page->set_heading(html_escape($wiki_page->title)); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Editor", $this->create_edit_html($wiki_page))); - } + public function display_page_editor(Page $page, WikiPage $wiki_page) + { + $page->set_title(html_escape($wiki_page->title)); + $page->set_heading(html_escape($wiki_page->title)); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Editor", $this->create_edit_html($wiki_page))); + } - protected function create_edit_html(WikiPage $page) { - $h_title = html_escape($page->title); - $i_revision = int_escape($page->revision) + 1; + protected function create_edit_html(WikiPage $page) + { + $h_title = html_escape($page->title); + $i_revision = $page->revision + 1; - global $user; - if($user->is_admin()) { - $val = $page->is_locked() ? " checked" : ""; - $lock = "
    Lock page: "; - } - else { - $lock = ""; - } - return " + global $user; + if ($user->can(Permissions::WIKI_ADMIN)) { + $val = $page->is_locked() ? " checked" : ""; + $lock = "
    Lock page: "; + } else { + $lock = ""; + } + return " ".make_form(make_link("wiki_admin/save"))." @@ -59,31 +61,32 @@ class WikiTheme extends Themelet {
    "; - } + } - protected function create_display_html(WikiPage $page) { - global $user; + protected function create_display_html(WikiPage $page) + { + global $user; - $owner = $page->get_owner(); + $owner = $page->get_owner(); - $tfe = new TextFormattingEvent($page->body); - send_event($tfe); + $tfe = new TextFormattingEvent($page->body); + send_event($tfe); - $edit = ""; - $edit .= Wiki::can_edit($user, $page) ? - " + $edit = "
    "; + $edit .= Wiki::can_edit($user, $page) ? + " " : - ""; - if($user->is_admin()) { - $edit .= " + ""; + if ($user->can(Permissions::WIKI_ADMIN)) { + $edit .= " "; - } - $edit .= "
    ".make_form(make_link("wiki_admin/edit"))." - + ".make_form(make_link("wiki_admin/delete_revision"))." - + ".make_form(make_link("wiki_admin/delete_all"))." @@ -91,10 +94,10 @@ class WikiTheme extends Themelet {
    "; + } + $edit .= ""; - return " + return "

    $tfe->formatted
    @@ -106,6 +109,5 @@ class WikiTheme extends Themelet {

    "; - } + } } - diff --git a/ext/word_filter/info.php b/ext/word_filter/info.php new file mode 100644 index 00000000..fa32e9d7 --- /dev/null +++ b/ext/word_filter/info.php @@ -0,0 +1,13 @@ + - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Simple search and replace - */ +formatted = $this->filter($event->formatted); - $event->stripped = $this->filter($event->stripped); - } + public function onTextFormatting(TextFormattingEvent $event) + { + $event->formatted = $this->filter($event->formatted); + $event->stripped = $this->filter($event->stripped); + } - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Word Filter"); - $sb->add_longtext_option("word_filter"); - $sb->add_label("
    (each line should be search term and replace term, separated by a comma)"); - $event->panel->add_block($sb); - } + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Word Filter"); + $sb->add_longtext_option("word_filter"); + $sb->add_label("
    (each line should be search term and replace term, separated by a comma)"); + $event->panel->add_block($sb); + } - /** - * @param string $text - * @return string - */ - private function filter(/*string*/ $text) { - $map = $this->get_map(); - foreach($map as $search => $replace) { - $search = trim($search); - $replace = trim($replace); - if($search[0] == '/') { - $text = preg_replace($search, $replace, $text); - } - else { - $search = "/\\b" . str_replace("/", "\\/", $search) . "\\b/i"; - $text = preg_replace($search, $replace, $text); - } - } - return $text; - } + private function filter(string $text): string + { + $map = $this->get_map(); + foreach ($map as $search => $replace) { + $search = trim($search); + $replace = trim($replace); + if ($search[0] == '/') { + $text = preg_replace($search, $replace, $text); + } else { + $search = "/\\b" . str_replace("/", "\\/", $search) . "\\b/i"; + $text = preg_replace($search, $replace, $text); + } + } + return $text; + } - /** - * @return string[] - */ - private function get_map() { - global $config; - $raw = $config->get_string("word_filter"); - $lines = explode("\n", $raw); - $map = array(); - foreach($lines as $line) { - $parts = explode(",", $line); - if(count($parts) == 2) { - $map[$parts[0]] = $parts[1]; - } - } - return $map; - } + /** + * #return string[] + */ + private function get_map(): array + { + global $config; + $raw = $config->get_string("word_filter") ?? ""; + $lines = explode("\n", $raw); + $map = []; + foreach ($lines as $line) { + $parts = explode(",", $line); + if (count($parts) == 2) { + $map[$parts[0]] = $parts[1]; + } + } + return $map; + } } - diff --git a/ext/word_filter/test.php b/ext/word_filter/test.php index 4ac1748d..044615b0 100644 --- a/ext/word_filter/test.php +++ b/ext/word_filter/test.php @@ -1,67 +1,76 @@ -set_string("word_filter", "whore,nice lady\na duck,a kitten\n white ,\tspace\ninvalid"); - } +set_string("word_filter", "whore,nice lady\na duck,a kitten\n white ,\tspace\ninvalid"); + } - public function _doThings($in, $out) { - global $user; - $this->log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - send_event(new CommentPostingEvent($image_id, $user, $in)); - $this->get_page("post/view/$image_id"); - $this->assert_text($out); - } + public function _doThings($in, $out) + { + global $user; + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + send_event(new CommentPostingEvent($image_id, $user, $in)); + $this->get_page("post/view/$image_id"); + $this->assert_text($out); + } - public function testRegular() { - $this->_doThings( - "posted by a whore", - "posted by a nice lady" - ); - } + public function testRegular() + { + $this->_doThings( + "posted by a whore", + "posted by a nice lady" + ); + } - public function testReplaceAll() { - $this->_doThings( - "a whore is a whore is a whore", - "a nice lady is a nice lady is a nice lady" - ); - } + public function testReplaceAll() + { + $this->_doThings( + "a whore is a whore is a whore", + "a nice lady is a nice lady is a nice lady" + ); + } - public function testMixedCase() { - $this->_doThings( - "monkey WhorE", - "monkey nice lady" - ); - } + public function testMixedCase() + { + $this->_doThings( + "monkey WhorE", + "monkey nice lady" + ); + } - public function testOnlyWholeWords() { - $this->_doThings( - "my name is whoretta", - "my name is whoretta" - ); - } + public function testOnlyWholeWords() + { + $this->_doThings( + "my name is whoretta", + "my name is whoretta" + ); + } - public function testMultipleWords() { - $this->_doThings( - "I would like a duck", - "I would like a kitten" - ); - } + public function testMultipleWords() + { + $this->_doThings( + "I would like a duck", + "I would like a kitten" + ); + } - public function testWhitespace() { - $this->_doThings( - "A colour is white", - "A colour is space" - ); - } + public function testWhitespace() + { + $this->_doThings( + "A colour is white", + "A colour is space" + ); + } - public function testIgnoreInvalid() { - $this->_doThings( - "The word was invalid", - "The word was invalid" - ); - } + public function testIgnoreInvalid() + { + $this->_doThings( + "The word was invalid", + "The word was invalid" + ); + } } - diff --git a/index.php b/index.php index eff7317b..cd04ca5b 100644 --- a/index.php +++ b/index.php @@ -43,29 +43,25 @@ * Each of these can be imported at the start of a function with eg "global $page, $user;" */ -if(!file_exists("data/config/shimmie.conf.php")) { - header("Location: install.php"); - exit; +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ +* Make sure that shimmie is correctly installed * +\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +if (!file_exists("data/config/shimmie.conf.php")) { + require_once "core/install.php"; + install(); + exit; } -if(!file_exists("vendor/")) { - //CHECK: Should we just point to install.php instead? Seems unsafe though. - print << - + Shimmie Error - - + +
    @@ -73,40 +69,105 @@ if(!file_exists("vendor/")) {

    Warning: Composer vendor folder does not exist!

    Shimmie is unable to find the composer vendor directory.
    - Have you followed the composer setup instructions found in the README? + Have you followed the composer setup instructions found in the + README?

    -

    If you are not intending to do any development with Shimmie, it is highly recommend you use one of the pre-packaged releases found on Github instead.

    +

    If you are not intending to do any development with Shimmie, + it is highly recommend you use one of the pre-packaged releases + found on Github instead.

    EOD; - http_response_code(500); - exit; + http_response_code(500); + exit; } +require_once "vendor/autoload.php"; + + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ +* Load files * +\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +@include_once "data/config/shimmie.conf.php"; +@include_once "data/config/extensions.conf.php"; +require_once "core/sys_config.php"; +require_once "core/polyfills.php"; +require_once "core/util.php"; + +global $cache, $config, $database, $user, $page, $_tracer; +_sanitise_environment(); +$_tracer = new EventTracer(); +$_tracer->begin("Bootstrap"); +_load_core_files(); +$cache = new Cache(CACHE_DSN); +$database = new Database(DATABASE_DSN); +$config = new DatabaseConfig($database); +ExtensionInfo::load_all_extension_info(); +Extension::determine_enabled_extensions(); +require_all(zglob("ext/{".Extension::get_enabled_extensions_as_string()."}/main.php")); +_load_theme_files(); +$page = new Page(); +_load_event_listeners(); +$_tracer->end(); + + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ +* Send events, display output * +\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + try { - require_once "core/_bootstrap.inc.php"; - ctx_log_start(@$_SERVER["REQUEST_URI"], true, true); + // $_tracer->mark(@$_SERVER["REQUEST_URI"]); + $_tracer->begin( + $_SERVER["REQUEST_URI"] ?? "No Request", + [ + "user"=>$_COOKIE["shm_user"] ?? "No User", + "ip"=>$_SERVER['REMOTE_ADDR'] ?? "No IP", + "user_agent"=>$_SERVER['HTTP_USER_AGENT'] ?? "No UA", + ] + ); - // start the page generation waterfall - $user = _get_user(); - if(PHP_SAPI === 'cli') { - send_event(new CommandEvent($argv)); - } - else { - send_event(new PageRequestEvent(_get_query())); - $page->display(); - } + if (!SPEED_HAX) { + send_event(new DatabaseUpgradeEvent()); + } + send_event(new InitExtEvent()); - // saving cache data and profiling data to disk can happen later - if(function_exists("fastcgi_finish_request")) fastcgi_finish_request(); - $database->commit(); - ctx_log_endok(); + // start the page generation waterfall + $user = _get_user(); + send_event(new UserLoginEvent($user)); + if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') { + send_event(new CommandEvent($argv)); + } else { + send_event(new PageRequestEvent(_get_query())); + $page->display(); + } + + if ($database->transaction===true) { + $database->commit(); + } + + // saving cache data and profiling data to disk can happen later + if (function_exists("fastcgi_finish_request")) { + fastcgi_finish_request(); + } +} catch (Exception $e) { + if ($database && $database->transaction===true) { + $database->rollback(); + } + _fatal_error($e); +} finally { + $_tracer->end(); + if (TRACE_FILE) { + if ( + empty($_SERVER["REQUEST_URI"]) + || ( + (microtime(true) - $_shm_load_start) > TRACE_THRESHOLD + && ($_SERVER["REQUEST_URI"] ?? "") != "/upload" + ) + ) { + $_tracer->flush(TRACE_FILE); + } + } } -catch(Exception $e) { - if($database) $database->rollback(); - _fatal_error($e); - ctx_log_ender(); -} - diff --git a/install.php b/install.php deleted file mode 100644 index 1efe82ac..00000000 --- a/install.php +++ /dev/null @@ -1,501 +0,0 @@ - - - - - Shimmie Installation - - - - - - -
    -

    Install Error

    -
    -

    Shimmie needs to be run via a web server with PHP support -- you - appear to be either opening the file from your hard disk, or your - web server is mis-configured and doesn't know how to handle PHP files.

    -

    If you've installed a web server on your desktop PC, you probably - want to visit the local web server.

    -

    -
    -
    -
    -
    -		
    -

    Install Error

    -

    Warning: Composer vendor folder does not exist!

    -
    -

    Shimmie is unable to find the composer vendor directory.
    - Have you followed the composer setup instructions found in the README? - -

    If you are not intending to do any development with Shimmie, it is highly recommend you use one of the pre-packaged releases found on Github instead.

    -
    -
    -
    -$name ... ";
    -	if($value) {
    -		echo "ok\n";
    -	}
    -	else {
    -		echo "failed\n";
    -	}
    -}
    -// }}}
    -
    -function do_install() { // {{{
    -	if(file_exists("data/config/auto_install.conf.php")) {
    -		require_once "data/config/auto_install.conf.php";
    -	}
    -	else if(@$_POST["database_type"] == "sqlite" && isset($_POST["database_name"])) {
    -		define('DATABASE_DSN', "sqlite:{$_POST["database_name"]}");
    -	}
    -	else if(isset($_POST['database_type']) && isset($_POST['database_host']) && isset($_POST['database_user']) && isset($_POST['database_name'])) {
    -		define('DATABASE_DSN', "{$_POST['database_type']}:user={$_POST['database_user']};password={$_POST['database_password']};host={$_POST['database_host']};dbname={$_POST['database_name']}");
    -	}
    -	else {
    -		ask_questions();
    -		return;
    -	}
    -
    -	define("DATABASE_KA", true);
    -	install_process();
    -} // }}}
    -
    -function ask_questions() { // {{{
    -	$warnings = array();
    -	$errors = array();
    -
    -	if(check_gd_version() == 0 && check_im_version() == 0) {
    -		$errors[] = "
    -			No thumbnailers could be found - install the imagemagick
    -			tools (or the PHP-GD library, of imagemagick is unavailable).
    -		";
    -	}
    -	else if(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.
    -		";
    -	}
    -
    -	$drivers = PDO::getAvailableDrivers();
    -	if(
    -		!in_array("mysql", $drivers) &&
    -		!in_array("pgsql", $drivers) &&
    -
    -		!in_array("sqlite", $drivers)
    -	) {
    -		$errors[] = "
    -			No database connection library could be found; shimmie needs
    -			PDO with either Postgres, MySQL, or SQLite drivers
    -		";
    -	}
    -
    -	$db_m = in_array("mysql", $drivers)  ? '' : "";
    -	$db_p = in_array("pgsql", $drivers)  ? '' : "";
    -	$db_s = in_array("sqlite", $drivers) ? '' : "";
    -
    -	$warn_msg = $warnings ? "

    Warnings

    ".implode("\n
    ", $warnings) : ""; - $err_msg = $errors ? "

    Errors

    ".implode("\n
    ", $errors) : ""; - - print << -

    Shimmie Installer

    - -
    - $warn_msg - $err_msg - -

    Database Install

    -
    -
    - - - - - - - - - - - - - - - - - - - - - - -
    Type:
    Host:
    Username:
    Password:
    DB Name:
    -
    - -
    - -

    Help

    - -

    - Please make sure the database you have chosen exists and is empty.
    - The username provided must have access to create tables within the database. -

    -

    - For SQLite the database name will be a filename on disk, relative to - where shimmie was installed. -

    -

    - Drivers can generally be downloaded with your OS package manager; - for Debian / Ubuntu you want php5-pgsql, php5-mysql, or php5-sqlite. -

    -
    - -EOD; -} // }}} - -/** - * This is where the install really takes place. - */ -function install_process() { // {{{ - build_dirs(); - create_tables(); - insert_defaults(); - write_config(); -} // }}} - -function create_tables() { // {{{ - try { - $db = new Database(); - - if ( $db->count_tables() > 0 ) { - print << -

    Shimmie Installer

    -

    Warning: The Database schema is not empty!

    -
    -

    Please ensure that the database you are installing Shimmie with is empty before continuing.

    -

    Once you have emptied the database of any tables, please hit 'refresh' to continue.

    -

    -
    - -EOD; - exit(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)", array()); - - $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 SCORE_DATETIME NOT NULL DEFAULT SCORE_NOW, - class VARCHAR(32) NOT NULL DEFAULT 'user', - email VARCHAR(128) - "); - $db->execute("CREATE INDEX users_name_idx ON users(name)", array()); - - $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 SCORE_DATETIME NOT NULL DEFAULT SCORE_NOW, - 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)", array()); - $db->execute("CREATE INDEX images_width_idx ON images(width)", array()); - $db->execute("CREATE INDEX images_height_idx ON images(height)", array()); - $db->execute("CREATE INDEX images_hash_idx ON images(hash)", array()); - - $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)", array()); - - $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)", array()); - $db->execute("CREATE INDEX images_tags_tag_id_idx ON image_tags(tag_id)", array()); - - $db->execute("INSERT INTO config(name, value) VALUES('db_version', 11)"); - $db->commit(); - } - catch(PDOException $e) { - handle_db_errors(TRUE, "An error occurred while trying to create the database tables necessary for Shimmie.", $e->getMessage(), 3); - } catch (Exception $e) { - handle_db_errors(FALSE, "An unknown error occurred while trying to insert data into the database.", $e->getMessage(), 4); - } -} // }}} - -function insert_defaults() { // {{{ - try { - $db = new Database(); - - $db->execute("INSERT INTO users(name, pass, joindate, class) VALUES(:name, :pass, now(), :class)", Array("name" => 'Anonymous', "pass" => null, "class" => 'anonymous')); - $db->execute("INSERT INTO config(name, value) VALUES(:name, :value)", Array("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)", Array("name" => 'thumb_engine', "value" => 'convert')); - } - $db->commit(); - } - catch(PDOException $e) { - handle_db_errors(TRUE, "An error occurred while trying to insert data into the database.", $e->getMessage(), 5); - } - catch (Exception $e) { - handle_db_errors(FALSE, "An unknown error occurred while trying to insert data into the database.", $e->getMessage(), 6); - } -} // }}} - -function build_dirs() { // {{{ - // *try* and make default dirs. Ignore any errors -- - // if something is amiss, we'll tell the user later - if(!file_exists("images")) @mkdir("images"); - if(!file_exists("thumbs")) @mkdir("thumbs"); - if(!file_exists("data") ) @mkdir("data"); - if(!is_writable("images")) @chmod("images", 0755); - if(!is_writable("thumbs")) @chmod("thumbs", 0755); - if(!is_writable("data") ) @chmod("data", 0755); - - // Clear file status cache before checking again. - clearstatcache(); - - if( - !file_exists("images") || !file_exists("thumbs") || !file_exists("data") || - !is_writable("images") || !is_writable("thumbs") || !is_writable("data") - ) { - print " -
    -

    Shimmie Installer

    -

    Directory Permissions Error:

    -
    -

    Shimmie needs to make three folders in it's directory, 'images', 'thumbs', and 'data', and they need to be writable by the PHP user.

    -

    If you see this error, if probably means the folders are owned by you, and they need to be writable by the web server.

    -

    PHP reports that it is currently running as user: ".$_ENV["USER"]." (". $_SERVER["USER"] .")

    -

    Once you have created these folders and / or changed the ownership of the shimmie folder, hit 'refresh' to continue.

    -

    -
    -
    - "; - exit(7); - } -} // }}} - -function write_config() { // {{{ - $file_content = '<' . '?php' . "\n" . - "define('DATABASE_DSN', '".DATABASE_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"); - print << -

    Shimmie Installer

    -

    Things are OK \o/

    -
    -

    If you aren't redirected, click here to Continue. -

    - -EOD; - } - else { - $h_file_content = htmlentities($file_content); - print << -

    Shimmie Installer

    -

    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 "<?php" or after the "?>" - -

    - -

    Once done, click here to Continue. -

    -

    - -EOD; - } - echo "\n"; -} // }}} - -/** - * @param boolean $isPDO - * @param string $errorMessage1 - * @param string $errorMessage2 - * @param integer $exitCode - */ -function handle_db_errors(/*bool*/ $isPDO, /*str*/ $errorMessage1, /*str*/ $errorMessage2, /*int*/ $exitCode) { - $errorMessage1Extra = ($isPDO ? "Please check and ensure that the database configuration options are all correct." : "Please check the server log files for more information."); - print << -

    Shimmie Installer

    -

    Unknown Error:

    -
    -

    {$errorMessage1}

    -

    {$errorMessage1Extra}

    -

    {$errorMessage2}

    -
    - -EOD; - exit($exitCode); -} -?> - - diff --git a/lib/context.php b/lib/context.php deleted file mode 100644 index 9372b3c7..00000000 --- a/lib/context.php +++ /dev/null @@ -1,62 +0,0 @@ - diff --git a/lib/shimmie.css b/lib/shimmie.css deleted file mode 100644 index 813f87ca..00000000 --- a/lib/shimmie.css +++ /dev/null @@ -1,32 +0,0 @@ - -ARTICLE SELECT {width: 150px;} -INPUT, TEXTAREA {box-sizing: border-box;} -TD>INPUT[type="button"] {width: 100%;} -TD>INPUT[type="submit"] {width: 100%;} -TD>INPUT[type="text"] {width: 100%;} -TD>INPUT[type="password"] {width: 100%;} -TD>SELECT {width: 100%;} -TD>TEXTAREA {width: 100%;} - -TABLE.form {width: 300px;} -TABLE.form TD, TABLE.form TH {vertical-align: middle;} -TABLE.form TBODY TD {text-align: left;} -TABLE.form TBODY TH {text-align: right; padding-right: 4px; width: 1%;} -TABLE.form TD + TH {padding-left: 8px;} - -*[onclick], -H3[class~="shm-toggler"], -.sortable TH { - cursor: pointer; -} -IMG {border: none;} -FORM {margin: 0px;} -IMG.lazy {display: none;} - -#flash { - background: #FF7; - display: block; - padding: 8px; - margin: 8px; - border: 1px solid #882; -} diff --git a/lib/shimmie.js b/lib/shimmie.js deleted file mode 100644 index ae6b98c5..00000000 --- a/lib/shimmie.js +++ /dev/null @@ -1,155 +0,0 @@ -/*jshint bitwise:false, curly:true, eqeqeq:true, evil:true, forin:false, noarg:true, noempty:true, nonew:true, undef:false, strict:false, browser:true */ - -$(document).ready(function() { - /** Load jQuery extensions **/ - //Code via: http://stackoverflow.com/a/13106698 - $.fn.highlight = function (fadeOut) { - fadeOut = typeof fadeOut !== 'undefined' ? fadeOut : 5000; - $(this).each(function () { - var el = $(this); - $("
    ") - .width(el.outerWidth()) - .height(el.outerHeight()) - .css({ - "position": "absolute", - "left": el.offset().left, - "top": el.offset().top, - "background-color": "#ffff99", - "opacity": ".7", - "z-index": "9999999", - "border-top-left-radius": parseInt(el.css("borderTopLeftRadius"), 10), - "border-top-right-radius": parseInt(el.css("borderTopRightRadius"), 10), - "border-bottom-left-radius": parseInt(el.css("borderBottomLeftRadius"), 10), - "border-bottom-right-radius": parseInt(el.css("borderBottomRightRadius"), 10) - }).appendTo('body').fadeOut(fadeOut).queue(function () { $(this).remove(); }); - }); - }; - - /** Setup jQuery.timeago **/ - $.timeago.settings.cutoff = 365 * 24 * 60 * 60 * 1000; // Display original dates older than 1 year - $("time").timeago(); - - /** Setup tablesorter **/ - $("table.sortable").tablesorter(); - - $(".shm-clink").each(function(idx, elm) { - var target_id = $(elm).data("clink-sel"); - if(target_id && $(target_id).length > 0) { - // if the target comment is already on this page, don't bother - // switching pages - $(elm).attr("href", target_id); - // highlight it when clicked - $(elm).click(function(e) { - // This needs jQuery UI - $(target_id).highlight(); - }); - // vanilla target name should already be in the URL tag, but this - // will include the anon ID as displayed on screen - $(elm).html("@"+$(target_id+" .username").html()); - } - }); - - try { - var sidebar_hidden = (Cookies.get("ui-sidebar-hidden") || "").split("|"); - for(var i in sidebar_hidden) { - if(sidebar_hidden.hasOwnProperty(i) && sidebar_hidden[i].length > 0) { - $(sidebar_hidden[i]+" .blockbody").hide(); - } - } - } - catch(err) { - var sidebar_hidden = []; - } - $(".shm-toggler").each(function(idx, elm) { - var tid = $(elm).data("toggle-sel"); - var tob = $(tid+" .blockbody"); - $(elm).click(function(e) { - tob.slideToggle("slow"); - if(sidebar_hidden.indexOf(tid) === -1) { - sidebar_hidden.push(tid); - } - else { - for (var i in sidebar_hidden) { - if (sidebar_hidden[i] === tid) { - sidebar_hidden.splice(i, 1); - } - } - } - Cookies.set("ui-sidebar-hidden", sidebar_hidden.join("|"), {expires: 365}); - }); - }); - - $(".shm-unlocker").each(function(idx, elm) { - var tid = $(elm).data("unlock-sel"); - var tob = $(tid); - $(elm).click(function(e) { - $(elm).attr("disabled", true); - tob.attr("disabled", false); - }); - }); - - if(document.location.hash.length > 3) { - var query = document.location.hash.substring(1); - - $('#prevlink').attr('href', function(i, attr) { - return attr + '?' + query; - }); - $('#nextlink').attr('href', function(i, attr) { - return attr + '?' + query; - }); - } - - /* - * If an image list has a data-query attribute, append - * that query string to all thumb links inside the list. - * This allows us to cache the same thumb for all query - * strings, adding the query in the browser. - */ - $(".shm-image-list").each(function(idx, elm) { - var query = $(this).data("query"); - if(query) { - $(this).find(".shm-thumb-link").each(function(idx2, elm2) { - $(this).attr("href", $(this).attr("href") + query); - }); - } - }); -}); - - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ -* LibShish-JS * -\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -function addEvent(obj, event, func, capture){ - if (obj.addEventListener){ - obj.addEventListener(event, func, capture); - } else if (obj.attachEvent){ - obj.attachEvent("on"+event, func); - } -} - - -function byId(id) { - return document.getElementById(id); -} - - -// used once in ext/setup/main -function getHTTPObject() { - if (window.XMLHttpRequest){ - return new XMLHttpRequest(); - } - else if(window.ActiveXObject){ - return new ActiveXObject("Microsoft.XMLHTTP"); - } -} - - -function replyTo(imageId, commentId, userId) { - var box = $("#comment_on_"+imageId); - var text = "[url=site://post/view/"+imageId+"#c"+commentId+"]@"+userId+"[/url]: "; - - box.focus(); - box.val(box.val() + text); - $("#c"+commentId).highlight(); -} diff --git a/lib/vendor/.gitkeep b/lib/vendor/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/vendor/css/.gitkeep b/lib/vendor/css/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/vendor/js/.gitkeep b/lib/vendor/js/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/vendor/swf/.gitkeep b/lib/vendor/swf/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/alert.svg b/tests/alert.svg new file mode 100644 index 00000000..7729c9cd --- /dev/null +++ b/tests/alert.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 961c0c0b..9ad5fe5b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,160 +1,258 @@ -onUserCreation(new UserCreationEvent("demo", "demo", "")); - $userPage->onUserCreation(new UserCreationEvent("test", "test", "")); +$_SERVER['QUERY_STRING'] = '/'; +if (file_exists("tests/trace.json")) { + unlink("tests/trace.json"); } -abstract class ShimmiePHPUnitTestCase extends \PHPUnit_Framework_TestCase { - protected $backupGlobalsBlacklist = array('database', 'config'); - private $images = array(); +global $cache, $config, $database, $user, $page, $_tracer; +_sanitise_environment(); +$tracer_enabled = true; +$_tracer = new EventTracer(); +$_tracer->begin("bootstrap"); +_load_core_files(); +$cache = new Cache(CACHE_DSN); +$dsn = getenv("DSN"); +$database = new Database($dsn ? $dsn : "sqlite::memory:"); +create_dirs(); +create_tables($database); +$config = new DatabaseConfig($database); +ExtensionInfo::load_all_extension_info(); +Extension::determine_enabled_extensions(); +require_all(zglob("ext/{".Extension::get_enabled_extensions_as_string()."}/main.php")); +_load_theme_files(); +$page = new Page(); +_load_event_listeners(); +$config->set_string("thumb_engine", "static"); # GD has less overhead per-call +$config->set_bool("nice_urls", true); +send_event(new DatabaseUpgradeEvent()); +send_event(new InitExtEvent()); +$_tracer->end(); - public function setUp() { - $class = str_replace("Test", "", get_class($this)); - if(!class_exists($class)) { - $this->markTestSkipped("$class not loaded"); - } - elseif(!ext_is_live($class)) { - $this->markTestSkipped("$class not supported with this database"); - } +abstract class ShimmiePHPUnitTestCase extends TestCase +{ + protected static $anon_name = "anonymous"; + protected static $admin_name = "demo"; + protected static $user_name = "test"; + protected $wipe_time = "test"; - // things to do after bootstrap and before request - // log in as anon - $this->log_out(); - } + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + global $_tracer; + $_tracer->begin(get_called_class()); - public function tearDown() { - foreach($this->images as $image_id) { - $this->delete_image($image_id); - } - } + self::create_user(self::$admin_name); + self::create_user(self::$user_name); + } - protected function get_page($page_name, $args=null) { - // use a fresh page - global $page; - if(!$args) $args = array(); - $_GET = $args; - $page = class_exists("CustomPage") ? new CustomPage() : new Page(); - send_event(new PageRequestEvent($page_name)); - if($page->mode == "redirect") { - $page->code = 302; - } - } + public function setUp(): void + { + global $database, $_tracer; + $_tracer->begin($this->getName()); + $_tracer->begin("setUp"); + $class = str_replace("Test", "", get_class($this)); + if (!ExtensionInfo::get_for_extension_class($class)->is_supported()) { + $this->markTestSkipped("$class not supported with this database"); + } - // page things - protected function assert_title($title) { - global $page; - $this->assertContains($title, $page->title); - } + // If we have a parent test, don't wipe out the state they gave us + if (!$this->getDependencyInput()) { + // things to do after bootstrap and before request + // log in as anon + self::log_out(); - protected function assert_no_title($title) { - global $page; - $this->assertNotContains($title, $page->title); - } + foreach ($database->get_col("SELECT id FROM images") as $image_id) { + send_event(new ImageDeletionEvent(Image::by_id((int)$image_id), true)); + } + } - /** - * @param integer $code - */ - protected function assert_response($code) { - global $page; - $this->assertEquals($code, $page->code); - } + $_tracer->end(); + $_tracer->begin("test"); + } - protected function page_to_text($section=null) { - global $page; - $text = $page->title . "\n"; - foreach($page->blocks as $block) { - if(is_null($section) || $section == $block->section) { - $text .= $block->header . "\n"; - $text .= $block->body . "\n\n"; - } - } - return $text; - } + public function tearDown(): void + { + global $_tracer; + $_tracer->end(); + $_tracer->end(); + $_tracer->clear(); + $_tracer->flush("tests/trace.json"); + } - protected function assert_text($text, $section=null) { - $this->assertContains($text, $this->page_to_text($section)); - } + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + global $_tracer; + $_tracer->end(); + } - /** - * @param string $text - */ - protected function assert_no_text($text, $section=null) { - $this->assertNotContains($text, $this->page_to_text($section)); - } + protected static function create_user(string $name) + { + if (is_null(User::by_name($name))) { + $userPage = new UserPage(); + $userPage->onUserCreation(new UserCreationEvent($name, $name, "")); + assert(!is_null(User::by_name($name)), "Creation of user $name failed"); + } + } - /** - * @param string $content - */ - protected function assert_content($content) { - global $page; - $this->assertContains($content, $page->data); - } + protected static function get_page($page_name, $args=null) + { + // use a fresh page + global $page; + if (!$args) { + $args = []; + } + $_GET = $args; + $_POST = []; + $page = new Page(); + send_event(new PageRequestEvent($page_name)); + if ($page->mode == PageMode::REDIRECT) { + $page->code = 302; + } + return $page; + } - /** - * @param string $content - */ - protected function assert_no_content($content) { - global $page; - $this->assertNotContains($content, $page->data); - } + protected static function post_page($page_name, $args=null) + { + // use a fresh page + global $page; + if (!$args) { + $args = []; + } + foreach ($args as $k=>$v) { + $args[$k] = (string)$v; + } + $_GET = []; + $_POST = $args; + $page = new Page(); + send_event(new PageRequestEvent($page_name)); + if ($page->mode == PageMode::REDIRECT) { + $page->code = 302; + } + } - // user things - protected function log_in_as_admin() { - global $user; - $user = User::by_name('demo'); - $this->assertNotNull($user); - } + // page things + protected function assert_title(string $title) + { + global $page; + $this->assertStringContainsString($title, $page->title); + } - protected function log_in_as_user() { - global $user; - $user = User::by_name('test'); - $this->assertNotNull($user); - } + protected function assert_title_matches($title) + { + global $page; + $this->assertStringMatchesFormat($title, $page->title); + } - protected function log_out() { - global $user, $config; - $user = User::by_id($config->get_int("anon_id", 0)); - $this->assertNotNull($user); - } + protected function assert_no_title(string $title) + { + global $page; + $this->assertStringNotContainsString($title, $page->title); + } - // post things - /** - * @param string $filename - * @param string $tags - * @return int - */ - protected function post_image($filename, $tags) { - $dae = new DataUploadEvent($filename, array( - "filename" => $filename, - "extension" => pathinfo($filename, PATHINFO_EXTENSION), - "tags" => Tag::explode($tags), - "source" => null, - )); - send_event($dae); - $this->images[] = $dae->image_id; - return $dae->image_id; - } + protected function assert_response(int $code) + { + global $page; + $this->assertEquals($code, $page->code); + } - /** - * @param int $image_id - */ - protected function delete_image($image_id) { - $img = Image::by_id($image_id); - if($img) { - $ide = new ImageDeletionEvent($img); - send_event($ide); - } - } + protected function page_to_text(string $section=null) + { + global $page; + if ($page->mode == PageMode::PAGE) { + $text = $page->title . "\n"; + foreach ($page->blocks as $block) { + if (is_null($section) || $section == $block->section) { + $text .= $block->header . "\n"; + $text .= $block->body . "\n\n"; + } + } + return $text; + } elseif ($page->mode == PageMode::DATA) { + return $page->data; + } else { + $this->assertTrue(false, "Page mode is not PAGE or DATA"); + } + } + + protected function assert_text(string $text, string $section=null) + { + $this->assertStringContainsString($text, $this->page_to_text($section)); + } + + protected function assert_no_text(string $text, string $section=null) + { + $this->assertStringNotContainsString($text, $this->page_to_text($section)); + } + + protected function assert_content(string $content) + { + global $page; + $this->assertStringContainsString($content, $page->data); + } + + protected function assert_no_content(string $content) + { + global $page; + $this->assertStringNotContainsString($content, $page->data); + } + + protected function assert_search_results($tags, $results) + { + $images = Image::find_images(0, null, $tags); + $ids = []; + foreach ($images as $image) { + $ids[] = $image->id; + } + $this->assertEquals($results, $ids); + } + + // user things + protected static function log_in_as_admin() + { + send_event(new UserLoginEvent(User::by_name(self::$admin_name))); + } + + protected static function log_in_as_user() + { + send_event(new UserLoginEvent(User::by_name(self::$user_name))); + } + + protected static function log_out() + { + global $config; + send_event(new UserLoginEvent(User::by_id($config->get_int("anon_id", 0)))); + } + + // post things + protected function post_image(string $filename, string $tags): int + { + $dae = new DataUploadEvent($filename, [ + "filename" => $filename, + "extension" => pathinfo($filename, PATHINFO_EXTENSION), + "tags" => Tag::explode($tags), + "source" => null, + ]); + send_event($dae); + return $dae->image_id; + } + + protected function delete_image(int $image_id) + { + $img = Image::by_id($image_id); + if ($img) { + $ide = new ImageDeletionEvent($img, true); + send_event($ide); + } + } } diff --git a/tests/defines.php b/tests/defines.php new file mode 100644 index 00000000..48431eca --- /dev/null +++ b/tests/defines.php @@ -0,0 +1,18 @@ + data/config/auto_install.conf.php +/usr/bin/php -d upload_max_filesize=50M -d post_max_size=50M -S 0.0.0.0:8000 tests/router.php diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 1fdaf756..9b86d9f4 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,7 +1,17 @@ - + + ../core/ + + ../ext/ + + + ../core + ../ext + ../themes/default + + diff --git a/tests/router.php b/tests/router.php index 5ba314d1..c35a69c0 100644 --- a/tests/router.php +++ b/tests/router.php @@ -1,23 +1,29 @@ disable_left(); + $page->disable_left(); - // parts for the whole page - $prev = $page_number - 1; - $next = $page_number + 1; + // parts for the whole page + $prev = $page_number - 1; + $next = $page_number + 1; - $h_prev = ($page_number <= 1) ? "Prev" : - "Prev"; - $h_index = "Index"; - $h_next = ($page_number >= $total_pages) ? "Next" : - "Next"; + $h_prev = ($page_number <= 1) ? "Prev" : + "Prev"; + $h_index = "Index"; + $h_next = ($page_number >= $total_pages) ? "Next" : + "Next"; - $nav = "$h_prev | $h_index | $h_next"; + $nav = "$h_prev | $h_index | $h_next"; - $page->set_title("Comments"); - $page->set_heading("Comments"); - $page->add_block(new Block("Navigation", $nav, "left")); - $this->display_paginator($page, "comment/list", null, $page_number, $total_pages); + $page->set_title("Comments"); + $page->set_heading("Comments"); + $page->add_block(new Block("Navigation", $nav, "left")); + $this->display_paginator($page, "comment/list", null, $page_number, $total_pages); - // parts for each image - $position = 10; - - $comment_captcha = $config->get_bool('comment_captcha'); - $comment_limit = $config->get_int("comment_list_count", 10); - - foreach($images as $pair) { - $image = $pair[0]; - $comments = $pair[1]; + // parts for each image + $position = 10; - $thumb_html = $this->build_thumb_html($image); + $comment_captcha = $config->get_bool('comment_captcha'); + $comment_limit = $config->get_int("comment_list_count", 10); - $s = "   "; - $un = $image->get_owner()->name; - $t = ""; - foreach($image->get_tag_array() as $tag) { - $u_tag = url_escape($tag); - $t .= "".html_escape($tag)." "; - } - $p = autodate($image->posted); + foreach ($images as $pair) { + $image = $pair[0]; + $comments = $pair[1]; - $r = ext_is_live("Ratings") ? "Rating ".Ratings::rating_to_human($image->rating) : ""; - $comment_html = "Date $p $s User $un $s $r
    Tags $t

     "; + $thumb_html = $this->build_thumb_html($image); - $comment_count = count($comments); - if($comment_limit > 0 && $comment_count > $comment_limit) { - //$hidden = $comment_count - $comment_limit; - $comment_html .= "

    showing $comment_limit of $comment_count comments

    "; - $comments = array_slice($comments, -$comment_limit); - } - foreach($comments as $comment) { - $comment_html .= $this->comment_to_html($comment); - } - if($can_post) { - if(!$user->is_anonymous()) { - $comment_html .= $this->build_postbox($image->id); - } - else { - if(!$comment_captcha) { - $comment_html .= $this->build_postbox($image->id); - } - else { - $comment_html .= "Add Comment"; - } - } - } + $s = "   "; + $un = $image->get_owner()->name; + $t = ""; + foreach ($image->get_tag_array() as $tag) { + $u_tag = url_escape($tag); + $t .= "".html_escape($tag)." "; + } + $p = autodate($image->posted); - $html = " + $r = Extension::is_enabled(RatingsInfo::KEY) ? "Rating ".Ratings::rating_to_human($image->rating) : ""; + $comment_html = "Date $p $s User $un $s $r
    Tags $t

     "; + + $comment_count = count($comments); + if ($comment_limit > 0 && $comment_count > $comment_limit) { + //$hidden = $comment_count - $comment_limit; + $comment_html .= "

    showing $comment_limit of $comment_count comments

    "; + $comments = array_slice($comments, -$comment_limit); + } + foreach ($comments as $comment) { + $comment_html .= $this->comment_to_html($comment); + } + if ($can_post) { + if (!$user->is_anonymous()) { + $comment_html .= $this->build_postbox($image->id); + } else { + if (!$comment_captcha) { + $comment_html .= $this->build_postbox($image->id); + } else { + $comment_html .= "Add Comment"; + } + } + } + + $html = " @@ -84,54 +78,49 @@ class CustomCommentListTheme extends CommentListTheme { "; - $page->add_block(new Block(" ", $html, "main", $position++)); - } - } + $page->add_block(new Block(" ", $html, "main", $position++)); + } + } - public function display_recent_comments($comments) { - // no recent comments in this theme - } + public function display_recent_comments(array $comments) + { + // no recent comments in this theme + } - /** - * @param Comment $comment - * @param bool $trim - * @return string - */ - protected function comment_to_html(Comment $comment, $trim=false) { - global $user; + protected function comment_to_html(Comment $comment, bool $trim=false): string + { + global $user; - $tfe = new TextFormattingEvent($comment->comment); - send_event($tfe); + $tfe = new TextFormattingEvent($comment->comment); + send_event($tfe); - //$i_uid = int_escape($comment->owner_id); - $h_name = html_escape($comment->owner_name); - //$h_poster_ip = html_escape($comment->poster_ip); - $h_comment = ($trim ? substr($tfe->stripped, 0, 50)."..." : $tfe->formatted); - $i_comment_id = int_escape($comment->comment_id); - $i_image_id = int_escape($comment->image_id); - $h_posted = autodate($comment->posted); + //$i_uid = $comment->owner_id; + $h_name = html_escape($comment->owner_name); + //$h_poster_ip = html_escape($comment->poster_ip); + $h_comment = ($trim ? substr($tfe->stripped, 0, 50)."..." : $tfe->formatted); + $i_comment_id = $comment->comment_id; + $i_image_id = $comment->image_id; + $h_posted = autodate($comment->posted); - $h_userlink = "$h_name"; - $h_del = ""; - if ($user->can("delete_comment")) { - $comment_preview = substr(html_unescape($tfe->stripped), 0, 50); - $j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview"); - $h_delete_script = html_escape("return confirm($j_delete_confirm_message);"); - $h_delete_link = make_link("comment/delete/$i_comment_id/$i_image_id"); - $h_del = " - Del"; - } - //$h_imagelink = $trim ? ">>>\n" : ""; - if($trim) { - return "

    $h_userlink $h_del
    $h_posted
    $h_comment

    "; - } - else { - return " + $h_userlink = "$h_name"; + $h_del = ""; + if ($user->can(Permissions::DELETE_COMMENT)) { + $comment_preview = substr(html_unescape($tfe->stripped), 0, 50); + $j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview"); + $h_delete_script = html_escape("return confirm($j_delete_confirm_message);"); + $h_delete_link = make_link("comment/delete/$i_comment_id/$i_image_id"); + $h_del = " - Del"; + } + //$h_imagelink = $trim ? ">>>\n" : ""; + if ($trim) { + return "

    $h_userlink $h_del
    $h_posted
    $h_comment

    "; + } else { + return "
    $thumb_html $comment_html
    $h_userlink
    $h_posted$h_del
    $h_comment
    "; - } - } + } + } } - diff --git a/themes/danbooru/custompage.class.php b/themes/danbooru/custompage.class.php deleted file mode 100644 index 4b36216c..00000000 --- a/themes/danbooru/custompage.class.php +++ /dev/null @@ -1,11 +0,0 @@ -left_enabled = false; - } -} - diff --git a/themes/danbooru/index.theme.php b/themes/danbooru/index.theme.php index 4c966ebd..5aa2f90f 100644 --- a/themes/danbooru/index.theme.php +++ b/themes/danbooru/index.theme.php @@ -1,75 +1,64 @@ -search_terms) == 0) { - $query = null; - $page_title = $config->get_string('title'); - } - else { - $search_string = implode(' ', $this->search_terms); - $query = url_escape($search_string); - $page_title = html_escape($search_string); - } + if (count($this->search_terms) == 0) { + $query = null; + $page_title = $config->get_string(SetupConfig::TITLE); + } else { + $search_string = Tag::implode($this->search_terms); + $query = url_escape(Tag::caret($search_string)); + $page_title = html_escape($search_string); + } - $nav = $this->build_navigation($this->page_number, $this->total_pages, $this->search_terms); - $page->set_title($page_title); - $page->set_heading($page_title); - $page->add_block(new Block("Search", $nav, "left", 0)); - if(count($images) > 0) { - if($query) { - $page->add_block(new Block("Images", $this->build_table($images, "search=$query"), "main", 10)); - $this->display_paginator($page, "post/list/$query", null, $this->page_number, $this->total_pages); - } - else { - $page->add_block(new Block("Images", $this->build_table($images, null), "main", 10)); - $this->display_paginator($page, "post/list", null, $this->page_number, $this->total_pages); - } - } - else { - $page->add_block(new Block("No Images Found", "No images were found to match the search criteria")); - } - } + $nav = $this->build_navigation($this->page_number, $this->total_pages, $this->search_terms); + $page->set_title($page_title); + $page->set_heading($page_title); + $page->add_block(new Block("Search", $nav, "left", 0)); + if (count($images) > 0) { + if ($query) { + $page->add_block(new Block("Images", $this->build_table($images, "search=$query"), "main", 10)); + $this->display_paginator($page, "post/list/$query", null, $this->page_number, $this->total_pages); + } else { + $page->add_block(new Block("Images", $this->build_table($images, null), "main", 10)); + $this->display_paginator($page, "post/list", null, $this->page_number, $this->total_pages); + } + } else { + $page->add_block(new Block("No Images Found", "No images were found to match the search criteria")); + } + } - /** - * @param int $page_number - * @param int $total_pages - * @param string[] $search_terms - * @return string - */ - protected function build_navigation($page_number, $total_pages, $search_terms) { - $h_search_string = count($search_terms) == 0 ? "" : html_escape(implode(" ", $search_terms)); - $h_search_link = make_link(); - $h_search = " + /** + * #param string[] $search_terms + */ + protected function build_navigation(int $page_number, int $total_pages, array $search_terms): string + { + $h_search_string = count($search_terms) == 0 ? "" : html_escape(implode(" ", $search_terms)); + $h_search_link = make_link(); + return "

    "; + } - return $h_search; - } - - /** - * @param Image[] $images - * @param string $query - * @return string - */ - protected function build_table($images, $query) { - $h_query = html_escape($query); - $table = "
    "; - foreach($images as $image) { - $table .= "\t" . $this->build_thumb_html($image) . "\n"; - } - $table .= "
    "; - return $table; - } + protected function build_table(array $images, ?string $query): string + { + $h_query = html_escape($query); + $table = "
    "; + foreach ($images as $image) { + $table .= "\t" . $this->build_thumb_html($image) . "\n"; + } + $table .= "
    "; + return $table; + } } - diff --git a/themes/danbooru/layout.class.php b/themes/danbooru/layout.class.php deleted file mode 100644 index 9ac3a7b4..00000000 --- a/themes/danbooru/layout.class.php +++ /dev/null @@ -1,267 +0,0 @@ - -* Link: http://trac.shishnet.org/shimmie2/ -* License: GPLv2 -* Description: This is a simple theme changing the css to make shimme -* look more like danbooru as well as adding a custom links -* bar and title to the top of every page. -*/ -//Small changes added by zshall -//Changed CSS and layout to make shimmie look even more like danbooru -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -Danbooru Theme - Notes (Bzchan) - -Files: default.php, style.css - -How to use a theme -- Copy the danbooru folder with all its contained files into the "themes" - directory in your shimmie installation. -- Log into your shimmie and change the Theme in the Board Config to your - desired theme. - -Changes in this theme include -- Adding and editing various elements in the style.css file. -- $site_name and $front_name retreival from config added. -- $custom_link and $title_link preparation just before html is outputed. -- Altered outputed html to include the custom links and removed heading - from being displayed (subheading is still displayed) -- Note that only the sidebar has been left aligned. Could not properly - left align the main block because blocks without headers currently do - not have ids on there div elements. (this was a problem because - paginator block must be centered and everything else left aligned) - -Tips -- You can change custom links to point to whatever pages you want as well as adding - more custom links. -- The main title link points to the Front Page set in your Board Config options. -- The text of the main title is the Title set in your Board Config options. -- Themes make no changes to your database or main code files so you can switch - back and forward to other themes all you like. - -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -class Layout { - public function display_page(Page $page) { - global $config, $user; - - $theme_name = $config->get_string('theme'); - //$base_href = $config->get_string('base_href'); - $data_href = get_base_href(); - $contact_link = contact_link(); - $header_html = $page->get_all_html_headers(); - - $left_block_html = ""; - $user_block_html = ""; - $main_block_html = ""; - $sub_block_html = ""; - - foreach($page->blocks as $block) { - switch($block->section) { - case "left": - $left_block_html .= $block->get_html(true); - break; - case "user": - $user_block_html .= $block->body; // $this->block_to_html($block, true); - break; - case "subheading": - $sub_block_html .= $block->body; // $this->block_to_html($block, true); - break; - case "main": - if($block->header == "Images") { - $block->header = " "; - } - $main_block_html .= $block->get_html(false); - break; - default: - print "

    error: {$block->header} using an unknown section ({$block->section})"; - break; - } - } - - $debug = get_debug_info(); - - $contact = empty($contact_link) ? "" : "
    Contact"; - - if(empty($this->subheading)) { - $subheading = ""; - } - else { - $subheading = "

    {$this->subheading}
    "; - } - - $site_name = $config->get_string('title'); // bzchan: change from normal default to get title for top of page - $main_page = $config->get_string('main_page'); // bzchan: change from normal default to get main page for top of page - - // bzchan: CUSTOM LINKS are prepared here, change these to whatever you like - $custom_links = ""; - if($user->is_anonymous()) { - $custom_links .= $this->navlinks(make_link('user_admin/login'), "My Account", array("user", "user_admin", "setup", "admin")); - } - else { - $custom_links .= $this->navlinks(make_link('user'), "My Account", array("user", "user_admin", "setup", "admin")); - } - $custom_links .= $this->navlinks(make_link('post/list'), "Posts", array("post")); - $custom_links .= $this->navlinks(make_link('comment/list'), "Comments", array("comment")); - $custom_links .= $this->navlinks(make_link('tags'), "Tags", array("tags")); - if(class_exists("Pools")) { - $custom_links .= $this->navlinks(make_link('pool/list'), "Pools", array("pool")); - } - $custom_links .= $this->navlinks(make_link('upload'), "Upload", array("upload")); - if(class_exists("Wiki")) { - $custom_links .= $this->navlinks(make_link('wiki'), "Wiki", array("wiki")); - $custom_links .= $this->navlinks(make_link('wiki/more'), "More »", array("wiki/more")); - } - - $custom_sublinks = ""; - // hack - $username = url_escape($user->name); - // hack - $qp = explode("/", ltrim(_get_query(), "/")); - // php sucks - switch($qp[0]) { - default: - $custom_sublinks .= $user_block_html; - break; - case "": - # FIXME: this assumes that the front page is - # post/list; in 99% of case it will either be - # post/list or home, and in the latter case - # the subnav links aren't shown, but it would - # be nice to be correct - case "post": - case "upload": - if(class_exists("NumericScore")){ $custom_sublinks .= "
  • Popular by Day/Month/Year
  • ";} - $custom_sublinks .= "
  • All
  • "; - if(class_exists("Favorites")){ $custom_sublinks .= "
  • My Favorites
  • ";} - if(class_exists("RSS_Images")){ $custom_sublinks .= "
  • Feed
  • ";} - if(class_exists("RandomImage")){ $custom_sublinks .= "
  • Random Image
  • ";} - if(class_exists("Wiki")){ $custom_sublinks .= "
  • Help
  • "; - }else{ $custom_sublinks .= "
  • Help
  • ";} - break; - case "comment": - $custom_sublinks .= "
  • All
  • "; - $custom_sublinks .= "
  • Help
  • "; - break; - case "pool": - $custom_sublinks .= "
  • List
  • "; - $custom_sublinks .= "
  • Create
  • "; - $custom_sublinks .= "
  • Changes
  • "; - $custom_sublinks .= "
  • Help
  • "; - break; - case "wiki": - $custom_sublinks .= "
  • Index
  • "; - $custom_sublinks .= "
  • Rules
  • "; - $custom_sublinks .= "
  • Help
  • "; - break; - case "tags": - case "alias": - $custom_sublinks .= "
  • Map
  • "; - $custom_sublinks .= "
  • Alphabetic
  • "; - $custom_sublinks .= "
  • Popularity
  • "; - $custom_sublinks .= "
  • Categories
  • "; - $custom_sublinks .= "
  • Aliases
  • "; - $custom_sublinks .= "
  • Help
  • "; - break; - } - - - // bzchan: failed attempt to add heading after title_link (failure was it looked bad) - //if($this->heading==$site_name)$this->heading = ''; - //$title_link = "

    $site_name/$this->heading

    "; - - // bzchan: prepare main title link - $title_link = "

    $site_name

    "; - - if($page->left_enabled) { - $left = ""; - $withleft = "withleft"; - } - else { - $left = ""; - $withleft = "noleft"; - } - - $flash = $page->get_cookie("flash_message"); - $flash_html = ""; - if($flash) { - $flash_html = "".nl2br(html_escape($flash))." [X]"; - } - - print << - - - - - - {$page->title} -$header_html - - - - -
    - $title_link - - -
    - $subheading - $sub_block_html - $left -
    - $flash_html - $main_block_html -
    -
    - Images © their respective owners, - Shimmie © - Shish & - The Team - 2007-2016, - based on the Danbooru concept. - $debug - $contact -
    - - -EOD; - } - - /** - * @param string $link - * @param string $desc - * @param string[] $pages_matched - * @return string - */ - private function navlinks($link, $desc, $pages_matched) { - /** - * Woo! We can actually SEE THE CURRENT PAGE!! (well... see it highlighted in the menu.) - */ - $html = null; - $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]) { - $html = "
  • $desc
  • "; - } - } - if(is_null($html)) {$html = "
  • $desc
  • ";} - return $html; - } -} - diff --git a/themes/danbooru/page.class.php b/themes/danbooru/page.class.php new file mode 100644 index 00000000..beb9ca93 --- /dev/null +++ b/themes/danbooru/page.class.php @@ -0,0 +1,168 @@ + + * Link: https://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: This is a simple theme changing the css to make shimme + * look more like danbooru as well as adding a custom links + * bar and title to the top of every page. + */ +//Small changes added by zshall +//Changed CSS and layout to make shimmie look even more like danbooru +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +Danbooru Theme - Notes (Bzchan) + +Files: default.php, style.css + +How to use a theme +- Copy the danbooru folder with all its contained files into the "themes" + directory in your shimmie installation. +- Log into your shimmie and change the Theme in the Board Config to your + desired theme. + +Changes in this theme include +- Adding and editing various elements in the style.css file. +- $site_name and $front_name retreival from config added. +- $custom_link and $title_link preparation just before html is outputed. +- Altered outputed html to include the custom links and removed heading + from being displayed (subheading is still displayed) +- Note that only the sidebar has been left aligned. Could not properly + left align the main block because blocks without headers currently do + not have ids on there div elements. (this was a problem because + paginator block must be centered and everything else left aligned) + +Tips +- You can change custom links to point to whatever pages you want as well as adding + more custom links. +- The main title link points to the Front Page set in your Board Config options. +- The text of the main title is the Title set in your Board Config options. +- Themes make no changes to your database or main code files so you can switch + back and forward to other themes all you like. + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +class Page extends BasePage +{ + /** @var bool */ + public $left_enabled = true; + + public function disable_left() + { + $this->left_enabled = false; + } + + public function render() + { + global $config; + + list($nav_links, $sub_links) = $this->get_nav_links(); + + $left_block_html = ""; + $user_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 "user": + $user_block_html .= $block->body; // $this->block_to_html($block, true); + break; + case "subheading": + $sub_block_html .= $block->body; // $this->block_to_html($block, true); + break; + case "main": + if ($block->header == "Images") { + $block->header = " "; + } + $main_block_html .= $block->get_html(false); + break; + default: + print "

    error: {$block->header} using an unknown section ({$block->section})"; + break; + } + } + + if (empty($this->subheading)) { + $subheading = ""; + } else { + $subheading = "

    {$this->subheading}
    "; + } + + $site_name = $config->get_string(SetupConfig::TITLE); // bzchan: change from normal default to get title for top of page + $main_page = $config->get_string(SetupConfig::MAIN_PAGE); // bzchan: change from normal default to get main page for top of page + + $custom_links = ""; + foreach ($nav_links as $nav_link) { + $custom_links .= "
  • ".$this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)."
  • "; + } + + $custom_sublinks = ""; + if (!empty($sub_links)) { + $custom_sublinks = "
    "; + foreach ($sub_links as $nav_link) { + $custom_sublinks .= "
  • ".$this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)."
  • "; + } + $custom_sublinks .= "
    "; + } + + // bzchan: failed attempt to add heading after title_link (failure was it looked bad) + //if($this->heading==$site_name)$this->heading = ''; + //$title_link = "

    $site_name/$this->heading

    "; + + // bzchan: prepare main title link + $title_link = "

    $site_name

    "; + + if ($this->left_enabled) { + $left = ""; + $withleft = "withleft"; + } else { + $left = ""; + $withleft = "noleft"; + } + + $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $head_html = $this->head_html(); + $footer_html = $this->footer_html(); + + print << + + $head_html + +
    + $title_link + + +
    + $subheading + $sub_block_html + $left +
    + $flash_html + $main_block_html +
    +
    $footer_html
    + + +EOD; + } + + public function navlinks(Link $link, string $desc, bool $active): ?string + { + $html = null; + if ($active) { + $html = "{$desc}"; + } else { + $html = "{$desc}"; + } + + return $html; + } +} diff --git a/themes/danbooru/tag_list.theme.php b/themes/danbooru/tag_list.theme.php index 6628ca29..31e174b8 100644 --- a/themes/danbooru/tag_list.theme.php +++ b/themes/danbooru/tag_list.theme.php @@ -1,9 +1,10 @@ -disable_left(); - parent::display_page($page); - } +class CustomTagListTheme extends TagListTheme +{ + public function display_page(Page $page) + { + $page->disable_left(); + parent::display_page($page); + } } - diff --git a/themes/danbooru/themelet.class.php b/themes/danbooru/themelet.class.php index b2badbb1..927d0c87 100644 --- a/themes/danbooru/themelet.class.php +++ b/themes/danbooru/themelet.class.php @@ -1,81 +1,66 @@ -build_paginator($page_number, $total_pages, $base, $query); - $page->add_block(new Block(null, $body, "main", 90)); - } +build_paginator($page_number, $total_pages, $base, $query); + $page->add_block(new Block(null, $body, "main", 90)); + } - /** - * @param string $base_url - * @param string $query - * @param int|string $page - * @param string $name - * @return string - */ - private function gen_page_link($base_url, $query, $page, $name) { - $link = make_link("$base_url/$page", $query); - return "$name"; - } + private function gen_page_link(string $base_url, ?string $query, int $page, string $name): string + { + $link = make_link("$base_url/$page", $query); + return "$name"; + } - /** - * @param string $base_url - * @param string $query - * @param int|string $page - * @param int $current_page - * @param string $name - * @return string - */ - private function gen_page_link_block($base_url, $query, $page, $current_page, $name) { - $paginator = ""; - if($page == $current_page) $paginator .= "$page"; - else $paginator .= $this->gen_page_link($base_url, $query, $page, $name); - return $paginator; - } + 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 .= "$page"; + } else { + $paginator .= $this->gen_page_link($base_url, $query, $page, $name); + } + return $paginator; + } - /** - * @param int $current_page - * @param int $total_pages - * @param string $base_url - * @param string $query - * @return string - */ - private function build_paginator($current_page, $total_pages, $base_url, $query) { - $next = $current_page + 1; - $prev = $current_page - 1; + private function build_paginator(int $current_page, int $total_pages, string $base_url, ?string $query): string + { + $next = $current_page + 1; + $prev = $current_page - 1; - $at_start = ($current_page <= 3 || $total_pages <= 3); - $at_end = ($current_page >= $total_pages -2); + $at_start = ($current_page <= 3 || $total_pages <= 3); + $at_end = ($current_page >= $total_pages -2); - $first_html = $at_start ? "" : $this->gen_page_link($base_url, $query, 1, "1"); - $prev_html = $at_start ? "" : $this->gen_page_link($base_url, $query, $prev, "<<"); - $next_html = $at_end ? "" : $this->gen_page_link($base_url, $query, $next, ">>"); - $last_html = $at_end ? "" : $this->gen_page_link($base_url, $query, $total_pages, "$total_pages"); + $first_html = $at_start ? "" : $this->gen_page_link($base_url, $query, 1, "1"); + $prev_html = $at_start ? "" : $this->gen_page_link($base_url, $query, $prev, "<<"); + $next_html = $at_end ? "" : $this->gen_page_link($base_url, $query, $next, ">>"); + $last_html = $at_end ? "" : $this->gen_page_link($base_url, $query, $total_pages, "$total_pages"); - $start = $current_page-2 > 1 ? $current_page-2 : 1; - $end = $current_page+2 <= $total_pages ? $current_page+2 : $total_pages; + $start = $current_page-2 > 1 ? $current_page-2 : 1; + $end = $current_page+2 <= $total_pages ? $current_page+2 : $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); + $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); - if(strlen($first_html) > 0) $pdots = "..."; - else $pdots = ""; + if (strlen($first_html) > 0) { + $pdots = "..."; + } else { + $pdots = ""; + } - if(strlen($last_html) > 0) $ndots = "..."; - else $ndots = ""; + if (strlen($last_html) > 0) { + $ndots = "..."; + } else { + $ndots = ""; + } - return "
    $prev_html $first_html $pdots $pages_html $ndots $last_html $next_html
    "; - } + return "
    $prev_html $first_html $pdots $pages_html $ndots $last_html $next_html
    "; + } } - diff --git a/themes/danbooru/upload.theme.php b/themes/danbooru/upload.theme.php index a7047cf3..31ce245e 100644 --- a/themes/danbooru/upload.theme.php +++ b/themes/danbooru/upload.theme.php @@ -1,14 +1,16 @@ -add_block(new Block("Upload", $this->build_upload_block(), "left", 20)); - } +class CustomUploadTheme extends UploadTheme +{ + public function display_block(Page $page) + { + // this theme links to /upload + // $page->add_block(new Block("Upload", $this->build_upload_block(), "left", 20)); + } - public function display_page(Page $page) { - $page->disable_left(); - parent::display_page($page); - } + public function display_page(Page $page) + { + $page->disable_left(); + parent::display_page($page); + } } - diff --git a/themes/danbooru/user.theme.php b/themes/danbooru/user.theme.php index 5c6cae4a..2293356a 100644 --- a/themes/danbooru/user.theme.php +++ b/themes/danbooru/user.theme.php @@ -1,12 +1,14 @@ -set_title("Login"); - $page->set_heading("Login"); - $page->disable_left(); - $html = " +class CustomUserPageTheme extends UserPageTheme +{ + public function display_login_page(Page $page) + { + global $config; + $page->set_title("Login"); + $page->set_heading("Login"); + $page->disable_left(); + $html = "
    @@ -21,43 +23,52 @@ class CustomUserPageTheme extends UserPageTheme {
    "; - if($config->get_bool("login_signup_enabled")) { - $html .= "Create Account"; - } - $page->add_block(new Block("Login", $html, "main", 90)); - } + if ($config->get_bool("login_signup_enabled")) { + $html .= "Create Account"; + } + $page->add_block(new Block("Login", $html, "main", 90)); + } - public function display_user_links(Page $page, User $user, $parts) { - // no block in this theme - } - public function display_login_block(Page $page) { - // no block in this theme - } + public function display_user_links(Page $page, User $user, $parts) + { + // no block in this theme + } + public function display_login_block(Page $page) + { + // no block in this theme + } - public function display_user_block(Page $page, User $user, $parts) { - $html = ""; - $blocked = array("Pools", "Pool Changes", "Alias Editor", "My Profile"); - foreach($parts as $part) { - if(in_array($part["name"], $blocked)) continue; - $html .= "
  • {$part["name"]}"; - } - $page->add_block(new Block("User Links", $html, "user", 90)); - } + public function display_user_block(Page $page, User $user, $parts) + { + $html = ""; + $blocked = ["Pools", "Pool Changes", "Alias Editor", "My Profile"]; + foreach ($parts as $part) { + if (in_array($part["name"], $blocked)) { + continue; + } + $html .= "
  • {$part["name"]}"; + } + $page->add_block(new Block("User Links", $html, "user", 90)); + } - public function display_signup_page(Page $page) { - global $config; - $tac = $config->get_string("login_tac", ""); + public function display_signup_page(Page $page) + { + global $config; + $tac = $config->get_string("login_tac", ""); - $tfe = new TextFormattingEvent($tac); - send_event($tfe); - $tac = $tfe->formatted; - - $reca = "".captcha_get_html().""; + $tfe = new TextFormattingEvent($tac); + send_event($tfe); + $tac = $tfe->formatted; + + $reca = "".captcha_get_html().""; - if(empty($tac)) {$html = "";} - else {$html = "

    $tac

    ";} + if (empty($tac)) { + $html = ""; + } else { + $html = "

    $tac

    "; + } - $html .= " + $html .= "
    @@ -70,37 +81,33 @@ class CustomUserPageTheme extends UserPageTheme { "; - $page->set_title("Create Account"); - $page->set_heading("Create Account"); - $page->disable_left(); - $page->add_block(new Block("Signup", $html)); - } + $page->set_title("Create Account"); + $page->set_heading("Create Account"); + $page->disable_left(); + $page->add_block(new Block("Signup", $html)); + } - /** - * @param Page $page - * @param array $uploads - * @param array $comments - */ - public function display_ip_list(Page $page, $uploads, $comments) { - $html = "
    Name
    "; - $html .= ""; - $html .= "
    Uploaded from: "; - foreach($uploads as $ip => $count) { - $html .= "
    $ip ($count)"; - } - $html .= "
    Commented from:"; - foreach($comments as $ip => $count) { - $html .= "
    $ip ($count)"; - } - $html .= "
    (Most recent at top)
    "; + public function display_ip_list(Page $page, array $uploads, array $comments, array $events) + { + $html = ""; + $html .= ""; + $html .= "
    Uploaded from: "; + foreach ($uploads as $ip => $count) { + $html .= "
    $ip ($count)"; + } + $html .= "
    Commented from:"; + foreach ($comments as $ip => $count) { + $html .= "
    $ip ($count)"; + } + $html .= "
    (Most recent at top)
    "; - $page->add_block(new Block("IPs", $html)); - } + $page->add_block(new Block("IPs", $html)); + } - public function display_user_page(User $duser, $stats) { - global $page; - $page->disable_left(); - parent::display_user_page($duser, $stats); - } + public function display_user_page(User $duser, $stats) + { + global $page; + $page->disable_left(); + parent::display_user_page($duser, $stats); + } } - diff --git a/themes/danbooru/view.theme.php b/themes/danbooru/view.theme.php index d7b5aae6..a0e11426 100644 --- a/themes/danbooru/view.theme.php +++ b/themes/danbooru/view.theme.php @@ -1,52 +1,59 @@ -set_title("Image {$image->id}: ".html_escape($image->get_tag_list())); - $page->set_heading(html_escape($image->get_tag_list())); - $page->add_block(new Block("Navigation", $this->build_navigation($image), "left", 0)); - $page->add_block(new Block("Statistics", $this->build_stats($image), "left", 15)); - $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "main", 10)); - $page->add_block(new Block(null, $this->build_pin($image), "main", 11)); - } - - private function build_stats(Image $image) { - $h_owner = html_escape($image->get_owner()->name); - $h_ownerlink = "$h_owner"; - $h_ip = html_escape($image->owner_ip); - $h_date = autodate($image->posted); - $h_filesize = to_shorthand_int($image->filesize); +class CustomViewImageTheme extends ViewImageTheme +{ + public function display_page(Image $image, $editor_parts) + { + global $page; + $page->set_heading(html_escape($image->get_tag_list())); + $page->add_block(new Block("Navigation", $this->build_navigation($image), "left", 0)); + $page->add_block(new Block("Statistics", $this->build_stats($image), "left", 15)); + $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "main", 10)); + $page->add_block(new Block(null, $this->build_pin($image), "main", 11)); + } - global $user; - if($user->can("view_ip")) { - $h_ownerlink .= " ($h_ip)"; - } + private function build_stats(Image $image) + { + $h_owner = html_escape($image->get_owner()->name); + $h_ownerlink = "$h_owner"; + $h_ip = html_escape($image->owner_ip); + $h_type = html_escape($image->get_mime_type()); + $h_date = autodate($image->posted); + $h_filesize = to_shorthand_int($image->filesize); - $html = " + global $user; + if ($user->can(Permissions::VIEW_IP)) { + $h_ownerlink .= " ($h_ip)"; + } + + $html = " Id: {$image->id}
    Posted: $h_date by $h_ownerlink
    Size: {$image->width}x{$image->height}
    Filesize: $h_filesize - "; +
    Type: $h_type"; - if(!is_null($image->source)) { - $h_source = html_escape($image->source); - if(substr($image->source, 0, 7) != "http://" && substr($image->source, 0, 8) != "https://") { - $h_source = "http://" . $h_source; - } - $html .= "
    Source: link"; - } + if ($image->length!=null) { + $h_length = format_milliseconds($image->length); + $html .= "
    Length: $h_length"; + } - if(ext_is_live("Ratings")) { - if($image->rating == null || $image->rating == "u"){ - $image->rating = "u"; - } - $h_rating = Ratings::rating_to_human($image->rating); - $html .= "
    Rating: $h_rating"; - } + if (!is_null($image->source)) { + $h_source = html_escape($image->source); + if (substr($image->source, 0, 7) != "http://" && substr($image->source, 0, 8) != "https://") { + $h_source = "http://" . $h_source; + } + $html .= "
    Source: link"; + } - return $html; - } + if (Extension::is_enabled(RatingsInfo::KEY)) { + if ($image->rating == null || $image->rating == "?") { + $image->rating = "?"; + } + $h_rating = Ratings::rating_to_human($image->rating); + $html .= "
    Rating: $h_rating"; + } + + return $html; + } } - diff --git a/themes/danbooru2/admin.theme.php b/themes/danbooru2/admin.theme.php index b46694de..dc446b38 100644 --- a/themes/danbooru2/admin.theme.php +++ b/themes/danbooru2/admin.theme.php @@ -1,11 +1,11 @@ -disable_left(); - parent::display_page(); - } +class CustomAdminPageTheme extends AdminPageTheme +{ + public function display_page() + { + global $page; + $page->disable_left(); + parent::display_page(); + } } - - diff --git a/themes/danbooru2/comment.theme.php b/themes/danbooru2/comment.theme.php index a9fef1dd..3399a489 100644 --- a/themes/danbooru2/comment.theme.php +++ b/themes/danbooru2/comment.theme.php @@ -1,76 +1,76 @@ -disable_left(); + $page->disable_left(); - // parts for the whole page - $prev = $page_number - 1; - $next = $page_number + 1; + // parts for the whole page + $prev = $page_number - 1; + $next = $page_number + 1; - $h_prev = ($page_number <= 1) ? "Prev" : - "Prev"; - $h_index = "Index"; - $h_next = ($page_number >= $total_pages) ? "Next" : - "Next"; + $h_prev = ($page_number <= 1) ? "Prev" : + "Prev"; + $h_index = "Index"; + $h_next = ($page_number >= $total_pages) ? "Next" : + "Next"; - $nav = "$h_prev | $h_index | $h_next"; + $nav = "$h_prev | $h_index | $h_next"; - $page->set_title("Comments"); - $page->set_heading("Comments"); - $page->add_block(new Block("Navigation", $nav, "left")); - $this->display_paginator($page, "comment/list", null, $page_number, $total_pages); + $page->set_title("Comments"); + $page->set_heading("Comments"); + $page->add_block(new Block("Navigation", $nav, "left")); + $this->display_paginator($page, "comment/list", null, $page_number, $total_pages); - // parts for each image - $position = 10; - - $comment_captcha = $config->get_bool('comment_captcha'); - $comment_limit = $config->get_int("comment_list_count", 10); - - foreach($images as $pair) { - $image = $pair[0]; - $comments = $pair[1]; + // parts for each image + $position = 10; - $thumb_html = $this->build_thumb_html($image); + $comment_captcha = $config->get_bool('comment_captcha'); + $comment_limit = $config->get_int("comment_list_count", 10); - $s = "   "; - $un = $image->get_owner()->name; - $t = ""; - foreach($image->get_tag_array() as $tag) { - $u_tag = url_escape($tag); - $t .= "".html_escape($tag)." "; - } - $p = autodate($image->posted); + foreach ($images as $pair) { + $image = $pair[0]; + $comments = $pair[1]; - $r = ext_is_live("Ratings") ? "Rating ".Ratings::rating_to_human($image->rating) : ""; - $comment_html = "Date $p $s User $un $s $r
    Tags $t

     "; + $thumb_html = $this->build_thumb_html($image); - $comment_count = count($comments); - if($comment_limit > 0 && $comment_count > $comment_limit) { - //$hidden = $comment_count - $comment_limit; - $comment_html .= "

    showing $comment_limit of $comment_count comments

    "; - $comments = array_slice($comments, -$comment_limit); - } - foreach($comments as $comment) { - $comment_html .= $this->comment_to_html($comment); - } - if($can_post) { - if(!$user->is_anonymous()) { - $comment_html .= $this->build_postbox($image->id); - } - else { - if(!$comment_captcha) { - $comment_html .= $this->build_postbox($image->id); - } - else { - $comment_html .= "Add Comment"; - } - } - } + $s = "   "; + $un = $image->get_owner()->name; + $t = ""; + foreach ($image->get_tag_array() as $tag) { + $u_tag = url_escape($tag); + $t .= "".html_escape($tag)." "; + } + $p = autodate($image->posted); - $html = " + $r = Extension::is_enabled(RatingsInfo::KEY) ? "Rating ".Ratings::rating_to_human($image->rating) : ""; + $comment_html = "Date $p $s User $un $s $r
    Tags $t

     "; + + $comment_count = count($comments); + if ($comment_limit > 0 && $comment_count > $comment_limit) { + //$hidden = $comment_count - $comment_limit; + $comment_html .= "

    showing $comment_limit of $comment_count comments

    "; + $comments = array_slice($comments, -$comment_limit); + } + foreach ($comments as $comment) { + $comment_html .= $this->comment_to_html($comment); + } + if ($can_post) { + if (!$user->is_anonymous()) { + $comment_html .= $this->build_postbox($image->id); + } else { + if (!$comment_captcha) { + $comment_html .= $this->build_postbox($image->id); + } else { + $comment_html .= "Add Comment"; + } + } + } + + $html = " @@ -78,50 +78,50 @@ class CustomCommentListTheme extends CommentListTheme { "; - $page->add_block(new Block(" ", $html, "main", $position++)); - } - } + $page->add_block(new Block(" ", $html, "main", $position++)); + } + } - public function display_recent_comments($comments) { - // no recent comments in this theme - } + public function display_recent_comments(array $comments) + { + // no recent comments in this theme + } - protected function comment_to_html(Comment $comment, $trim=false) { - global $user; + protected function comment_to_html(Comment $comment, bool $trim=false): string + { + global $user; - $tfe = new TextFormattingEvent($comment->comment); - send_event($tfe); + $tfe = new TextFormattingEvent($comment->comment); + send_event($tfe); - //$i_uid = int_escape($comment->owner_id); - $h_name = html_escape($comment->owner_name); - //$h_poster_ip = html_escape($comment->poster_ip); - $h_comment = ($trim ? substr($tfe->stripped, 0, 50)."..." : $tfe->formatted); - $i_comment_id = int_escape($comment->comment_id); - $i_image_id = int_escape($comment->image_id); - $h_posted = autodate($comment->posted); + //$i_uid = $comment->owner_id; + $h_name = html_escape($comment->owner_name); + //$h_poster_ip = html_escape($comment->poster_ip); + $h_comment = ($trim ? substr($tfe->stripped, 0, 50)."..." : $tfe->formatted); + $i_comment_id = $comment->comment_id; + $i_image_id = $comment->image_id; + $h_posted = autodate($comment->posted); - $h_userlink = "$h_name"; - $h_del = ""; - if ($user->can("delete_comment")) { - $comment_preview = substr(html_unescape($tfe->stripped), 0, 50); - $j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview"); - $h_delete_script = html_escape("return confirm($j_delete_confirm_message);"); - $h_delete_link = make_link("comment/delete/$i_comment_id/$i_image_id"); - $h_del = " - Del"; - } - //$h_imagelink = $trim ? ">>>\n" : ""; - if($trim) { - return "

    $h_userlink $h_del
    $h_posted
    $h_comment

    "; - } - else { - return " + $h_userlink = "$h_name"; + $h_del = ""; + if ($user->can(Permissions::DELETE_COMMENT)) { + $comment_preview = substr(html_unescape($tfe->stripped), 0, 50); + $j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview"); + $h_delete_script = html_escape("return confirm($j_delete_confirm_message);"); + $h_delete_link = make_link("comment/delete/$i_comment_id/$i_image_id"); + $h_del = " - Del"; + } + //$h_imagelink = $trim ? ">>>\n" : ""; + if ($trim) { + return "

    $h_userlink $h_del
    $h_posted
    $h_comment

    "; + } else { + return "
    $thumb_html $comment_html
    $h_userlink
    $h_posted$h_del
    $h_comment
    "; - } - } + } + } } - diff --git a/themes/danbooru2/custompage.class.php b/themes/danbooru2/custompage.class.php deleted file mode 100644 index d8aca86c..00000000 --- a/themes/danbooru2/custompage.class.php +++ /dev/null @@ -1,9 +0,0 @@ -left_enabled = false; - } -} - diff --git a/themes/danbooru2/ext_manager.theme.php b/themes/danbooru2/ext_manager.theme.php index 86afd724..4ca58ccc 100644 --- a/themes/danbooru2/ext_manager.theme.php +++ b/themes/danbooru2/ext_manager.theme.php @@ -1,24 +1,16 @@ -disable_left(); - parent::display_table($page, $extensions, $editable); - } +class CustomExtManagerTheme extends ExtManagerTheme +{ + public function display_table(Page $page, array $extensions, bool $editable) + { + $page->disable_left(); + parent::display_table($page, $extensions, $editable); + } - /** - * @param Page $page - * @param ExtensionInfo $info - */ - public function display_doc(Page $page, ExtensionInfo $info) { - $page->disable_left(); - parent::display_doc($page, $info); - } + public function display_doc(Page $page, ExtensionInfo $info) + { + $page->disable_left(); + parent::display_doc($page, $info); + } } - - diff --git a/themes/danbooru2/index.theme.php b/themes/danbooru2/index.theme.php index 3f462887..796fb660 100644 --- a/themes/danbooru2/index.theme.php +++ b/themes/danbooru2/index.theme.php @@ -1,75 +1,51 @@ -display_page_header($page, $images); - if(count($this->search_terms) == 0) { - $query = null; - $page_title = $config->get_string('title'); - } - else { - $search_string = implode(' ', $this->search_terms); - $query = url_escape($search_string); - $page_title = html_escape($search_string); - } + $nav = $this->build_navigation($this->page_number, $this->total_pages, $this->search_terms); + $page->add_block(new Block("Search", $nav, "left", 0)); - $nav = $this->build_navigation($this->page_number, $this->total_pages, $this->search_terms); - $page->set_title($page_title); - $page->set_heading($page_title); - $page->add_block(new Block("Search", $nav, "left", 0)); - if(count($images) > 0) { - if($query) { - $page->add_block(new Block("Images", $this->build_table($images, "search=$query"), "main", 10)); - $this->display_paginator($page, "post/list/$query", null, $this->page_number, $this->total_pages); - } - else { - $page->add_block(new Block("Images", $this->build_table($images, null), "main", 10)); - $this->display_paginator($page, "post/list", null, $this->page_number, $this->total_pages); - } - } - else { - $page->add_block(new Block("No Images Found", "No images were found to match the search criteria")); - } - } + if (count($images) > 0) { + $this->display_page_images($page, $images); + } else { + $this->display_error(404, "No Images Found", "No images were found to match the search criteria"); + } + } - /** - * @param int $page_number - * @param int $total_pages - * @param string[] $search_terms - * @return string - */ - protected function build_navigation($page_number, $total_pages, $search_terms) { - $h_search_string = count($search_terms) == 0 ? "" : html_escape(implode(" ", $search_terms)); - $h_search_link = make_link(); - $h_search = " + /** + * #param string[] $search_terms + */ + protected function build_navigation(int $page_number, int $total_pages, array $search_terms): string + { + $h_search_string = count($search_terms) == 0 ? "" : html_escape(implode(" ", $search_terms)); + $h_search_link = make_link(); + return "

    "; + } - return $h_search; - } - - /** - * @param Image[] $images - * @param string $query - * @return string - */ - protected function build_table($images, $query) { - $h_query = html_escape($query); - $table = "
    "; - foreach($images as $image) { - $table .= "\t" . $this->build_thumb_html($image) . "\n"; - } - $table .= "
    "; - return $table; - } + /** + * #param Image[] $images + */ + protected function build_table(array $images, ?string $query): string + { + $h_query = html_escape($query); + $table = "
    "; + foreach ($images as $image) { + $table .= "\t" . $this->build_thumb_html($image) . "\n"; + } + $table .= "
    "; + return $table; + } } - diff --git a/themes/danbooru2/layout.class.php b/themes/danbooru2/layout.class.php deleted file mode 100644 index 73433c42..00000000 --- a/themes/danbooru2/layout.class.php +++ /dev/null @@ -1,293 +0,0 @@ -, updated by Daniel Oaks -* Link: http://trac.shishnet.org/shimmie2/ -* License: GPLv2 -* Description: This is a simple theme changing the css to make shimme -* look more like danbooru as well as adding a custom links -* bar and title to the top of every page. -*/ -//Small changes added by zshall -//Changed CSS and layout to make shimmie look even more like danbooru -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -Danbooru 2 Theme - Notes (Bzchan) - -Files: default.php, style.css - -How to use a theme -- Copy the danbooru2 folder with all its contained files into the "themes" - directory in your shimmie installation. -- Log into your shimmie and change the Theme in the Board Config to your - desired theme. - -Changes in this theme include -- Adding and editing various elements in the style.css file. -- $site_name and $front_name retreival from config added. -- $custom_link and $title_link preparation just before html is outputed. -- Altered outputed html to include the custom links and removed heading - from being displayed (subheading is still displayed) -- Note that only the sidebar has been left aligned. Could not properly - left align the main block because blocks without headers currently do - not have ids on there div elements. (this was a problem because - paginator block must be centered and everything else left aligned) - -Tips -- You can change custom links to point to whatever pages you want as well as adding - more custom links. -- The main title link points to the Front Page set in your Board Config options. -- The text of the main title is the Title set in your Board Config options. -- Themes make no changes to your database or main code files so you can switch - back and forward to other themes all you like. - -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -class Layout { - public function display_page($page) { - global $config, $user; - - //$theme_name = $config->get_string('theme'); - //$base_href = $config->get_string('base_href'); - //$data_href = get_base_href(); - $contact_link = contact_link(); - $header_html = $page->get_all_html_headers(); - - $left_block_html = ""; - $user_block_html = ""; - $main_block_html = ""; - $sub_block_html = ""; - - foreach($page->blocks as $block) { - switch($block->section) { - case "left": - $left_block_html .= $block->get_html(true); - break; - case "user": - $user_block_html .= $block->body; // $this->block_to_html($block, true); - break; - case "subheading": - $sub_block_html .= $block->body; // $this->block_to_html($block, true); - break; - case "main": - if($block->header == "Images") { - $block->header = " "; - } - $main_block_html .= $block->get_html(false); - break; - default: - print "

    error: {$block->header} using an unknown section ({$block->section})"; - break; - } - } - - $debug = get_debug_info(); - - $contact = empty($contact_link) ? "" : "
    Contact"; - - if(empty($this->subheading)) { - $subheading = ""; - } - else { - $subheading = "

    {$this->subheading}
    "; - } - - $site_name = $config->get_string('title'); // bzchan: change from normal default to get title for top of page - $main_page = $config->get_string('main_page'); // bzchan: change from normal default to get main page for top of page - - // bzchan: CUSTOM LINKS are prepared here, change these to whatever you like - $custom_links = ""; - if($user->is_anonymous()) { - $custom_links .= $this->navlinks(make_link('user_admin/login'), "Sign in", array("user", "user_admin", "setup", "admin")); - } - else { - $custom_links .= $this->navlinks(make_link('user'), "My Account", array("user", "user_admin")); - } - if($user->is_admin()) { - $custom_links .= $this->navlinks(make_link('admin'), "Admin", array("admin", "ext_manager", "setup")); - } - $custom_links .= $this->navlinks(make_link('post/list'), "Posts", array("post", "upload", "", "random_image")); - $custom_links .= $this->navlinks(make_link('comment/list'), "Comments", array("comment")); - $custom_links .= $this->navlinks(make_link('tags'), "Tags", array("tags", "alias")); - if(class_exists("Pools")) { - $custom_links .= $this->navlinks(make_link('pool/list'), "Pools", array("pool")); - } - if(class_exists("Wiki")) { - $custom_links .= $this->navlinks(make_link('wiki'), "Wiki", array("wiki")); - $custom_links .= $this->navlinks(make_link('wiki/more'), "More »", array("wiki/more")); - } - - $custom_sublinks = ""; - // hack - $username = url_escape($user->name); - // hack - $qp = explode("/", ltrim(_get_query(), "/")); - // php sucks - switch($qp[0]) { - default: - case "ext_doc": - $custom_sublinks .= $user_block_html; - break; - case "user": - case "user_admin": - if($user->is_anonymous()) { - $custom_sublinks .= "
  • Sign up
  • "; - // $custom_sublinks .= "
  • Reset Password
  • "; - // $custom_sublinks .= "
  • Login Reminder
  • "; - } else { - $custom_sublinks .= "
  • Sign out
  • "; - } - break; - case "": - # FIXME: this assumes that the front page is - # post/list; in 99% of case it will either be - # post/list or home, and in the latter case - # the subnav links aren't shown, but it would - # be nice to be correct - case "random_image": - case "post": - case "upload": - if(class_exists("NumericScore")){ $custom_sublinks .= "
  • Popular by Day/Month/Year
  • ";} - $custom_sublinks .= "
  • Listing
  • "; - if(class_exists("Favorites")){ $custom_sublinks .= "
  • My Favorites
  • ";} - if(class_exists("RSS_Images")){ $custom_sublinks .= "
  • Feed
  • ";} - if(class_exists("RandomImage")){ $custom_sublinks .= "
  • Random
  • ";} - $custom_sublinks .= "
  • Upload
  • "; - if(class_exists("Wiki")){ $custom_sublinks .= "
  • Help
  • "; - }else{ $custom_sublinks .= "
  • Help
  • ";} - break; - case "comment": - $custom_sublinks .= "
  • All
  • "; - $custom_sublinks .= "
  • Help
  • "; - break; - case "pool": - $custom_sublinks .= "
  • List
  • "; - $custom_sublinks .= "
  • Create
  • "; - $custom_sublinks .= "
  • Changes
  • "; - $custom_sublinks .= "
  • Help
  • "; - break; - case "wiki": - $custom_sublinks .= "
  • Index
  • "; - $custom_sublinks .= "
  • Rules
  • "; - $custom_sublinks .= "
  • Help
  • "; - break; - case "tags": - case "alias": - $custom_sublinks .= "
  • Map
  • "; - $custom_sublinks .= "
  • Alphabetic
  • "; - $custom_sublinks .= "
  • Popularity
  • "; - $custom_sublinks .= "
  • Categories
  • "; - $custom_sublinks .= "
  • Aliases
  • "; - $custom_sublinks .= "
  • Help
  • "; - break; - case "admin": - case "ext_manager": - case "setup": - if($user->is_admin()) { - $custom_sublinks .= "
  • Extension Manager
  • "; - $custom_sublinks .= "
  • Board Config
  • "; - $custom_sublinks .= "
  • Alias Editor
  • "; - } else { - $custom_sublinks .= "
  • I think you might be lost
  • "; - } - break; - } - - - // bzchan: failed attempt to add heading after title_link (failure was it looked bad) - //if($this->heading==$site_name)$this->heading = ''; - //$title_link = "

    $site_name/$this->heading

    "; - - // bzchan: prepare main title link - $title_link = "

    $site_name

    "; - - if($page->left_enabled) { - $left = ""; - $withleft = "withleft"; - } - else { - $left = ""; - $withleft = "noleft"; - } - - $flash = $page->get_cookie("flash_message"); - $flash_html = ""; - if($flash) { - $flash_html = "".nl2br(html_escape($flash))." [X]"; - } - - print << - - - - - - {$page->title} -$header_html - - - -
    - $title_link - - -
    - $subheading - $sub_block_html - $left -
    - $flash_html - $main_block_html -
    -
    - Running Shimmie – - Images © their respective owners, - Shimmie © - Shish & - The Team - 2007-2016, - based on the Danbooru concept
    - $debug - $contact -
    - - -EOD; - } - - /** - * @param string $link - * @param string $desc - * @param string[] $pages_matched - * @return string - */ - private function navlinks($link, $desc, $pages_matched) { - /** - * Woo! We can actually SEE THE CURRENT PAGE!! (well... see it highlighted in the menu.) - */ - $html = ""; - $url = _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]) { - $html = "
  • $desc
  • "; - } - } - if(empty($html)) {$html = "
  • $desc
  • ";} - return $html; - } -} - diff --git a/themes/danbooru2/page.class.php b/themes/danbooru2/page.class.php new file mode 100644 index 00000000..32e96a39 --- /dev/null +++ b/themes/danbooru2/page.class.php @@ -0,0 +1,167 @@ +, updated by Daniel Oaks + * Link: https://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: This is a simple theme changing the css to make shimme + * look more like danbooru as well as adding a custom links + * bar and title to the top of every page. + */ +//Small changes added by zshall +//Changed CSS and layout to make shimmie look even more like danbooru +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +Danbooru 2 Theme - Notes (Bzchan) + +Files: default.php, style.css + +How to use a theme +- Copy the danbooru2 folder with all its contained files into the "themes" + directory in your shimmie installation. +- Log into your shimmie and change the Theme in the Board Config to your + desired theme. + +Changes in this theme include +- Adding and editing various elements in the style.css file. +- $site_name and $front_name retreival from config added. +- $custom_link and $title_link preparation just before html is outputed. +- Altered outputed html to include the custom links and removed heading + from being displayed (subheading is still displayed) +- Note that only the sidebar has been left aligned. Could not properly + left align the main block because blocks without headers currently do + not have ids on there div elements. (this was a problem because + paginator block must be centered and everything else left aligned) + +Tips +- You can change custom links to point to whatever pages you want as well as adding + more custom links. +- The main title link points to the Front Page set in your Board Config options. +- The text of the main title is the Title set in your Board Config options. +- Themes make no changes to your database or main code files so you can switch + back and forward to other themes all you like. + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +class Page extends BasePage +{ + public $left_enabled = true; + public function disable_left() + { + $this->left_enabled = false; + } + + public function render() + { + global $config; + + list($nav_links, $sub_links) = $this->get_nav_links(); + + $left_block_html = ""; + $user_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 "user": + $user_block_html .= $block->body; // $this->block_to_html($block, true); + break; + case "subheading": + $sub_block_html .= $block->body; // $this->block_to_html($block, true); + break; + case "main": + if ($block->header == "Images") { + $block->header = " "; + } + $main_block_html .= $block->get_html(false); + break; + default: + print "

    error: {$block->header} using an unknown section ({$block->section})"; + break; + } + } + + if (empty($this->subheading)) { + $subheading = ""; + } else { + $subheading = "

    {$this->subheading}
    "; + } + + $site_name = $config->get_string(SetupConfig::TITLE); // bzchan: change from normal default to get title for top of page + $main_page = $config->get_string(SetupConfig::MAIN_PAGE); // bzchan: change from normal default to get main page for top of page + + $custom_links = ""; + foreach ($nav_links as $nav_link) { + $custom_links .= "
  • ".$this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)."
  • "; + } + + $custom_sublinks = ""; + if (!empty($sub_links)) { + $custom_sublinks = "
    "; + foreach ($sub_links as $nav_link) { + $custom_sublinks .= "
  • ".$this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)."
  • "; + } + $custom_sublinks .= "
    "; + } + + // bzchan: failed attempt to add heading after title_link (failure was it looked bad) + //if($this->heading==$site_name)$this->heading = ''; + //$title_link = "

    $site_name/$this->heading

    "; + + // bzchan: prepare main title link + $title_link = "

    $site_name

    "; + + if ($this->left_enabled) { + $left = ""; + $withleft = "withleft"; + } else { + $left = ""; + $withleft = "noleft"; + } + + $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $head_html = $this->head_html(); + $footer_html = $this->footer_html(); + + print << + + $head_html + +
    + $title_link + + +
    + $subheading + $sub_block_html + $left +
    + $flash_html + $main_block_html +
    +
    $footer_html
    + + +EOD; + } + + public function navlinks(Link $link, string $desc, bool $active): ?string + { + $html = null; + if ($active) { + $html = "{$desc}"; + } else { + $html = "{$desc}"; + } + + return $html; + } +} diff --git a/themes/danbooru2/style.css b/themes/danbooru2/style.css index 610d6bef..a4b05818 100644 --- a/themes/danbooru2/style.css +++ b/themes/danbooru2/style.css @@ -16,22 +16,21 @@ margin:0; padding:0 1rem 0 2rem; } HEADER ul#navbar li { +margin:0; +} +HEADER ul#navbar li a { display:inline-block; margin:0 0.15rem; padding:0.4rem 0.6rem; } -HEADER ul#navbar li:first-child { -margin-left: -0.6rem; -} HEADER ul#navbar li:first-child a { +margin-left: -0.6rem; color: #FF3333; font-weight: bold; } -HEADER ul#navbar li.current-page { +HEADER ul#navbar li a.current-page { background-color:#EEEEFF; border-radius:0.2rem 0.2rem 0 0; -} -HEADER ul#navbar li.current-page a { font-weight:bold; } HEADER ul#subnavbar { diff --git a/themes/danbooru2/tag_list.theme.php b/themes/danbooru2/tag_list.theme.php index 6628ca29..31e174b8 100644 --- a/themes/danbooru2/tag_list.theme.php +++ b/themes/danbooru2/tag_list.theme.php @@ -1,9 +1,10 @@ -disable_left(); - parent::display_page($page); - } +class CustomTagListTheme extends TagListTheme +{ + public function display_page(Page $page) + { + $page->disable_left(); + parent::display_page($page); + } } - diff --git a/themes/danbooru2/themelet.class.php b/themes/danbooru2/themelet.class.php index b2badbb1..927d0c87 100644 --- a/themes/danbooru2/themelet.class.php +++ b/themes/danbooru2/themelet.class.php @@ -1,81 +1,66 @@ -build_paginator($page_number, $total_pages, $base, $query); - $page->add_block(new Block(null, $body, "main", 90)); - } +build_paginator($page_number, $total_pages, $base, $query); + $page->add_block(new Block(null, $body, "main", 90)); + } - /** - * @param string $base_url - * @param string $query - * @param int|string $page - * @param string $name - * @return string - */ - private function gen_page_link($base_url, $query, $page, $name) { - $link = make_link("$base_url/$page", $query); - return "$name"; - } + private function gen_page_link(string $base_url, ?string $query, int $page, string $name): string + { + $link = make_link("$base_url/$page", $query); + return "$name"; + } - /** - * @param string $base_url - * @param string $query - * @param int|string $page - * @param int $current_page - * @param string $name - * @return string - */ - private function gen_page_link_block($base_url, $query, $page, $current_page, $name) { - $paginator = ""; - if($page == $current_page) $paginator .= "$page"; - else $paginator .= $this->gen_page_link($base_url, $query, $page, $name); - return $paginator; - } + 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 .= "$page"; + } else { + $paginator .= $this->gen_page_link($base_url, $query, $page, $name); + } + return $paginator; + } - /** - * @param int $current_page - * @param int $total_pages - * @param string $base_url - * @param string $query - * @return string - */ - private function build_paginator($current_page, $total_pages, $base_url, $query) { - $next = $current_page + 1; - $prev = $current_page - 1; + private function build_paginator(int $current_page, int $total_pages, string $base_url, ?string $query): string + { + $next = $current_page + 1; + $prev = $current_page - 1; - $at_start = ($current_page <= 3 || $total_pages <= 3); - $at_end = ($current_page >= $total_pages -2); + $at_start = ($current_page <= 3 || $total_pages <= 3); + $at_end = ($current_page >= $total_pages -2); - $first_html = $at_start ? "" : $this->gen_page_link($base_url, $query, 1, "1"); - $prev_html = $at_start ? "" : $this->gen_page_link($base_url, $query, $prev, "<<"); - $next_html = $at_end ? "" : $this->gen_page_link($base_url, $query, $next, ">>"); - $last_html = $at_end ? "" : $this->gen_page_link($base_url, $query, $total_pages, "$total_pages"); + $first_html = $at_start ? "" : $this->gen_page_link($base_url, $query, 1, "1"); + $prev_html = $at_start ? "" : $this->gen_page_link($base_url, $query, $prev, "<<"); + $next_html = $at_end ? "" : $this->gen_page_link($base_url, $query, $next, ">>"); + $last_html = $at_end ? "" : $this->gen_page_link($base_url, $query, $total_pages, "$total_pages"); - $start = $current_page-2 > 1 ? $current_page-2 : 1; - $end = $current_page+2 <= $total_pages ? $current_page+2 : $total_pages; + $start = $current_page-2 > 1 ? $current_page-2 : 1; + $end = $current_page+2 <= $total_pages ? $current_page+2 : $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); + $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); - if(strlen($first_html) > 0) $pdots = "..."; - else $pdots = ""; + if (strlen($first_html) > 0) { + $pdots = "..."; + } else { + $pdots = ""; + } - if(strlen($last_html) > 0) $ndots = "..."; - else $ndots = ""; + if (strlen($last_html) > 0) { + $ndots = "..."; + } else { + $ndots = ""; + } - return "
    $prev_html $first_html $pdots $pages_html $ndots $last_html $next_html
    "; - } + return "
    $prev_html $first_html $pdots $pages_html $ndots $last_html $next_html
    "; + } } - diff --git a/themes/danbooru2/upload.theme.php b/themes/danbooru2/upload.theme.php index a7047cf3..31ce245e 100644 --- a/themes/danbooru2/upload.theme.php +++ b/themes/danbooru2/upload.theme.php @@ -1,14 +1,16 @@ -add_block(new Block("Upload", $this->build_upload_block(), "left", 20)); - } +class CustomUploadTheme extends UploadTheme +{ + public function display_block(Page $page) + { + // this theme links to /upload + // $page->add_block(new Block("Upload", $this->build_upload_block(), "left", 20)); + } - public function display_page(Page $page) { - $page->disable_left(); - parent::display_page($page); - } + public function display_page(Page $page) + { + $page->disable_left(); + parent::display_page($page); + } } - diff --git a/themes/danbooru2/user.theme.php b/themes/danbooru2/user.theme.php index 5c6cae4a..2293356a 100644 --- a/themes/danbooru2/user.theme.php +++ b/themes/danbooru2/user.theme.php @@ -1,12 +1,14 @@ -set_title("Login"); - $page->set_heading("Login"); - $page->disable_left(); - $html = " +class CustomUserPageTheme extends UserPageTheme +{ + public function display_login_page(Page $page) + { + global $config; + $page->set_title("Login"); + $page->set_heading("Login"); + $page->disable_left(); + $html = "
    @@ -21,43 +23,52 @@ class CustomUserPageTheme extends UserPageTheme {
    "; - if($config->get_bool("login_signup_enabled")) { - $html .= "Create Account"; - } - $page->add_block(new Block("Login", $html, "main", 90)); - } + if ($config->get_bool("login_signup_enabled")) { + $html .= "Create Account"; + } + $page->add_block(new Block("Login", $html, "main", 90)); + } - public function display_user_links(Page $page, User $user, $parts) { - // no block in this theme - } - public function display_login_block(Page $page) { - // no block in this theme - } + public function display_user_links(Page $page, User $user, $parts) + { + // no block in this theme + } + public function display_login_block(Page $page) + { + // no block in this theme + } - public function display_user_block(Page $page, User $user, $parts) { - $html = ""; - $blocked = array("Pools", "Pool Changes", "Alias Editor", "My Profile"); - foreach($parts as $part) { - if(in_array($part["name"], $blocked)) continue; - $html .= "
  • {$part["name"]}"; - } - $page->add_block(new Block("User Links", $html, "user", 90)); - } + public function display_user_block(Page $page, User $user, $parts) + { + $html = ""; + $blocked = ["Pools", "Pool Changes", "Alias Editor", "My Profile"]; + foreach ($parts as $part) { + if (in_array($part["name"], $blocked)) { + continue; + } + $html .= "
  • {$part["name"]}"; + } + $page->add_block(new Block("User Links", $html, "user", 90)); + } - public function display_signup_page(Page $page) { - global $config; - $tac = $config->get_string("login_tac", ""); + public function display_signup_page(Page $page) + { + global $config; + $tac = $config->get_string("login_tac", ""); - $tfe = new TextFormattingEvent($tac); - send_event($tfe); - $tac = $tfe->formatted; - - $reca = "".captcha_get_html().""; + $tfe = new TextFormattingEvent($tac); + send_event($tfe); + $tac = $tfe->formatted; + + $reca = "".captcha_get_html().""; - if(empty($tac)) {$html = "";} - else {$html = "

    $tac

    ";} + if (empty($tac)) { + $html = ""; + } else { + $html = "

    $tac

    "; + } - $html .= " + $html .= "
    @@ -70,37 +81,33 @@ class CustomUserPageTheme extends UserPageTheme { "; - $page->set_title("Create Account"); - $page->set_heading("Create Account"); - $page->disable_left(); - $page->add_block(new Block("Signup", $html)); - } + $page->set_title("Create Account"); + $page->set_heading("Create Account"); + $page->disable_left(); + $page->add_block(new Block("Signup", $html)); + } - /** - * @param Page $page - * @param array $uploads - * @param array $comments - */ - public function display_ip_list(Page $page, $uploads, $comments) { - $html = "
    Name
    "; - $html .= ""; - $html .= "
    Uploaded from: "; - foreach($uploads as $ip => $count) { - $html .= "
    $ip ($count)"; - } - $html .= "
    Commented from:"; - foreach($comments as $ip => $count) { - $html .= "
    $ip ($count)"; - } - $html .= "
    (Most recent at top)
    "; + public function display_ip_list(Page $page, array $uploads, array $comments, array $events) + { + $html = ""; + $html .= ""; + $html .= "
    Uploaded from: "; + foreach ($uploads as $ip => $count) { + $html .= "
    $ip ($count)"; + } + $html .= "
    Commented from:"; + foreach ($comments as $ip => $count) { + $html .= "
    $ip ($count)"; + } + $html .= "
    (Most recent at top)
    "; - $page->add_block(new Block("IPs", $html)); - } + $page->add_block(new Block("IPs", $html)); + } - public function display_user_page(User $duser, $stats) { - global $page; - $page->disable_left(); - parent::display_user_page($duser, $stats); - } + public function display_user_page(User $duser, $stats) + { + global $page; + $page->disable_left(); + parent::display_user_page($duser, $stats); + } } - diff --git a/themes/danbooru2/view.theme.php b/themes/danbooru2/view.theme.php index d4472067..6cce5847 100644 --- a/themes/danbooru2/view.theme.php +++ b/themes/danbooru2/view.theme.php @@ -1,70 +1,69 @@ -set_title("Image {$image->id}: ".html_escape($image->get_tag_list())); - $page->set_heading(html_escape($image->get_tag_list())); - $page->add_block(new Block("Search", $this->build_navigation($image), "left", 0)); - $page->add_block(new Block("Information", $this->build_information($image), "left", 15)); - $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "main", 15)); - } +class CustomViewImageTheme extends ViewImageTheme +{ + public function display_page(Image $image, $editor_parts) + { + global $page; + $page->set_heading(html_escape($image->get_tag_list())); + $page->add_block(new Block("Search", $this->build_navigation($image), "left", 0)); + $page->add_block(new Block("Information", $this->build_information($image), "left", 15)); + $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "main", 15)); + } - /** - * @param Image $image - * @return string - */ - private function build_information(Image $image) { - $h_owner = html_escape($image->get_owner()->name); - $h_ownerlink = "$h_owner"; - $h_ip = html_escape($image->owner_ip); - $h_date = autodate($image->posted); - $h_filesize = to_shorthand_int($image->filesize); + private function build_information(Image $image): string + { + $h_owner = html_escape($image->get_owner()->name); + $h_ownerlink = "$h_owner"; + $h_ip = html_escape($image->owner_ip); + $h_type = html_escape($image->get_mime_type()); + $h_date = autodate($image->posted); + $h_filesize = to_shorthand_int($image->filesize); - global $user; - if($user->can("view_ip")) { - $h_ownerlink .= " ($h_ip)"; - } + global $user; + if ($user->can(Permissions::VIEW_IP)) { + $h_ownerlink .= " ($h_ip)"; + } - $html = " + $html = " ID: {$image->id}
    Uploader: $h_ownerlink
    Date: $h_date
    Size: $h_filesize ({$image->width}x{$image->height}) +
    Type: $h_type "; - if(!is_null($image->source)) { - $h_source = html_escape($image->source); - if(substr($image->source, 0, 7) != "http://" && substr($image->source, 0, 8) != "https://") { - $h_source = "http://" . $h_source; - } - $html .= "
    Source: link"; - } + if ($image->length!=null) { + $h_length = format_milliseconds($image->length); + $html .= "
    Length: $h_length"; + } - if(ext_is_live("Ratings")) { - if($image->rating == null || $image->rating == "u"){ - $image->rating = "u"; - } - if(ext_is_live("Ratings")) { - $h_rating = Ratings::rating_to_human($image->rating); - $html .= "
    Rating: $h_rating"; - } - } - return $html; - } + if (!is_null($image->source)) { + $h_source = html_escape($image->source); + if (substr($image->source, 0, 7) != "http://" && substr($image->source, 0, 8) != "https://") { + $h_source = "http://" . $h_source; + } + $html .= "
    Source: link"; + } - /** - * @param Image $image - * @return string - */ - protected function build_navigation(Image $image) { - //$h_pin = $this->build_pin($image); - $h_search = " + if (Extension::is_enabled(RatingsInfo::KEY)) { + if ($image->rating == null || $image->rating == "?") { + $image->rating = "?"; + } + if (Extension::is_enabled(RatingsInfo::KEY)) { + $h_rating = Ratings::rating_to_human($image->rating); + $html .= "
    Rating: $h_rating"; + } + } + + return $html; + } + + protected function build_navigation(Image $image): string + { + //$h_pin = $this->build_pin($image); + $h_search = "
    @@ -73,7 +72,6 @@ class CustomViewImageTheme extends ViewImageTheme {
    "; - return "$h_search"; - } + return "$h_search"; + } } - diff --git a/themes/default/layout.class.php b/themes/default/layout.class.php deleted file mode 100644 index 4dc94739..00000000 --- a/themes/default/layout.class.php +++ /dev/null @@ -1,93 +0,0 @@ -get_string('theme', 'default'); - //$data_href = get_base_href(); - $contact_link = contact_link(); - $header_html = $page->get_all_html_headers(); - - $left_block_html = ""; - $main_block_html = ""; - $sub_block_html = ""; - - foreach($page->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 "

    error: {$block->header} using an unknown section ({$block->section})"; - break; - } - } - - $debug = get_debug_info(); - - $contact = empty($contact_link) ? "" : "
    Contact"; - - $wrapper = ""; - if(strlen($page->heading) > 100) { - $wrapper = ' style="height: 3em; overflow: auto;"'; - } - - $flash = $page->get_cookie("flash_message"); - $flash_html = ""; - if($flash) { - $flash_html = "".nl2br(html_escape($flash))." [X]"; - } - - print << - - - - - - {$page->title} -$header_html - - - -

    - {$page->heading} - $sub_block_html -
    - -
    - $flash_html - $main_block_html -
    -
    - Images © their respective owners, - Shimmie © - Shish & - The Team - 2007-2016, - based on the Danbooru concept. - $debug - $contact -
    - - -EOD; - } -} - diff --git a/themes/default/page.class.php b/themes/default/page.class.php new file mode 100644 index 00000000..5fc0f7ef --- /dev/null +++ b/themes/default/page.class.php @@ -0,0 +1,4 @@ +H3 { background: #CCC; @@ -46,7 +46,7 @@ TD { text-align: center; } -TABLE.zebra {border-spacing: 0px; border: 2px solid #CCC;} +TABLE.zebra {border-spacing: 0; border: 2px solid #CCC;} TABLE.zebra TD, TABLE.zebra TH {vertical-align: middle; padding: 4px;} TABLE.zebra THEAD TD, TABLE.zebra THEAD TH {border-bottom: 2px solid #CCC;} TABLE.zebra TFOOT TD, TABLE.zebra TFOOT TH {border-top: 2px solid #CCC;} @@ -89,11 +89,11 @@ NAV TABLE { } NAV INPUT { width: 100%; - padding: 0px; + padding: 0; } NAV SELECT { width: 100%; - padding: 0px; + padding: 0; } TABLE.tag_list { diff --git a/themes/default/themelet.class.php b/themes/default/themelet.class.php index 77c927c3..59021a28 100644 --- a/themes/default/themelet.class.php +++ b/themes/default/themelet.class.php @@ -1,3 +1,4 @@ -get_string('title'); - $page->set_title($page_title); - $page->set_heading($page_title); - $page->disable_left(); - $page->add_block(new Block(null, $this->build_upload_box(), "main", 0)); - $page->add_block(new Block(null, "
    ", "main", 80)); - $this->display_paginator($page, "comment/list", null, $page_number, $total_pages); + $page_title = $config->get_string(SetupConfig::TITLE); + $page->set_title($page_title); + $page->set_heading($page_title); + $page->disable_left(); + $page->add_block(new Block(null, $this->build_upload_box(), "main", 0)); + $page->add_block(new Block(null, "
    ", "main", 80)); + $this->display_paginator($page, "comment/list", null, $page_number, $total_pages); - // parts for each image - $position = 10; - foreach($images as $pair) { - $image = $pair[0]; - $comments = $pair[1]; + // parts for each image + $position = 10; + foreach ($images as $pair) { + $image = $pair[0]; + $comments = $pair[1]; - $h_filename = html_escape($image->filename); - $h_filesize = to_shorthand_int($image->filesize); - $w = $image->width; - $h = $image->height; + $h_filename = html_escape($image->filename); + $h_filesize = to_shorthand_int($image->filesize); + $w = $image->width; + $h = $image->height; - $comment_html = ""; - $comment_id = 0; - foreach($comments as $comment) { - $this->inner_id = $comment_id++; - $comment_html .= $this->comment_to_html($comment, false); - } + $comment_html = ""; + $comment_id = 0; + foreach ($comments as $comment) { + $this->inner_id = $comment_id++; + $comment_html .= $this->comment_to_html($comment, false); + } - $html = "

     


    "; - $html .= "File: id}")."\">$h_filename - ($h_filesize, {$w}x{$h}) - "; - $html .= html_escape($image->get_tag_list()); - $html .= "
    "; - $html .= "
    " . $this->build_thumb_html($image) . "
    "; - $html .= "
    $comment_html
    "; - $html .= "
    "; + $html = "

     


    "; + $html .= "File: id}")."\">$h_filename - ($h_filesize, {$w}x{$h}) - "; + $html .= html_escape($image->get_tag_list()); + $html .= "
    "; + $html .= "
    " . $this->build_thumb_html($image) . "
    "; + $html .= "
    $comment_html
    "; + $html .= "
    "; - $page->add_block(new Block(null, $html, "main", $position++)); - } - } - - public function display_recent_comments($comments) { - // sidebar fails in this theme - } + $page->add_block(new Block(null, $html, "main", $position++)); + } + } - public function build_upload_box() { - return "[[ insert upload-and-comment extension here ]]"; - } + public function display_recent_comments(array $comments) + { + // sidebar fails in this theme + } + + public function build_upload_box() + { + return "[[ insert upload-and-comment extension here ]]"; + } - protected function comment_to_html(Comment $comment, $trim=false) { - $inner_id = $this->inner_id; // because custom themes can't add params, because PHP - global $user; + protected function comment_to_html(Comment $comment, bool $trim=false): string + { + $inner_id = $this->inner_id; // because custom themes can't add params, because PHP + global $user; - $tfe = new TextFormattingEvent($comment->comment); - send_event($tfe); + $tfe = new TextFormattingEvent($comment->comment); + send_event($tfe); - //$i_uid = int_escape($comment->owner_id); - $h_name = html_escape($comment->owner_name); - //$h_poster_ip = html_escape($comment->poster_ip); - $h_comment = ($trim ? substr($tfe->stripped, 0, 50)."..." : $tfe->formatted); - $i_comment_id = int_escape($comment->comment_id); - $i_image_id = int_escape($comment->image_id); + //$i_uid = $comment->owner_id; + $h_name = html_escape($comment->owner_name); + //$h_poster_ip = html_escape($comment->poster_ip); + $h_comment = ($trim ? substr($tfe->stripped, 0, 50)."..." : $tfe->formatted); + $i_comment_id = $comment->comment_id; + $i_image_id = $comment->image_id; - $h_userlink = "$h_name"; - $h_date = $comment->posted; - $h_del = ""; - if ($user->can("delete_comment")) { - $comment_preview = substr(html_unescape($tfe->stripped), 0, 50); - $j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview"); - $h_delete_script = html_escape("return confirm($j_delete_confirm_message);"); - $h_delete_link = make_link("comment/delete/$i_comment_id/$i_image_id"); - $h_del = " - Del"; - } - $h_reply = "[Reply]"; + $h_userlink = "$h_name"; + $h_date = $comment->posted; + $h_del = ""; + if ($user->can(Permissions::DELETE_COMMENT)) { + $comment_preview = substr(html_unescape($tfe->stripped), 0, 50); + $j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview"); + $h_delete_script = html_escape("return confirm($j_delete_confirm_message);"); + $h_delete_link = make_link("comment/delete/$i_comment_id/$i_image_id"); + $h_del = " - Del"; + } + $h_reply = "[Reply]"; - if($inner_id == 0) { - return "
    $h_userlink$h_del $h_date No.$i_comment_id $h_reply

    $h_comment

    "; - } - else { - return "
    >>". - "
    $h_userlink$h_del $h_date No.$i_comment_id $h_reply

    $h_comment

    " . - "
    "; - } - } + if ($inner_id == 0) { + return "
    $h_userlink$h_del $h_date No.$i_comment_id $h_reply

    $h_comment

    "; + } else { + return "
    >>". + "
    $h_userlink$h_del $h_date No.$i_comment_id $h_reply

    $h_comment

    " . + "
    "; + } + } } - diff --git a/themes/futaba/custompage.class.php b/themes/futaba/custompage.class.php deleted file mode 100644 index d8aca86c..00000000 --- a/themes/futaba/custompage.class.php +++ /dev/null @@ -1,9 +0,0 @@ -left_enabled = false; - } -} - diff --git a/themes/futaba/layout.class.php b/themes/futaba/layout.class.php deleted file mode 100644 index cd79d626..00000000 --- a/themes/futaba/layout.class.php +++ /dev/null @@ -1,99 +0,0 @@ -get_string('theme', 'default'); - $data_href = get_base_href(); - $contact_link = contact_link(); - $header_html = $page->get_all_html_headers(); - - $left_block_html = ""; - $main_block_html = ""; - $sub_block_html = ""; - - foreach($page->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->body; // $this->block_to_html($block, true); - break; - default: - print "

    error: {$block->header} using an unknown section ({$block->section})"; - break; - } - } - - $debug = get_debug_info(); - - $contact = empty($contact_link) ? "" : "
    Contact"; - - if(empty($page->subheading)) { - $subheading = ""; - } - else { - $subheading = "

    {$page->subheading}
    "; - } - - if($page->left_enabled) { - $left = ""; - $withleft = "withleft"; - } - else { - $left = ""; - $withleft = ""; - } - - $flash = $page->get_cookie("flash_message"); - $flash_html = ""; - if($flash) { - $flash_html = "".nl2br(html_escape($flash))." [X]"; - } - - print << - - - - - - {$page->title} -$header_html - - - - -
    -

    {$page->heading}

    - $subheading - $sub_block_html -
    - $left -
    - $flash_html - $main_block_html -
    -
    -
    - Images © their respective owners, - Shimmie © - Shish & - The Team - 2007-2016, - based on the Danbooru concept. -
    Futaba theme based on 4chan's layout and CSS :3 - $debug - $contact -
    - - -EOD; - } -} - diff --git a/themes/futaba/page.class.php b/themes/futaba/page.class.php new file mode 100644 index 00000000..452514ef --- /dev/null +++ b/themes/futaba/page.class.php @@ -0,0 +1,75 @@ +left_enabled = false; + } + + public function render() + { + $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->body; // $this->block_to_html($block, true); + break; + default: + print "

    error: {$block->header} using an unknown section ({$block->section})"; + break; + } + } + + if (empty($this->subheading)) { + $subheading = ""; + } else { + $subheading = "

    {$this->subheading}
    "; + } + + if ($this->left_enabled) { + $left = ""; + $withleft = "withleft"; + } else { + $left = ""; + $withleft = ""; + } + + $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $head_html = $this->head_html(); + $footer_html = $this->footer_html(); + + print << + + $head_html + +
    +

    {$this->heading}

    + $subheading + $sub_block_html +
    + $left +
    + $flash_html + $main_block_html +
    +
    +
    + $footer_html +
    + + +EOD; + } +} diff --git a/themes/futaba/style.css b/themes/futaba/style.css index 34805b86..4ea6a219 100644 --- a/themes/futaba/style.css +++ b/themes/futaba/style.css @@ -10,8 +10,8 @@ BODY { color: #800000; padding-left: 5px; padding-right: 5px; - margin-right: 0px; - margin-left: 0px; + margin-right: 0; + margin-left: 0; margin-top: 5px; } H1 { @@ -30,7 +30,7 @@ FOOTER { A, A:visited {text-decoration: none; color: #0000EE;} A:hover {text-decoration: underline; color: #DD0000;} -HR {border: none; border-top: 1px solid #D9BFB7; height: 0px; clear: both;} +HR {border: none; border-top: 1px solid #D9BFB7; height: 0; clear: both;} NAV { width: 150px; @@ -45,11 +45,11 @@ NAV TD { } NAV INPUT { width: 100%; - padding: 0px; + padding: 0; } NAV SELECT { width: 100%; - padding: 0px; + padding: 0; } NAV H3 { text-align: left; @@ -92,12 +92,11 @@ TABLE.tag_list>TBODY>TR>TD:after { } .comment { /*background: #FFFFEE;*/ - border-width: 0px; + border-width: 0; } .reply, .paginator { margin-bottom: 2px; font-size: 10pt; - padding: 0px 5px; background: #F0E0D6; color: #800000; border: 1px solid #D9BFB7; @@ -107,7 +106,7 @@ TABLE.tag_list>TBODY>TR>TD:after { } .reply P { margin-left: 32px; - margin-bottom: 0px; + margin-bottom: 0; } .setupblock { diff --git a/themes/futaba/themelet.class.php b/themes/futaba/themelet.class.php index b4add7d3..00ad522e 100644 --- a/themes/futaba/themelet.class.php +++ b/themes/futaba/themelet.class.php @@ -1,87 +1,67 @@ -futaba_build_paginator($page_number, $total_pages, $base, $query); - $page->add_block(new Block(null, $body, "main", 90)); - } + /** + * Add a generic paginator. + */ + 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->futaba_build_paginator($page_number, $total_pages, $base, $query); + $page->add_block(new Block(null, $body, "main", 90)); + } - /** - * Generate a single HTML link. - * - * @param string $base_url - * @param string $query - * @param int|string $page - * @param string $name - * @return string - */ - public function futaba_gen_page_link($base_url, $query, $page, $name) { - $link = make_link("$base_url/$page", $query); - return "[{$name}]"; - } + /** + * Generate a single HTML link. + */ + public function futaba_gen_page_link(string $base_url, ?string $query, int $page, string $name): string + { + $link = make_link("$base_url/$page", $query); + return "[{$name}]"; + } - /** - * @param string $base_url - * @param string $query - * @param int|string $page - * @param int|string $current_page - * @param string $name - * @return string - */ - public function futaba_gen_page_link_block($base_url, $query, $page, $current_page, $name) { - $paginator = ""; - if($page == $current_page) $paginator .= ""; - $paginator .= $this->futaba_gen_page_link($base_url, $query, $page, $name); - if($page == $current_page) $paginator .= ""; - return $paginator; - } + public function futaba_gen_page_link_block(string $base_url, ?string $query, int $page, int $current_page, string $name): string + { + $paginator = ""; + if ($page == $current_page) { + $paginator .= ""; + } + $paginator .= $this->futaba_gen_page_link($base_url, $query, $page, $name); + if ($page == $current_page) { + $paginator .= ""; + } + return $paginator; + } - /** - * Build the paginator. - * - * @param int $current_page - * @param int $total_pages - * @param string $base_url - * @param string $query - * @return string - */ - public function futaba_build_paginator($current_page, $total_pages, $base_url, $query) { - $next = $current_page + 1; - $prev = $current_page - 1; - //$rand = mt_rand(1, $total_pages); + public function futaba_build_paginator(int $current_page, int $total_pages, string $base_url, ?string $query): string + { + $next = $current_page + 1; + $prev = $current_page - 1; + //$rand = mt_rand(1, $total_pages); - $at_start = ($current_page <= 1 || $total_pages <= 1); - $at_end = ($current_page >= $total_pages); + $at_start = ($current_page <= 1 || $total_pages <= 1); + $at_end = ($current_page >= $total_pages); - //$first_html = $at_start ? "First" : $this->futaba_gen_page_link($base_url, $query, 1, "First"); - $prev_html = $at_start ? "Prev" : $this->futaba_gen_page_link($base_url, $query, $prev, "Prev"); - //$random_html = $this->futaba_gen_page_link($base_url, $query, $rand, "Random"); - $next_html = $at_end ? "Next" : $this->futaba_gen_page_link($base_url, $query, $next, "Next"); - //$last_html = $at_end ? "Last" : $this->futaba_gen_page_link($base_url, $query, $total_pages, "Last"); + //$first_html = $at_start ? "First" : $this->futaba_gen_page_link($base_url, $query, 1, "First"); + $prev_html = $at_start ? "Prev" : $this->futaba_gen_page_link($base_url, $query, $prev, "Prev"); + //$random_html = $this->futaba_gen_page_link($base_url, $query, $rand, "Random"); + $next_html = $at_end ? "Next" : $this->futaba_gen_page_link($base_url, $query, $next, "Next"); + //$last_html = $at_end ? "Last" : $this->futaba_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; + $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->futaba_gen_page_link_block($base_url, $query, $i, $current_page, $i); - } - $pages_html = implode(" ", $pages); + $pages = []; + foreach (range($start, $end) as $i) { + $pages[] = $this->futaba_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". - // "
    << $pages_html >>

    "; - return "

    {$prev_html} {$pages_html} {$next_html}

    "; - } + //return "

    $first_html | $prev_html | $random_html | $next_html | $last_html". + // "
    << $pages_html >>

    "; + return "

    {$prev_html} {$pages_html} {$next_html}

    "; + } } - diff --git a/themes/futaba/view.theme.php b/themes/futaba/view.theme.php index 54b66e88..c58fe3af 100644 --- a/themes/futaba/view.theme.php +++ b/themes/futaba/view.theme.php @@ -1,11 +1,11 @@ -set_title("Image {$image->id}: ".html_escape($image->get_tag_list())); - $page->set_heading(html_escape($image->get_tag_list())); - $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "main", 10)); - } +class CustomViewImageTheme extends ViewImageTheme +{ + public function display_page(Image $image, $editor_parts) + { + global $page; + $page->set_heading(html_escape($image->get_tag_list())); + $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "main", 10)); + } } - diff --git a/themes/lite/comment.theme.php b/themes/lite/comment.theme.php index 2cda33c3..1149dd06 100644 --- a/themes/lite/comment.theme.php +++ b/themes/lite/comment.theme.php @@ -1,20 +1,14 @@ -rr(parent::comment_to_html($comment, $trim)); - } +class CustomCommentListTheme extends CommentListTheme +{ + protected function comment_to_html(Comment $comment, bool $trim=false): string + { + return $this->rr(parent::comment_to_html($comment, $trim)); + } - /** - * @param int $image_id - * @return string - */ - protected function build_postbox($image_id) { - return $this->rr(parent::build_postbox($image_id)); - } + protected function build_postbox(int $image_id): string + { + return $this->rr(parent::build_postbox($image_id)); + } } diff --git a/themes/lite/custompage.class.php b/themes/lite/custompage.class.php deleted file mode 100644 index b5c3ea29..00000000 --- a/themes/lite/custompage.class.php +++ /dev/null @@ -1,14 +0,0 @@ -left_enabled = false; - } -} - diff --git a/themes/lite/layout.class.php b/themes/lite/layout.class.php deleted file mode 100644 index 337d60a3..00000000 --- a/themes/lite/layout.class.php +++ /dev/null @@ -1,275 +0,0 @@ - -* Link: http://seemslegit.com -* License: GPLv2 -* Description: A mashup of Default, Danbooru, the interface on qwebirc, and -* some other sites, packaged in a light blue color. -*/ -class Layout { - - /** - * turns the Page into HTML. - * - * @param Page $page - */ - public function display_page(Page $page) { - global $config, $user; - - $theme_name = $config->get_string('theme', 'lite'); - $site_name = $config->get_string('title'); - $data_href = get_base_href(); - $contact_link = contact_link(); - $header_html = $page->get_all_html_headers(); - - $menu = ""; - - $left_block_html = ""; - $main_block_html = ""; - $sub_block_html = ""; - $user_block_html = ""; - - foreach($page->blocks as $block) { - switch($block->section) { - case "left": - $left_block_html .= $this->block_to_html($block, true, "left"); - break; - case "main": - $main_block_html .= $this->block_to_html($block, false, "main"); - break; - case "user": - $user_block_html .= $block->body; - break; - case "subheading": - $sub_block_html .= $this->block_to_html($block, false, "main"); - break; - default: - print "

    error: {$block->header} using an unknown section ({$block->section})"; - break; - } - } - - $custom_sublinks = "

    "; - // hack - $username = url_escape($user->name); - // hack - $qp = explode("/", ltrim(_get_query(), "/")); - $cs = ""; - - // php sucks - switch($qp[0]) { - default: - $cs = $user_block_html; - break; - case "": - # FIXME: this assumes that the front page is - # post/list; in 99% of case it will either be - # post/list or home, and in the latter case - # the subnav links aren't shown, but it would - # be nice to be correct - case "post": - if(class_exists("NumericScore")){ - $cs .= "Popular by Day/Month/Year "; - } - $cs .= "All"; - if(class_exists("Favorites")){ $cs .= "My Favorites";} - if(class_exists("RSS_Images")){ $cs .= "Feed";} - if(class_exists("Random_Image")){ $cs .= "Random Image";} - if(class_exists("Wiki")){ $cs .= "Help"; - }else{ $cs .= "Help";} - break; - case "comment": - $cs .= "All"; - $cs .= "Feed"; - $cs .= "Help"; - break; - case "pool": - $cs .= "List"; - $cs .= "Create"; - $cs .= "Changes"; - $cs .= "Help"; - break; - case "wiki": - $cs .= "Index"; - $cs .= "Rules"; - $cs .= "Help"; - break; - case "tags": - case "alias": - $cs .= "Map"; - $cs .= "Alphabetic"; - $cs .= "Popularity"; - $cs .= "Categories"; - $cs .= "Aliases"; - $cs .= "Help"; - break; - case "upload": - if(class_exists("Wiki")) { $cs .= "Guidelines"; } - break; - case "random": - $cs .= "Shuffle"; - $cs .= "Download"; - break; - case "featured": - $cs .= "Download"; - break; - } - - if($cs == "") { - $custom_sublinks = ""; - } else { - $custom_sublinks .= "$cs
    "; - } - - $debug = get_debug_info(); - - $contact = empty($contact_link) ? "" : "
    Contact"; - //$subheading = empty($page->subheading) ? "" : "
    {$page->subheading}
    "; - - /*$wrapper = ""; - if(strlen($page->heading) > 100) { - $wrapper = ' style="height: 3em; overflow: auto;"'; - }*/ - if($page->left_enabled == false) { - $left_block_html = ""; - $main_block_html = "
    {$main_block_html}
    "; - } else { - $left_block_html = ""; - $main_block_html = "
    {$main_block_html}
    "; - } - - $flash = $page->get_cookie("flash_message"); - $flash_html = ""; - if(!empty($flash)) { - $flash_html = "".nl2br(html_escape($flash))." [X]"; - } - - print << - - - - - - {$page->title} - $header_html - - - -
    - $menu - $custom_sublinks - $sub_block_html -
    - $left_block_html - $flash_html - $main_block_html -
    - Images © their respective owners, - Shimmie © - Shish & - The Team - 2007-2016, - based on the Danbooru concept.
    - Lite Theme by Zach - $debug - $contact -
    - - -EOD; - } /* end of function display_page() */ - - - /** - * A handy function which does exactly what it says in the method name. - * - * @param Block $block - * @param bool $hidable - * @param string $salt - * @return string - */ - public function block_to_html(Block $block, $hidable=false, $salt="") { - $h = $block->header; - $b = $block->body; - $i = str_replace(' ', '_', $h) . $salt; - $html = "
    "; - if(!is_null($h)) { - if($salt == "main") { - $html .= ""; - } else { - $html .= ""; - } - } - if(!is_null($b)) { - if($salt =="main") { - $html .= "
    {$b}
    "; - } - else { - $html .= " - - "; - } - } - $html .= "
    "; - return $html; - } - - /** - * @param string $link - * @param string $desc - * @param string[] $pages_matched - * @return null|string - */ - public function navlinks($link, $desc, $pages_matched) { - /** - * Woo! We can actually SEE THE CURRENT PAGE!! (well... see it highlighted in the menu.) - */ - $html = null; - $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]) { - $html = "{$desc}"; - } - } - - if(is_null($html)) {$html = "{$desc}";} - - return $html; - } - -} /* end of class Layout */ diff --git a/themes/lite/page.class.php b/themes/lite/page.class.php new file mode 100644 index 00000000..84b27cb2 --- /dev/null +++ b/themes/lite/page.class.php @@ -0,0 +1,146 @@ + + * Link: http://seemslegit.com + * License: GPLv2 + * Description: A mashup of Default, Danbooru, the interface on qwebirc, and + * some other sites, packaged in a light blue color. + */ + +class Page extends BasePage +{ + /** @var bool */ + public $left_enabled = true; + + public function disable_left() + { + $this->left_enabled = false; + } + + public function render() + { + global $config; + + list($nav_links, $sub_links) = $this->get_nav_links(); + $theme_name = $config->get_string(SetupConfig::THEME, 'lite'); + $site_name = $config->get_string(SetupConfig::TITLE); + $data_href = get_base_href(); + + $menu = ""; + + $left_block_html = ""; + $main_block_html = ""; + $sub_block_html = ""; + $user_block_html = ""; + + foreach ($this->blocks as $block) { + switch ($block->section) { + case "left": + $left_block_html .= $this->block_to_html($block, true, "left"); + break; + case "main": + $main_block_html .= $this->block_to_html($block, false, "main"); + break; + case "user": + $user_block_html .= $block->body; + break; + case "subheading": + $sub_block_html .= $this->block_to_html($block, false, "main"); + break; + default: + print "

    error: {$block->header} using an unknown section ({$block->section})"; + break; + } + } + + $custom_sublinks = ""; + if (!empty($sub_links)) { + $custom_sublinks = "

    "; + foreach ($sub_links as $nav_link) { + $custom_sublinks .= $this->navlinks($nav_link->link, $nav_link->description, $nav_link->active); + } + $custom_sublinks .= "
    "; + } + + if ($this->left_enabled == false) { + $left_block_html = ""; + $main_block_html = "
    {$main_block_html}
    "; + } else { + $left_block_html = ""; + $main_block_html = "
    {$main_block_html}
    "; + } + + $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $head_html = $this->head_html(); + $footer_html = $this->footer_html(); + + print << + + $head_html + +
    + $menu + $custom_sublinks + $sub_block_html +
    + $left_block_html + $flash_html + $main_block_html +
    + $footer_html +
    + + +EOD; + } /* end of function display_page() */ + + public function block_to_html(Block $block, bool $hidable=false, string $salt=""): string + { + $h = $block->header; + $b = $block->body; + $i = str_replace(' ', '_', $h) . $salt; + $html = "
    "; + if (!is_null($h)) { + if ($salt == "main") { + $html .= ""; + } else { + $html .= ""; + } + } + if (!is_null($b)) { + if ($salt =="main") { + $html .= "
    {$b}
    "; + } else { + $html .= " + + "; + } + } + $html .= "
    "; + return $html; + } + + public function navlinks(Link $link, string $desc, bool $active): ?string + { + $html = null; + if ($active) { + $html = "{$desc}"; + } else { + $html = "{$desc}"; + } + + return $html; + } +} diff --git a/themes/lite/setup.theme.php b/themes/lite/setup.theme.php index 42a1d210..f78b6ce0 100644 --- a/themes/lite/setup.theme.php +++ b/themes/lite/setup.theme.php @@ -1,4 +1,4 @@ -header; - $b = $block->body; - $i = preg_replace('/[^a-zA-Z0-9]/', '_', $h) . "-setup"; - $html = " +class CustomSetupTheme extends SetupTheme +{ + protected function sb_to_html(SetupBlock $block) + { + $h = $block->header; + $b = $block->body; + $i = preg_replace('/[^a-zA-Z0-9]/', '_', $h) . "-setup"; + $html = " - - - - - - -
    -
    - -
    - - - - -
    - $h_search - {$toolbar_block_html} - -
    - -
    -
    - - {$subtoolbar_block_html} -
    -
    -
    - Drawer -
    - $drawer_block_html -
    - -
    -
    -
    -
    - $head_block_html - $sub_block_html -
    -
    -
    -
    - -
    - - $left_block_html -
    -
    -
    - - -
    - $flash_html - $main_block_html -
    -
    -
    -
    - $debug - $contact -
    -
    -
    - -
      -
    • Layout Top
    • -
    • Layout Right
    • -
    • Layout Bottom
    • -
    • Layout Left
    • -
    - - -EOD; - } - - public function rework_navigation(Block $block){ - $h = $block->header; - $b = $block->body; - $i = $block->id; - - $dom = new DomDocument(); - $dom->loadHTML($b); - $output = array(); - $html = "
    \n\n
    \n"; - return $html; - } - - /** - * Get the HTML for this block. from core - * - * @param bool $hidable - * @return string - */ - public function get_html(Block $block, $section="main", $hidable=false, $extra_class="") { - $h = $block->header; - $b = $block->body; - $i = $block->id;//blotter extention id has `!` - - if($section == "toolbar"){ - $html = "
    \n\n
    \n"; - return $html; - } - if($section == "subtoolbar"){ - $html = "
    \n\n
    \n"; - return $html; - } - if($section == "full"){ - $html = "
    "; - $h_toggler = $hidable ? " shm-toggler" : ""; - if(!empty($h)) $html .="

    $h

    "; - if(!empty($b)) $html .="
    $b
    "; - $html .= "
    \n"; - return $html; - } - if($section == "third"){ - $html = "
    "; - $h_toggler = $hidable ? " shm-toggler" : ""; - if(!empty($h)) $html .="

    $h

    "; - if(!empty($b)) $html .="
    $b
    "; - $html .= "
    \n"; - return $html; - } - $html = "
    "; - $h_toggler = $hidable ? " shm-toggler" : ""; - if(!empty($h)) $html .= "

    $h

    "; - if(!empty($b)) $html .= "
    $b
    "; - $html .= "
    \n"; - return $html; - } - -} - - -//@todo fix ext/blotter id tag -//@todo fix table row error for ext/ip_ban -//@todo fix table row error for ext/image_hash_ban -//@todo fix table row error for ext/untag -//@todo fix ext private-messages gives Uncaught TypeError: Cannot read property 'href' of null when no messages are there.. diff --git a/themes/material/page.class.php b/themes/material/page.class.php new file mode 100644 index 00000000..8db7aa12 --- /dev/null +++ b/themes/material/page.class.php @@ -0,0 +1,273 @@ +get_nav_links(); + $theme_name = $config->get_string(SetupConfig::THEME, 'material'); + $site_name = $config->get_string(SetupConfig::TITLE); + $data_href = get_base_href(); + // $main_page = $config->get_string(SetupConfig::MAIN_PAGE); + $contact_link = contact_link(); + $site_link = make_link(); + $header_html = $this->get_all_html_headers(); + + $left_block_html = ""; + $main_block_html = ""; + $head_block_html = ""; + $sub_block_html = ""; + $drawer_block_html = ""; //use exampled in user.theme.php & view.theme.php + $toolbar_block_html = ""; // not used at this point + $subtoolbar_block_html = ""; // use exampled in user.theme.php + // $navigation = ""; + + $h_search = " +
    +
    + +
    + + + +
    +
    +
    + "; + + foreach ($this->blocks as $block) { + switch ($block->section) { + case "toolbar": + $toolbar_block_html .= $this->get_html($block, "toolbar"); + break; + case "subtoolbar": + $subtoolbar_block_html .= $this->get_html($block, "subtoolbar"); + break; + case "left": + if ($block->header == "Navigation") { + $subtoolbar_block_html = $this->rework_navigation($block); + break; + } + // $left_block_html .= $block->get_html(true); + $left_block_html .= $this->get_html($block, "full", true, "left-blocks nav-card mdl-cell--4-col-tablet"); + break; + case "head": + $head_block_html .= $this->get_html($block, "third", true, "nav-card head-blocks"); + break; + case "drawer": + $drawer_block_html .= $this->get_html($block, "full", true, "nav-card drawer-blocks"); + break; + case "main": + // $main_block_html .= $block->get_html(false); + $main_block_html .= $this->get_html($block, "main", true, ""); + break; + case "subheading": + // $sub_block_html .= $block->body; // $this->block_to_html($block, true); + $sub_block_html .= $this->get_html($block, "third", true, "nav-card"); + break; + default: + print "

    error: {$block->header} using an unknown section ({$block->section})"; + break; + } + } + + $debug = get_debug_info(); + + $contact = empty($contact_link) ? "" : "
    Contact"; + /*$subheading = empty($this->subheading) ? "" : "

    {$this->subheading}
    "; + + $wrapper = ""; + if(strlen($this->heading) > 100) { + $wrapper = ' style="height: 3em; overflow: auto;"'; + } + */ + + $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + + print << + + + + + + {$this->title} + + + + $header_html + + + + + + + +
    +
    + +
    + + + + +
    + $h_search + {$toolbar_block_html} + +
    + +
    +
    + + {$subtoolbar_block_html} +
    +
    +
    + Drawer +
    + $drawer_block_html +
    + +
    +
    +
    +
    + $head_block_html + $sub_block_html +
    +
    +
    +
    + +
    + + $left_block_html +
    +
    +
    + + +
    + $flash_html + $main_block_html +
    +
    +
    +
    + $debug + $contact +
    +
    +
    + +
      +
    • Layout Top
    • +
    • Layout Right
    • +
    • Layout Bottom
    • +
    • Layout Left
    • +
    + + +EOD; + } + + public function rework_navigation(Block $block) + { + // $h = $block->header; + $b = $block->body; + $i = $block->id; + + $dom = new DomDocument(); + $dom->loadHTML($b); + // $output = []; + $html = "
    \n\n
    \n"; + return $html; + } + + /** + * Get the HTML for this block. from core + */ + public function get_html(Block $block, string $section="main", bool $hidable=false, string $extra_class=""): string + { + $h = $block->header; + $b = $block->body; + $i = $block->id;//blotter extention id has `!` + + if ($section == "toolbar") { + $html = "
    \n\n
    \n"; + return $html; + } + if ($section == "subtoolbar") { + $html = "
    \n\n
    \n"; + return $html; + } + if ($section == "full") { + $html = "
    "; + $h_toggler = $hidable ? " shm-toggler" : ""; + if (!empty($h)) { + $html .="

    $h

    "; + } + if (!empty($b)) { + $html .="
    $b
    "; + } + $html .= "
    \n"; + return $html; + } + if ($section == "third") { + $html = "
    "; + $h_toggler = $hidable ? " shm-toggler" : ""; + if (!empty($h)) { + $html .="

    $h

    "; + } + if (!empty($b)) { + $html .="
    $b
    "; + } + $html .= "
    \n"; + return $html; + } + $html = "
    "; + $h_toggler = $hidable ? " shm-toggler" : ""; + if (!empty($h)) { + $html .= "

    $h

    "; + } + if (!empty($b)) { + $html .= "
    $b
    "; + } + $html .= "
    \n"; + return $html; + } +} diff --git a/themes/material/themelet.class.php b/themes/material/themelet.class.php index 77c927c3..59021a28 100644 --- a/themes/material/themelet.class.php +++ b/themes/material/themelet.class.php @@ -1,3 +1,4 @@ -add_block(new Block("Upload", $this->build_upload_block(), "left", 20)); - // } +class CustomUploadTheme extends UploadTheme +{ + // public function display_block(Page $page) { + // $page->add_block(new Block("Upload", $this->build_upload_block(), "left", 20)); + // } // - // public function display_full(Page $page) { - // $page->add_block(new Block("Upload", "Disk nearly full, uploads disabled", "left", 20)); - // } + // public function display_full(Page $page) { + // $page->add_block(new Block("Upload", "Disk nearly full, uploads disabled", "left", 20)); + // } } diff --git a/themes/material/user.theme.php b/themes/material/user.theme.php index ab401801..82a6f122 100644 --- a/themes/material/user.theme.php +++ b/themes/material/user.theme.php @@ -1,18 +1,21 @@ -name); - $html = " | "; - foreach($parts as $part) { - $html .= "{$part["name"]} | "; - } - $page->add_block(new Block("Logged in as $h_name", $html, "drawer", 90)); - } +class CustomUserPageTheme extends UserPageTheme +{ + public function display_user_block(Page $page, User $user, $parts) + { + $h_name = html_escape($user->name); + $html = " | "; + foreach ($parts as $part) { + $html .= "{$part["name"]} | "; + } + $page->add_block(new Block("Logged in as $h_name", $html, "drawer", 90)); + } - public function display_login_block(Page $page) { - global $config; - $html = " + public function display_login_block(Page $page) + { + global $config; + $html = "
    @@ -21,9 +24,9 @@ class CustomUserPageTheme extends UserPageTheme {
    Name
    "; - if($config->get_bool("login_signup_enabled")) { - $html .= "Create Account"; - } - $page->add_block(new Block("Login", $html, "head", 90)); - } + if ($config->get_bool("login_signup_enabled")) { + $html .= "Create Account"; + } + $page->add_block(new Block("Login", $html, "head", 90)); + } } diff --git a/themes/material/view.theme.php b/themes/material/view.theme.php index 8b243e6a..f81b56e0 100644 --- a/themes/material/view.theme.php +++ b/themes/material/view.theme.php @@ -1,82 +1,71 @@ -set_heading(html_escape($image->get_tag_list())); + $page->add_block(new Block(null, $this->build_pin($image), "subtoolbar", 0)); + $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "left", 20)); + } + + public function display_admin_block(Page $page, $parts) + { + if (count($parts) > 0) { + $page->add_block(new Block("Image Controls", join("
    ", $parts), "drawer", 50)); + } + } + + protected function build_pin(Image $image) + { + if (isset($_GET['search'])) { + $query = "search=".url_escape($_GET['search']); + } else { + $query = null; + } + + $h_prev = 'id}", $query).'">Prev'; + $h_index = "Current"; + $h_next = 'id}", $query).'">Next'; + + return $h_prev.$h_index.$h_next; + } - /* - * Build a page showing $image and some info about it - */ - public function display_page(Image $image, $editor_parts) { - global $page; + protected function build_info(Image $image, $editor_parts) + { + global $user; - $h_metatags = str_replace(" ", ", ", html_escape($image->get_tag_list())); + if (count($editor_parts) == 0) { + return ($image->is_locked() ? "
    [Image Locked]" : ""); + } - $page->set_title("Image {$image->id}: ".html_escape($image->get_tag_list())); - $page->add_html_header(""); - $page->add_html_header(""); - $page->add_html_header(""); - $page->add_html_header("get_thumb_link())."\">"); - $page->add_html_header("id}"))."\">"); - $page->set_heading(html_escape($image->get_tag_list())); - $page->add_block(new Block(null, $this->build_pin($image), "subtoolbar", 0)); - $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "left", 20)); - } - - public function display_admin_block(Page $page, $parts) { - if(count($parts) > 0) { - $page->add_block(new Block("Image Controls", join("
    ", $parts), "drawer", 50)); - } - } - - protected function build_pin(Image $image) { - - global $database; - - if(isset($_GET['search'])) { - $search_terms = explode(' ', $_GET['search']); - $query = "search=".url_escape($_GET['search']); - } - else { - $search_terms = array(); - $query = null; - } - - $h_prev = 'id}", $query).'">Prev'; - $h_index = "Current"; - $h_next = 'id}", $query).'">Next'; - - return $h_prev.$h_index.$h_next; - } - - - protected function build_info(Image $image, $editor_parts) { - global $user; - - if(count($editor_parts) == 0) return ($image->is_locked() ? "
    [Image Locked]" : ""); - - $html = make_form(make_link("post/set"))." + $html = make_form(make_link("post/set"))." "; - foreach($editor_parts as $part) { - $html .= $part; - } - if( - (!$image->is_locked() || $user->can("edit_image_lock")) && - $user->can("edit_image_tag") - ) { - $html .= " + foreach ($editor_parts as $part) { + $html .= $part; + } + if ( + (!$image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK)) && + $user->can(Permissions::EDIT_IMAGE_TAG) + ) { + $html .= " "; - } - $html .= " + } + $html .= "
    "; - return $html; - } - + return $html; + } } diff --git a/themes/rule34v2/.gitignore b/themes/rule34v2/.gitignore new file mode 100644 index 00000000..13c95f72 --- /dev/null +++ b/themes/rule34v2/.gitignore @@ -0,0 +1,8 @@ +*.png +*.jpg +*.gif +*.mp3 +*.html +ad* +ads* +random* diff --git a/themes/rule34v2/bg.png b/themes/rule34v2/bg.png new file mode 100644 index 00000000..45e1b42c Binary files /dev/null and b/themes/rule34v2/bg.png differ diff --git a/themes/rule34v2/favicon.ico b/themes/rule34v2/favicon.ico new file mode 100644 index 00000000..2ec940d8 Binary files /dev/null and b/themes/rule34v2/favicon.ico differ diff --git a/themes/rule34v2/flags/china-flag.png b/themes/rule34v2/flags/china-flag.png new file mode 100644 index 00000000..b54e1c73 Binary files /dev/null and b/themes/rule34v2/flags/china-flag.png differ diff --git a/themes/rule34v2/flags/dutch-flag.png b/themes/rule34v2/flags/dutch-flag.png new file mode 100644 index 00000000..99d69fde Binary files /dev/null and b/themes/rule34v2/flags/dutch-flag.png differ diff --git a/themes/rule34v2/flags/english-flag.png b/themes/rule34v2/flags/english-flag.png new file mode 100644 index 00000000..c171928b Binary files /dev/null and b/themes/rule34v2/flags/english-flag.png differ diff --git a/themes/rule34v2/flags/finnish-flag.png b/themes/rule34v2/flags/finnish-flag.png new file mode 100644 index 00000000..eddfdba4 Binary files /dev/null and b/themes/rule34v2/flags/finnish-flag.png differ diff --git a/themes/rule34v2/flags/german-flag.png b/themes/rule34v2/flags/german-flag.png new file mode 100644 index 00000000..1b9d04f6 Binary files /dev/null and b/themes/rule34v2/flags/german-flag.png differ diff --git a/themes/rule34v2/flags/italian-flag.png b/themes/rule34v2/flags/italian-flag.png new file mode 100644 index 00000000..c687ea2b Binary files /dev/null and b/themes/rule34v2/flags/italian-flag.png differ diff --git a/themes/rule34v2/flags/norway-flag.png b/themes/rule34v2/flags/norway-flag.png new file mode 100644 index 00000000..0e9bb5a8 Binary files /dev/null and b/themes/rule34v2/flags/norway-flag.png differ diff --git a/themes/rule34v2/flags/port-flag.png b/themes/rule34v2/flags/port-flag.png new file mode 100644 index 00000000..b883e280 Binary files /dev/null and b/themes/rule34v2/flags/port-flag.png differ diff --git a/themes/rule34v2/flags/russian-flag.png b/themes/rule34v2/flags/russian-flag.png new file mode 100644 index 00000000..95333f6f Binary files /dev/null and b/themes/rule34v2/flags/russian-flag.png differ diff --git a/themes/rule34v2/flags/spain-flag.png b/themes/rule34v2/flags/spain-flag.png new file mode 100644 index 00000000..4c2921f0 Binary files /dev/null and b/themes/rule34v2/flags/spain-flag.png differ diff --git a/themes/rule34v2/flags/swedish-flag.png b/themes/rule34v2/flags/swedish-flag.png new file mode 100644 index 00000000..b0e4a760 Binary files /dev/null and b/themes/rule34v2/flags/swedish-flag.png differ diff --git a/themes/rule34v2/header.inc b/themes/rule34v2/header.inc new file mode 100644 index 00000000..cfd26bab --- /dev/null +++ b/themes/rule34v2/header.inc @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + +
    + + + +
    + + +
    diff --git a/themes/rule34v2/home.theme.php b/themes/rule34v2/home.theme.php new file mode 100644 index 00000000..83604879 --- /dev/null +++ b/themes/rule34v2/home.theme.php @@ -0,0 +1,76 @@ +set_mode("data"); + $page->add_auto_html_headers(); + $hh = $page->get_all_html_headers(); + $page->set_data( + << + + $sitename + + + + $hh + + + + $body + + +EOD + ); + } + + public function build_body(string $sitename, string $main_links, string $main_text, string $contact_link, $num_comma, string $counter_text) + { + $main_links_html = empty($main_links) ? "" : ""; + $message_html = empty($main_text) ? "" : "
    $main_text
    "; + $counter_html = empty($counter_text) ? "" : "
    $counter_text
    "; + $contact_link = empty($contact_link) ? "" : "
    Contact –"; + $search_html = " + + "; + return " +
    +

    $sitename

    + $main_links_html + $search_html + $message_html + $counter_html + +
    "; + } +} diff --git a/themes/rule34v2/index.theme.php b/themes/rule34v2/index.theme.php new file mode 100644 index 00000000..04aab4fe --- /dev/null +++ b/themes/rule34v2/index.theme.php @@ -0,0 +1,39 @@ +can("delete_image") ? "can-del" : ""; + $h_query = html_escape($query); + + $table = "
    "; + foreach ($images as $image) { + $table .= $this->build_thumb_html($image); + } + $table .= "
    "; + return $table; + } + + public function display_page(Page $page, $images) + { + $this->display_page_header($page, $images); + + $nav = $this->build_navigation($this->page_number, $this->total_pages, $this->search_terms); + if (!empty($this->search_terms)) { + $page->_search_query = $this->search_terms; + } + $page->add_block(new Block("Navigation", $nav, "left", 0)); + + if (count($images) > 0) { + $this->display_page_images($page, $images); + } else { + $this->display_error( + 404, + "No Images Found", + "No images were found to match the search criteria. Try looking up a character/series/artist by another name if they go by more than one. Remember to use underscores in place of spaces and not to use commas. If you came to this page by following a link, try using the search box directly instead. See the FAQ for more information." + ); + } + } +} diff --git a/themes/rule34v2/menuh.css b/themes/rule34v2/menuh.css new file mode 100644 index 00000000..1077a50d --- /dev/null +++ b/themes/rule34v2/menuh.css @@ -0,0 +1,121 @@ +/* Begin CSS Drop Down Menu */ + +a:link.menu { color:#FF0000; text-decoration: none; } + +a:visited.menu { color: #FF0000; text-decoration: none; } + +a:hover.menu { color: #FF0000; text-decoration: none; } + +a:active.menu { color: #FF0000; text-decoration: none; } + +#menuh-container + { + font-size: 1em; + float: left; + top:0; + left: 5%; + width: 100%; + margin: 0; + } + +#menuh + { + font-size: small; + font-family: arial, helvetica, sans-serif; + width:100%; + margin-top: 0; + } + +#menuh a.sub_option + { + border: 1px solid #555; + /*background-image:url(topban.jpg);*/ + } + +#menuh a + { + text-align: center; + background: #ACE4A3; + display:block; + white-space:nowrap; + margin: 0; + padding: 0.2em; + } + +#menuh a, #menuh a:visited /* menu at rest */ + { + color: #000099; + text-decoration:none; + } + +#menuh a:hover /* menu at mouse-over */ + { + color: #000000; + } + +#menuh a.top_parent, #menuh a.top_parent:hover /* attaches down-arrow to all top-parents */ + { + /*background-image: url(navdown_white.gif);*/ + background-position: right center; + background-repeat: no-repeat; + } + +#menuh a.parent, #menuh a.parent:hover /* attaches side-arrow to all parents */ + { + /*background-image: url(nav_white.gif);*/ + background-position: right center; + border: 1px solid #555; + background-repeat: no-repeat; + } + +#menuh ul + { + list-style:none; + margin:0; + padding:0; + float:left; + width:9em; /* width of all menu boxes */ + } + +#menuh li + { + position:relative; + min-height: 1px; /* Sophie Dennis contribution for IE7 */ + vertical-align: bottom; /* Sophie Dennis contribution for IE7 */ + } + +#menuh ul ul + { + position:absolute; + z-index:500; + top:auto; + display:none; + padding: 1em; + margin:-1em 0 0 -1em; + } + +#menuh ul ul ul + { + top:0; + left:100%; + } + +div#menuh li:hover + { + cursor:pointer; + z-index:100; + } + +div#menuh li:hover ul ul, +div#menuh li li:hover ul ul, +div#menuh li li li:hover ul ul, +div#menuh li li li li:hover ul ul +{display:none;} + +div#menuh li:hover ul, +div#menuh li li:hover ul, +div#menuh li li li:hover ul, +div#menuh li li li li:hover ul +{display:block;} + +/* End CSS Drop Down Menu */ diff --git a/themes/rule34v2/page.class.php b/themes/rule34v2/page.class.php new file mode 100644 index 00000000..b5e9f387 --- /dev/null +++ b/themes/rule34v2/page.class.php @@ -0,0 +1,123 @@ +get_string('theme', 'default'); + $data_href = get_base_href(); + $header_html = $this->get_all_html_headers(); + + $left_block_html = ""; + $right_block_html = ""; + $main_block_html = ""; + $head_block_html = ""; + $sub_block_html = ""; + + foreach ($this->blocks as $block) { + switch ($block->section) { + case "left": + $left_block_html .= $block->get_html(true); + break; + case "right": + $right_block_html .= $block->get_html(true); + break; + case "head": + $head_block_html .= "".$block->get_html(false).""; + break; + case "main": + $main_block_html .= $block->get_html(false); + break; + case "subheading": + $sub_block_html .= $block->body; // $block->get_html(true); + break; + default: + print "

    error: {$block->header} using an unknown section ({$block->section})"; + break; + } + } + + $query = !empty($this->_search_query) ? html_escape(Tag::implode($this->_search_query)) : ""; + assert(!is_null($query)); # used in header.inc, do not remove :P + $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $generated = autodate(date('c')); + $footer_html = $this->footer_html(); + + print << + + + {$this->title} + + + + +$header_html + + + + + + + + + + + + $sub_block_html + +

    + +
    + $flash_html + + $main_block_html +
    + +
    + + Terms of use + !!! + Privacy policy + !!! + 18 U.S.C. §2257
    +
    +
    + + BTC: 193gutWtgirF7js14ivcXfnfQgXv9n5BZo + ETH: 0x68B88a00e69Bde88E9db1b9fC10b8011226e26aF + +
    +
    +Thank you! + + Page generated $generated. + $footer_html +
    + + + + + + +EOD; + } +} diff --git a/themes/rule34v2/rule34_logo_top.png b/themes/rule34v2/rule34_logo_top.png new file mode 100644 index 00000000..98298cb0 Binary files /dev/null and b/themes/rule34v2/rule34_logo_top.png differ diff --git a/themes/rule34v2/style.css b/themes/rule34v2/style.css new file mode 100644 index 00000000..06ccd4c3 --- /dev/null +++ b/themes/rule34v2/style.css @@ -0,0 +1,366 @@ + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* things common to all pages * +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +BODY { + background: url(bg.png) #ACE4A3; + font-family: "Arial", sans-serif; + font-size: 14px; + margin: 0; +} +#header { + border-bottom: 1px solid #7EB977; + margin-top: 0; + margin-bottom: 16px; + padding: 8px; + background: #ACE4A3; + text-align: center; +} +H1 { + font-size: 5em; + margin: 0; + padding: 0; +} +H1 A { + color: black; +} +H3 { + text-align: center; + margin: 0; +} +THEAD { + font-weight: bold; +} +TD { + vertical-align: top; + text-align: center; +} + +#subtitle { + width: 256px; + font-size: 0.75em; + margin: -16px auto auto; + text-align: center; + border: 1px solid black; + border-top: none; + background: #DDD; +} +#flash { + background: #FF7; + display: block; + padding: 8px; + margin: 8px; + border: 1px solid #882; +} + +TABLE.zebra {background: #ACE4A3; border-collapse: collapse; border: 1px solid #7EB977;} +TABLE.zebra TD {font-size: 0.8em;margin: 0; border-top: 1px solid #7EB977; padding: 2px;} +TABLE.zebra TR:nth-child(odd) {background: #9CD493;} +TABLE.zebra TR:nth-child(even) {background: #ACE4A3;} + +FOOTER { + clear: both; + padding: 8px; + font-size: 0.7em; + text-align: center; + border-top: 1px solid #7EB977; + background: #ACE4A3; +} + +A {color: #000099; text-decoration: none; font-weight: bold;} +A:hover {color: #000099; text-decoration: underline;} +A:visited {color: #000099; text-decoration: none} +A:active {color: #000099; text-decoration: underline;} + +UL { + text-align: left; +} + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* the navigation bar, and all its blocks * +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +NAV { + width: 250px; + float: left; + text-align: center; + margin-left: 16px; +} +NAV .blockbody { + font-size: 0.85em; + text-align: center; +} +NAV TABLE { + width: 100%; +} +NAV TD { + vertical-align: middle; +} +NAV INPUT { + width: 100%; + padding: 0; +} +NAV SELECT { + width: 100%; + padding: 0; +} + + +.comment .info { + background: #ACE4A3; + border: 1px solid #7EB977; +} + +.more:after { + content: " >>>"; +} + +.tag_count:before { + content: "("; +} +.tag_count:after { + content: ")"; +} + +#imagelist .blockbody, +#paginator .blockbody { + background: none; + border: none; + box-shadow: none; +} + +#commentlistimage .blockbody, +#commentlistrecent .blockbody { + background: none; + border: none; + box-shadow: none; + padding: 0; +} + +#commentlistimage .blockbody .comment, +#commentlistrecent .blockbody .comment { + margin-left: 0; + margin-right: 0; +} + + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* the main part of each page * +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +ARTICLE { + margin-left: 276px; + margin-right: 16px; + text-align: center; + height: 1%; + margin-top: 16px; +} +ARTICLE TABLE { + width: 90%; + margin: auto; +} +NAV SECTION:first-child H3 { + margin-top: 0; +} + + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* specific page types * +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#pagelist { + margin-top: 32px; +} + +#tagmap A { + padding: 8px 4px 8px 4px; +} + +SECTION>.blockbody, .comment, .setupblock { + background: #ACE4A3; + margin: 8px; + border: 1px solid #7EB977; + padding: 8px; +} + +SECTION>H3 { + text-align: center; + background: #9CD493; + margin: 8px; + border: 1px solid #7EB977; + padding: 8px; +} + +.thumb { + width: 226px; + display: inline-block; + zoom: 1; /* ie6 */ + *display: inline; /* ie6 */ + text-align: center; + margin-bottom: 8px; +} +.thumb IMG { + border: 1px solid #7EB977; + background: #ACE4A3; + padding: 4px; +} + +div#twitter_update_list li { + list-style:none; + padding-bottom:0; + text-align:left; + margin-top:5px; + margin-bottom:5px; + border: solid 1px #000; + background: url(bg.png); +} + +.username { + font-weight: bold; +} + +#bans TD, .image_info TD { + vertical-align: middle; +} +#bans INPUT { + font-size: 0.85em; +} + +.need-del { + display: none; +} +.can-del .need-del { + display: inline; +} + + +.unread { + color: red; +} + +UL.tagit { + margin: 0; +} +ul.tagit li.tagit-new { + width: 50px; +} + + +[data-tags~="animated"]>A>IMG { background: #CC00CC; } +[data-ext="mp4"]>A>IMG, +[data-ext="webm"]>A>IMG { background: #0000FF; } + +#menuh-container { + float: none; + width: 500px; + margin: auto; +} + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* responsive overrides * +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +@media (max-width: 750px) { + .atoz, #paginator { + font-size: 2em; + } + .header-sites { + display: none; + } + SECTION>.blockbody { + overflow-x: auto; + } +} + +/* responsive padding */ +@media (max-width: 1024px) { + NAV {margin-left: 0;} + ARTICLE {margin-right: 0; margin-left: 242px;} +} +@media (max-width: 750px) { + NAV {margin-left: 0;} + ARTICLE {margin-right: 0; margin-left: 250px;} +} + +/* responsive navbar */ +#nav-toggle {display: none;} +@media (max-width: 750px) { + TD#nav-toggle {display: table-cell; width: 40px;} + #nav-toggle A {border: 1px solid black; border-radius: 8px;} + #nav-toggle A:hover {text-decoration: none;} + + NAV>SECTION>.blockbody, + NAV>SECTION>.blockbody>.comment { + margin: 0; + } + NAV>SECTION>H3 { + margin: 0; + } + + BODY.navHidden #menuh-container {display: none;} + BODY.navHidden NAV {display: none;} + BODY.navHidden ARTICLE {margin-left: 0;} + +/* + NAV { + position: fixed; + top: 6.5em; + bottom: 0px; + overflow-y: scroll; + } + */ +} + +/* sticky header */ +@media (max-width: 750px) { + BODY.navHidden {padding-top: 5.4em} +} +@media (max-width: 750px) { + #header {position: fixed; top: 0; left: 0; z-index: 99999999999;} + .ui-autocomplete {z-index: 999999999999;} + BODY {padding-top: 7em} +} + +/* responsive header */ +#Uploadleft {display: none;} +#Uploadhead {display: block;} +#UserBlockleft {display: none;} +#UserBlockhead {display: block;} +#Loginleft {display: none;} +#Loginhead {display: block;} +.headcol {width: 250px; font-size: 0.85em;} +.headbox {width: 80%; margin: auto;} +#big-logo {display: table-cell;} +#mini-logo {display: none;} +@media (max-width: 1024px) { + #Uploadleft {display: block;} + #Uploadhead {display: none;} + #UserBlockleft {display: block;} + #UserBlockhead {display: none;} + #Loginleft {display: block;} + #Loginhead {display: none;} + .headcol {display: none;} + .headbox {width: 100%; margin: auto;} + #big-logo {display: none;} + #mini-logo {display: table-cell; width: 100px;} + + /* hide nav-search when header-search is sticky */ + ARTICLE {margin-top: 0;} + #Navigationleft .blockbody {font-size: 1.5em;} + #Navigationleft .blockbody P, + #Navigationleft .blockbody FORM + {display: none;} +} + +/* responsive comments */ +.comment_list_table {width: 100%;} + +/* responsive misc */ +@media (max-width: 750px) { + #shm-main-image { max-width: 95%; } +} + +#ed91727bc9c7a73fdcec6db562e63151main { + overflow: scroll; +} diff --git a/themes/rule34v2/tag_edit.theme.php b/themes/rule34v2/tag_edit.theme.php new file mode 100644 index 00000000..4e92654e --- /dev/null +++ b/themes/rule34v2/tag_edit.theme.php @@ -0,0 +1,38 @@ +get_tag_list()); + return " + + Tags + + + + + "; + } + + public function get_source_editor_html(Image $image): string + { + global $user; + $h_source = html_escape($image->get_source()); + $f_source = $this->format_source($image->get_source()); + $style = "overflow: hidden; white-space: nowrap; max-width: 350px; text-overflow: ellipsis;"; + return " + + Source Link + + ".($user->can("edit_image_source") ? " +
    $f_source
    + + " : " +
    $f_source
    + ")." + + + "; + } +} diff --git a/themes/rule34v2/themelet.class.php b/themes/rule34v2/themelet.class.php new file mode 100644 index 00000000..cd4f0a18 --- /dev/null +++ b/themes/rule34v2/themelet.class.php @@ -0,0 +1,39 @@ +get("thumb-block:{$image->id}"); + if ($cached) { + return $cached; + } + + $i_id = (int) $image->id; + $h_view_link = make_link('post/view/'.$i_id); + $h_image_link = $image->get_image_link(); + $h_thumb_link = $image->get_thumb_link(); + $h_tip = html_escape($image->get_tooltip()); + $h_tags = strtolower($image->get_tag_list()); + $h_ext = strtolower($image->ext); + + // 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); + } + + $html = "
    ". + ''.$h_tip.''. + '
    Image Only'. + " - Ban". + "
    \n"; + + // cache for ages; will be cleared in ext/index:onImageInfoSet + $cache->set("thumb-block:{$image->id}", $html, 86400); + + return $html; + } +} diff --git a/themes/rule34v2/upload.theme.php b/themes/rule34v2/upload.theme.php new file mode 100644 index 00000000..766ac422 --- /dev/null +++ b/themes/rule34v2/upload.theme.php @@ -0,0 +1,30 @@ +add_block(new Block("Upload", $this->build_upload_block(), "head", 20)); + $page->add_block(new Block("Upload", $this->build_upload_block(), "left", 20)); + } + + public function display_full(Page $page) + { + $page->add_block(new Block("Upload", "Disk nearly full, uploads disabled", "head", 20)); + } + + public function display_page(Page $page) + { + parent::display_page($page); + $html = " + Tagging Guide + "; + $page->add_block(new Block(null, $html, "main", 19)); + } + + protected function build_upload_block(): string + { + $url = make_link("upload"); + return "Upload"; + } +} diff --git a/themes/rule34v2/user.theme.php b/themes/rule34v2/user.theme.php new file mode 100644 index 00000000..14b2e9b9 --- /dev/null +++ b/themes/rule34v2/user.theme.php @@ -0,0 +1,109 @@ +name); + $lines = []; + foreach ($parts as $part) { + $lines[] = "{$part["name"]}"; + } + if (count($lines) < 6) { + $html = implode("\n
    ", $lines); + } else { + $html = implode(" | \n", $lines); + } + $page->add_block(new Block("Logged in as $h_name", $html, "head", 90, "UserBlockhead")); + $page->add_block(new Block("Logged in as $h_name", $html, "left", 15, "UserBlockleft")); + } + + public function display_login_block(Page $page) + { + global $config; + $html = " +
    + + + + +
    Name
    Password
    +
    + "; + if ($config->get_bool("login_signup_enabled")) { + $html .= "Create Account"; + } + $page->add_block(new Block("Login", $html, "head", 90)); + $page->add_block(new Block("Login", $html, "left", 15)); + } + + public function display_signup_page(Page $page) + { + global $config; + $tac = $config->get_string("login_tac", ""); + + if ($config->get_bool("login_tac_bbcode")) { + $tfe = new TextFormattingEvent($tac); + send_event($tfe); + $tac = $tfe->formatted; + } + + $form = SHM_SIMPLE_FORM( + "user_admin/create", + TABLE( + ["class"=>"form"], + TBODY( + TR( + TH("Name"), + TD(INPUT(["type"=>'text', "name"=>'name', "required"=>true])) + ), + TR( + TH("Password"), + TD(INPUT(["type"=>'password', "name"=>'pass1', "required"=>true])) + ), + TR( + TH(rawHTML("Repeat Password")), + TD(INPUT(["type"=>'password', "name"=>'pass2', "required"=>true])) + ), + TR( + TH(rawHTML("Email")), + TD(INPUT(["type"=>'email', "name"=>'email', "required"=>true])) + ), + TR( + TD(["colspan"=>"2"], rawHTML(captcha_get_html())) + ), + ), + TFOOT( + TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>"Create Account"]))) + ) + ) + ); + + $html = emptyHTML( + $tac ? P(rawHTML($tac)) : null, + $form + ); + + $page->set_title("Create Account"); + $page->set_heading("Create Account"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Signup", (string)$html)); + } +} diff --git a/themes/warm/layout.class.php b/themes/warm/layout.class.php deleted file mode 100644 index 917ede3e..00000000 --- a/themes/warm/layout.class.php +++ /dev/null @@ -1,109 +0,0 @@ -get_string('theme', 'default'); - $site_name = $config->get_string('title'); - $data_href = get_base_href(); - $main_page = $config->get_string('main_page'); - $contact_link = contact_link(); - $header_html = $page->get_all_html_headers(); - - $left_block_html = ""; - $main_block_html = ""; - $head_block_html = ""; - $sub_block_html = ""; - - foreach($page->blocks as $block) { - switch($block->section) { - case "left": - $left_block_html .= $block->get_html(true); - break; - case "head": - $head_block_html .= "".$block->get_html(false).""; - break; - case "main": - $main_block_html .= $block->get_html(false); - break; - case "subheading": - $sub_block_html .= $block->body; // $this->block_to_html($block, true); - break; - default: - print "

    error: {$block->header} using an unknown section ({$block->section})"; - break; - } - } - - $debug = get_debug_info(); - - $contact = empty($contact_link) ? "" : "
    Contact"; - /*$subheading = empty($page->subheading) ? "" : "

    {$page->subheading}
    "; - - $wrapper = ""; - if(strlen($page->heading) > 100) { - $wrapper = ' style="height: 3em; overflow: auto;"'; - } - */ - - $flash = $page->get_cookie("flash_message"); - $flash_html = ""; - if($flash) { - $flash_html = "".nl2br(html_escape($flash))." [X]"; - } - - print << - - - - - - {$page->title} -$header_html - - - -
    - - - - $head_block_html - - - $sub_block_html -
    - -
    - $flash_html - $main_block_html -
    -
    - Images © their respective owners, - Shimmie © - Shish & - The Team - 2007-2016, - based on the Danbooru concept. - $debug - $contact -
    - - -EOD; - } -} - diff --git a/themes/warm/page.class.php b/themes/warm/page.class.php new file mode 100644 index 00000000..eac493f4 --- /dev/null +++ b/themes/warm/page.class.php @@ -0,0 +1,72 @@ +get_string(SetupConfig::TITLE); + $data_href = get_base_href(); + $main_page = $config->get_string(SetupConfig::MAIN_PAGE); + + $left_block_html = ""; + $main_block_html = ""; + $head_block_html = ""; + $sub_block_html = ""; + + foreach ($this->blocks as $block) { + switch ($block->section) { + case "left": + $left_block_html .= $block->get_html(true); + break; + case "head": + $head_block_html .= "".$block->get_html(false).""; + break; + case "main": + $main_block_html .= $block->get_html(false); + break; + case "subheading": + $sub_block_html .= $block->body; // $this->block_to_html($block, true); + break; + default: + print "

    error: {$block->header} using an unknown section ({$block->section})"; + break; + } + } + + $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $head_html = $this->head_html(); + $footer_html = $this->footer_html(); + + print << + + $head_html + +

    + + + + $head_block_html + + + $sub_block_html +
    + +
    + $flash_html + $main_block_html +
    +
    + $footer_html +
    + + +EOD; + } +} diff --git a/themes/warm/style.css b/themes/warm/style.css index 0b117631..2d54532d 100644 --- a/themes/warm/style.css +++ b/themes/warm/style.css @@ -7,11 +7,11 @@ BODY { background: url(bg.png); font-family: "Arial", sans-serif; font-size: 14px; - margin: 0px; + margin: 0; } HEADER { border-bottom: 1px solid #B89F7C; - margin-top: 0px; + margin-top: 0; margin-bottom: 16px; padding: 8px; background: #FCD9A9; @@ -19,15 +19,15 @@ HEADER { } H1 { font-size: 5em; - margin: 0px; - padding: 0px; + margin: 0; + padding: 0; } H1 A { color: black; } H3 { text-align: center; - margin: 0px; + margin: 0; } THEAD { font-weight: bold; @@ -43,15 +43,14 @@ CODE { #subtitle { width: 256px; font-size: 0.75em; - margin: auto; - margin-top: -16px; + margin: -16px auto auto; text-align: center; border: 1px solid black; border-top: none; background: #DDD; } -TABLE.zebra {border-spacing: 0px; border: 1px solid #B89F7C; } +TABLE.zebra {border-spacing: 0; border: 1px solid #B89F7C; } TABLE.zebra TD, TABLE.zebra TH {vertical-align: middle; padding: 4px;} TABLE.zebra THEAD TD, TABLE.zebra THEAD TH {border-bottom: 2px solid #B89F7C;} TABLE.zebra TFOOT TD, TABLE.zebra TFOOT TH {border-top: 2px solid #B89F7C;} @@ -110,11 +109,11 @@ NAV TD { } NAV INPUT { width: 100%; - padding: 0px; + padding: 0; } NAV SELECT { width: 100%; - padding: 0px; + padding: 0; } TABLE.tag_list { diff --git a/themes/warm/themelet.class.php b/themes/warm/themelet.class.php index 77c927c3..59021a28 100644 --- a/themes/warm/themelet.class.php +++ b/themes/warm/themelet.class.php @@ -1,3 +1,4 @@ -add_block(new Block("Upload", $this->build_upload_block(), "head", 20)); - } +class CustomUploadTheme extends UploadTheme +{ + public function display_block(Page $page) + { + $page->add_block(new Block("Upload", $this->build_upload_block(), "head", 20)); + } - public function display_full(Page $page) { - $page->add_block(new Block("Upload", "Disk nearly full, uploads disabled", "head", 20)); - } + public function display_full(Page $page) + { + $page->add_block(new Block("Upload", "Disk nearly full, uploads disabled", "head", 20)); + } } - diff --git a/themes/warm/user.theme.php b/themes/warm/user.theme.php index 96db52b8..4b5343b2 100644 --- a/themes/warm/user.theme.php +++ b/themes/warm/user.theme.php @@ -1,18 +1,21 @@ -name); - $html = " | "; - foreach($parts as $part) { - $html .= "{$part["name"]} | "; - } - $page->add_block(new Block("Logged in as $h_name", $html, "head", 90)); - } +class CustomUserPageTheme extends UserPageTheme +{ + public function display_user_block(Page $page, User $user, $parts) + { + $h_name = html_escape($user->name); + $html = " | "; + foreach ($parts as $part) { + $html .= "{$part["name"]} | "; + } + $page->add_block(new Block("Logged in as $h_name", $html, "head", 90)); + } - public function display_login_block(Page $page) { - global $config; - $html = " + public function display_login_block(Page $page) + { + global $config; + $html = "
    @@ -21,10 +24,9 @@ class CustomUserPageTheme extends UserPageTheme {
    Name
    "; - if($config->get_bool("login_signup_enabled")) { - $html .= "Create Account"; - } - $page->add_block(new Block("Login", $html, "head", 90)); - } + if ($config->get_bool("login_signup_enabled")) { + $html .= "Create Account"; + } + $page->add_block(new Block("Login", $html, "head", 90)); + } } -