diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..19be60f4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +vendor +.git +*.phar +data +images +thumbs +composer.lock +*.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/.gitignore b/.gitignore index 4002f868..e2053c51 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,9 @@ backup data images thumbs -!lib/images *.phar *.sqlite -/lib/vendor/ +.php_cs.cache #Composer composer.phar 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/.scrutinizer.yml b/.scrutinizer.yml index 3dba09a6..f672bc78 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -3,7 +3,7 @@ imports: - php filter: - excluded_paths: [lib/*,ext/*/lib/*,ext/tagger/script.js,ext/chatbox/*] + excluded_paths: [ext/*/lib/*,ext/tagger/script.js,ext/chatbox/*] tools: external_code_coverage: true diff --git a/.travis.yml b/.travis.yml index 0743ff0d..2b479dfd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,10 @@ language: php php: - - 5.6 - - 7.0 - - 7.1 + - 7.3 + +services: + - mysql + - postgresql sudo: false @@ -11,8 +13,6 @@ env: - DB=mysql - DB=pgsql - DB=sqlite - allow_failures: - - DB=sqlite cache: directories: @@ -35,11 +35,13 @@ install: 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 ; + echo ' data/config/auto_install.conf.php ; + fi + - if [[ "$DB" == "sqlite" ]]; then + 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 + - php index.php script: - vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..b5bc7242 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM debian:testing-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 /app/ +WORKDIR /app +RUN composer install + +COPY . /app/ +RUN mkdir -p data/config && \ + echo " data/config/auto_install.conf.php && \ + php index.php && \ + ./vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-text && \ + rm -rf data +CMD "/app/tests/docker-init.sh" diff --git a/README.markdown b/README.markdown index 60748185..853a37ee 100644 --- a/README.markdown +++ b/README.markdown @@ -29,7 +29,7 @@ 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) +- [Stable PHP](https://en.wikipedia.org/wiki/PHP#Release_history) (7.1+ as of writing) - GD or ImageMagick # Installation @@ -50,40 +50,35 @@ check out one of the versioned branches. 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 +86,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 +95,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 diff --git a/composer.json b/composer.json index 1e112111..746582e0 100644 --- a/composer.json +++ b/composer.json @@ -23,12 +23,15 @@ ], "require" : { - "php" : ">=5.6", + "php" : ">=7.1", + "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", "enshrined/svg-sanitize" : "0.8.2", "bower-asset/jquery" : "1.12.3", @@ -36,34 +39,24 @@ "bower-asset/tablesorter" : "dev-master", "bower-asset/mediaelement" : "2.21.1", "bower-asset/js-cookie" : "2.1.1" - }, + }, "require-dev" : { - "phpunit/phpunit" : "5.*" + "phpunit/phpunit" : "7.*" }, - "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 c7eb5942..11545556 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,10 @@ { "_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": "7f6f5b16df991e848ec468b49c856dea", + "content-hash": "b1d9675735299d17a988bc9dfc8e5973", "packages": [ { "name": "bower-asset/jquery", @@ -17,8 +17,7 @@ "dist": { "type": "zip", "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/3a43d7e563314bf32970b773dd31ecf2b90813dd", - "reference": "3a43d7e563314bf32970b773dd31ecf2b90813dd", - "shasum": null + "reference": "3a43d7e563314bf32970b773dd31ecf2b90813dd" }, "type": "bower-asset", "license": [ @@ -36,8 +35,7 @@ "dist": { "type": "zip", "url": "https://api.github.com/repos/rmm5t/jquery-timeago/zipball/67c11951ae9b6020341c1056a42b5406162db40c", - "reference": "67c11951ae9b6020341c1056a42b5406162db40c", - "shasum": null + "reference": "67c11951ae9b6020341c1056a42b5406162db40c" }, "require": { "bower-asset/jquery": ">=1.4" @@ -58,8 +56,7 @@ "dist": { "type": "zip", "url": "https://api.github.com/repos/js-cookie/js-cookie/zipball/5c830fb71a2bd3acce9cb733d692e13316991891", - "reference": "5c830fb71a2bd3acce9cb733d692e13316991891", - "shasum": null + "reference": "5c830fb71a2bd3acce9cb733d692e13316991891" }, "type": "bower-asset", "license": [ @@ -77,8 +74,7 @@ "dist": { "type": "zip", "url": "https://api.github.com/repos/mediaelement/mediaelement/zipball/6e80b260172f4ddc3b0bbee046775d2ba4c6f9b7", - "reference": "6e80b260172f4ddc3b0bbee046775d2ba4c6f9b7", - "shasum": null + "reference": "6e80b260172f4ddc3b0bbee046775d2ba4c6f9b7" }, "type": "bower-asset", "license": [ @@ -96,8 +92,7 @@ "dist": { "type": "zip", "url": "https://api.github.com/repos/christianbach/tablesorter/zipball/07e0918254df3c2057d6d8e4653a0769f1881412", - "reference": "07e0918254df3c2057d6d8e4653a0769f1881412", - "shasum": null + "reference": "07e0918254df3c2057d6d8e4653a0769f1881412" }, "type": "bower-asset", "license": [ @@ -107,21 +102,21 @@ }, { "name": "dapphp/securimage", - "version": "3.6.6", + "version": "3.6.7", "source": { "type": "git", "url": "https://github.com/dapphp/securimage.git", - "reference": "6eea2798f56540fa88356c98f282d6391a72be15" + "reference": "1ecb884797c66e01a875c058def46c85aecea45b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dapphp/securimage/zipball/6eea2798f56540fa88356c98f282d6391a72be15", - "reference": "6eea2798f56540fa88356c98f282d6391a72be15", + "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,12 @@ "description": "PHP CAPTCHA Library", "homepage": "https://www.phpcaptcha.org", "keywords": [ + "Forms", + "anti-spam", "captcha", "security" ], - "time": "2017-11-21T02:29:19+00:00" + "time": "2018-03-09T06:07:41+00:00" }, { "name": "enshrined/svg-sanitize", @@ -248,24 +245,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": { @@ -277,15 +276,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", @@ -296,37 +295,83 @@ "reference": "fd4ff50eb577457c1b7b887401663e91e77625ae" }, "type": "library" + }, + { + "name": "shish/eventtracer-php", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/shish/eventtracer-php.git", + "reference": "f49a597b9b048421fcd5077be68adbdfbdd133fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shish/eventtracer-php/zipball/f49a597b9b048421fcd5077be68adbdfbdd133fe", + "reference": "f49a597b9b048421fcd5077be68adbdfbdd133fe", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-posix": "*", + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7" + }, + "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": "2019-07-07T12:22:39+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": "7c71fc2932158d00f24f10635bf3b3b8b6ee5b68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/7c71fc2932158d00f24f10635bf3b3b8b6ee5b68", + "reference": "7c71fc2932158d00f24f10635bf3b3b8b6ee5b68", "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.2.x-dev" } }, "autoload": { @@ -346,34 +391,37 @@ } ], "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-07-02T13:37:32+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.7.0", + "version": "1.x-dev", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + "reference": "9012edbd1604a93cee7e7422d07a2c5776c56e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", - "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/9012edbd1604a93cee7e7422d07a2c5776c56e0c", + "reference": "9012edbd1604a93cee7e7422d07a2c5776c56e0c", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.1" + }, + "replace": { + "myclabs/deep-copy": "self.version" }, "require-dev": { "doctrine/collections": "^1.0", "doctrine/common": "^2.6", - "phpunit/phpunit": "^4.1" + "phpunit/phpunit": "^7.1" }, "type": "library", "autoload": { @@ -396,27 +444,27 @@ "object", "object graph" ], - "time": "2017-10-19T19:58:43+00:00" + "time": "2019-08-26T15:40:39+00:00" }, { - "name": "phpdocumentor/reflection-common", - "version": "1.0.1", + "name": "phar-io/manifest", + "version": "1.0.3", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + "url": "https://github.com/phar-io/manifest.git", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" }, "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/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", "shasum": "" }, "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^2.0", + "php": "^5.6 || ^7.0" }, "type": "library", "extra": { @@ -424,11 +472,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": "2018-07-08T19:23:20+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": "63a995caa1ca9e5590304cd845c15ad6d482a62a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", + "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/", @@ -450,33 +598,39 @@ "reflection", "static analysis" ], - "time": "2017-09-11T18:02:19+00:00" + "time": "2018-08-07T13:53:10+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.3.2", + "version": "4.3.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2" + "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bf329f6c1aadea3299f08ee804682b7c45b326a2", - "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e", + "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", - "phpdocumentor/reflection-common": "^1.0.0", - "phpdocumentor/type-resolver": "^0.4.0", + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", + "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", "webmozart/assert": "^1.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" + "doctrine/instantiator": "^1.0.5", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, "autoload": { "psr-4": { "phpDocumentor\\Reflection\\": [ @@ -495,41 +649,40 @@ } ], "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-11-10T14:09:06+00:00" + "time": "2019-09-12T14:27:41+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.4.0", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" + "php": "^7.1", + "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" + "ext-tokenizer": "^7.1", + "mockery/mockery": "~1", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -542,7 +695,8 @@ "email": "me@mikevanriel.com" } ], - "time": "2017-07-14T14:27:02+00:00" + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2019-08-22T18:11:29+00:00" }, { "name": "phpspec/prophecy", @@ -550,34 +704,34 @@ "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401" + "reference": "bcdce71d674f4f7b86df0ff3a418d43b7fe141f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401", - "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bcdce71d674f4f7b86df0ff3a418d43b7fe141f7", + "reference": "bcdce71d674f4f7b86df0ff3a418d43b7fe141f7", "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/comparator": "^1.1|^2.0|^3.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + "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.8.x-dev" } }, "autoload": { - "psr-0": { - "Prophecy\\": "src/" + "psr-4": { + "Prophecy\\": "src/Prophecy" } }, "notification-url": "https://packagist.org/downloads/", @@ -605,44 +759,44 @@ "spy", "stub" ], - "time": "2018-02-19T10:16:54+00:00" + "time": "2019-09-09T15:23:21+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "4.0.x-dev", + "version": "6.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" }, "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/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", "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.1", + "phpunit/php-file-iterator": "^2.0", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.1 || ^4.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" }, "require-dev": { - "ext-xdebug": "^2.1.4", - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^7.0" }, "suggest": { - "ext-xdebug": "^2.5.1" + "ext-xdebug": "^2.6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-master": "6.1-dev" } }, "autoload": { @@ -657,7 +811,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -668,29 +822,32 @@ "testing", "xunit" ], - "time": "2017-04-02T07:44:40+00:00" + "time": "2018-10-31T16:06:48+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "1.4.x-dev", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + "reference": "7f0f29702170e2786b2df813af970135765de6fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/7f0f29702170e2786b2df813af970135765de6fc", + "reference": "7f0f29702170e2786b2df813af970135765de6fc", "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": { @@ -705,7 +862,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -715,7 +872,7 @@ "filesystem", "iterator" ], - "time": "2017-11-27T13:52:08+00:00" + "time": "2019-07-02T07:44:20+00:00" }, { "name": "phpunit/php-text-template", @@ -760,28 +917,28 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.x-dev", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "9513098641797ce5f459dbc1de5a54c29b0ec1fb" + "reference": "37d2894f3650acccb6e57207e63eb9699c1a82a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/9513098641797ce5f459dbc1de5a54c29b0ec1fb", - "reference": "9513098641797ce5f459dbc1de5a54c29b0ec1fb", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/37d2894f3650acccb6e57207e63eb9699c1a82a6", + "reference": "37d2894f3650acccb6e57207e63eb9699c1a82a6", "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": { @@ -796,7 +953,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -805,33 +962,33 @@ "keywords": [ "timer" ], - "time": "2018-01-06T05:27:16+00:00" + "time": "2019-07-02T07:42:03+00:00" }, { "name": "phpunit/php-token-stream", - "version": "1.4.x-dev", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "58bd196ce8bc49389307b3787934a5117db80fea" + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/58bd196ce8bc49389307b3787934a5117db80fea", - "reference": "58bd196ce8bc49389307b3787934a5117db80fea", + "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": { @@ -854,55 +1011,57 @@ "keywords": [ "tokenizer" ], - "time": "2017-12-04T15:11:28+00:00" + "time": "2019-09-17T06:23:10+00:00" }, { "name": "phpunit/phpunit", - "version": "5.7.x-dev", + "version": "7.5.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c" + "reference": "316afa6888d2562e04aeb67ea7f2017a0eb41661" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", - "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/316afa6888d2562e04aeb67ea7f2017a0eb41661", + "reference": "316afa6888d2562e04aeb67ea7f2017a0eb41661", "shasum": "" }, "require": { + "doctrine/instantiator": "^1.1", "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.6|^2.0.1", - "symfony/yaml": "~2.1|~3.0|~4.0" + "myclabs/deep-copy": "^1.7", + "phar-io/manifest": "^1.0.2", + "phar-io/version": "^2.0", + "php": "^7.1", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^6.0.7", + "phpunit/php-file-iterator": "^2.0.1", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1", + "sebastian/comparator": "^3.0", + "sebastian/diff": "^3.0", + "sebastian/environment": "^4.0", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0", + "sebastian/version": "^2.0.1" }, "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2" + "phpunit/phpunit-mock-objects": "*" }, "require-dev": { "ext-pdo": "*" }, "suggest": { + "ext-soap": "*", "ext-xdebug": "*", - "phpunit/php-invoker": "~1.1" + "phpunit/php-invoker": "^2.0" }, "bin": [ "phpunit" @@ -910,7 +1069,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.7.x-dev" + "dev-master": "7.5-dev" } }, "autoload": { @@ -936,66 +1095,7 @@ "testing", "xunit" ], - "time": "2018-02-01T05:50:59+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": "2019-09-14T09:08:39+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -1003,12 +1103,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "3488be0a7b346cd6e5361510ed07e88f9bea2e88" + "reference": "5e860800beea5ea4c8590df866338c09c20d3a48" }, "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/5e860800beea5ea4c8590df866338c09c20d3a48", + "reference": "5e860800beea5ea4c8590df866338c09c20d3a48", "shasum": "" }, "require": { @@ -1040,34 +1140,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": "2019-07-02T07:44:03+00:00" }, { "name": "sebastian/comparator", - "version": "1.2.x-dev", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "18a5d97c25f408f48acaf6d1b9f4079314c5996a" + "reference": "9a1267ac19ecd74163989bcb3e01c5c9587f9e3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/18a5d97c25f408f48acaf6d1b9f4079314c5996a", - "reference": "18a5d97c25f408f48acaf6d1b9f4079314c5996a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/9a1267ac19ecd74163989bcb3e01c5c9587f9e3b", + "reference": "9a1267ac19ecd74163989bcb3e01c5c9587f9e3b", "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": { @@ -1098,38 +1198,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": "2019-07-02T07:45:15+00:00" }, { "name": "sebastian/diff", - "version": "1.4.x-dev", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" + "reference": "d7e7810940c78f3343420f76adf92dc437b7a557" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/d7e7810940c78f3343420f76adf92dc437b7a557", + "reference": "d7e7810940c78f3343420f76adf92dc437b7a557", "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": { @@ -1154,34 +1255,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-07-02T07:43:30+00:00" }, { "name": "sebastian/environment", - "version": "2.0.x-dev", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + "reference": "520187a48d1fd3714dd4ebfd8eb914a4d69d26a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/520187a48d1fd3714dd4ebfd8eb914a4d69d26a7", + "reference": "520187a48d1fd3714dd4ebfd8eb914a4d69d26a7", "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": { @@ -1206,34 +1313,34 @@ "environment", "hhvm" ], - "time": "2016-11-26T07:53:53+00:00" + "time": "2019-09-07T09:51:27+00:00" }, { "name": "sebastian/exporter", - "version": "2.0.x-dev", + "version": "dev-master", "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": { @@ -1246,6 +1353,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -1254,17 +1365,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", @@ -1273,27 +1380,27 @@ "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": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "cea85a84b00f2795341ebbbca4fa396347f2494e" + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" }, "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/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.2|~5.0" + "phpunit/phpunit": "^6.0" }, "suggest": { "ext-uopz": "*" @@ -1301,7 +1408,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1324,33 +1431,34 @@ "keywords": [ "global state" ], - "time": "2017-02-23T14:11:06+00:00" + "time": "2017-04-27T15:39:26+00:00" }, { "name": "sebastian/object-enumerator", - "version": "2.0.x-dev", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "c956fe7a68318639f694fc6bba0c89b7cdf1b08c" + "reference": "63e5a3e0881ebf28c9fbb2a2e12b77d373850c12" }, "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/63e5a3e0881ebf28c9fbb2a2e12b77d373850c12", + "reference": "63e5a3e0881ebf28c9fbb2a2e12b77d373850c12", "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": { @@ -1370,32 +1478,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": "2019-07-02T07:43:46+00:00" }, { - "name": "sebastian/recursion-context", - "version": "2.0.x-dev", + "name": "sebastian/object-reflector", + "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "7e4d7c56f6e65d215f71ad913a5256e5439aca1c" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "3053ae3e6286fdf98769f18ec10894dbc6260a34" }, "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/3053ae3e6286fdf98769f18ec10894dbc6260a34", + "reference": "3053ae3e6286fdf98769f18ec10894dbc6260a34", "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": "2019-07-02T07:44:36+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "a58220ae18565f6004bbe15321efc4470bfe02fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/a58220ae18565f6004bbe15321efc4470bfe02fd", + "reference": "a58220ae18565f6004bbe15321efc4470bfe02fd", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -1423,7 +1576,7 @@ ], "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": "2019-07-02T07:43:54+00:00" }, { "name": "sebastian/resource-operations", @@ -1431,21 +1584,21 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "fadc83f7c41fb2924e542635fea47ae546816ece" + "reference": "d67fc89d3107c396d161411b620619f3e7a7c270" }, "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/d67fc89d3107c396d161411b620619f3e7a7c270", + "reference": "d67fc89d3107c396d161411b620619f3e7a7c270", "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": { @@ -1465,7 +1618,7 @@ ], "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": "2019-07-02T07:42:50+00:00" }, { "name": "sebastian/version", @@ -1511,43 +1664,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": "6af42631dcf89e9c616242c900d6c52bd53bd1bb" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "550ebaac289296ce228a706d0867afc34687e3f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/6af42631dcf89e9c616242c900d6c52bd53bd1bb", - "reference": "6af42631dcf89e9c616242c900d6c52bd53bd1bb", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", + "reference": "550ebaac289296ce228a706d0867afc34687e3f4", "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.12-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\Yaml\\": "" + "Symfony\\Polyfill\\Ctype\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "bootstrap.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1556,38 +1703,84 @@ ], "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": "2018-02-16T09:50:28+00:00" + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2019-08-06T08:03:45+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": "0df1908962e7a3071564e857d86874dad1ef204a" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^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" + } + ], + "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.5.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4", + "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0", + "symfony/polyfill-ctype": "^1.8" }, "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" + "phpunit/phpunit": "^4.8.36 || ^7.5.13" }, "type": "library", "extra": { @@ -1616,18 +1809,21 @@ "check", "validate" ], - "time": "2018-01-29T19:49:41+00:00" + "time": "2019-08-24T08:43:50+00:00" } ], "aliases": [], "minimum-stability": "dev", "stability-flags": { + "shish/eventtracer-php": 20, "bower-asset/tablesorter": 20 }, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=5.6" + "php": ">=7.1", + "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 @@ -begin("Bootstrap"); +$_tracer->begin("Opening core files"); +$_shm_files = array_merge( + zglob("core/*.php"), + zglob("core/{".ENABLED_MODS."}/*.php"), + zglob("ext/*/info.php") +); +foreach ($_shm_files as $_shm_filename) { + if (basename($_shm_filename)[0] != "_") { + require_once $_shm_filename; + } +} +unset($_shm_files); +unset($_shm_filename); +$_tracer->end(); + +// connect to the database +$_tracer->begin("Connecting to DB"); +$database = new Database(); +$config = new DatabaseConfig($database); +$_tracer->end(); + +$_tracer->begin("Loading extension info"); +ExtensionInfo::load_all_extension_info(); +Extension::determine_enabled_extensions(); +$_tracer->end(); + +$_tracer->begin("Opening enabled extension files"); +$_shm_files = zglob("ext/{".Extension::get_enabled_extensions_as_string()."}/main.php"); +foreach ($_shm_files as $_shm_filename) { + if (basename($_shm_filename)[0] != "_") { + require_once $_shm_filename; + } +} +unset($_shm_files); +unset($_shm_filename); +$_tracer->end(); + +// load the theme parts +$_tracer->begin("Loading themelets"); +foreach (_get_themelet_files(get_theme()) as $themelet) { + require_once $themelet; +} +unset($themelet); +$page = class_exists("CustomPage") ? new CustomPage() : new Page(); +$_tracer->end(); + +// hook up event handlers +$_tracer->begin("Loading event listeners"); +_load_event_listeners(); +$_tracer->end(); + +send_event(new InitExtEvent()); +$_tracer->end(); diff --git a/core/_install.php b/core/_install.php new file mode 100644 index 00000000..1b78499e --- /dev/null +++ b/core/_install.php @@ -0,0 +1,459 @@ + + + + + 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";
+    } elseif (@$_POST["database_type"] == DatabaseDriver::SQLITE) {
+        $id = bin2hex(random_bytes(5));
+        define('DATABASE_DSN', "sqlite:data/shimmie.{$id}.sqlite");
+    } elseif (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("CACHE_DSN", null);
+    define("DATABASE_KA", true);
+    install_process();
+} // }}}
+
+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) : ""; + + 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 php-pgsql, php-mysql, or php-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)", []); + + $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)", []); + + $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)", []); + $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) { + 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)", ["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->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("data")) { + @mkdir("data"); + } + if (!is_writable("data")) { + @chmod("data", 0755); + } + + // Clear file status cache before checking again. + clearstatcache(); + + if (!file_exists("data") || !is_writable("data")) { + print " +
+

Shimmie Installer

+

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.

+

+
+
+ "; + 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"; +} // }}} + +function handle_db_errors(bool $isPDO, string $errorMessage1, string $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/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..b3a2baeb --- /dev/null +++ b/core/basethemelet.php @@ -0,0 +1,139 @@ +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")); + } + + private function gen_page_link(string $base_url, ?string $query, string $page, string $name): string + { + $link = make_link($base_url.'/'.$page, $query); + return ''.$name.''; + } + + private function gen_page_link_block(string $base_url, ?string $query, string $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, $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..70c8c690 --- /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..4d5fc05f --- /dev/null +++ b/core/cacheengine.php @@ -0,0 +1,229 @@ +memcache = new Memcache; + @$this->memcache->pconnect($hp[0], $hp[1]); + } + + public function get(string $key) + { + return $this->memcache->get($key); + } + + public function set(string $key, $val, int $time=0) + { + $this->memcache->set($key, $val, false, $time); + } + + public function delete(string $key) + { + $this->memcache->delete($key); + } +} + +class MemcachedCache implements CacheEngine +{ + /** @var ?Memcached */ + public $memcache=null; + + public function __construct(string $args) + { + $hp = explode(":", $args); + $this->memcache = new Memcached; + #$this->memcache->setOption(Memcached::OPT_COMPRESSION, False); + #$this->memcache->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP); + #$this->memcache->setOption(Memcached::OPT_PREFIX_KEY, phpversion()); + $this->memcache->addServer($hp[0], $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], $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->delete($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] == "memcache") { + $c = new MemcacheCache($matches[2]); + } elseif ($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..291192f4 --- /dev/null +++ b/core/captcha.php @@ -0,0 +1,58 @@ +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\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..06047a77 --- /dev/null +++ b/core/config.php @@ -0,0 +1,397 @@ +values[$name] = parse_shorthand_int($value); + $this->save($name); + } + + public function set_float(string $name, ?string $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, $value): void + { + $this->values[$name] = bool_escape($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 + { + return $this->get($name, $default); + } + + 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 HardcodeConfig + * + * For testing, mostly. + */ +class HardcodeConfig extends BaseConfig +{ + public function __construct(array $dict) + { + $this->values = $dict; + } + + public function save(string $name=null): void + { + // 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 +{ + public function __construct(string $filename) + { + if (file_exists($filename)) { + $config = []; + 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"); + } + } + + public function save(string $name=null): void + { + // 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; + + 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 + ) { + $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 = $this->database->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"]; + } + $this->database->cache->set($cache_name, $this->values); + } + } + + public function save(string $name=null): void + { + 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 + $this->database->cache->set("config", $this->values); + } +} + +/** + * Class MockConfig + */ +class MockConfig extends HardcodeConfig +{ + public function __construct(array $config=[]) + { + $config["db_version"] = "999"; + $config["anon_id"] = "0"; + parent::__construct($config); + } +} 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..5a5a40cb --- /dev/null +++ b/core/database.php @@ -0,0 +1,439 @@ +cache = new Cache(CACHE_DSN); + } + + private function connect_db(): void + { + # 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 = []; + $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() == DatabaseDriver::SQLITE) { + $ka = false; + } + + $db_params = [ + 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(): void + { + if (preg_match("/^([^:]*)/", DATABASE_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)) { + 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."); + } + } else { + throw new SCoreException("

Database Transaction Error: Unable to call commit() as there is no connection currently open."); + } + } + + public function rollback(): bool + { + 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."); + } + } else { + throw new SCoreException("

Database Transaction Error: Unable to call rollback() as there is no connection currently open."); + } + } + + public function escape(string $input): string + { + if (is_null($this->db)) { + $this->connect_db(); + } + return $this->db->Quote($input); + } + + 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 execute(string $query, array $args=[]): PDOStatement + { + try { + if (is_null($this->db)) { + $this->connect_db(); + } + $stmt = $this->db->prepare( + "-- " . str_replace("%2F", "/", urlencode(@$_GET['q'])). "\n" . + $query + ); + // $stmt = $this->db->prepare($query); + if (!array_key_exists(0, $args)) { + foreach ($args as $name=>$value) { + if (is_int($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. + */ + 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. + */ + 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[0]; + } + + /** + * Get the ID of the last inserted row. + */ + public function get_last_insert_id(string $seq): int + { + if ($this->engine->name == DatabaseDriver::PGSQL) { + return $this->db->lastInsertId($seq); + } else { + return $this->db->lastInsertId(); + } + } + + /** + * 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}"); + } + } +} + +class MockDatabase extends Database +{ + /** @var int */ + private $query_id = 0; + /** @var array */ + private $responses = []; + /** @var ?NoCache */ + public $cache = null; + + public function __construct(array $responses = []) + { + $this->cache = new NoCache(); + $this->responses = $responses; + } + + public function execute(string $query, array $params=[]): PDOStatement + { + 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++]; + } + + public function _execute(string $query, array $params=[]) + { + 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++]; + } + + public function get_all(string $query, array $args=[]): array + { + return $this->_execute($query, $args); + } + public function get_row(string $query, array $args=[]): ?array + { + return $this->_execute($query, $args); + } + public function get_col(string $query, array $args=[]): array + { + return $this->_execute($query, $args); + } + public function get_pairs(string $query, array $args=[]): array + { + return $this->_execute($query, $args); + } + public function get_one(string $query, array $args=[]) + { + return $this->_execute($query, $args); + } + + public function get_last_insert_id(string $seq): int + { + return $this->query_id; + } + + public function scoreql_to_sql(string $sql): string + { + return $sql; + } + + public function create_table(string $name, string $def): void + { + } + + public function connect_engine(): void + { + } +} diff --git a/core/dbengine.php b/core/dbengine.php new file mode 100644 index 00000000..03b4b851 --- /dev/null +++ b/core/dbengine.php @@ -0,0 +1,216 @@ +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); + $data = str_replace(SCORE::DATETIME, "DATETIME", $data); + $data = str_replace(SCORE::NOW, "\"1970-01-01\"", $data); + $data = str_replace(SCORE::STRNORM, "", $data); + $data = str_replace(SCORE::ILIKE, "LIKE", $data); + return $data; + } + + public function create_table_sql(string $name, string $data): string + { + $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 = 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]';"); + } + $db->exec("SET statement_timeout TO ".DATABASE_TIMEOUT.";"); + } + + public function scoreql_to_sql(string $data): string + { + $data = str_replace(SCORE::AIPK, "SERIAL PRIMARY KEY", $data); + $data = str_replace(SCORE::INET, "INET", $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, "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; + } + + public function create_table_sql(string $name, string $data): string + { + $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 = 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); + $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; + } + + 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"; + } +} 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/email.php b/core/email.php new file mode 100644 index 00000000..c7982212 --- /dev/null +++ b/core/email.php @@ -0,0 +1,132 @@ +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(): bool + { + $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..a0e25176 --- /dev/null +++ b/core/event.php @@ -0,0 +1,318 @@ + 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) + { + 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); + + // voodoo so that an arg can contain a slash; is + // this still needed? + if (strpos($path, "^") !== false) { + $unescaped = []; + 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 + */ + 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 { + return null; + } + } + + /** + * Returns the number of arguments the page request has. + */ + public function count_args(): int + { + return int_escape($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($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('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 = []; + + /** + * #param string[] $args + */ + public function __construct(array $args) + { + 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; + } + } + + 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; + + 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; + + public function __construct(string $section, int $priority, string $message, array $args) + { + $this->section = $section; + $this->priority = $priority; + $this->message = $message; + $this->args = $args; + $this->time = time(); + } +} 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 = $error; + } +} 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..428ef2f8 --- /dev/null +++ b/core/extension.php @@ -0,0 +1,450 @@ +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 +{ + public $key; + + /** @var Themelet this theme's Themelet object */ + public $theme; + + 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 Exception("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); + } +} + +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 = []; + + private $supported = null; + 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() + { + if (empty($this->key)) { + throw new Exception("key field is required"); + } + if (empty($this->name)) { + throw new Exception("name field is required for extension $this->key"); + } + if (!empty($this->visibility)&&!in_array($this->visibility, self::VALID_VISIBILITY)) { + throw new Exception("Invalid visibility for extension $this->key"); + } + if (!is_array($this->db_support)) { + throw new Exception("db_support has to be an array for extension $this->key"); + } + if (!is_array($this->authors)) { + throw new Exception("authors has to be an array for extension $this->key"); + } + if (!is_array($this->dependencies)) { + throw new Exception("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 (get_declared_classes() as $class) { + $rclass = new ReflectionClass($class); + if ($rclass->isAbstract()) { + // don't do anything + } elseif (is_subclass_of($class, "ExtensionInfo")) { + $extension_info = new $class(); + if (array_key_exists($extension_info->key, self::$all_info_by_key)) { + throw new Exception("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 +{ + 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(Image::IMAGE_DIR, $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(Image::IMAGE_DIR, $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; + $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) { + 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)) { + $this->theme->display_image($page, $event->image); + } + } + + /* + public function onSetupBuilding(SetupBuildingEvent $event) { + $sb = $this->setup(); + if($sb) $event->panel->add_block($sb); + } + + protected function setup() {} + */ + + abstract protected function supported_ext(string $ext): bool; + abstract protected function check_contents(string $tmpname): bool; + abstract protected function create_image_from_data(string $filename, array $metadata); + abstract protected function create_thumb(string $hash, string $type): bool; +} 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..3064fa03 --- /dev/null +++ b/core/imageboard/event.php @@ -0,0 +1,151 @@ +image = $image; + } +} + +class ImageAdditionException extends SCoreException +{ + public $error; + + public function __construct(string $error) + { + $this->error = $error; + } +} + +/** + * 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) + { + $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) + { + $this->id = $id; + $this->image = $image; + } +} + +class ImageReplaceException extends SCoreException +{ + /** @var string */ + public $error; + + public function __construct(string $error) + { + $this->error = $error; + } +} + +/** + * 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) + { + $this->hash = $hash; + $this->type = $type; + $this->force = $force; + $this->generated = false; + } +} + + +/* + * ParseLinkTemplateEvent: + * $link -- the formatted link + * $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 $original; + /** @var Image */ + public $image; + + public function __construct(string $link, Image $image) + { + $this->link = $link; + $this->original = $link; + $this->image = $image; + } + + public function replace(string $needle, string $replace): void + { + $this->link = str_replace($needle, $replace, $this->link); + } +} diff --git a/core/imageboard/image.php b/core/imageboard/image.php new file mode 100644 index 00000000..4ad9ae75 --- /dev/null +++ b/core/imageboard/image.php @@ -0,0 +1,1044 @@ + $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)); + + $this->id = int_escape($this->id); + $this->height = int_escape($this->height); + $this->width = int_escape($this->width); + } + } + + 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; + $row = $database->get_row("SELECT images.* FROM images WHERE hash=:hash", ["hash"=>$hash]); + return ($row ? new Image($row) : null); + } + + 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 ($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"); + } + } + + list($tag_conditions, $img_conditions) = self::terms_to_conditions($tags); + + $result = Image::get_accelerated_result($tag_conditions, $img_conditions, $start, $limit); + if (!$result) { + $querylet = Image::build_search_querylet($tag_conditions, $img_conditions); + $querylet->append(new Querylet(" ORDER BY ".(Image::$order_sql ?: "images.".$config->get_string("index_order")))); + if ($limit!=null) { + $querylet->append(new Querylet(" LIMIT :limit ", ["limit" => $limit])); + } + $querylet->append(new Querylet(" OFFSET :offset ", ["offset"=>$start])); + #var_dump($querylet->sql); var_dump($querylet->variables); + $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, 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); + } + } + + /* + * Accelerator stuff + */ + public static function get_acceleratable(array $tag_conditions): ?array + { + $ret = [ + "yays" => [], + "nays" => [], + ]; + $yays = 0; + $nays = 0; + foreach ($tag_conditions as $tq) { + if (strpos($tq->tag, "*") !== false) { + // can't deal with wildcards + return null; + } + if ($tq->positive) { + $yays++; + $ret["yays"][] = $tq->tag; + } else { + $nays++; + $ret["nays"][] = $tq->tag; + } + } + if ($yays > 1 || $nays > 0) { + return $ret; + } + return null; + } + + public static function get_accelerated_result(array $tag_conditions, array $img_conditions, int $offset, ?int $limit): ?PDOStatement + { + if (!SEARCH_ACCEL || !empty($img_conditions) || isset($_GET['DISABLE_ACCEL'])) { + return null; + } + + global $database; + + $req = Image::get_acceleratable($tag_conditions); + if (!$req) { + return null; + } + $req["offset"] = $offset; + $req["limit"] = $limit; + + $response = Image::query_accelerator($req); + if ($response) { + $list = implode(",", $response); + $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; + } + + public static function get_accelerated_count(array $tag_conditions, array $img_conditions): ?int + { + if (!SEARCH_ACCEL || !empty($img_conditions) || isset($_GET['DISABLE_ACCEL'])) { + return null; + } + + $req = Image::get_acceleratable($tag_conditions); + if (!$req) { + return null; + } + $req["count"] = true; + + return Image::query_accelerator($req); + } + + public static function query_accelerator($req) + { + global $_tracer; + $fp = @fsockopen("127.0.0.1", 21212); + if (!$fp) { + return null; + } + $req_str = json_encode($req); + $_tracer->begin("Accelerator Query", ["req"=>$req_str]); + fwrite($fp, $req_str); + $data = ""; + while (($buffer = fgets($fp, 4096)) !== false) { + $data .= $buffer; + } + $_tracer->end(); + if (!feof($fp)) { + die("Error: unexpected fgets() fail in query_accelerator($req_str)\n"); + } + fclose($fp); + return json_decode($data); + } + + /* + * Image-related utility functions + */ + + /** + * Count the number of image results for a given search + * + * #param string[] $tags + */ + public static function count_images(array $tags=[]): int + { + 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); + } + } elseif ($tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) { + $total = $database->get_one( + $database->scoreql_to_sql("SELECT count FROM tags WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag)"), + ["tag"=>$tags[0]] + ); + } else { + if (Extension::is_enabled(RatingsInfo::KEY)) { + $tags[] = "rating:*"; + } + list($tag_conditions, $img_conditions) = self::terms_to_conditions($tags); + $total = Image::get_accelerated_count($tag_conditions, $img_conditions); + if (is_null($total)) { + $querylet = Image::build_search_querylet($tag_conditions, $img_conditions); + $total = $database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables); + } + } + 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=[]): float + { + global $config; + return ceil(Image::count_images($tags) / $config->get_int('index_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 = new SearchTermParseEvent(null, $terms); + send_event($stpe); + 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 = new SearchTermParseEvent($term, $terms); + send_event($stpe); + 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; + list($tag_conditions, $img_conditions) = self::terms_to_conditions($tags); + $querylet = Image::build_search_querylet($tag_conditions, $img_conditions); + $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}", null, ["image_id" => $this->id]); + } + } + + /** + * 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 + { + return $this->parse_link_template('$id - $tags.$ext'); + } + + /** + * 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); + } + return $this->parse_link_template($image_link); + } elseif ($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. + */ + public function get_tooltip(): string + { + global $config; + $tt = $this->parse_link_template($config->get_string(ImageConfig::TIP), "no_escape"); + + // Removes the size tag if the file is an mp3 + if ($this->ext === 'mp3') { + $iitip = $tt; + $mp3tip = ["0x0"]; + $h_tip = str_replace($mp3tip, " ", $iitip); + + // Makes it work with a variation of the default tooltips (I.E $tags // $filesize // $size) + $justincase = [" //", "// ", " //", "// ", " "]; + 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. + */ + 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)", null, ["image_id" => $this->id]); + } + } + + /** + * 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", null, ["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(): 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 $database; + + $unfiltered_tags = array_unique($unfiltered_tags); + + $tags = []; + foreach ($unfiltered_tags as $tag) { + if (mb_strlen($tag, 'UTF-8') > 255) { + flash_message("Can't set a tag longer than 255 characters"); + continue; + } + if (startsWith($tag, "-")) { + flash_message("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( + $database->scoreql_to_sql(" + SELECT id + FROM tags + WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(: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 tag = :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( + $database->scoreql_to_sql(" + UPDATE tags + SET count = count + 1 + WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag) + "), + ["tag"=>$tag] + ); + } + + log_info("core_image", "Tags for Image #{$this->id} set to: ".Tag::implode($tags), null, ["image_id" => $this->id]); + $database->cache->delete("image-{$this->id}-tags"); + } + } + + /** + * Send list of metatags to be parsed. + * + * #param string[] $metatags + */ + public function parse_metatags(array $metatags, int $image_id): void + { + 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(): 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.')', null, ["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(): void + { + log_info("core_image", 'Removed Image File ('.$this->hash.')', null, ["image_id" => $this->id]); + @unlink($this->get_image_filename()); + @unlink($this->get_thumb_filename()); + } + + public function parse_link_template(string $tmpl, string $_escape="url_escape", int $n=0): string + { + 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(SetupConfig::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 $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($this->hash, $n+1); // hash doesn't change + $choice = $choices[$n]; + $tmpl = $pre.$choice.$post; + } + + return $tmpl; + } + + /** + * #param string[] $terms + */ + private static function build_search_querylet(array $tag_conditions, array $img_conditions): Querylet + { + global $database; + + $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 + "); + } + + // more than one positive tag, or more than zero negative tags + else { + $query = Image::build_accurate_search_querylet($tag_conditions); + } + + /* + * 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)); + } + + return $query; + } + + /** + * #param TagQuerylet[] $tag_conditions + */ + private static function build_accurate_search_querylet(array $tag_conditions): Querylet + { + global $database; + + $positive_tag_id_array = []; + $positive_wildcard_id_array = []; + $negative_tag_id_array = []; + + foreach ($tag_conditions as $tq) { + $sq = " + SELECT id + FROM tags + WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:tag) + "; + if ($database->get_driver_name() === DatabaseDriver::SQLITE) { + $sq .= "ESCAPE '\\'"; + } + $tag_ids = $database->get_col( + $database->scoreql_to_sql($sq), + ["tag" => Tag::sqlify($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); + } + } + + $sql = ""; + 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 "; + + $sql = " + 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); + $sql = " + 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"); + } + + return new Querylet($sql); + } +} diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php new file mode 100644 index 00000000..bead57bc --- /dev/null +++ b/core/imageboard/misc.php @@ -0,0 +1,219 @@ +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 string $base + * @return array + */ +function add_dir(string $base): array +{ + $results = []; + + foreach (list_files($base) as $full_path) { + $short_path = str_replace($base, "", $full_path); + $filename = basename($full_path); + + $tags = path_to_tags($short_path); + $result = "$short_path (".str_replace(" ", ", ", $tags).")... "; + try { + add_image($full_path, $filename, $tags); + $result .= "ok"; + } catch (UploadException $ex) { + $result .= "failed: ".$ex->getMessage(); + } + $results[] = $result; + } + + return $results; +} + +/** + * Sends a DataUploadEvent for a file. + * + * @param string $tmpname + * @param string $filename + * @param string $tags + * @throws UploadException + */ +function add_image(string $tmpname, string $filename, string $tags): 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; + $event = new DataUploadEvent($tmpname, $metadata); + send_event($event); +} + +/** + * Gets an the extension defined in MIME_TYPE_MAP for a file. + * + * @param String $file_path + * @return String The extension that was found. + * @throws UploadException if the mimetype could not be determined, or if an extension for hte mimetype could not be found. + */ +function get_extension_from_mime(String $file_path): String +{ + $mime = mime_content_type($file_path); + if (!empty($mime)) { + $ext = get_extension($mime); + if (!empty($ext)) { + return $ext; + } + throw new UploadException("Could not determine extension for mimetype ".$mime); + } + throw new UploadException("Could not determine file mime type: ".$file_path); +} + +/** + * 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) +{ + global $config; + + $inname = warehouse_path(Image::IMAGE_DIR, $hash); + $outname = warehouse_path(Image::THUMBNAIL_DIR, $hash); + $tsize = get_thumbnail_max_size_scaled(); + + 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) + )); +} + + +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); +} diff --git a/core/imageboard/search.php b/core/imageboard/search.php new file mode 100644 index 00000000..2960663f --- /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..7fa8b0b7 --- /dev/null +++ b/core/imageboard/tag.php @@ -0,0 +1,116 @@ + 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 = ["tagme"]; + } + + /* resolve aliases */ + $new = []; + $i = 0; + $tag_count = count($tag_array); + while ($i<$tag_count) { + $tag = $tag_array[$i]; + $negative = ''; + if (!empty($tag) && ($tag[0] == '-')) { + $negative = '-'; + $tag = substr($tag, 1); + } + + $newtags = $database->get_one( + $database->scoreql_to_sql(" + SELECT newtag + FROM aliases + WHERE SCORE_STRNORM(oldtag)=SCORE_STRNORM(: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 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; + } +} diff --git a/core/logging.php b/core/logging.php new file mode 100644 index 00000000..22a0431d --- /dev/null +++ b/core/logging.php @@ -0,0 +1,71 @@ += $threshold)) { + print date("c")." $section: $message\n"; + } + if (!is_null($flash)) { + flash_message($flash); + } +} + +// More shorthand ways of logging +function log_debug(string $section, string $message, ?string $flash=null, $args=[]) +{ + log_msg($section, SCORE_LOG_DEBUG, $message, $flash, $args); +} +function log_info(string $section, string $message, ?string $flash=null, $args=[]) +{ + log_msg($section, SCORE_LOG_INFO, $message, $flash, $args); +} +function log_warning(string $section, string $message, ?string $flash=null, $args=[]) +{ + log_msg($section, SCORE_LOG_WARNING, $message, $flash, $args); +} +function log_error(string $section, string $message, ?string $flash=null, $args=[]) +{ + log_msg($section, SCORE_LOG_ERROR, $message, $flash, $args); +} +function log_critical(string $section, string $message, ?string $flash=null, $args=[]) +{ + log_msg($section, SCORE_LOG_CRITICAL, $message, $flash, $args); +} + + +/** + * 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/page.php b/core/page.php new file mode 100644 index 00000000..4b8c8f39 --- /dev/null +++ b/core/page.php @@ -0,0 +1,614 @@ +mode = $mode; + } + + /** + * Set the page's MIME type. + */ + public function set_type(string $type): void + { + $this->type = $type; + } + + + //@} + // ============================================== + /** @name "data" mode */ + //@{ + + /** @var string; public only for unit test */ + public $data = ""; + + /** @var string; */ + public $file = null; + + /** @var string; public only for unit test */ + public $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): void + { + $this->file = $file; + } + + /** + * Set the recommended download filename. + */ + public function set_filename(string $filename, string $disposition = "attachment"): void + { + $this->filename = $filename; + $this->disposition = $disposition; + } + + + //@} + // ============================================== + /** @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). + */ + public function set_redirect(string $redirect): void + { + $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 = []; + + /** @var string[] */ + public $http_headers = []; + + /** @var string[][] */ + public $cookies = []; + + /** @var Block[] */ + public $blocks = []; + + /** + * 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; + } + + /** + * 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; + } + + /** + * Removes all currently set HTML headers (Be careful..). + */ + public function delete_all_html_headers(): void + { + $this->html_headers = []; + } + + /** + * Add a Block of data to the page. + */ + public function add_block(Block $block): void + { + $this->blocks[] = $block; + } + + + //@} + // ============================================== + + /** + * Display the page according to the mode and data given. + */ + public function display(): void + { + 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 PageMode::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"); + $pnbe = new PageNavBuildingEvent(); + send_event($pnbe); + + $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 = new PageSubNavBuildingEvent($active_link->name); + send_event($psnbe); + $sub_links = $psnbe->links; + } else { + // Otherwise we query for the sub-items under each of the tabs + foreach ($nav_links as $link) { + $psnbe = new PageSubNavBuildingEvent($link->name); + send_event($psnbe); + + // 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"); + + $this->add_auto_html_headers(); + $layout = new Layout(); + $layout->display_page($page, $nav_links, $sub_links); + 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: " . strlen($size)); + header('Accept-Ranges: bytes'); + + if (isset($_SERVER['HTTP_RANGE'])) { + $c_start = $start; + $c_end = $end; + 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 - substr($range, 1); + } else { + $range = explode('-', $range); + $c_start = $range[0]; + $c_end = (isset($range[1]) && is_numeric($range[1])) ? $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); + + + $fp = fopen($this->file, 'r'); + try { + fseek($fp, $start); + $buffer = 1024 * 64; + while (!feof($fp) && ($p = ftell($fp)) <= $end) { + if ($p + $buffer > $end) { + $buffer = $end - $p + 1; + } + set_time_limit(0); + echo fread($fp, $buffer); + 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); + } + break; + case PageMode::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(): 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/handle_static/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/handle_static/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); + } +} + +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) + { + $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/permissions.php b/core/permissions.php new file mode 100644 index 00000000..bd60b0c0 --- /dev/null +++ b/core/permissions.php @@ -0,0 +1,83 @@ +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)) { + // ignore + } elseif (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; +} + +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', + 'flv' => 'video/x-flv', + '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); +} + +/** + * 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")) { + 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 +{ + /* + Shish: I have a feeling that these three lines are important, possibly for searching for tags with slashes in them like fate/stay_night + green-ponies: indeed~ + + $input = str_replace('^', '^^', $input); + $input = str_replace('/', '^s', $input); + $input = str_replace('\\', '^b', $input); + + /* The function idn_to_ascii is used to support Unicode domains / URLs as well. + See here for more: http://php.net/manual/en/function.filter-var.php + However, it is only supported by PHP version 5.3 and up + + if (function_exists('idn_to_ascii')) { + return filter_var(idn_to_ascii($input), FILTER_SANITIZE_URL); + } else { + return filter_var($input, FILTER_SANITIZE_URL); + } + */ + if (is_null($input)) { + return ""; + } + $input = str_replace('^', '^^', $input); + $input = str_replace('/', '^s', $input); + $input = str_replace('\\', '^b', $input); + $input = rawurlencode($input); + return $input; +} + +/** + * Make sure some data is safe to be used in SQL context + */ +function sql_escape(string $input): string +{ + global $database; + return $database->escape($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($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; + } +} + + +/** + * 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($matches[2], $matches[3], $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($matches[2], $matches[3], $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('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 get_class_from_file(string $file): string +{ + $fp = fopen($file, 'r'); + $class = $buffer = ''; + $i = 0; + while (!$class) { + if (feof($fp)) { + break; + } + + $buffer .= fread($fp, 512); + $tokens = token_get_all($buffer); + + if (strpos($buffer, '{') === false) { + continue; + } + + for (;$iisAbstract()) { + // 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->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 (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); +} + + +/** @private */ +global $_shm_event_count; +$_shm_event_count = 0; + +/** + * Send an event to all registered Extensions. + */ +function send_event(Event $event): void +{ + global $tracer_enabled; + + global $_shm_event_listeners, $_shm_event_count, $_tracer; + 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 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(); + } +} diff --git a/core/sys_config.inc.php b/core/sys_config.php similarity index 73% rename from core/sys_config.inc.php rename to core/sys_config.php index 235c7ab0..8692f97e 100644 --- a/core/sys_config.inc.php +++ b/core/sys_config.php @@ -19,16 +19,18 @@ * */ -/** @private */ -function _d($name, $value) {if(!defined($name)) define($name, $value);} +function _d(string $name, $value): void +{ + if (!defined($name)) { + define($name, $value); + } +} _d("DATABASE_DSN", null); // string PDO database connection details _d("DATABASE_KA", true); // string Keep database connection alive +_d("DATABASE_TIMEOUT", 10000);// int Time to wait for each statement to complete _d("CACHE_DSN", null); // string cache connection details _d("DEBUG", false); // boolean print various debugging details -_d("DEBUG_SQL", false); // boolean dump SQL queries to data/sql.log -_d("DEBUG_CACHE", false); // boolean dump cache queries to data/cache.log _d("COVERAGE", false); // boolean activate xdebug coverage monitor -_d("CONTEXT", null); // string file to log performance data into _d("CACHE_HTTP", false); // boolean output explicit HTTP caching headers _d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes _d("SPEED_HAX", false); // boolean do some questionable things in the name of performance @@ -36,18 +38,17 @@ _d("COMPILE_ELS", false); // boolean pre-build the list of event listeners _d("NICE_URLS", false); // boolean force niceurl mode _d("SEARCH_ACCEL", false); // boolean use search accelerator _d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse -_d("VERSION", '2.6.2'); // string shimmie version +_d("VERSION", '2.7-beta'); // string shimmie version _d("TIMEZONE", null); // string timezone -_d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,comment,tag_list,index,tag_edit,alias_editor"); // extensions to always enable _d("EXTRA_EXTS", ""); // string optional extra extensions _d("BASE_URL", null); // string force a specific base URL (default is auto-detect) -_d("MIN_PHP_VERSION", '5.6');// string minium supported PHP version +_d("MIN_PHP_VERSION", '7.1');// string minimum supported PHP version +_d("TRACE_FILE", null); // string file to log performance data into +_d("TRACE_THRESHOLD", 0.0); // float log pages which take more time than this many seconds +_d("ENABLED_MODS", "imageboard"); /* * Calculated settings - you should never need to change these * directly, only the things they're built from */ _d("SCORE_VERSION", 'develop/'.VERSION); // string SCore version -_d("ENABLED_EXTS", CORE_EXTS.",".EXTRA_EXTS); - - diff --git a/core/tests/polyfills.test.php b/core/tests/polyfills.test.php new file mode 100644 index 00000000..e5899f0a --- /dev/null +++ b/core/tests/polyfills.test.php @@ -0,0 +1,119 @@ +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); + } + + 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_shorthand_int() + { + $this->assertEquals(to_shorthand_int(1231231231), "1.1GB"); + + $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_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/\\/\/") + ); + } +} diff --git a/core/tests/util.test.php b/core/tests/util.test.php new file mode 100644 index 00000000..e04b01ae --- /dev/null +++ b/core/tests/util.test.php @@ -0,0 +1,65 @@ +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) + ); + } +} diff --git a/core/urls.php b/core/urls.php new file mode 100644 index 00000000..457bfe1b --- /dev/null +++ b/core/urls.php @@ -0,0 +1,119 @@ +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); + } + + 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; + } 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..a1e46680 --- /dev/null +++ b/core/user.php @@ -0,0 +1,236 @@ +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"]}'"); + } + } + + public static function by_session(string $name, string $session): ?User + { + global $config, $database; + $row = $database->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]); + $database->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 $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", ["id"=>$id]); + if ($id === 1) { + $database->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($database->scoreql_to_sql("SELECT * FROM users WHERE SCORE_STRNORM(name) = SCORE_STRNORM(:name)"), ["name"=>$name]); + return is_null($row) ? null : new User($row); + } + + public static function by_name_and_pass(string $name, string $pass): ?User + { + $user = User::by_name($name); + if ($user) { + if ($user->passhash == md5(strtolower($name) . $pass)) { + log_info("core-user", "Migrating from md5 to bcrypt for ".html_escape($name)); + $user->set_password($pass); + } + if (password_verify($pass, $user->passhash)) { + log_info("core-user", "Logged in as ".html_escape($name)." ({$user->class->name})"); + return $user; + } else { + log_warning("core-user", "Failed to log in as ".html_escape($name)." (Invalid password)"); + } + } else { + log_warning("core-user", "Failed to log in as ".html_escape($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 Exception("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 ""; + } + } + 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()); + } +} 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..5c60c9a0 --- /dev/null +++ b/core/userclass.php @@ -0,0 +1,235 @@ +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)) { + $val = $this->abilities[$ability]; + return $val; + } 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 '".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, [ + 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::MASS_TAG_EDIT => false, + + Permissions::VIEW_IP => false, # view IP addresses associated with things + Permissions::BAN_IP => 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::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::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::ARTISTS_ADMIN => false, + Permissions::BLOTTER_ADMIN => false, + Permissions::FORUM_ADMIN => false, + Permissions::NOTES_ADMIN => false, + Permissions::POOLS_ADMIN => false, + Permissions::TIPS_ADMIN => false, +]); + +new UserClass("anonymous", "base", [ +]); + +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::CREATE_IMAGE_REPORT => true, + Permissions::EDIT_IMAGE_RATING => 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::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::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, +]); + +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..fe3bec45 --- /dev/null +++ b/core/util.php @@ -0,0 +1,618 @@ +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); + } +} + +/** + * 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 = parse_shorthand_int($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 + } +} + +/** + * 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; +} + + +/** + * 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... + */ +function flash_message(string $text, string $type="info"): void +{ + 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, "/"); +} + +/** + * A shorthand way to send a TextFormattingEvent and get the results. + */ +function format_text(string $string): string +{ + $tfe = new TextFormattingEvent($string); + send_event($tfe); + 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 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); + + $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; +} + +/** + * Get the active contents of a .php file + */ +function manual_include(string $fname): ?string +{ + static $included = []; + + 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; +} + + +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; +} + + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ +* 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 $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; +} + + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ +* Request initialisation stuff * +\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +/** @privatesection */ + +function _version_check(): void +{ + 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(): void +{ + global $_tracer; + + 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); + } + + $_tracer = new EventTracer(); + + if (COVERAGE) { + _start_coverage(); + register_shutdown_function("_end_coverage"); + } + + 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 = []; + 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/{".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. + */ +function _fatal_error(Exception $e): void +{ + $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... + */ +function _decaret(string $str): string +{ + $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; +} + +function _get_user(): 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; +} + +function _get_query(): string +{ + return (@$_POST["q"]?:@$_GET["q"])?:"/"; +} + + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ +* Code coverage * +\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +function _start_coverage(): void +{ + 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(): void +{ + 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()))); + } +} + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ +* 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; +} diff --git a/ext/admin/info.php b/ext/admin/info.php new file mode 100644 index 00000000..84db9927 --- /dev/null +++ b/ext/admin/info.php @@ -0,0 +1,33 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: Various things to make admins' lives easier + * Documentation: + + */ + +class AdminPageInfo extends ExtensionInfo +{ + public const KEY = "admin"; + + public $key = self::KEY; + public $name = "Admin Controls"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Various things to make admins' lives easier"; + public $documentation = +"Various moderate-level tools for admins; for advanced, obscure, and possibly dangerous tools see the shimmie2-utils script set +

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..6e13a76b 100644 --- a/ext/admin/main.php +++ b/ext/admin/main.php @@ -1,271 +1,281 @@ - * 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) - */ /** * Sent when the admin page is ready to be added to */ -class AdminBuildingEvent extends Event { - /** @var \Page */ - public $page; +class AdminBuildingEvent extends Event +{ + /** @var Page */ + public $page; - /** - * @param Page $page - */ - public function __construct(Page $page) { - $this->page = $page; - } + public function __construct(Page $page) + { + $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) + { + $this->action = $action; + } } -class AdminPage extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; +class AdminPage extends Extension +{ + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; - 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); + 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($user->check_auth_token()) { - log_info("admin", "Util: $action"); - set_time_limit(0); - send_event($aae); - } + if ($user->check_auth_token()) { + log_info("admin", "Util: $action"); + set_time_limit(0); + send_event($aae); + } - if($aae->redirect) { - $page->set_mode("redirect"); - $page->set_redirect(make_link("admin")); - } - } - } - } - } + if ($aae->redirect) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("admin")); + } + } + } + } + } - 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(); - } - } + public function onCommand(CommandEvent $event) + { + if ($event->cmd == "help") { + print "\tget-page [query string]\n"; + print "\t\teg 'get-page post/list'\n\n"; + print "\tregen-thumb [hash]\n"; + print "\t\tregenerate a thumbnail\n\n"; + } + if ($event->cmd == "get-page") { + global $page; + send_event(new PageRequestEvent($event->args[0])); + $page->display(); + } + if ($event->cmd == "regen-thumb") { + $image = Image::by_hash($event->args[0]); + if ($image) { + print("Regenerating thumb for image {$image->id} ({$image->hash})\n"); + send_event(new ThumbnailGenerationEvent($image->hash, $image->ext, true)); + } else { + print("Can't find image with hash {$event->args[0]}\n"); + } + } + } - public function onAdminBuilding(AdminBuildingEvent $event) { - $this->theme->display_page(); - $this->theme->display_form(); - } + public function onAdminBuilding(AdminBuildingEvent $event) + { + $this->theme->display_page(); + $this->theme->display_form(); + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("manage_admintools")) { - $event->add_link("Board Admin", make_link("admin")); - } - } + 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 onAdminAction(AdminActionEvent $event) { - $action = $event->action; - if(method_exists($this, $action)) { - $event->redirect = $this->$action(); - } - } + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::MANAGE_ADMINTOOLS)) { + $event->add_link("Board Admin", make_link("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 onAdminAction(AdminActionEvent $event) + { + $action = $event->action; + if (method_exists($this, $action)) { + $event->redirect = $this->$action(); + } + } - private function delete_by_query() { - global $page; - $query = $_POST['query']; - $reason = @$_POST['reason']; - assert(strlen($query) > 1); + // public function onPostListBuilding(PostListBuildingEvent $event) + // { + // global $user; + // if ($user->can("manage_admintools") && !empty($event->search_terms)) { + // $event->add_control($this->theme->dbq_html(Tag::implode($event->search_terms))); + // } + // } - 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 delete_by_query() + { + global $page; + $query = $_POST['query']; + $reason = @$_POST['reason']; - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/list")); - return false; - } + assert(strlen($query) > 1); - 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; - } + $images = Image::find_images(0, 1000000, Tag::explode($query)); + $count = count($images); + log_warning("admin", "Mass-deleting $count images from $query", "Mass deleted $count images"); + foreach ($images as $image) { + if ($reason && class_exists("ImageBan")) { + send_event(new AddImageHashBanEvent($image->hash, $reason)); + } + send_event(new ImageDeletionEvent($image, 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; - } + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/list")); + return false; + } - private function recount_tag_use() { - global $database; - $database->Execute(" + 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)" + ), ["tag1" => $_POST['tag'], "tag2" => $_POST['tag']]); + log_info("admin", "Fixed the case of ".html_escape($_POST['tag']), "Fixed case"); + return true; + } + + private function lowercase_all_tags() + { + global $database; + $database->execute("UPDATE tags SET tag=lower(tag)"); + log_warning("admin", "Set all tags to lowercase", "Set all tags to lowercase"); + return true; + } + + private function recount_tag_use() + { + global $database; + $database->Execute(" UPDATE tags SET count = COALESCE( (SELECT COUNT(image_id) FROM image_tags WHERE tag_id=tags.id GROUP BY tag_id), 0 ) "); - $database->Execute("DELETE FROM tags WHERE count=0"); - log_warning("admin", "Re-counted tags", true); - return true; - } + $database->Execute("DELETE FROM tags WHERE count=0"); + log_warning("admin", "Re-counted tags", "Re-counted tags"); + return true; + } - private function database_dump() { - global $page; + 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']; + $matches = []; + 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; - } + switch ($software) { + case DatabaseDriver::MYSQL: + $cmd = "mysqldump -h$hostname -u$username -p$password $database"; + break; + case DatabaseDriver::PGSQL: + putenv("PGPASSWORD=$password"); + $cmd = "pg_dump -h $hostname -U $username $database"; + break; + case DatabaseDriver::SQLITE: + $cmd = "sqlite3 $database .dump"; + break; + default: + $cmd = false; + } - //FIXME: .SQL dump is empty if cmd doesn't exist + //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)); - } + if ($cmd) { + $page->set_mode(PageMode::DATA); + $page->set_type("application/x-unknown"); + $page->set_filename('shimmie-'.date('Ymd').'.sql'); + $page->set_data(shell_exec($cmd)); + } - return false; - } + return false; + } - private function download_all_images() { - global $database, $page; + private function download_all_images() + { + global $database, $page; - $images = $database->get_all("SELECT hash, ext FROM images"); - $filename = data_path('imgdump-'.date('Ymd').'.zip'); + $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(); - } + $zip = new ZipArchive; + if ($zip->open($filename, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE) === true) { + foreach ($images as $img) { + $img_loc = warehouse_path(Image::IMAGE_DIR, $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? + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link($filename)); //TODO: Delete file after downloaded? - return false; // we do want a redirect, but a manual one - } + return false; // we do want a redirect, but a manual one + } - private function reset_image_ids() { + 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); + //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 + if ($matches['proto'] == DatabaseDriver::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'])); + AND REFERENCED_TABLE_NAME = 'images'", ["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; + $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};"; - } + foreach ($tables as $table) { + $sql .= "UPDATE {$table} SET image_id={$i} WHERE image_id={$id};"; + } - $sql .= " SET FOREIGN_KEY_CHECKS=1;"; - $database->execute($sql); + $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 - } + $i++; + } + $database->execute("ALTER TABLE images AUTO_INCREMENT=".(count($ids) + 1)); + } elseif ($matches['proto'] == DatabaseDriver::PGSQL) { + //TODO: Make this work with PostgreSQL + } elseif ($matches['proto'] == DatabaseDriver::SQLITE) { + //TODO: Make this work with SQLite + } return true; } } - diff --git a/ext/admin/test.php b/ext/admin/test.php index 3f893899..39dc5508 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"); +class AdminPageTest extends ShimmiePHPUnitTestCase +{ + public function testAuth() + { + $this->get_page('admin'); + $this->assert_response(403); + $this->assert_title("Permission Denied"); - $this->log_in_as_user(); - $this->get_page('admin'); - $this->assert_response(403); - $this->assert_title("Permission Denied"); + $this->log_in_as_user(); + $this->get_page('admin'); + $this->assert_response(403); + $this->assert_title("Permission Denied"); - $this->log_in_as_admin(); - $this->get_page('admin'); - $this->assert_response(200); - $this->assert_title("Admin Tools"); - } + $this->log_in_as_admin(); + $this->get_page('admin'); + $this->assert_response(200); + $this->assert_title("Admin Tools"); + } - public function testLowercase() { - $ts = time(); // we need a tag that hasn't been used before + public function testLowercase() + { + $ts = time(); // we need a tag that hasn't been used before - $this->log_in_as_admin(); - $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"); - $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"); - $this->get_page('admin'); - $this->assert_title("Admin Tools"); - //$this->click("All tags to lowercase"); - 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')); - $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"); - $this->delete_image($image_id_1); - } + $this->delete_image($image_id_1); + } - # 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() + { + $this->log_in_as_admin(); + $this->get_page('admin'); + $this->assert_title("Admin Tools"); - //$this->click("Recount tag use"); - send_event(new AdminActionEvent('recount_tag_use')); - } + //$this->click("Recount tag use"); + send_event(new AdminActionEvent('recount_tag_use')); + } - public function testDump() { - $this->log_in_as_admin(); - $this->get_page('admin'); - $this->assert_title("Admin Tools"); + public function testDump() + { + $this->log_in_as_admin(); + $this->get_page('admin'); + $this->assert_title("Admin Tools"); - // 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); - } + // 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); + } - 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 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"); - $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/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->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); - } + $this->delete_image($image_id_1); + $this->delete_image($image_id_2); + $this->delete_image($image_id_3); + } } - diff --git a/ext/admin/theme.php b/ext/admin/theme.php index 64ee1a92..4c083060 100644 --- a/ext/admin/theme.php +++ b/ext/admin/theme.php @@ -1,77 +1,77 @@ 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, $database; - $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 = ""; + $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)); + if ($database->get_driver_name() == DatabaseDriver::MYSQL) { + $html .= $this->button("Reset image IDs", "reset_image_ids", true); + } + $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)); - } + $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") . " + public function dbq_html($terms) + { + if (Extension::is_enabled(TrashInfo::KEY)) { + $warning = "This delete method will bypass the trash
"; + } + if (class_exists("ImageBan")) { + $h_reason = ""; + } + $html = $warning.make_form(make_link("admin/delete_by_query"), "POST") . " $h_reason "; - return $html; - } + return $html; + } } - diff --git a/ext/alias_editor/info.php b/ext/alias_editor/info.php new file mode 100644 index 00000000..4990ac6d --- /dev/null +++ b/ext/alias_editor/info.php @@ -0,0 +1,24 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: Edit the alias list + * Documentation: + */ + +class AliasEditorInfo extends ExtensionInfo +{ + public const KEY = "alias_editor"; + + public $key = self::KEY; + public $name = "Alias Editor"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Edit the alias list"; + public $documentation = 'The list is visible at /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..ad50a464 100644 --- a/ext/alias_editor/main.php +++ b/ext/alias_editor/main.php @@ -1,178 +1,162 @@ - * 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 - */ -class AddAliasEvent extends Event { - /** @var string */ - public $oldtag; - /** @var string */ - public $newtag; +class AddAliasEvent extends Event +{ + /** @var string */ + public $oldtag; + /** @var string */ + public $newtag; - /** - * @param string $oldtag - * @param string $newtag - */ - public function __construct($oldtag, $newtag) { - $this->oldtag = trim($oldtag); - $this->newtag = trim($newtag); - } + public function __construct(string $oldtag, string $newtag) + { + $this->oldtag = trim($oldtag); + $this->newtag = trim($newtag); + } } -class AddAliasException extends SCoreException {} - -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;} +class AddAliasException extends SCoreException +{ } +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(Permissions::MANAGE_ALIAS_LIST)) { + if (isset($_POST['oldtag']) && isset($_POST['newtag'])) { + try { + $aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']); + send_event($aae); + $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)) { + if (isset($_POST['oldtag'])) { + $database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", ["oldtag" => $_POST['oldtag']]); + log_info("alias_editor", "Deleted alias for ".$_POST['oldtag'], "Deleted alias"); + + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("alias/list")); + } + } + } elseif ($event->get_arg(0) == "list") { + $page_number = $event->get_arg(1); + if (is_null($page_number) || !is_numeric($page_number)) { + $page_number = 0; + } elseif ($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, + ["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); + } 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; + $pair = ["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"); + } elseif ($database->get_row("SELECT * FROM aliases WHERE oldtag=:newtag", ["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}", "Added 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) + { + $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. + */ + public function get_priority(): int + { + return 60; + } +} diff --git a/ext/alias_editor/test.php b/ext/alias_editor/test.php index 0b8e4512..b2bbe00c 100644 --- a/ext/alias_editor/test.php +++ b/ext/alias_editor/test.php @@ -1,104 +1,107 @@ get_page('alias/list'); - $this->assert_response(200); - $this->assert_title("Alias List"); - } +class AliasEditorTest extends ShimmiePHPUnitTestCase +{ + public function testAliasList() + { + $this->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() + { + // 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 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(); + 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_in_as_admin(); + $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"); + # 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/list'); - $this->assert_text("test1"); + $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_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); + $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); - $this->get_page('alias/list'); - $this->click("Remove"); - $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"); - # 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"); + # 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_text("onetag,multi 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"); + // FIXME: known broken + //$this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases + //$this->assert_title("onetag"); + //$this->assert_no_text("No Images Found"); + $this->get_page("post/list/multi/1"); + $this->assert_title("multi"); + $this->assert_no_text("No Images Found"); + $this->get_page("post/list/multi%20tag/1"); + $this->assert_title("multi tag"); + $this->assert_no_text("No Images Found"); + $this->delete_image($image_id_1); + $this->delete_image($image_id_2); - $this->get_page('alias/list'); - $this->click("Remove"); - $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"); - $this->log_out(); + $this->log_out(); - $this->get_page('alias/list'); - $this->assert_title("Alias List"); - $this->assert_no_text("Add"); - } + $this->get_page('alias/list'); + $this->assert_title("Alias List"); + $this->assert_no_text("Add"); + } } - diff --git a/ext/alias_editor/theme.php b/ext/alias_editor/theme.php index 02b7a3a1..732139d4 100644 --- a/ext/alias_editor/theme.php +++ b/ext/alias_editor/theme.php @@ -1,22 +1,20 @@ $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(array $aliases, int $pageNumber, int $totalPages): void + { + global $page, $user; - $can_manage = $user->can("manage_alias_list"); - if($can_manage) { - $h_action = "Action"; - $h_add = " + $can_manage = $user->can(Permissions::MANAGE_ALIAS_LIST); + if ($can_manage) { + $h_action = "Action"; + $h_add = " ".make_form(make_link("alias/add"))." @@ -25,20 +23,19 @@ class AliasEditorTheme extends Themelet { "; - } - else { - $h_action = ""; - $h_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 = ""; + 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 .= " + $h_aliases .= "$h_old$h_new"; + if ($can_manage) { + $h_aliases .= " ".make_form(make_link("alias/remove"))." @@ -46,10 +43,10 @@ class AliasEditorTheme extends Themelet { "; - } - $h_aliases .= ""; - } - $html = " + } + $h_aliases .= ""; + } + $html = " $h_action$h_aliases @@ -58,22 +55,21 @@ class AliasEditorTheme extends Themelet {

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)); - } + $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); - } + $this->display_paginator($page, "alias/list", null, $pageNumber, $totalPages); + } } - 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/arrowkey_navigation/info.php b/ext/arrowkey_navigation/info.php new file mode 100644 index 00000000..ee6b88f2 --- /dev/null +++ b/ext/arrowkey_navigation/info.php @@ -0,0 +1,23 @@ + + * 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 ArrowkeyNavigationInfo extends ExtensionInfo +{ + public const KEY = "arrowkey_navigation"; + + public $key = self::KEY; + public $name = "Arrow Key Navigation"; + public $url = "http://www.drudexsoftware.com/"; + public $authors = ["Drudex Software"=>"support@drudexsoftware.com"]; + public $license = self::LICENSE_GPLV2; + public $description = "Allows viewers no navigate between images using the left & right arrow keys."; + public $documentation = +"Simply enable this extension in the extension manager to enable arrow key navigation."; +} diff --git a/ext/arrowkey_navigation/main.php b/ext/arrowkey_navigation/main.php index 023ca87b..640d2b12 100644 --- a/ext/arrowkey_navigation/main.php +++ b/ext/arrowkey_navigation/main.php @@ -1,49 +1,38 @@ - * 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); - } - } +class ArrowkeyNavigation extends Extension +{ + /** + * Adds functionality for post/view on images. + */ + 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 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; + /** + * Adds functionality for post/list. + */ + 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); + } + } - $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; + /** + * Returns info about the current page number. + */ + private function get_list_pageinfo(PageRequestEvent $event): array + { + global $config, $database; - // get the amount of images per page - $images_per_page = $config->get_int('index_images'); + // 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); - } + // 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", + ["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; + // 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, - ); + // Create return array + $pageinfo = [ + "prev" => $prefix.$prev, + "next" => $prefix.$next, + ]; - return $pageinfo; - } + return $pageinfo; + } } - diff --git a/ext/artists/info.php b/ext/artists/info.php new file mode 100644 index 00000000..5da88896 --- /dev/null +++ b/ext/artists/info.php @@ -0,0 +1,23 @@ + + * Alpha + * License: GPLv2 + * Description: Simple artists extension + * Documentation: + * + */ +class ArtistsInfo extends ExtensionInfo +{ + public const KEY = "artists"; + + public $key = self::KEY; + public $name = "Artists System"; + public $url = self::SHIMMIE_URL; + public $authors = ["Sein Kraft"=>"mail@seinkraft.info","Alpha"=>"alpha@furries.com.ar"]; + public $license = self::LICENSE_GPLV2; + public $description = "Simple artists extension"; + public $beta = true; +} diff --git a/ext/artists/main.php b/ext/artists/main.php index 5276881f..e0a4cf7e 100644 --- a/ext/artists/main.php +++ b/ext/artists/main.php @@ -1,61 +1,66 @@ - * Alpha - * License: GPLv2 - * Description: Simple artists extension - * Documentation: - * - */ -class AuthorSetEvent extends Event { - /** @var \Image */ - public $image; - /** @var \User */ - public $user; - /** @var string */ - public $author; - /** - * @param Image $image - * @param User $user - * @param string $author - */ - public function __construct(Image $image, User $user, /*string*/ $author) { +class AuthorSetEvent extends Event +{ + /** @var Image */ + public $image; + /** @var User */ + public $user; + /** @var string */ + public $author; + + public function __construct(Image $image, User $user, string $author) + { $this->image = $image; $this->user = $user; $this->author = $author; } } -class Artists extends Extension { - public function onImageInfoSet(ImageInfoSetEvent $event) { +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"])); - } - } + if (isset($_POST["tag_edit__author"])) { + send_event(new AuthorSetEvent($event->image, $user, $_POST["tag_edit__author"])); + } + } - public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) { + 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) + { + $matches = []; + if (preg_match("/^(author|artist)[=|:](.*)$/i", $event->term, $matches)) { + $char = $matches[1]; + $event->add_querylet(new Querylet("Author = :author_char", ["author_char"=>$char])); + } + } - public function onInitExt(InitExtEvent $event) { - global $config, $database; - - if ($config->get_int("ext_artists_version") < 1) { + 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 onInitExt(InitExtEvent $event) + { + global $config, $database; + + if ($config->get_int("ext_artists_version") < 1) { $database->create_table("artists", " id SCORE_AIPK, user_id INTEGER NOT NULL, @@ -65,7 +70,7 @@ class Artists extends Extension { 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, @@ -105,47 +110,53 @@ class Artists extends Extension { } } - 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) + [$artistName, $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 +166,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; @@ -196,8 +204,8 @@ class Artists extends Extension { $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); @@ -206,9 +214,9 @@ class Artists extends Extension { //$this->theme->show_new_member_composer($artistID); //$this->theme->show_new_url_composer($artistID); } - + $this->theme->sidebar_options("editor", $artistID, $userIsAdmin); - + break; } @@ -219,14 +227,13 @@ class Artists extends Extension { $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 +241,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,14 +249,14 @@ 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; } @@ -257,7 +264,7 @@ class Artists extends Extension { { $artistID = $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,13 +289,12 @@ 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; } @@ -297,7 +303,7 @@ class Artists extends Extension { $aliasID = $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 +319,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,13 +330,12 @@ 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; } @@ -339,7 +344,7 @@ class Artists extends Extension { $urlID = $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 +360,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 +370,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 +384,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 +400,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 +411,134 @@ 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)); + $result = $database->get_row("SELECT author FROM images WHERE id = ?", [$imageID]); return stripslashes($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]); 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 = ?", [$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]); 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) + [$artistID, $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 $database->get_one("SELECT artist_id FROM artist_urls WHERE 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 $database->get_one("SELECT artist_id FROM artist_members WHERE 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 $database->get_one("SELECT name FROM artists WHERE 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 $database->get_one("SELECT artist_id FROM artist_alias WHERE 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 $database->get_one("SELECT artist_id FROM artist_members WHERE 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 $database->get_one("SELECT artist_id FROM artist_urls WHERE 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 = ?", [$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 = ?", [$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 = ?", [$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 = $database->get_row("SELECT * FROM artist_alias WHERE id = ?", [$aliasID]); $result["alias"] = stripslashes($result["alias"]); return $result; } - /** - * @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 = $database->get_row("SELECT * FROM artist_urls WHERE id = ?", [$urlID]); $result["url"] = stripslashes($result["url"]); return $result; } - /** - * @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 = $database->get_row("SELECT * FROM artist_members WHERE id = ?", [$memberID]); $result["name"] = stripslashes($result["name"]); return $result; } - 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) + [$name, $notes, $userID, $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) + [$alias, $userID, $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) + [$url, $userID, $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) + [$memberName, $userID, $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)); + ", [$user->id, $name, $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) + [$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) + [$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) + [$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) + [$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( + private function get_artist_id(string $name): int + { + global $database; + return (int)$database->get_one( "SELECT id FROM artists WHERE name = ?", - array($name) + [$name] ); - } + } - /** - * @param string $alias - * @return int - */ - private function get_artistID_by_aliasName($alias) { + private function get_artistID_by_aliasName(string $alias): int + { global $database; return (int)$database->get_one( "SELECT artist_id FROM artist_alias WHERE alias = ?", - array($alias) + [$alias] ); } - - /** - * @param int $artistID - */ - private function delete_artist($artistID) { - assert(is_numeric($artistID)); - + private function delete_artist(int $artistID) + { global $database; $database->execute( "DELETE FROM artists WHERE id = ? ", - array($artistID) + [$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($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 @@ -1010,21 +900,22 @@ class Artists extends Extension { ) ORDER BY updated DESC LIMIT ?, ? - ", array( + ", + [ $pageNumber * $artistsPerPage , $artistsPerPage - )); - - $number_of_listings = count($listing); + ] + ); - 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) + [$artistID, $url, $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) + [$artistID, $alias, $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) + [$artistID, $member, $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 = ? AND name = ?", - array($artistID, $member) + [$artistID, $member] + ); + return ($result != 0); + } + + private function url_exists(int $artistID, string $url): bool + { + global $database; + + $result = $database->get_one( + "SELECT COUNT(1) FROM artist_urls WHERE artist_id = ? AND url = ?", + [$artistID, $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 = ? ORDER BY alias ASC - ", array($artistID)); + ", [$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..23ee4cfb 100644 --- a/ext/artists/test.php +++ b/ext/artists/test.php @@ -1,9 +1,10 @@ get_page("post/list/author=bob/1"); - #$this->assert_response(200); - } +class ArtistTest extends ShimmiePHPUnitTestCase +{ + public function testSearch() + { + # FIXME: check that the results are there + $this->get_page("post/list/author=bob/1"); + #$this->assert_response(200); + } } - diff --git a/ext/artists/theme.php b/ext/artists/theme.php index cc30e6bd..825bc570 100644 --- a/ext/artists/theme.php +++ b/ext/artists/theme.php @@ -1,13 +1,10 @@ "; - } + } - /** - * @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().'
FromTo
Author @@ -16,105 +13,104 @@ class ArtistsTheme extends Themelet {
@@ -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/autocomplete/info.php b/ext/autocomplete/info.php new file mode 100644 index 00000000..3d420496 --- /dev/null +++ b/ext/autocomplete/info.php @@ -0,0 +1,17 @@ + + * Description: Adds autocomplete to search & tagging. + */ + +class AutoCompleteInfo extends ExtensionInfo +{ + public const KEY = "autocomplete"; + + public $key = self::KEY; + public $name = "Autocomplete"; + public $authors = ["Daku"=>"admin@codeanimu.net"]; + public $description = "Adds autocomplete to search & tagging."; +} diff --git a/ext/autocomplete/main.php b/ext/autocomplete/main.php index 24e1e87f..48d862c4 100644 --- a/ext/autocomplete/main.php +++ b/ext/autocomplete/main.php @@ -1,47 +1,66 @@ - * Description: Adds autocomplete to search & tagging. - */ -class AutoComplete extends Extension { - public function get_priority() {return 30;} // before Home +class AutoComplete extends Extension +{ + public function get_priority(): int + { + return 30; + } // before Home - public function onPageRequest(PageRequestEvent $event) { - global $page, $database; + public function onPageRequest(PageRequestEvent $event) + { + global $page, $database; - if($event->page_matches("api/internal/autocomplete")) { - if(!isset($_GET["s"])) return; + if ($event->page_matches("api/internal/autocomplete")) { + if (!isset($_GET["s"])) { + return; + } - //$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"]; - } + $page->set_mode(PageMode::DATA); + $page->set_type("application/json"); - $res = $database->cache->get($cache_key); - if(!$res) { - $res = $database->get_pairs($database->scoreql_to_sql(" + $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 = $database->cache->get($cache_key); + if (!$res) { + $res = $database->get_pairs( + $database->scoreql_to_sql(" SELECT tag, count FROM tags - WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:search) + WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:search) + -- OR SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:cat_search) AND count > 0 ORDER BY count DESC - $limitSQL"), $SQLarr - ); - $database->cache->set($cache_key, $res, 600); - } + $limitSQL"), + $SQLarr + ); + $database->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..6a5c25fd 100644 --- a/ext/autocomplete/script.js +++ b/ext/autocomplete/script.js @@ -1,7 +1,7 @@ $(function(){ 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/theme.php b/ext/autocomplete/theme.php index 462d2bfd..334a7807 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..6b56c045 --- /dev/null +++ b/ext/ban_words/info.php @@ -0,0 +1,36 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: For stopping spam and other comment abuse + * Documentation: + * + */ + +class BanWordsInfo extends ExtensionInfo +{ + public const KEY = "ban_words"; + + public $key = self::KEY; + public $name = "Comment Word Ban"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "For stopping spam and other comment abuse"; + public $documentation = +"Allows an administrator to ban certain words +from comments. This can be a very simple but effective way +of stopping spam; just add \"viagra\", \"porn\", etc to the +banned words list. +

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..c6f636a1 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" - */ -class BanWords extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config; - $config->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, Exception $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..35ba6aa6 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"); - } - } +class BanWordsTest extends ShimmiePHPUnitTestCase +{ + public function check_blocked($image_id, $words) + { + global $user; + try { + send_event(new CommentPostingEvent($image_id, $user, $words)); + $this->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..06798aab --- /dev/null +++ b/ext/bbcode/info.php @@ -0,0 +1,40 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: Turns BBCode into HTML + */ + +class BBCodeInfo extends ExtensionInfo +{ + public const KEY = "bbcode"; + + public $key = self::KEY; + public $name = "BBCode"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $core = true; + public $description = "Turns BBCode into HTML"; + public $documentation = +" Supported tags: +

    +
  • [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..fd6c7658 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) - *
- */ -class BBCode extends FormatterExtension { - /** - * @param string $text - * @return string - */ - public function format(/*string*/ $text) { - $text = $this->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', '', $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..ff7b3c35 --- /dev/null +++ b/ext/bbcode/script.js @@ -0,0 +1,18 @@ +$(document).ready(function() { + $(".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..2f8dcbc5 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"); - } +class BBCodeTest extends ShimmiePHPUnitTestCase +{ + public function testBasics() + { + $this->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..23d24604 --- /dev/null +++ b/ext/blocks/info.php @@ -0,0 +1,21 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: Add HTML to some space (News, Ads, etc) + */ + +class BlocksInfo extends ExtensionInfo +{ + public const KEY = "blocks"; + + public $key = self::KEY; + public $name = "Generic Blocks"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Add HTML to some space (News, Ads, etc)"; +} diff --git a/ext/blocks/main.php b/ext/blocks/main.php index d1f0f6cf..de2c2ab9 100644 --- a/ext/blocks/main.php +++ b/ext/blocks/main.php @@ -1,17 +1,12 @@ - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Add HTML to some space (News, Ads, etc) - */ -class Blocks extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config, $database; - if($config->get_int("ext_blocks_version") < 1) { - $database->create_table("blocks", " +class Blocks extends Extension +{ + public function onInitExt(InitExtEvent $event) + { + global $config, $database; + if ($config->get_int("ext_blocks_version") < 1) { + $database->create_table("blocks", " id SCORE_AIPK, pages VARCHAR(128) NOT NULL, title VARCHAR(128) NOT NULL, @@ -19,73 +14,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)", []); + $config->set_int("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 $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 = $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); + } + } + + 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(" + ", [$_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(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(" + ", [$_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")); - } - } - } + ", [$_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(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..fc92f69a 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"); - } +class BlocksTest extends ShimmiePHPUnitTestCase +{ + public function testBlocks() + { + $this->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..32f7d870 100644 --- a/ext/blocks/theme.php +++ b/ext/blocks/theme.php @@ -1,46 +1,47 @@ "; - 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 .= ""; + $html = ""; + foreach ($blocks as $block) { + $html .= make_form(make_link("blocks/update")); + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= "\n"; + $html .= ""; + $html .= ""; + $html .= "\n"; + $html .= "\n"; + } + $html .= make_form(make_link("blocks/add")); + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= ""; + $html .= "\n"; + $html .= ""; + $html .= "
    TitleAreaPriorityPagesDelete
     
    TitleAreaPriorityPages
    "; - $page->set_title("Blocks"); - $page->set_heading("Blocks"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Block Editor", $html)); - } + $page->set_title("Blocks"); + $page->set_heading("Blocks"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Block Editor", $html)); + } } - diff --git a/ext/blotter/info.php b/ext/blotter/info.php new file mode 100644 index 00000000..d03891ec --- /dev/null +++ b/ext/blotter/info.php @@ -0,0 +1,22 @@ + [http://seemslegit.com/] + * License: GPLv2 + * Description: + */ +class BlotterInfo extends ExtensionInfo +{ + public const KEY = "blotter"; + + public $key = self::KEY; + public $name = "Blotter"; + public $url = "http://seemslegit.com/"; + public $authors = ["Zach Hall"=>"zach@sosguy.net"]; + public $license = self::LICENSE_GPLV2; + public $description = "Displays brief updates about whatever you want on every page. +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..3b4e98dc 100644 --- a/ext/blotter/main.php +++ b/ext/blotter/main.php @@ -1,132 +1,152 @@ [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", " + +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", " id SCORE_AIPK, entry_date SCORE_DATETIME DEFAULT SCORE_NOW, 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(), ?, ?)", + ["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"); + } - 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")) { + 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(), ?, ?)", + [$entry_text, $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/test.php b/ext/blotter/test.php index eafec499..53b7b34e 100644 --- a/ext/blotter/test.php +++ b/ext/blotter/test.php @@ -1,34 +1,38 @@ log_in_as_admin(); - //$this->assert_text("Blotter Editor"); - //$this->click("Blotter Editor"); - //$this->log_out(); - } +class BlotterTest extends ShimmiePHPUnitTestCase +{ + public function testLogin() + { + $this->log_in_as_admin(); + //$this->assert_text("Blotter Editor"); + //$this->click("Blotter Editor"); + //$this->log_out(); + } - 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 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(); - $this->get_page("blotter/editor"); - //$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..9a965467 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)); - } +class BlotterTheme extends Themelet +{ + public function display_editor($entries) + { + global $page; + $html = $this->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..ba353e4c --- /dev/null +++ b/ext/browser_search/info.php @@ -0,0 +1,30 @@ + + * 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: + * + */ + +class BrowserSearchInfo extends ExtensionInfo +{ + public const KEY = "browser_search"; + + public $key = self::KEY; + public $name = "Browser Search"; + public $url = "http://atravelinggeek.com/"; + public $authors = ["ATravelingGeek"=>"atg@atravelinggeek.com"]; + public $license = self::LICENSE_GPLV2; + public $version = "0.1c, October 26, 2007"; + public $description = "Allows the user to add a browser 'plugin' to search the site with real-time suggestions"; + public $documentation = +"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..f29b5765 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 - */ -class BrowserSearch extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config; - $config->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/please_dont_use_this_tag_as_it_would_break_stuff__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/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(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/handle_static/static/favicon.ico")); - // Now for the XML - $xml = " + // Now for the XML + $xml = " $search_title UTF-8 @@ -50,55 +40,53 @@ 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") && + !$config->get_bool("disable_search_suggestions") + ) { + // We have to build some json stuff + $tag_search = $event->get_arg(0); - 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); - - // 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 ($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", [$tag_search."%"]); + } else { + $tags = $database->execute("SELECT tag FROM tags WHERE tag LIKE ? AND count > 0 ORDER BY count DESC LIMIT 30", [$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"],[],[]] - $json_tag_list = ""; + // 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 = ""; - $tags_array = array(); - foreach($tags as $tag) { - array_push($tags_array,$tag['tag']); - } + $tags_array = []; + foreach ($tags as $tag) { + array_push($tags_array, $tag['tag']); + } - $json_tag_list .= implode("\",\"", $tags_array); + $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); - } - } + // And now for the final output + $json_string = "[\"$tag_search\",[\"$json_tag_list\"],[],[]]"; + $page->set_mode(PageMode::DATA); + $page->set_data($json_string); + } + } - public function onSetupBuilding(SetupBuildingEvent $event) { - $sort_by = array(); - $sort_by['Alphabetical'] = 'a'; - $sort_by['Tag Count'] = 't'; + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sort_by = []; + $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_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); + } } - diff --git a/ext/browser_search/test.php b/ext/browser_search/test.php index 3d77f423..8e289af1 100644 --- a/ext/browser_search/test.php +++ b/ext/browser_search/test.php @@ -1,8 +1,9 @@ get_page("browser_search/please_dont_use_this_tag_as_it_would_break_stuff__search.xml"); - $this->get_page("browser_search/test"); - } +class BrowserSearchTest extends ShimmiePHPUnitTestCase +{ + public function testBasic() + { + $this->get_page("browser_search/please_dont_use_this_tag_as_it_would_break_stuff__search.xml"); + $this->get_page("browser_search/test"); + } } - diff --git a/ext/bulk_actions/info.php b/ext/bulk_actions/info.php new file mode 100644 index 00000000..00c66576 --- /dev/null +++ b/ext/bulk_actions/info.php @@ -0,0 +1,23 @@ +, contributions by Shish and Agasa. + */ + + +class BulkActionsInfo extends ExtensionInfo +{ + public const KEY = "bulk_actions"; + + public $key = self::KEY; + public $name = "Bulk Actions"; + public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"]; + public $license = self::LICENSE_WTFPL; + public $description = "Provides query and selection-based bulk action support"; + public $documentation = "Provides bulk action section in list view. Allows performing actions against a set of images based on query or manual selection. Based on Mass Tagger by Christian Walde , 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..dbae2b4c --- /dev/null +++ b/ext/bulk_actions/main.php @@ -0,0 +1,271 @@ +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; + /** @var PageRequestEvent */ + public $page_request; + + public function __construct(String $action, PageRequestEvent $pageRequestEvent, Generator $items) + { + $this->action = $action; + $this->page_request = $pageRequestEvent; + $this->items = $items; + } +} + +class BulkActions extends Extension +{ + public function onPostListBuilding(PostListBuildingEvent $event) + { + global $config, $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 onBulkAction(BulkActionEvent $event) + { + global $user; + + switch ($event->action) { + case "bulk_delete": + if ($user->can(Permissions::DELETE_IMAGE)) { + $i = $this->delete_items($event->items); + flash_message("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); + flash_message("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); + flash_message("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)) { + $newEvent = new BulkActionEvent($action, $event, $items); + send_event($newEvent); + } + + + $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 + { + $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) { + flash_message("Error while removing {$image->id}: " . $e->getMessage(), "error"); + } + } + 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 + { + $total = 0; + foreach ($items as $image) { + try { + send_event(new SourceSetEvent($image, $source)); + $total++; + } catch (Exception $e) { + flash_message("Error while setting source for {$image->id}: " . $e->getMessage(), "error"); + } + } + return $total; + } +} diff --git a/ext/bulk_actions/script.js b/ext/bulk_actions/script.js new file mode 100644 index 00000000..288449d9 --- /dev/null +++ b/ext/bulk_actions/script.js @@ -0,0 +1,197 @@ +/*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" . + "" . + "" . + $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..333bf0ba --- /dev/null +++ b/ext/bulk_add/info.php @@ -0,0 +1,31 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: Bulk add server-side images + * Documentation: + */ + +class BulkAddInfo extends ExtensionInfo +{ + public const KEY = "builk_add"; + + public $key = self::KEY; + public $name = "Bulk Add"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Bulk add server-side images"; + public $documentation = +" 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 +"; +} diff --git a/ext/bulk_add/main.php b/ext/bulk_add/main.php index fa532526..98c4c638 100644 --- a/ext/bulk_add/main.php +++ b/ext/bulk_add/main.php @@ -1,74 +1,62 @@ - * 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 - */ -class BulkAddEvent extends Event { - public $dir, $results; +class BulkAddEvent extends Event +{ + public $dir; + public $results; - public function __construct($dir) { - $this->dir = $dir; - $this->results = array(); - } + public function __construct(string $dir) + { + $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 +{ + 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 = new BulkAddEvent($_POST['dir']); + send_event($bae); + foreach ($bae->results as $result) { + $this->theme->add_status("Adding files", $result); + } + $this->theme->display_upload_results($page); + } + } + } - 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 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 onAdminBuilding(AdminBuildingEvent $event) { - $this->theme->display_admin_block(); - } + 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"; - } - } + 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..6ffc5fb8 100644 --- a/ext/bulk_add/test.php +++ b/ext/bulk_add/test.php @@ -1,37 +1,42 @@ log_in_as_admin(); +class BulkAddTest extends ShimmiePHPUnitTestCase +{ + public function testBulkAdd() + { + $this->log_in_as_admin(); - $this->get_page('admin'); - $this->assert_title("Admin Tools"); + $this->get_page('admin'); + $this->assert_title("Admin Tools"); - $bae = new BulkAddEvent('asdf'); - send_event($bae); - $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(); + // 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')); + $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" + # 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=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=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->get_page("post/list/hash=e106ea2983e1b77f11e00c0c54e53805/1"); + $this->assert_title(new PatternExpectation("/^Image \d+: data/")); + $this->click("Delete"); - $this->log_out(); - } + $this->log_out(); + } } diff --git a/ext/bulk_add/theme.php b/ext/bulk_add/theme.php index 98cd9b7f..7ed68914 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..a2e6af69 --- /dev/null +++ b/ext/bulk_add_csv/info.php @@ -0,0 +1,34 @@ + + * License: GPLv2 + * Description: Bulk add server-side images with metadata from CSV file + * Documentation: + * + * + */ + +class BulkAddCSVInfo extends ExtensionInfo +{ + public const KEY = "bulk_add_csv"; + + public $key = self::KEY; + public $name = "Bulk Add CSV"; + public $url = self::SHIMMIE_URL; + public $authors = ["velocity37"=>"velocity37@gmail.com"]; + public $license = self::LICENSE_GPLV2; + public $description = "Bulk add server-side images with metadata from CSV file"; + public $documentation = +"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..bf86fef0 100644 --- a/ext/bulk_add_csv/main.php +++ b/ext/bulk_add_csv/main.php @@ -1,147 +1,124 @@ - * 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

    - * - */ -class BulkAddCSV extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; - if($event->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 +{ + 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 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 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; - public function onAdminBuilding(AdminBuildingEvent $event) { - $this->theme->display_admin_block(); - } + //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]); + } + } + } - /** - * 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)); + public function onAdminBuilding(AdminBuildingEvent $event) + { + $this->theme->display_admin_block(); + } - $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)); - } - } - } + /** + * 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)); - 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); - } + $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 = 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, ["s", "q", "e"])) { + $ratingevent = new RatingSetEvent(Image::by_id($event->image_id), $rating); + send_event($ratingevent); + } + 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..9f4ec371 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/info.php b/ext/bulk_remove/info.php new file mode 100644 index 00000000..7f90b825 --- /dev/null +++ b/ext/bulk_remove/info.php @@ -0,0 +1,23 @@ + + * Link: http://www.drudexsoftware.com/ + * License: GPLv2 + * Description: Allows admin to delete many images at once through Board Admin. + * Documentation: + * + */ +class BulkRemoveInfo extends ExtensionInfo +{ + public const KEY = "bulk_remove"; + + public $key = self::KEY; + public $name = "Bulk Remove"; + public $beta = true; + public $url = "http://www.drudexsoftware.com/"; + public $authors = ["Drudex Software"=>"support@drudexsoftware.com"]; + public $license = self::LICENSE_GPLV2; + public $description = "Allows admin to delete many images at once through Board Admin."; +} diff --git a/ext/bulk_remove/main.php b/ext/bulk_remove/main.php index 592d85f6..1d2517b3 100644 --- a/ext/bulk_remove/main.php +++ b/ext/bulk_remove/main.php @@ -1,27 +1,25 @@ - * 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!
    +class BulkRemove extends Extension +{ + public function onPageRequest(PageRequestEvent $event) + { + global $user; + if ($event->page_matches("bulk_remove") && $user->can(Permissions::BULK_ADD) && $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. @@ -40,94 +38,95 @@ class BulkRemove extends Extension { "; - $page->add_block(new Block("Bulk Remove", $html)); - } + $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) + // returns a list of images to be removed + private function determine_images() + { + // set vars + $images_for_removal = []; + $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; + + // 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); } - // 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 ". + } + + + // 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 [ + "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"]; - } -} + $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..600d1eab --- /dev/null +++ b/ext/comment/info.php @@ -0,0 +1,25 @@ + + * 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) + */ + +class CommentListInfo extends ExtensionInfo +{ + public const KEY = "comment"; + + public $key = self::KEY; + public $name = "Image Comments"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Allow users to make comments on images"; + public $documentation = "Formatting is done with the standard formatting API (normally BBCode)"; + public $core = true; +} diff --git a/ext/comment/main.php b/ext/comment/main.php index dfb769e0..ffaccb15 100644 --- a/ext/comment/main.php +++ b/ext/comment/main.php @@ -1,35 +1,22 @@ - * 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) - */ require_once "vendor/ifixit/php-akismet/akismet.class.php"; -class CommentPostingEvent extends Event { - /** @var int */ - public $image_id; - /** @var \User */ - public $user; - /** @var string */ - public $comment; +class CommentPostingEvent extends Event +{ + /** @var int */ + public $image_id; + /** @var User */ + public $user; + /** @var string */ + public $comment; - /** - * @param int $image_id - * @param \User $user - * @param string $comment - */ - public function __construct($image_id, $user, $comment) { - assert('is_numeric($image_id)'); - $this->image_id = $image_id; - $this->user = $user; - $this->comment = $comment; - } + public function __construct(int $image_id, User $user, string $comment) + { + $this->image_id = $image_id; + $this->user = $user; + $this->comment = $comment; + } } /** @@ -37,77 +24,85 @@ 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) + { + $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 +{ + public $owner; + public $owner_id; + public $owner_name; + public $owner_email; + public $owner_class; + public $comment; + public $comment_id; + public $image_id; + public $poster_ip; + 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 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']; + } - /** - * @param User $user - * @return mixed - */ - public static function count_comments_by_user($user) { - global $database; - return $database->get_one(" + public static function count_comments_by_user(User $user): int + { + global $database; + return $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, $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); - if($config->get_int("ext_comments_version") < 3) { - // shortcut to latest - if($config->get_int("ext_comments_version") < 1) { - $database->create_table("comments", " + if ($config->get_int("ext_comments_version") < 3) { + // shortcut to latest + if ($config->get_int("ext_comments_version") < 1) { + $database->create_table("comments", " id SCORE_AIPK, image_id INTEGER NOT NULL, owner_id INTEGER NOT NULL, @@ -117,15 +112,15 @@ class CommentList extends Extension { 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)", []); + $config->set_int("ext_comments_version", 3); + } - // the whole history - if($config->get_int("ext_comments_version") < 1) { - $database->create_table("comments", " + // the whole history + if ($config->get_int("ext_comments_version") < 1) { + $database->create_table("comments", " id SCORE_AIPK, image_id INTEGER NOT NULL, owner_id INTEGER NOT NULL, @@ -133,284 +128,318 @@ class CommentList extends Extension { posted SCORE_DATETIME DEFAULT NULL, 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)", []); + $config->set_int("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 ($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($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 ($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"); + } - // 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($_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($event->get_arg(1))); + flash_message("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)); + } + flash_message("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 = int_escape($event->get_arg(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 = 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); + } - 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 $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 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) + { + $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)")); - } - 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; - } + 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 = User::by_name($matches[1]); + if (!is_null($user)) { + $user_id = $user->id; + } else { + $user_id = -1; + } - $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)")); - } - } + $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)")); + } + } -// page building {{{ - /** - * @param int $current_page - */ - private function build_page(/*int*/ $current_page) { - global $database, $user; + 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); + } + } - $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(" + // page building {{{ + private function build_page(int $current_page) + { + global $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(" 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); + $database->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($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; - } + // get 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 +449,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 +468,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 +486,170 @@ 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; + // add / remove / edit comments {{{ + 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 = int_escape($config->get_int('comment_window')); + $max = int_escape($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", null, ["image_id"=>$image_id, "comment_id"=>$cid]); + } - /** - * @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/test.php b/ext/comment/test.php index 93230a71..8967a595 100644 --- a/ext/comment/test.php +++ b/ext/comment/test.php @@ -1,110 +1,111 @@ set_int("comment_limit", 100); - $this->log_out(); - } +class CommentListTest extends ShimmiePHPUnitTestCase +{ + public function setUp() + { + global $config; + parent::setUp(); + $config->set_int("comment_limit", 100); + $this->log_out(); + } - public function tearDown() { - global $config; - $config->set_int("comment_limit", 10); - parent::tearDown(); - } + public function tearDown() + { + 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->assertContains("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->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->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->assertContains("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->assertContains("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() + { + $this->markTestIncomplete(); - $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 + $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"); - # delete it - $this->click("Del"); - $this->assert_title("Image $image_id: pbx"); - $this->assert_no_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(); - } + # tidy up + $this->delete_image($image_id); + $this->log_out(); + } } diff --git a/ext/comment/theme.php b/ext/comment/theme.php index f017bdb3..31b77a7d 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'); + + foreach ($images as $pair) { + $image = $pair[0]; + $comments = $pair[1]; - $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]; + $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 = ""; - - $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 = ' + $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,163 @@ 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; + + 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")); - $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; + + //$search_terms = array('I','have','no','idea','what','this','does!'); + //$u_tags = url_escape(Tag::implode($search_terms)); + //$query = empty($u_tags) ? "" : '/'.$u_tags; - $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"; - $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); - } + $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); + } - /** - * @param \Comment $comment - * @param bool $trim - * @return string - */ - protected function comment_to_html(Comment $comment, $trim=false) { - global $config, $user; + protected function comment_to_html(Comment $comment, bool $trim=false): string + { + global $config, $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_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); + $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); - 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.''; - } + 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 = " + $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 = "
    "; + } + $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,22 +266,19 @@ 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() : ""; + $i_image_id = int_escape($image_id); + $hash = CommentList::get_hash(); + $h_captcha = $config->get_bool("comment_captcha") ? captcha_get_html() : ""; - return ' + return '
    '.make_form(make_link("comment/add")).' @@ -321,6 +289,28 @@ class CommentListTheme extends Themelet {
    '; - } -} + } + 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/info.php b/ext/cron_uploader/info.php new file mode 100644 index 00000000..138d1565 --- /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..f9e5557f 100644 --- a/ext/cron_uploader/main.php +++ b/ext/cron_uploader/main.php @@ -1,83 +1,96 @@ - * 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 + + +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 + + const QUEUE_DIR = "queue"; + const UPLOADED_DIR = "uploaded"; + const FAILED_DIR = "failed_to_upload"; + + const CONFIG_KEY = "cron_uploader_key"; + const CONFIG_COUNT = "cron_uploader_count"; + const CONFIG_DIR = "cron_uploader_dir"; + + /** + * Lists all log events this session + * @var string + */ + private $upload_info = ""; + + /** + * Lists all files & info required to upload. + * @var array + */ + private $image_queue = []; + + /** + * 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. + */ + public function onPageRequest(PageRequestEvent $event) + { + global $config, $user; + + if ($event->page_matches("cron_upload")) { + $this->upload_key = $config->get_string(self::CONFIG_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->set_dir(); + + $lockfile = fopen($this->root_dir . "/.lock", "w"); + if (!flock($lockfile, LOCK_EX | LOCK_NB)) { + throw new Exception("Cron upload process is already running"); + } + try { + $this->process_upload(); // Start upload + } finally { + flock($lockfile, LOCK_UN); + fclose($lockfile); + } + } elseif ($user->can(Permissions::BULK_ADD)) { + $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 . "/" . self::QUEUE_DIR; + $uploaded_dir = $this->root_dir . "/" . self::UPLOADED_DIR; + $failed_dir = $this->root_dir . "/" . self::FAILED_DIR; + + $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
    @@ -105,8 +118,8 @@ class CronUploader extends Extension {
    Cron Command:
    Create a cron job with the command above.
    Read the documentation if you're not sure what to do.
    "; - - $install_html = " + + $install_html = " This cron uploader is fairly easy to use but has to be configured first.
    1. Install & activate this plugin.
    @@ -130,301 +143,335 @@ class CronUploader extends Extension {
    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); - } - public function onInitExt(InitExtEvent $event) { - global $config; - // Set default values - if ($config->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:
    + $page->set_title("Cron Uploader"); + $page->set_heading("Cron Uploader"); + + $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); + } + + public function onInitExt(InitExtEvent $event) + { + global $config; + // Set default values + $config->set_default_int(self::CONFIG_COUNT, 1); + $this->set_dir(); + + $this->upload_key = $config->get_string(self::CONFIG_KEY, ""); + if (empty($this->upload_key)) { + $this->upload_key = $this->generate_key(); + + $config->set_string(self::CONFIG_KEY, $this->upload_key); + } + } + + 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(self::CONFIG_COUNT, "How many to upload each time"); + $sb->add_text_option(self::CONFIG_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."); - $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); + $event->panel->add_block($sb); + } - // 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\"."); - } + /* + * Generates a unique key for the website to prevent unauthorized access. + */ + private function generate_key() + { + $length = 20; + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $randomString = ''; - /** - * 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); - } + 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(self::CONFIG_DIR, ""); + + // Sets new default dir if not in config yet/anymore + if ($dir == "") { + $dir = data_path("cron_uploader"); + $config->set_string(self::CONFIG_DIR, $dir); + } + + // Make the directory if it doesn't exist yet + if (!is_dir($dir . "/" . self::QUEUE_DIR . "/")) { + mkdir($dir . "/" . self::QUEUE_DIR . "/", 0775, true); + } + if (!is_dir($dir . "/" . self::UPLOADED_DIR . "/")) { + mkdir($dir . "/" . self::UPLOADED_DIR . "/", 0775, true); + } + if (!is_dir($dir . "/" . self::FAILED_DIR . "/")) { + mkdir($dir . "/" . self::FAILED_DIR . "/", 0775, true); + } + + $this->root_dir = $dir; + return $dir; + } + + /** + * Returns amount of files & total size of dir. + */ + public function scan_dir(string $path): array + { + $bytestotal = 0; + $nbfiles = 0; + + $ite = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS); + 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 ['total_files' => $nbfiles, 'total_mb' => $size_mb]; + } + + /** + * Uploads the image & handles everything + */ + public function process_upload(int $upload_count = 0): bool + { + global $config, $database; + + //set_time_limit(0); + + + $output_subdir = date('Ymd-His', time()) . "/"; + $this->generate_image_queue(); + + // Gets amount of imgs to upload + if ($upload_count == 0) { + $upload_count = $config->get_int(self::CONFIG_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); + + $merged = 0; + $added = 0; + $failed = 0; + + // Upload the file(s) + for ($i = 0; $i < $upload_count && sizeof($this->image_queue) > 0; $i++) { + $img = array_pop($this->image_queue); + + try { + $database->beginTransaction(); + $this->add_upload_info("Adding file: {$img[1]} - 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) { + $failed++; + $this->move_uploaded($img[0], $img[1], $output_subdir, true); + $msgNumber = $this->add_upload_info("(" . gettype($e) . ") " . $e->getMessage()); + $msgNumber = $this->add_upload_info($e->getTraceAsString()); + + try { + $database->rollback(); + } catch (Exception $e) { + } + } + } + + + $msgNumber = $this->add_upload_info("Items added: $added"); + $msgNumber = $this->add_upload_info("Items merged: $merged"); + $msgNumber = $this->add_upload_info("Items failed: $failed"); + + + // Display & save upload log + $this->handle_log(); + + return true; + } + + private function move_uploaded($path, $filename, $output_subdir, $corrupt = false) + { + global $config; + + // Create + $newDir = $this->root_dir; + + $relativeDir = dirname(substr($path, strlen($this->root_dir) + 7)); + + // Determine which dir to move to + if ($corrupt) { + // Move to corrupt dir + $newDir .= "/" . self::FAILED_DIR . "/" . $output_subdir . $relativeDir; + $info = "ERROR: Image was not uploaded."; + } else { + $newDir .= "/" . self::UPLOADED_DIR . "/" . $output_subdir . $relativeDir; + $info = "Image successfully uploaded. "; + } + $newDir = str_replace("//", "/", $newDir . "/"); + + if (!is_dir($newDir)) { + mkdir($newDir, 0775, true); + } + + // move file to correct dir + rename($path, $newDir . $filename); + + $this->add_upload_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 + $infomsg = ""; // Will contain info message + if ($event->image_id == -1) { + throw new Exception("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}"; + } + $msgNumber = $this->add_upload_info($infomsg); + + // Set tags + $img = Image::by_id($event->image_id); + $img->set_tags(array_merge($tagArray, $img->get_tag_array())); + + return $event; + } + + private function generate_image_queue(): void + { + $base = $this->root_dir . "/" . self::QUEUE_DIR; + + if (!is_dir($base)) { + $this->add_upload_info("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)) { + $pathinfo = pathinfo($fullpath); + + $relativePath = substr($fullpath, strlen($base)); + $tags = path_to_tags($relativePath); + + $img = [ + 0 => $fullpath, + 1 => $pathinfo ["basename"], + 2 => $tags + ]; + array_push($this->image_queue, $img); + } + } + } + + /** + * Adds a message to the info being published at the end + */ + private function add_upload_info(string $text, int $addon = 0): int + { + $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(PageMode::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); + } } - diff --git a/ext/custom_html_headers/info.php b/ext/custom_html_headers/info.php new file mode 100644 index 00000000..5ac483d3 --- /dev/null +++ b/ext/custom_html_headers/info.php @@ -0,0 +1,30 @@ + + * Link: http://www.drudexsoftware.com + * License: GPLv2 + * Description: Allows admins to modify & set custom <head> content + * Documentation: + * + */ +class CustomHtmlHeadersInfo extends ExtensionInfo +{ + public const KEY = "custom_html_headers"; + + public $key = self::KEY; + public $name = "Custom HTML Headers"; + public $url = "http://www.drudexsoftware.com"; + public $authors = ["Drudex Software"=>"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..5c6c6f18 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 { + +class CustomHtmlHeaders extends Extension +{ # Adds setup block for custom 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" => 0, + "as prefix" => 1, + "as suffix" => 2 + ], "
    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_int("sitename_in_title", 0); + } + + # 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_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 - } + } + + private function handle_modified_page_title() + { + global $config, $page; + + // get config values + $site_title = $config->get_string(SetupConfig::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 + elseif ($sitename_in_title == 2) { + $page->title = "$page->title - $site_title"; + } // as suffix } + } } - diff --git a/ext/danbooru_api/info.php b/ext/danbooru_api/info.php new file mode 100644 index 00000000..89047d3b --- /dev/null +++ b/ext/danbooru_api/info.php @@ -0,0 +1,61 @@ + +Description: Allow Danbooru apps like Danbooru Uploader for Firefox to communicate with Shimmie +Documentation: + +*/ + +class DanbooruApiInfo extends ExtensionInfo +{ + public const KEY = "danbooru_api"; + + public $key = self::KEY; + public $name = "Danbooru Client API"; + public $authors = ["JJS"=>"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..304bd855 100644 --- a/ext/danbooru_api/main.php +++ b/ext/danbooru_api/main.php @@ -1,269 +1,222 @@ -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 -
    -CHANGELOG -13-OCT-08 8:00PM CST - JJS -Bugfix - Properly escape source attribute +class DanbooruApi extends Extension +{ + public function onPageRequest(PageRequestEvent $event) + { + if ($event->page_matches("api") && ($event->get_arg(0) == 'danbooru')) { + $this->api_danbooru($event); + } + } -17-SEP-08 10:00PM CST - JJS -Bugfix for changed page name checker in PageRequestEvent + // Danbooru API + private function api_danbooru(PageRequestEvent $event) + { + global $page; + $page->set_mode(PageMode::DATA); -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 + 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()); + } -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 + // 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(PageMode::REDIRECT); + $page->set_redirect($fixedlocation); + } + } -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. + /** + * 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; -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 + 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)); + } + } -*/ - -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] + ); + 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 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']); + } 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", + [$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($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 +226,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, $config, $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..4fd2812f 100644 --- a/ext/danbooru_api/test.php +++ b/ext/danbooru_api/test.php @@ -1,23 +1,25 @@ log_in_as_admin(); +class DanbooruApiTest extends ShimmiePHPUnitTestCase +{ + public function testSearch() + { + $this->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 + $this->get_page("api/danbooru/post/show/$image_id"); + //$this->assert_response(302); // FIXME - $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..fd8df943 --- /dev/null +++ b/ext/downtime/info.php @@ -0,0 +1,28 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: Show a "down for maintenance" page + * Documentation: + * + */ + +class DowntimeInfo extends ExtensionInfo +{ + public const KEY = "downtime"; + + public $key = self::KEY; + public $name = "Downtime"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Show a \"down for maintenance\" page"; + public $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."; +} diff --git a/ext/downtime/main.php b/ext/downtime/main.php index 3eb44164..979b119f 100644 --- a/ext/downtime/main.php +++ b/ext/downtime/main.php @@ -1,46 +1,44 @@ - * 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. - */ -class Downtime extends Extension { - public function get_priority() {return 10;} +class Downtime extends Extension +{ + public function get_priority(): int + { + return 10; + } - 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); - } + 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); + } - public function onPageRequest(PageRequestEvent $event) { - global $config, $page, $user; + public function onPageRequest(PageRequestEvent $event) + { + global $config, $page, $user; - 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); - } - } + 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; - } + 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..fb5bec90 100644 --- a/ext/downtime/test.php +++ b/ext/downtime/test.php @@ -1,39 +1,42 @@ set_bool("downtime", false); - } +class DowntimeTest extends ShimmiePHPUnitTestCase +{ + public function tearDown() + { + global $config; + $config->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..99c4cffc 100644 --- a/ext/downtime/theme.php +++ b/ext/downtime/theme.php @@ -1,31 +1,35 @@ 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(<<set_mode(PageMode::DATA); + $page->set_code(503); + $page->set_data( + << Downtime @@ -63,5 +67,5 @@ class DowntimeTheme extends Themelet { EOD ); - } + } } diff --git a/ext/emoticons/info.php b/ext/emoticons/info.php new file mode 100644 index 00000000..8c19165d --- /dev/null +++ b/ext/emoticons/info.php @@ -0,0 +1,30 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: Lets users use graphical smilies + * Documentation: + * + */ + +class EmoticonsInfo extends ExtensionInfo +{ + public const KEY = "emoticons"; + + public $key = self::KEY; + public $name = "Emoticon Filter"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $dependencies = [EmoticonListInfo::KEY]; + public $description = "Lets users use graphical smilies"; + public $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."; +} diff --git a/ext/emoticons/main.php b/ext/emoticons/main.php index e6245daf..73d5bee2 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. - */ + /** * Class Emoticons */ -class Emoticons extends FormatterExtension { - /** - * @param string $text - * @return string - */ - public function format(/*string*/ $text) { - $data_href = get_base_href(); - $text = preg_replace("/:([a-z]*?):/s", "", $text); - return $text; - } +class Emoticons extends FormatterExtension +{ + public function format(string $text): string + { + $data_href = get_base_href(); + $text = preg_replace("/:([a-z]*?):/s", "", $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..2867dbb6 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..601ad61d --- /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..212ebd20 --- /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:
    "; + $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..3245f10c --- /dev/null +++ b/ext/et/info.php @@ -0,0 +1,25 @@ + + * License: GPLv2 + * Description: Show various bits of system information + * Documentation: + */ + +class ETInfo extends ExtensionInfo +{ + public const KEY = "et"; + + public $key = self::KEY; + public $name = "System Info"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Show various bits of system information"; + public $documentation = +"Knowing the information that this extension shows can be very useful for debugging. There's also an option to send +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."; +} diff --git a/ext/et/main.php b/ext/et/main.php index 21f1f77a..db7851be 100644 --- a/ext/et/main.php +++ b/ext/et/main.php @@ -1,85 +1,92 @@ - * 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. - */ -class ET extends Extension { - public function onPageRequest(PageRequestEvent $event) { - global $user; - if($event->page_matches("system_info")) { - if($user->can("view_sysinfo")) { - $this->theme->display_info_page($this->get_info()); - } - } - } +class ET extends Extension +{ + 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()); + } + } + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("view_sysinfo")) { - $event->add_link("System Info", make_link("system_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(); - $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 onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::VIEW_SYSINTO)) { + $event->add_link("System Info", make_link("system_info")); + } + } - $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() + { + 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_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'; - 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 (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); + + //$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..c9508107 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"); - } +class ETTest extends ShimmiePHPUnitTestCase +{ + public function testET() + { + $this->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..0582fddf 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,6 @@ EOD; of web servers / databases / etc I need to support. EOD; - return $html; - } + return $html; + } } - diff --git a/ext/ext_manager/info.php b/ext/ext_manager/info.php new file mode 100644 index 00000000..80ade6e9 --- /dev/null +++ b/ext/ext_manager/info.php @@ -0,0 +1,26 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Visibility: admin + * Description: A thing for point & click extension management + * Documentation: + */ + +class ExtManagerInfo extends ExtensionInfo +{ + public const KEY = "ext_manager"; + + public $key = self::KEY; + public $name = "Extension Manager"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $visibility = self::VISIBLE_ADMIN; + public $description = "A thing for point & click extension management"; + public $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"; + public $core = true; +} diff --git a/ext/ext_manager/main.php b/ext/ext_manager/main.php index 3a19809d..8a4add73 100644 --- a/ext/ext_manager/main.php +++ b/ext/ext_manager/main.php @@ -1,213 +1,151 @@ - * 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 - */ -/** - * @private - * @return int - */ -function __extman_extcmp(ExtensionInfo $a, ExtensionInfo $b) { - return strcmp($a->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 +{ + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; + if ($event->page_matches("ext_manager")) { + if ($user->can(Permissions::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", "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); + } + } - 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")); - } - } + if ($event->page_matches("ext_doc")) { + $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); + } + } + } - /** - * @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; - } + 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([]); + } + } - private function set_things($settings) { - $core = explode(",", CORE_EXTS); - $extras = array(); + 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"); + } + } + } - foreach(glob("ext/*/main.php") as $main) { - $matches = array(); - preg_match("#ext/(.*)/main.php#", $main, $matches); - $fname = $matches[1]; - - 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")); + } else { + $event->add_link("Help", make_link("ext_doc")); + } + } /** - * @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) { + $matches = []; + 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..6af85a07 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"); +class ExtManagerTest extends ShimmiePHPUnitTestCase +{ + public function testAuth() + { + $this->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..27cc896b 100644 --- a/ext/ext_manager/theme.php +++ b/ext/ext_manager/theme.php @@ -1,15 +1,15 @@ Enabled" : ""; - $html = " - ".make_form(make_link("ext_manager/set"))." +class ExtManagerTheme extends Themelet +{ + /** + * #param ExtensionInfo[] $extensions + */ + public function display_table(Page $page, array $extensions, bool $editable) + { + $h_en = $editable ? "Enabled" : ""; + $html = " + " . make_form(make_link("ext_manager/set")) . " @@ -21,123 +21,139 @@ class ExtManagerTheme extends Themelet { "; - foreach($extensions as $extension) { - if(!$editable && $extension->visibility == "admin") continue; + foreach ($extensions as $extension) { + if ((!$editable && $extension->visibility === ExtensionInfo::VISIBLE_ADMIN) + || $extension->visibility === ExtensionInfo::VISIBLE_HIDDEN) { + continue; + } - $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. + $h_name = html_escape(($extension->beta===true ? "[BETA] ":"").(empty($extension->name) ? $extension->key : $extension->name)); + $h_description = html_escape($extension->description); + $h_link = make_link("ext_doc/" . url_escape($extension->key)); - $html .= " - + $h_enabled = ($extension->is_enabled() === true ? " checked='checked'" : ""); + $h_disabled = ($extension->is_supported()===false || $extension->core===true? " disabled ": " "); + + //baseline_open_in_new_black_18dp.png + + $h_enabled_box = $editable ? "" : ""; + $h_docs = ($extension->documentation ? "" : ""); //TODO: A proper "docs" symbol would be preferred here. + + $html .= " + {$h_enabled_box} - + - + "; - } - $h_set = $editable ? "" : ""; - $html .= " + } + $h_set = $editable ? "" : ""; + $html .= " $h_set
    {$h_name} {$h_docs}{$h_description}{$h_description} ".$extension->get_support_info()."
    "; - $page->set_title("Extensions"); - $page->set_heading("Extensions"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Extension Manager", $html)); - } + $page->set_title("Extensions"); + $page->set_heading("Extensions"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Extension Manager", $html)); + } - /* - 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"; + /* + public function display_blocks(Page $page, $extensions) { + global $user; + $col_1 = ""; + $col_2 = ""; + foreach($extensions as $extension) { + $ext_name = $extension->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) ? + "name))."\">Documentation" : ""; + $h_author = html_escape($extension->author); + $h_description = html_escape($extension->description); + $h_enabled = $extension->is_enabled() ? " checked='checked'" : ""; + $h_author_link = empty($h_email) ? + "$h_author" : + "$h_author"; - $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
    - - "; + $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", $html)); - } - */ + $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 = ""; - 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 = " + public function display_doc(Page $page, ExtensionInfo $info) + { + $author = ""; + if (count($info->authors) > 0) { + $author = "
    Author"; + if (count($info->authors) > 1) { + $author .= "s"; + } + $author .= ":"; + foreach ($info->authors as $auth=>$email) { + if (!empty($email)) { + $author .= "" . html_escape($auth) . ""; + } else { + $author .= html_escape($auth); + } + $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 +

    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", $html)); + } } - diff --git a/ext/favorites/info.php b/ext/favorites/info.php new file mode 100644 index 00000000..0e49b8d1 --- /dev/null +++ b/ext/favorites/info.php @@ -0,0 +1,25 @@ + + * License: GPLv2 + * Description: Allow users to favorite images + * Documentation: + */ + +class FavoritesInfo extends ExtensionInfo +{ + public const KEY = "favorites"; + + public $key = self::KEY; + public $name = "Favorites"; + public $authors = ["Daniel Marschall"=>"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..cd246b32 100644 --- a/ext/favorites/main.php +++ b/ext/favorites/main.php @@ -1,163 +1,181 @@ - * 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 - */ -class FavoriteSetEvent extends Event { - /** @var int */ - public $image_id; - /** @var \User */ - public $user; - /** @var bool */ - public $do_set; +class FavoriteSetEvent extends Event +{ + /** @var int */ + public $image_id; + /** @var User */ + public $user; + /** @var bool */ + public $do_set; - /** - * @param int $image_id - * @param User $user - * @param bool $do_set - */ - public function __construct(/*int*/ $image_id, User $user, /*boolean*/ $do_set) { - assert(is_int($image_id)); - assert(is_bool($do_set)); + public function __construct(int $image_id, User $user, bool $do_set) + { + assert(is_int($image_id)); + assert(is_bool($do_set)); - $this->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 +{ + public function onInitExt(InitExtEvent $event) + { + global $config; + if ($config->get_int("ext_favorites_version", 0) < 1) { + $this->install(); + } + } - 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($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 ( + 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', $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) + { + $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 = User::by_name($matches[1]); + if (!is_null($user)) { + $user_id = $user->id; + } else { + $user_id = -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) { + $block = new Block(); + $block->header = "Favorites"; + $block->body = $this->theme->get_help_html(); + $event->add_block($block); + } + } - 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"); + } + } + } + + private function install() + { + global $database; + global $config; + + 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", " image_id INTEGER NOT NULL, user_id INTEGER NOT NULL, created_at SCORE_DATETIME NOT NULL, @@ -165,53 +183,52 @@ class Favorites extends Extension { 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)", []); + $config->set_int("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 ($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)"); - 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;"); + $config->set_int("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) { + $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..59c97fcc 100644 --- a/ext/favorites/test.php +++ b/ext/favorites/test.php @@ -1,29 +1,30 @@ log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test"); +class FavoritesTest extends ShimmiePHPUnitTestCase +{ + public function testFavorites() + { + $this->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"); + $this->get_page("post/view/$image_id"); + $this->assert_title("Image $image_id: test"); + $this->assert_no_text("Favorited By"); - $this->markTestIncomplete(); + $this->markTestIncomplete(); - $this->click("Favorite"); - $this->assert_text("Favorited By"); + $this->click("Favorite"); + $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"); + $this->get_page("post/list/favorited_by=test/1"); + $this->assert_title("Image $image_id: test"); + $this->assert_text("Favorited By"); - $this->get_page("user/test"); - $this->assert_text("Images favorited: 1"); - $this->click("Images favorited"); - $this->assert_title("Image $image_id: test"); + $this->get_page("user/test"); + $this->assert_text("Images favorited: 1"); + $this->click("Images favorited"); + $this->assert_title("Image $image_id: test"); - $this->click("Un-Favorite"); - $this->assert_no_text("Favorited By"); - } + $this->click("Un-Favorite"); + $this->assert_no_text("Favorited By"); + } } - diff --git a/ext/favorites/theme.php b/ext/favorites/theme.php index ae502ab2..cd03afbb 100644 --- a/ext/favorites/theme.php +++ b/ext/favorites/theme.php @@ -1,11 +1,13 @@ id); - $name = $is_favorited ? "unset" : "set"; - $label = $is_favorited ? "Un-Favorite" : "Favorite"; - $html = " +class FavoritesTheme extends Themelet +{ + public function get_voter_html(Image $image, $is_favorited) + { + $i_image_id = int_escape($image->id); + $name = $is_favorited ? "unset" : "set"; + $label = $is_favorited ? "Un-Favorite" : "Favorite"; + $html = " ".make_form(make_link("change_favorite"))." @@ -13,24 +15,46 @@ class FavoritesTheme extends Themelet { "; - return $html; - } + 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. - - foreach($username_array as $row) { - $username = html_escape($row); - $html .= "
    $username"; - } + reset($username_array); // rewind to first element in array. + + 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..b28277e1 --- /dev/null +++ b/ext/featured/info.php @@ -0,0 +1,35 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: Bring a specific image to the users' attentions + * Documentation: + * + */ + +class FeaturedInfo extends ExtensionInfo +{ + public const KEY = "featured"; + + public $key = self::KEY; + public $name = "Featured Image"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Bring a specific image to the users' attentions"; + public $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."; +} diff --git a/ext/featured/main.php b/ext/featured/main.php index 85e2459e..8d4d96c9 100644 --- a/ext/featured/main.php +++ b/ext/featured/main.php @@ -1,89 +1,74 @@ - * 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. - */ -class Featured extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config; - $config->set_default_int('featured_id', 0); - } +class Featured extends Extension +{ + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_int('featured_id', 0); + } - 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 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 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 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 (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("edit_feature")) { - $event->add_part($this->theme->get_buttons_html($event->image->id)); - } - } + 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..45800e3b 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"); +class FeaturedTest extends ShimmiePHPUnitTestCase +{ + public function testFeatured() + { + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - # FIXME: test that regular users can't feature things + # FIXME: test that regular users can't feature things - $this->log_in_as_admin(); - $this->get_page("post/view/$image_id"); - $this->assert_title("Image $image_id: pbx"); + $this->log_in_as_admin(); + $this->get_page("post/view/$image_id"); + $this->assert_title("Image $image_id: pbx"); - $this->markTestIncomplete(); + $this->markTestIncomplete(); - $this->click("Feature This"); - $this->get_page("post/list"); - $this->assert_text("Featured Image"); + $this->click("Feature This"); + $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); + $this->get_page("featured_image/download"); + $this->assert_response(200); - $this->get_page("featured_image/view"); - $this->assert_response(200); + $this->get_page("featured_image/view"); + $this->assert_response(200); - $this->delete_image($image_id); - $this->log_out(); + $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->get_page("post/list"); + $this->assert_no_text("Featured Image"); + } } - diff --git a/ext/featured/theme.php b/ext/featured/theme.php index 9fc6a74f..b601a6b7 100644 --- a/ext/featured/theme.php +++ b/ext/featured/theme.php @@ -1,48 +1,36 @@ 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 " + public function get_buttons_html(int $image_id): string + { + global $user; + return " ".make_form(make_link("featured_image/set"))." ".$user->get_auth_html()." "; - } + } - /** - * @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 = 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); - return " + return " {$h_tip} "; - } + } } - diff --git a/ext/forum/info.php b/ext/forum/info.php new file mode 100644 index 00000000..5671e8b2 --- /dev/null +++ b/ext/forum/info.php @@ -0,0 +1,21 @@ + + * Alpha + * License: GPLv2 + * Description: Rough forum extension + * Documentation: + */ + +class ForumInfo extends ExtensionInfo +{ + public const KEY = "dorum"; + + public $key = self::KEY; + public $name = "Forum"; + public $authors = ["Sein Kraft"=>"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..064c09fe 100644 --- a/ext/forum/main.php +++ b/ext/forum/main.php @@ -1,12 +1,4 @@ - * Alpha - * License: GPLv2 - * Description: Rough forum extension - * Documentation: - */ /* Todo: *Quote buttons on posts @@ -15,14 +7,16 @@ Todo: *Smiley filter, word filter, etc should work with our extension */ -class Forum extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config, $database; +class Forum extends Extension +{ + public function onInitExt(InitExtEvent $event) + { + global $config, $database; - // shortcut to latest + // shortcut to latest - if ($config->get_int("forum_version") < 1) { - $database->create_table("forum_threads", " + 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, @@ -31,9 +25,9 @@ class Forum extends Extension { uptodate SCORE_DATETIME NOT NULL, 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, @@ -42,392 +36,362 @@ class Forum extends Extension { 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=?", [$event->display_user->id]); + $posts_count = $database->get_one("SELECT COUNT(*) FROM forum_posts WHERE 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 = ?", [$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 = ? ", [$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; + $pageNumber = $event->get_arg(1); + $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 (is_null($pageNumber) || !is_numeric($pageNumber)) { + $pageNumber = 0; + } elseif ($pageNumber <= 0) { + $pageNumber = 0; + } elseif ($pageNumber >= $totalPages) { + $pageNumber = $totalPages - 1; + } else { + $pageNumber--; + } - $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 = $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 = ?", [$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 (is_null($pageNumber) || !is_numeric($pageNumber)) { + $pageNumber = 0; + } elseif ($pageNumber <= 0) { + $pageNumber = 0; + } elseif ($pageNumber >= $totalPages) { + $pageNumber = $totalPages - 1; + } else { + $pageNumber--; + } - $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] + ); - $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 + global $database; + $database->execute("INSERT INTO forum_posts (thread_id, user_id, date, message) VALUES - (?, ?, now(), ?)" - , array($threadID, $userID, $message)); + (?, ?, now(), ?)", [$threadID, $userID, $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=?", [$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 = ?", [$threadID]); + $database->execute("DELETE FROM forum_posts WHERE 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 = ?", [$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= ?)", [$threadID]); + if ($result==1) { + return true; + } else { + return false; + } + } } diff --git a/ext/forum/theme.php b/ext/forum/theme.php index 74d7c5df..f6d44a1f 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,60 +81,59 @@ 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); - + + $message = stripslashes($message); + $user = "".$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 .= " @@ -149,20 +151,18 @@ class ForumTheme extends Themelet { "; - } - + $html .= "
    UserMessage
    "; $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/google_analytics/info.php b/ext/google_analytics/info.php new file mode 100644 index 00000000..a62cc880 --- /dev/null +++ b/ext/google_analytics/info.php @@ -0,0 +1,24 @@ + + * Link: http://drudexsoftware.com + * License: GPLv2 + * Description: Integrates Google Analytics tracking + * Documentation: + * + */ +class GoogleAnalyticsInfo extends ExtensionInfo +{ + public const KEY = "google_analytics"; + + public $key = self::KEY; + public $name = "Google Analytics"; + public $url = "http://drudexsoftware.com"; + public $authors = ["Drudex Software"=>"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..fb702dfb 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/info.php b/ext/handle_404/info.php new file mode 100644 index 00000000..f18367c8 --- /dev/null +++ b/ext/handle_404/info.php @@ -0,0 +1,23 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Visibility: admin + * Description: If no other extension puts anything onto the page, show 404 + */ +class Handle404Info extends ExtensionInfo +{ + public const KEY = "handle_404"; + + public $key = self::KEY; + public $name = "404 Detector"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $visibility = self::VISIBLE_ADMIN; + public $description = "If no other extension puts anything onto the page, show 404"; + public $core = true; +} diff --git a/ext/handle_404/main.php b/ext/handle_404/main.php index a17e80f7..69ca4123 100644 --- a/ext/handle_404/main.php +++ b/ext/handle_404/main.php @@ -1,53 +1,36 @@ - * 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"; +class Handle404 extends Extension +{ + public function onPageRequest(PageRequestEvent $event) + { + global $config, $page; + // hax. + if ($page->mode == PageMode::PAGE && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) { + $h_pagename = html_escape(implode('/', $event->args)); + 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'")); + } + } - $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; + } - 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;} + public function get_priority(): int + { + return 99; + } } - diff --git a/ext/handle_404/test.php b/ext/handle_404/test.php index 2d7c9f73..e8bb27be 100644 --- a/ext/handle_404/test.php +++ b/ext/handle_404/test.php @@ -1,14 +1,12 @@ 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); - } +class Handle404Test extends ShimmiePHPUnitTestCase +{ + public function test404Handler() + { + $this->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/handle_archive/info.php b/ext/handle_archive/info.php new file mode 100644 index 00000000..174e18d3 --- /dev/null +++ b/ext/handle_archive/info.php @@ -0,0 +1,25 @@ + + * Description: Allow users to upload archives (zip, etc) + * Documentation: + * + */ + +class ArchiveFileHandlerInfo extends ExtensionInfo +{ + public const KEY = "handle_archive"; + + public $key = self::KEY; + public $name = "Handle Archives"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $description = "Allow users to upload archives (zip, etc)"; + public $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\""; +} diff --git a/ext/handle_archive/main.php b/ext/handle_archive/main.php index ad43c4ac..d78ac219 100644 --- a/ext/handle_archive/main.php +++ b/ext/handle_archive/main.php @@ -1,56 +1,47 @@ - * 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" - */ -class ArchiveFileHandler extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config; - $config->set_default_string('archive_extract_command', 'unzip -d "%d" "%f"'); - } +class ArchiveFileHandler extends Extension +{ + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_string('archive_extract_command', 'unzip -d "%d" "%f"'); + } - 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 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 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 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 + } + } - /** - * @param string $ext - * @return bool - */ - private function supported_ext($ext) { - $exts = array("zip"); - return in_array(strtolower($ext), $exts); - } + private function supported_ext($ext) + { + $exts = ["zip"]; + return in_array(strtolower($ext), $exts); + } } diff --git a/ext/handle_flash/info.php b/ext/handle_flash/info.php new file mode 100644 index 00000000..9d997aaa --- /dev/null +++ b/ext/handle_flash/info.php @@ -0,0 +1,19 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * Description: Handle Flash files. + */ + +class FlashFileHandlerInfo extends ExtensionInfo +{ + public const KEY = "handle_flash"; + + public $key = self::KEY; + public $name = "Handle Flash"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $description = "Handle Flash files."; +} diff --git a/ext/handle_flash/main.php b/ext/handle_flash/main.php index 719648d4..df93b408 100644 --- a/ext/handle_flash/main.php +++ b/ext/handle_flash/main.php @@ -1,67 +1,71 @@ - * Link: http://code.shishnet.org/shimmie2/ - * Description: Handle Flash files. (No thumbnail is generated for flash files) - */ -class FlashFileHandler extends DataHandlerExtension { - /** - * @param string $hash - * @return bool - */ - protected function create_thumb($hash) { - copy("ext/handle_flash/thumb.jpg", warehouse_path("thumbs", $hash)); - return true; - } +class FlashFileHandler extends DataHandlerExtension +{ + public function onMediaCheckProperties(MediaCheckPropertiesEvent $event) + { + switch ($event->ext) { + case "swf": + $event->lossless = true; + $event->video = true; - /** - * @param string $ext - * @return bool - */ - protected function supported_ext($ext) { - $exts = array("swf"); - return in_array(strtolower($ext), $exts); - } + $info = getimagesize($event->file_name); + if (!$info) { + return null; + } - /** - * @param string $filename - * @param array $metadata - * @return Image|null - */ - protected function create_image_from_data(/*string*/ $filename, /*array*/ $metadata) { - $image = new Image(); + $event->width = $info[0]; + $event->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']; + break; + } + } - $info = getimagesize($filename); - if(!$info) return null; + protected function create_thumb(string $hash, string $type): bool + { + global $config; - $image->width = $info[0]; - $image->height = $info[1]; + if (!Media::create_thumbnail_ffmpeg($hash)) { + copy("ext/handle_flash/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash)); + } + return true; + } - return $image; - } + protected function supported_ext(string $ext): bool + { + $exts = ["swf"]; + return in_array(strtolower($ext), $exts); + } - /** - * @param string $file - * @return bool - */ - protected function check_contents(/*string*/ $file) { - if (!file_exists($file)) return false; + protected function create_image_from_data(string $filename, array $metadata) + { + $image = new Image(); - $fp = fopen($file, "r"); - $head = fread($fp, 3); - fclose($fp); - if (!in_array($head, array("CWS", "FWS"))) return false; + $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 true; - } + + + return $image; + } + + protected function check_contents(string $tmpname): bool + { + if (!file_exists($tmpname)) { + return false; + } + + $fp = fopen($tmpname, "r"); + $head = fread($fp, 3); + fclose($fp); + if (!in_array($head, ["CWS", "FWS"])) { + return false; + } + + return true; + } } - diff --git a/ext/handle_flash/theme.php b/ext/handle_flash/theme.php index e4557088..a630da5e 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..64ba9984 --- /dev/null +++ b/ext/handle_ico/info.php @@ -0,0 +1,18 @@ + + * Description: Handle windows icons + */ + +class IcoFileHandlerInfo extends ExtensionInfo +{ + public const KEY = "handle_ico"; + + public $key = self::KEY; + public $name = "Handle ICO"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $description = "Handle windows icons"; +} diff --git a/ext/handle_ico/main.php b/ext/handle_ico/main.php index 31d5e337..70d9b753 100644 --- a/ext/handle_ico/main.php +++ b/ext/handle_ico/main.php @@ -1,114 +1,71 @@ - * Description: Handle windows icons - */ -class IcoFileHandler extends Extension { - public function onDataUpload(DataUploadEvent $event) { - if($this->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 +{ + const SUPPORTED_EXTENSIONS = ["ico", "ani", "cur"]; - public function onThumbnailGeneration(ThumbnailGenerationEvent $event) { - if($this->supported_ext($event->type)) { - $this->create_thumb($event->hash); - } - } - public function onDisplayingImage(DisplayingImageEvent $event) { - global $page; - if($this->supported_ext($event->image->ext)) { - $this->theme->display_image($page, $event->image); - } - } + public function onMediaCheckProperties(MediaCheckPropertiesEvent $event) + { + if (in_array($event->ext, self::SUPPORTED_EXTENSIONS)) { + $event->lossless = true; + $event->video = false; + $event->audio = false; - /** - * @param string $ext - * @return bool - */ - private function supported_ext($ext) { - $exts = array("ico", "ani", "cur"); - return in_array(strtolower($ext), $exts); - } + $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 $filename - * @param mixed[] $metadata - * @return Image - */ - private function create_image_from_data($filename, $metadata) { - $image = new Image(); + $width = $subheader['width']; + $height = $subheader['height']; + $event->width = $width == 0 ? 256 : $width; + $event->height = $height == 0 ? 256 : $height; + } + } - $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); + protected function supported_ext(string $ext): bool + { + return in_array(strtolower($ext), self::SUPPORTED_EXTENSIONS); + } - $width = $subheader['width']; - $height = $subheader['height']; - $image->width = $width == 0 ? 256 : $width; - $image->height = $height == 0 ? 256 : $height; + protected function create_image_from_data(string $filename, array $metadata) + { + $image = new Image(); - $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']; + $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; - } + 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)); - } + protected function check_contents(string $file): bool + { + 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 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; + } + } } - diff --git a/ext/handle_ico/test.php b/ext/handle_ico/test.php index fa130100..0d58666b 100644 --- a/ext/handle_ico/test.php +++ b/ext/handle_ico/test.php @@ -1,12 +1,13 @@ 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 +class IcoHandlerTest extends ShimmiePHPUnitTestCase +{ + public function testIcoHander() + { + $this->log_in_as_user(); + $image_id = $this->post_image("ext/handle_static/static/favicon.ico", "shimmie favicon"); + $this->get_page("post/view/$image_id"); // test for no crash - # FIXME: test that the thumb works - # FIXME: test that it gets displayed properly - } + # 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..522512e0 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..df17f2ad --- /dev/null +++ b/ext/handle_mp3/info.php @@ -0,0 +1,18 @@ + + * Description: Handle MP3 files + */ + +class MP3FileHandlerInfo extends ExtensionInfo +{ + public const KEY = "handle_mp3"; + + public $key = self::KEY; + public $name = "Handle MP3"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $description = "Handle MP3 files"; +} diff --git a/ext/handle_mp3/main.php b/ext/handle_mp3/main.php index 4f0eae9e..53987eea 100644 --- a/ext/handle_mp3/main.php +++ b/ext/handle_mp3/main.php @@ -1,68 +1,63 @@ - * Description: Handle MP3 files - */ -class MP3FileHandler extends DataHandlerExtension { - /** - * @param string $hash - * @return bool - */ - protected function create_thumb($hash) { - copy("ext/handle_mp3/thumb.jpg", warehouse_path("thumbs", $hash)); - return true; - } +class MP3FileHandler extends DataHandlerExtension +{ + public function onMediaCheckProperties(MediaCheckPropertiesEvent $event) + { + switch ($event->ext) { + case "mp3": + $event->audio = true; + $event->video = false; + $event->lossless = false; + break; + } + // TODO: Buff out audio format support, length scanning + } - /** - * @param string $ext - * @return bool - */ - protected function supported_ext($ext) { - $exts = array("mp3"); - return in_array(strtolower($ext), $exts); - } + protected function create_thumb(string $hash, string $type): bool + { + copy("ext/handle_mp3/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash)); + return true; + } - /** - * @param string $filename - * @param mixed[] $metadata - * @return Image|null - */ - protected function create_image_from_data($filename, $metadata) { - $image = new Image(); + protected function supported_ext(string $ext): bool + { + $exts = ["mp3"]; + return in_array(strtolower($ext), $exts); + } - //NOTE: No need to set width/height as we don't use it. - $image->width = 1; - $image->height = 1; + protected function create_image_from_data(string $filename, array $metadata) + { + $image = new Image(); - $image->filesize = $metadata['size']; - $image->hash = $metadata['hash']; + //NOTE: No need to set width/height as we don't use it. + $image->width = 1; + $image->height = 1; - //Filename is renamed to "artist - title.mp3" when the user requests download by using the download attribute & jsmediatags.js - $image->filename = $metadata['filename']; + $image->filesize = $metadata['size']; + $image->hash = $metadata['hash']; - $image->ext = $metadata['extension']; - $image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']); - $image->source = $metadata['source']; + //Filename is renamed to "artist - title.mp3" when the user requests download by using the download attribute & jsmediatags.js + $image->filename = $metadata['filename']; - return $image; - } + $image->ext = $metadata['extension']; + $image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']); + $image->source = $metadata['source']; - /** - * @param $file - * @return bool - */ - protected function check_contents($file) { - $success = FALSE; - if (file_exists($file)) { - $mimeType = mime_content_type($file); + return $image; + } - $success = ($mimeType == 'audio/mpeg'); - } + protected function check_contents(string $tmpname): bool + { + $success = false; - return $success; - } + if (file_exists($tmpname)) { + $mimeType = getMimeType($tmpname); + + $success = ($mimeType == 'audio/mpeg'); + } + + return $success; + } } - diff --git a/ext/handle_mp3/theme.php b/ext/handle_mp3/theme.php index 95868018..0b382b74 100644 --- a/ext/handle_mp3/theme.php +++ b/ext/handle_mp3/theme.php @@ -1,11 +1,13 @@ 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(); + $fname = url_escape($image->filename); //Most of the time this will be the title/artist of the song. + $html = "

    Download"; - $page->add_html_header(""); - $page->add_block(new Block("Music", $html, "main", 10)); - } + $page->add_html_header(""); + $page->add_block(new Block("Music", $html, "main", 10)); + } } - diff --git a/ext/handle_pixel/info.php b/ext/handle_pixel/info.php new file mode 100644 index 00000000..2dc2d17c --- /dev/null +++ b/ext/handle_pixel/info.php @@ -0,0 +1,20 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * Description: Handle JPEG, PNG, GIF, WEBP, etc files + */ + +class PixelFileHandlerInfo extends ExtensionInfo +{ + public const KEY = "handle_pixel"; + + public $key = self::KEY; + public $name = "Handle Pixel"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $description = "Handle JPEG, PNG, GIF, WEBP, etc files"; + public $core = true; +} diff --git a/ext/handle_pixel/main.php b/ext/handle_pixel/main.php index 149677eb..3016e8e3 100644 --- a/ext/handle_pixel/main.php +++ b/ext/handle_pixel/main.php @@ -1,98 +1,106 @@ - * Link: http://code.shishnet.org/shimmie2/ - * Description: Handle JPEG, PNG, GIF, etc files - */ -class PixelFileHandler extends DataHandlerExtension { - /** - * @param string $ext - * @return bool - */ - protected function supported_ext($ext) { - $exts = array("jpg", "jpeg", "gif", "png"); - $ext = (($pos = strpos($ext,'?')) !== false) ? substr($ext,0,$pos) : $ext; - return in_array(strtolower($ext), $exts); - } +class PixelFileHandler extends DataHandlerExtension +{ + const SUPPORTED_EXTENSIONS = ["jpg", "jpeg", "gif", "png", "webp"]; - /** - * @param string $filename - * @param array $metadata - * @return Image|null - */ - protected function create_image_from_data(/*string*/ $filename, /*array*/ $metadata) { - $image = new Image(); - $info = getimagesize($filename); - if(!$info) return null; + public function onMediaCheckProperties(MediaCheckPropertiesEvent $event) + { + if (in_array($event->ext, Media::LOSSLESS_FORMATS)) { + $event->lossless = true; + } elseif ($event->ext=="webp") { + $event->lossless = Media::is_lossless_webp($event->file_name); + } - $image->width = $info[0]; - $image->height = $info[1]; + if (in_array($event->ext, self::SUPPORTED_EXTENSIONS)) { + if ($event->lossless==null) { + $event->lossless = false; + } + $event->audio = false; + switch ($event->ext) { + case "gif": + $event->video = Media::is_animated_gif($event->file_name); + break; + case "webp": + $event->video = Media::is_animated_webp($event->file_name); + break; + default: + $event->video = false; + break; + } - $image->filesize = $metadata['size']; - $image->hash = $metadata['hash']; - $image->filename = (($pos = strpos($metadata['filename'],'?')) !== false) ? substr($metadata['filename'],0,$pos) : $metadata['filename']; - $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']; + $info = getimagesize($event->file_name); + if (!$info) { + return null; + } - return $image; - } + $event->width = $info[0]; + $event->height = $info[1]; + } + } - /** - * @param string $file - * @return bool - */ - protected function check_contents(/*string*/ $file) { - $valid = Array(IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG); - if(!file_exists($file)) return false; - $info = getimagesize($file); - if(is_null($info)) return false; - if(in_array($info[2], $valid)) return true; - return false; - } - /** - * @param string $hash - * @return bool - */ - protected function create_thumb(/*string*/ $hash) { - $outname = warehouse_path("thumbs", $hash); - if(file_exists($outname)) { - return true; - } - return $this->create_thumb_force($hash); - } - /** - * @param string $hash - * @return bool - */ - protected function create_thumb_force(/*string*/ $hash) { - global $config; + protected function supported_ext(string $ext): bool + { + $ext = (($pos = strpos($ext, '?')) !== false) ? substr($ext, 0, $pos) : $ext; + return in_array(strtolower($ext), self::SUPPORTED_EXTENSIONS); + } - $inname = warehouse_path("images", $hash); - $outname = warehouse_path("thumbs", $hash); + protected function create_image_from_data(string $filename, array $metadata) + { + $image = new Image(); - $ok = false; + $image->filesize = $metadata['size']; + $image->hash = $metadata['hash']; + $image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename']; + $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']; - switch($config->get_string("thumb_engine")) { - default: - case 'gd': - $ok = $this->make_thumb_gd($inname, $outname); - break; - case 'convert': - $ok = $this->make_thumb_convert($inname, $outname); - break; - } + return $image; + } - return $ok; - } + protected function check_contents(string $tmpname): bool + { + $valid = [IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_WEBP]; + if (!file_exists($tmpname)) { + return false; + } + $info = getimagesize($tmpname); + if (is_null($info)) { + return false; + } + if (in_array($info[2], $valid)) { + return true; + } + return false; + } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { - $event->add_part(" + protected function create_thumb(string $hash, string $type): bool + { + try { + create_image_thumb($hash, $type); + return true; + } catch (InsufficientMemoryException $e) { + $tsize = get_thumbnail_max_size_scaled(); + $thumb = imagecreatetruecolor($tsize[0], min($tsize[1], 64)); + $white = imagecolorallocate($thumb, 255, 255, 255); + $black = imagecolorallocate($thumb, 0, 0, 0); + imagefill($thumb, 0, 0, $white); + log_warning("handle_pixel", "Insufficient memory while creating thumbnail: ".$e->getMessage()); + imagestring($thumb, 5, 10, 24, "Image Too Large :(", $black); + return true; + } catch (Exception $e) { + log_error("handle_pixel", "Error while creating thumbnail: ".$e->getMessage()); + return false; + } + } + + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) + { + $event->add_part("

    ", 20); - - $u_ilink = $event->image->get_image_link(); - $nu_enabled = (strpos($u_ilink, '?') !== false ? "" : ""); - $event->add_part(" -
    - $nu_enabled - -
    - ", 21); - } - -// IM thumber {{{ - - /** - * @param string $inname - * @param string $outname - * @return bool - */ - private function make_thumb_convert(/*string*/ $inname, /*string*/ $outname) { - global $config; - - $w = $config->get_int("thumb_width"); - $h = $config->get_int("thumb_height"); - $q = $config->get_int("thumb_quality"); - $convert = $config->get_string("thumb_convert_path"); - - // ffff imagemagic fails sometimes, not sure why - //$format = "'%s' '%s[0]' -format '%%[fx:w] %%[fx:h]' info:"; - //$cmd = sprintf($format, $convert, $inname); - //$size = shell_exec($cmd); - //$size = explode(" ", trim($size)); - $size = getimagesize($inname); - if($size[0] > $size[1]*5) $size[0] = $size[1]*5; - if($size[1] > $size[0]*5) $size[1] = $size[0]*5; - - // running the call with cmd.exe requires quoting for our paths - $format = '"%s" "%s[0]" -extent %ux%u -flatten -strip -thumbnail %ux%u -quality %u jpg:"%s"'; - $cmd = sprintf($format, $convert, $inname, $size[0], $size[1], $w, $h, $q, $outname); - $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('handle_pixel', "Generating thumnail with command `$cmd`, returns $ret"); - - if($config->get_bool("thumb_optim", false)) { - exec("jpegoptim $outname", $output, $ret); - } - - return true; - } -// }}} -// epeg thumber {{{ - /** - * @param string $inname - * @param string $outname - * @return bool - */ - private function make_thumb_epeg(/*string*/ $inname, /*string*/ $outname) { - global $config; - $w = $config->get_int("thumb_width"); - exec("epeg $inname -c 'Created by EPEG' --max $w $outname"); - return true; - } - // }}} -// GD thumber {{{ - /** - * @param string $inname - * @param string $outname - * @return bool - */ - private function make_thumb_gd(/*string*/ $inname, /*string*/ $outname) { - global $config; - $thumb = $this->get_thumb($inname); - $ok = imagejpeg($thumb, $outname, $config->get_int('thumb_quality')); - imagedestroy($thumb); - return $ok; - } - - /** - * @param string $tmpname - * @return resource - */ - private function get_thumb(/*string*/ $tmpname) { - global $config; - - $info = getimagesize($tmpname); - $width = $info[0]; - $height = $info[1]; - - $memory_use = (filesize($tmpname)*2) + ($width*$height*4) + (4*1024*1024); - $memory_limit = get_memory_limit(); - - if($memory_use > $memory_limit) { - $w = $config->get_int('thumb_width'); - $h = $config->get_int('thumb_height'); - $thumb = imagecreatetruecolor($w, min($h, 64)); - $white = imagecolorallocate($thumb, 255, 255, 255); - $black = imagecolorallocate($thumb, 0, 0, 0); - imagefill($thumb, 0, 0, $white); - imagestring($thumb, 5, 10, 24, "Image Too Large :(", $black); - return $thumb; - } - else { - if($width > $height*5) $width = $height*5; - if($height > $width*5) $height = $width*5; - - $image = imagecreatefromstring(file_get_contents($tmpname)); - $tsize = get_thumbnail_size($width, $height); - - $thumb = imagecreatetruecolor($tsize[0], $tsize[1]); - imagecopyresampled( - $thumb, $image, 0, 0, 0, 0, - $tsize[0], $tsize[1], $width, $height - ); - return $thumb; - } - } -// }}} + } } - diff --git a/ext/handle_pixel/test.php b/ext/handle_pixel/test.php index 0eed282a..31bfd048 100644 --- a/ext/handle_pixel/test.php +++ b/ext/handle_pixel/test.php @@ -1,12 +1,13 @@ log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - //$this->assert_response(302); +class PixelHandlerTest extends ShimmiePHPUnitTestCase +{ + public function testPixelHander() + { + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + //$this->assert_response(302); - # FIXME: test that the thumb works - # FIXME: test that it gets displayed properly - } + # FIXME: test that the thumb works + # FIXME: test that it gets displayed properly + } } - diff --git a/ext/handle_pixel/theme.php b/ext/handle_pixel/theme.php index 6b0a159c..278ed0d7 100644 --- a/ext/handle_pixel/theme.php +++ b/ext/handle_pixel/theme.php @@ -1,35 +1,36 @@ get_image_link(); - if($config->get_bool("image_show_meta") && function_exists("exif_read_data")) { - # FIXME: only read from jpegs? - $exif = @exif_read_data($image->get_image_filename(), 0, true); - if($exif) { - $head = ""; - foreach ($exif as $key => $section) { - foreach ($section as $name => $val) { - if($key == "IFD0") { + $u_ilink = $image->get_image_link(); + if ($config->get_bool(ImageConfig::SHOW_META) && function_exists(ImageIO::EXIF_READ_FUNCTION)) { + # FIXME: only read from jpegs? + $exif = @exif_read_data($image->get_image_filename(), 0, true); + if ($exif) { + $head = ""; + foreach ($exif as $key => $section) { + foreach ($section as $name => $val) { + if ($key == "IFD0") { // Cheap fix for array'd values in EXIF-data if (is_array($val)) { $val = implode(',', $val); } - $head .= html_escape("$name: $val")."
    \n"; - } - } - } - if($head) { - $page->add_block(new Block("EXIF Info", $head, "left")); - } - } - } + $head .= html_escape("$name: $val")."
    \n"; + } + } + } + if ($head) { + $page->add_block(new Block("EXIF Info", $head, "left")); + } + } + } - $html = "main image"; - $page->add_block(new Block("Image", $html, "main", 10)); - } + $html = "main image"; + $page->add_block(new Block("Image", $html, "main", 10)); + } } - diff --git a/ext/handle_static/info.php b/ext/handle_static/info.php new file mode 100644 index 00000000..1ba6ea9c --- /dev/null +++ b/ext/handle_static/info.php @@ -0,0 +1,24 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Visibility: admin + * Description: If Shimmie can't handle a request, check static files ($theme/static/$filename, then ext/handle_static/static/$filename) + */ + +class HandleStaticInfo extends ExtensionInfo +{ + public const KEY = "handle_static"; + + public $key = self::KEY; + public $name = "Static File Handler"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $visibility = self::VISIBLE_ADMIN; + public $description = 'If Shimmie can\'t handle a request, check static files ($theme/static/$filename, then ext/handle_static/static/$filename)'; + public $core = true; +} diff --git a/ext/handle_static/main.php b/ext/handle_static/main.php new file mode 100644 index 00000000..fb33d470 --- /dev/null +++ b/ext/handle_static/main.php @@ -0,0 +1,52 @@ +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/handle_static/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/handle_static/modernizr-3.3.1.custom.js similarity index 100% rename from lib/vendor/js/modernizr-3.3.1.custom.js rename to ext/handle_static/modernizr-3.3.1.custom.js diff --git a/lib/shimmie.js b/ext/handle_static/script.js similarity index 50% rename from lib/shimmie.js rename to ext/handle_static/script.js index ae6b98c5..e06c39c7 100644 --- a/lib/shimmie.js +++ b/ext/handle_static/script.js @@ -32,23 +32,6 @@ $(document).ready(function() { /** 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) { @@ -87,69 +70,4 @@ $(document).ready(function() { 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/static/README.txt b/ext/handle_static/static/README.txt similarity index 100% rename from lib/static/README.txt rename to ext/handle_static/static/README.txt diff --git a/lib/static/apple-touch-icon.png b/ext/handle_static/static/apple-touch-icon.png similarity index 100% rename from lib/static/apple-touch-icon.png rename to ext/handle_static/static/apple-touch-icon.png diff --git a/lib/static/favicon.ico b/ext/handle_static/static/favicon.ico similarity index 100% rename from lib/static/favicon.ico rename to ext/handle_static/static/favicon.ico diff --git a/lib/static/favicon.png b/ext/handle_static/static/favicon.png similarity index 100% rename from lib/static/favicon.png rename to ext/handle_static/static/favicon.png diff --git a/lib/static/favicon.svg b/ext/handle_static/static/favicon.svg similarity index 100% rename from lib/static/favicon.svg rename to ext/handle_static/static/favicon.svg diff --git a/lib/static/favicon_64.png b/ext/handle_static/static/favicon_64.png similarity index 100% rename from lib/static/favicon_64.png rename to ext/handle_static/static/favicon_64.png diff --git a/lib/static/grey.gif b/ext/handle_static/static/grey.gif similarity index 100% rename from lib/static/grey.gif rename to ext/handle_static/static/grey.gif diff --git a/lib/static/robots.txt b/ext/handle_static/static/robots.txt similarity index 100% rename from lib/static/robots.txt rename to ext/handle_static/static/robots.txt diff --git a/ext/handle_static/style.css b/ext/handle_static/style.css new file mode 100644 index 00000000..444fa296 --- /dev/null +++ b/ext/handle_static/style.css @@ -0,0 +1,74 @@ + +ARTICLE SELECT {width: 150px;} +INPUT, TEXTAREA {box-sizing: border-box;} +TD>INPUT[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: 0px;} +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: 0px; + padding: 2px; +} +#installer H1 { + border-bottom: 1px solid black; + border-radius: 16px 16px 0px 0px; +} +#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/handle_static/test.php b/ext/handle_static/test.php new file mode 100644 index 00000000..2f0f7b43 --- /dev/null +++ b/ext/handle_static/test.php @@ -0,0 +1,9 @@ +get_page('favicon.ico'); + $this->assert_response(200); + } +} diff --git a/ext/handle_svg/info.php b/ext/handle_svg/info.php new file mode 100644 index 00000000..68e59cb8 --- /dev/null +++ b/ext/handle_svg/info.php @@ -0,0 +1,19 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * Description: Handle static SVG files. + */ + +class SVGFileHandlerInfo extends ExtensionInfo +{ + public const KEY = "handle_svg"; + + public $key = self::KEY; + public $name = "Handle SVG"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $description = "Handle static SVG files."; +} diff --git a/ext/handle_svg/main.php b/ext/handle_svg/main.php index 2847a092..4e8a173d 100644 --- a/ext/handle_svg/main.php +++ b/ext/handle_svg/main.php @@ -1,140 +1,149 @@ - * Link: http://code.shishnet.org/shimmie2/ - * Description: Handle static SVG files. (No thumbnail is generated for SVG files) - */ - use enshrined\svgSanitize\Sanitizer; -class SVGFileHandler extends Extension { - public function onDataUpload(DataUploadEvent $event) { - if($this->supported_ext($event->type) && $this->check_contents($event->tmpname)) { - $hash = $event->hash; +class SVGFileHandler extends DataHandlerExtension +{ + public function onMediaCheckProperties(MediaCheckPropertiesEvent $event) + { + switch ($event->ext) { + case "svg": + $event->lossless = true; + $event->video = false; + $event->audio = false; - $sanitizer = new Sanitizer(); - $sanitizer->removeRemoteReferences(true); - $dirtySVG = file_get_contents($event->tmpname); - $cleanSVG = $sanitizer->sanitize($dirtySVG); - file_put_contents(warehouse_path("images", $hash), $cleanSVG); + $msp = new MiniSVGParser($event->file_name); + $event->width = $msp->width; + $event->height = $msp->height; - send_event(new ThumbnailGenerationEvent($event->hash, $event->type)); - $image = $this->create_image_from_data(warehouse_path("images", $hash), $event->metadata); - if(is_null($image)) { - throw new UploadException("SVG handler failed to create image object from data"); - } - $iae = new ImageAdditionEvent($image); - send_event($iae); - $event->image_id = $iae->image->id; - } - } + break; + } + } - public function onThumbnailGeneration(ThumbnailGenerationEvent $event) { - if($this->supported_ext($event->type)) { - $hash = $event->hash; - copy("ext/handle_svg/thumb.jpg", warehouse_path("thumbs", $hash)); - } - } + public function onDataUpload(DataUploadEvent $event) + { + if ($this->supported_ext($event->type) && $this->check_contents($event->tmpname)) { + $hash = $event->hash; - public function onDisplayingImage(DisplayingImageEvent $event) { - global $page; - if($this->supported_ext($event->image->ext)) { - $this->theme->display_image($page, $event->image); - } - } + $sanitizer = new Sanitizer(); + $sanitizer->removeRemoteReferences(true); + $dirtySVG = file_get_contents($event->tmpname); + $cleanSVG = $sanitizer->sanitize($dirtySVG); + file_put_contents(warehouse_path(Image::IMAGE_DIR, $hash), $cleanSVG); - public function onPageRequest(PageRequestEvent $event) { - global $page; - if($event->page_matches("get_svg")) { - $id = int_escape($event->get_arg(0)); - $image = Image::by_id($id); - $hash = $image->hash; + send_event(new ThumbnailGenerationEvent($event->hash, $event->type)); + $image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $hash), $event->metadata); + if (is_null($image)) { + throw new UploadException("SVG handler failed to create image object from data"); + } + $iae = new ImageAdditionEvent($image); + send_event($iae); + $event->image_id = $iae->image->id; + $event->merged = $iae->merged; + } + } - $page->set_type("image/svg+xml"); - $page->set_mode("data"); + 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_svg", "Could not generate thumbnail. " . $e->getMessage()); + copy("ext/handle_svg/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash)); + return false; + } + } - $sanitizer = new Sanitizer(); - $sanitizer->removeRemoteReferences(true); - $dirtySVG = file_get_contents(warehouse_path("images", $hash)); - $cleanSVG = $sanitizer->sanitize($dirtySVG); - $page->set_data($cleanSVG); - } - } + public function onDisplayingImage(DisplayingImageEvent $event) + { + global $page; + if ($this->supported_ext($event->image->ext)) { + $this->theme->display_image($page, $event->image); + } + } - /** - * @param string $ext - * @return bool - */ - private function supported_ext($ext) { - $exts = array("svg"); - return in_array(strtolower($ext), $exts); - } + public function onPageRequest(PageRequestEvent $event) + { + global $page; + if ($event->page_matches("get_svg")) { + $id = int_escape($event->get_arg(0)); + $image = Image::by_id($id); + $hash = $image->hash; - /** - * @param string $filename - * @param mixed[] $metadata - * @return Image - */ - private function create_image_from_data($filename, $metadata) { - $image = new Image(); + $page->set_type("image/svg+xml"); + $page->set_mode(PageMode::DATA); - $msp = new MiniSVGParser($filename); - $image->width = $msp->width; - $image->height = $msp->height; + $sanitizer = new Sanitizer(); + $sanitizer->removeRemoteReferences(true); + $dirtySVG = file_get_contents(warehouse_path(Image::IMAGE_DIR, $hash)); + $cleanSVG = $sanitizer->sanitize($dirtySVG); + $page->set_data($cleanSVG); + } + } - $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 supported_ext(string $ext): bool + { + $exts = ["svg"]; + return in_array(strtolower($ext), $exts); + } - return $image; - } + protected function create_image_from_data(string $filename, array $metadata): Image + { + $image = new Image(); - /** - * @param string $file - * @return bool - */ - private function check_contents($file) { - if(!file_exists($file)) return false; + $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']; - $msp = new MiniSVGParser($file); - return bool_escape($msp->valid); - } + return $image; + } + + protected function check_contents(string $file): bool + { + if (!file_exists($file)) { + return false; + } + + $msp = new MiniSVGParser($file); + return bool_escape($msp->valid); + } } -class MiniSVGParser { - /** @var bool */ - public $valid=false; - /** @var int */ - public $width=0; - /** @var int */ - public $height=0; +class MiniSVGParser +{ + /** @var bool */ + public $valid=false; + /** @var int */ + public $width=0; + /** @var int */ + public $height=0; - /** @var int */ - private $xml_depth=0; + /** @var int */ + private $xml_depth=0; - /** @param string $file */ - function __construct($file) { - $xml_parser = xml_parser_create(); - xml_set_element_handler($xml_parser, array($this, "startElement"), array($this, "endElement")); - $this->valid = bool_escape(xml_parse($xml_parser, file_get_contents($file), true)); - xml_parser_free($xml_parser); - } + public function __construct(string $file) + { + $xml_parser = xml_parser_create(); + xml_set_element_handler($xml_parser, [$this, "startElement"], [$this, "endElement"]); + $this->valid = bool_escape(xml_parse($xml_parser, file_get_contents($file), true)); + xml_parser_free($xml_parser); + } - function startElement($parser, $name, $attrs) { - if($name == "SVG" && $this->xml_depth == 0) { - $this->width = int_escape($attrs["WIDTH"]); - $this->height = int_escape($attrs["HEIGHT"]); - } - $this->xml_depth++; - } + public function startElement($parser, $name, $attrs) + { + if ($name == "SVG" && $this->xml_depth == 0) { + $this->width = int_escape($attrs["WIDTH"]); + $this->height = int_escape($attrs["HEIGHT"]); + } + $this->xml_depth++; + } - function endElement($parser, $name) { - $this->xml_depth--; - } + public function endElement($parser, $name) + { + $this->xml_depth--; + } } - diff --git a/ext/handle_svg/test.php b/ext/handle_svg/test.php index f8aaa96c..ef8ff86d 100644 --- a/ext/handle_svg/test.php +++ b/ext/handle_svg/test.php @@ -1,22 +1,24 @@ log_in_as_user(); - $image_id = $this->post_image("tests/test.svg", "something"); - $this->get_page("post/view/$image_id"); // test for no crash - $this->get_page("get_svg/$image_id"); // test for no crash - $this->assert_content("www.w3.org"); +class SVGHandlerTest extends ShimmiePHPUnitTestCase +{ + public function testSVGHander() + { + $this->log_in_as_user(); + $image_id = $this->post_image("tests/test.svg", "something"); + $this->get_page("post/view/$image_id"); // test for no crash + $this->get_page("get_svg/$image_id"); // test for no crash + $this->assert_content("www.w3.org"); - # FIXME: test that the thumb works - # FIXME: test that it gets displayed properly - } + # FIXME: test that the thumb works + # FIXME: test that it gets displayed properly + } - public function testAbuiveSVG() { - $this->log_in_as_user(); - $image_id = $this->post_image("tests/alert.svg", "something"); - $this->get_page("post/view/$image_id"); - $this->get_page("get_svg/$image_id"); - $this->assert_no_content("script"); - } + public function testAbuiveSVG() + { + $this->log_in_as_user(); + $image_id = $this->post_image("tests/alert.svg", "something"); + $this->get_page("post/view/$image_id"); + $this->get_page("get_svg/$image_id"); + $this->assert_no_content("script"); + } } - diff --git a/ext/handle_svg/theme.php b/ext/handle_svg/theme.php index 5b7def3b..908e0156 100644 --- a/ext/handle_svg/theme.php +++ b/ext/handle_svg/theme.php @@ -1,13 +1,14 @@ id}/{$image->id}.svg"); -// $ilink = $image->get_image_link(); - $html = " +class SVGFileHandlerTheme extends Themelet +{ + public function display_image(Page $page, Image $image) + { + $ilink = make_link("get_svg/{$image->id}/{$image->id}.svg"); + // $ilink = $image->get_image_link(); + $html = " "; - $page->add_block(new Block("Image", $html, "main", 10)); - } + $page->add_block(new Block("Image", $html, "main", 10)); + } } - diff --git a/ext/handle_video/info.php b/ext/handle_video/info.php new file mode 100644 index 00000000..52c357c3 --- /dev/null +++ b/ext/handle_video/info.php @@ -0,0 +1,28 @@ + + * Modified By: Shish , jgen , im-mi + * License: GPLv2 + * Description: Handle FLV, MP4, OGV and WEBM video files. + * Documentation: + */ + +class VideoFileHandlerInfo extends ExtensionInfo +{ + public const KEY = "handle_video"; + + public $key = self::KEY; + public $name = "Handle Video"; + public $authors = ["velocity37"=>"velocity37@gmail.com",self::SHISH_NAME=>self::SHISH_EMAIL, "jgen"=>"jeffgenovy@gmail.com", "im-mi"=>"im.mi.mail.mi@gmail.com"]; + public $license = self::LICENSE_GPLV2; + public $description = "Handle FLV, MP4, OGV and WEBM video files."; + public $documentation = +"Based heavily on \"Handle MP3\" by Shish.

    +FLV: Flash player
    +MP4: HTML5 with Flash fallback
    +OGV, WEBM: HTML5
    +MP4's flash fallback is forced with a bit of Javascript as some browsers won't fallback if they can't play H.264. +In the future, it may be necessary to change the user agent checks to reflect the current state of H.264 support.

    "; +} diff --git a/ext/handle_video/main.php b/ext/handle_video/main.php index a31ac781..5e383d92 100644 --- a/ext/handle_video/main.php +++ b/ext/handle_video/main.php @@ -1,200 +1,140 @@ - * Modified By: Shish , jgen , im-mi - * License: GPLv2 - * Description: Handle FLV, MP4, OGV and WEBM video files. - * Documentation: - * Based heavily on "Handle MP3" by Shish.

    - * FLV: Flash player
    - * MP4: HTML5 with Flash fallback
    - * OGV, WEBM: HTML5
    - * MP4's flash fallback is forced with a bit of Javascript as some browsers won't fallback if they can't play H.264. - * In the future, it may be necessary to change the user agent checks to reflect the current state of H.264 support.

    - */ -class VideoFileHandler extends DataHandlerExtension { - public function onInitExt(InitExtEvent $event) { - global $config; +class VideoFileHandler extends DataHandlerExtension +{ + const SUPPORTED_MIME = [ + 'video/webm', + 'video/mp4', + 'video/ogg', + 'video/flv', + 'video/x-flv' + ]; + const SUPPORTED_EXT = ["flv", "mp4", "m4v", "ogv", "webm"]; - if($config->get_int("ext_handle_video_version") < 1) { - if($ffmpeg = shell_exec((PHP_OS == 'WINNT' ? 'where' : 'which') . ' ffmpeg')) { - //ffmpeg exists in PATH, check if it's executable, and if so, default to it instead of static - if(is_executable(strtok($ffmpeg, PHP_EOL))) { - $config->set_default_string('video_thumb_engine', 'ffmpeg'); - $config->set_default_string('thumb_ffmpeg_path', 'ffmpeg'); - } - } else { - $config->set_default_string('video_thumb_engine', 'static'); - $config->set_default_string('thumb_ffmpeg_path', ''); - } + public function onInitExt(InitExtEvent $event) + { + global $config; - // By default we generate thumbnails ignoring the aspect ratio of the video file. - // - // Why? - This allows Shimmie to work with older versions of FFmpeg by default, - // rather than completely failing out of the box. If people complain that their - // thumbnails are distorted, then they can turn this feature on manually later. - $config->set_default_bool('video_thumb_ignore_aspect_ratio', TRUE); + if ($config->get_int("ext_handle_video_version") < 1) { + // This used to set the ffmpeg path. It does not do this anymore, that is now in the base graphic extension. + $config->set_int("ext_handle_video_version", 1); + log_info("handle_video", "extension installed"); + } - $config->set_int("ext_handle_video_version", 1); - log_info("handle_video", "extension installed"); - } + $config->set_default_bool('video_playback_autoplay', true); + $config->set_default_bool('video_playback_loop', true); + } - $config->set_default_bool('video_playback_autoplay', TRUE); - $config->set_default_bool('video_playback_loop', TRUE); - } + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Video Options"); + $sb->add_bool_option("video_playback_autoplay", "Autoplay: "); + $sb->add_label("
    "); + $sb->add_bool_option("video_playback_loop", "Loop: "); + $event->panel->add_block($sb); + } - public function onSetupBuilding(SetupBuildingEvent $event) { - //global $config; + public function onMediaCheckProperties(MediaCheckPropertiesEvent $event) + { + if (in_array($event->ext, self::SUPPORTED_EXT)) { + $event->video = true; + try { + $data = Media::get_ffprobe_data($event->file_name); - $thumbers = array( - 'None' => 'static', - 'ffmpeg' => 'ffmpeg' - ); + if (is_array($data)) { + if (array_key_exists("streams", $data)) { + $video = false; + $audio = true; + $streams = $data["streams"]; + if (is_array($streams)) { + foreach ($streams as $stream) { + if (is_array($stream)) { + if (array_key_exists("codec_type", $stream)) { + $type = $stream["codec_type"]; + switch ($type) { + case "audio": + $audio = true; + break; + case "video": + $video = true; + break; + } + } + if (array_key_exists("width", $stream) && !empty($stream["width"]) + && is_numeric($stream["width"]) && intval($stream["width"]) > ($event->width) ?? 0) { + $event->width = intval($stream["width"]); + } + if (array_key_exists("height", $stream) && !empty($stream["height"]) + && is_numeric($stream["height"]) && intval($stream["height"]) > ($event->height) ?? 0) { + $event->height = intval($stream["height"]); + } + } + } + $event->video = $video; + $event->audio = $audio; + } + } + if (array_key_exists("format", $data)&& is_array($data["format"])) { + $format = $data["format"]; + if (array_key_exists("duration", $format) && is_numeric($format["duration"])) { + $event->length = floor(floatval($format["duration"]) * 1000); + } + } + } + } catch (MediaException $e) { + } + } + } - $sb = new SetupBlock("Video Thumbnail Options"); + /** + * Generate the Thumbnail image for particular file. + */ + protected function create_thumb(string $hash, string $type): bool + { + return Media::create_thumbnail_ffmpeg($hash); + } - $sb->add_choice_option("video_thumb_engine", $thumbers, "Engine: "); + protected function supported_ext(string $ext): bool + { + return in_array(strtolower($ext), self::SUPPORTED_EXT); + } - //if($config->get_string("video_thumb_engine") == "ffmpeg") { - $sb->add_label("
    Path to ffmpeg: "); - $sb->add_text_option("thumb_ffmpeg_path"); - //} + protected function create_image_from_data(string $filename, array $metadata): Image + { + $image = new Image(); - // Some older versions of ffmpeg have trouble with the automatic aspect ratio scaling. - // This adds an option in the Board Config to disable the aspect ratio scaling. - $sb->add_label("
    "); - $sb->add_bool_option("video_thumb_ignore_aspect_ratio", "Ignore aspect ratio when creating thumbnails: "); + switch (getMimeType($filename)) { + case "video/webm": + $image->ext = "webm"; + break; + case "video/mp4": + $image->ext = "mp4"; + break; + case "video/ogg": + $image->ext = "ogv"; + break; + case "video/flv": + $image->ext = "flv"; + break; + case "video/x-flv": + $image->ext = "flv"; + break; + } - $event->panel->add_block($sb); - - $sb = new SetupBlock("Video Playback Options"); - $sb->add_bool_option("video_playback_autoplay", "Autoplay: "); - $sb->add_label("
    "); - $sb->add_bool_option("video_playback_loop", "Loop: "); - $event->panel->add_block($sb); - } + $image->filesize = $metadata['size']; + $image->hash = $metadata['hash']; + $image->filename = $metadata['filename']; + $image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']); + $image->source = $metadata['source']; - /** - * Generate the Thumbnail image for particular file. - * - * @param string $hash - * @return bool Returns true on successful thumbnail creation. - */ - protected function create_thumb($hash) { - global $config; + return $image; + } - $ok = false; - - switch($config->get_string("video_thumb_engine")) - { - default: - case 'static': - $outname = warehouse_path("thumbs", $hash); - copy("ext/handle_video/thumb.jpg", $outname); - $ok = true; - break; - - case 'ffmpeg': - $ffmpeg = escapeshellcmd($config->get_string("thumb_ffmpeg_path")); - - $w = (int)$config->get_int("thumb_width"); - $h = (int)$config->get_int("thumb_height"); - $inname = escapeshellarg(warehouse_path("images", $hash)); - $outname = escapeshellarg(warehouse_path("thumbs", $hash)); - - if ($config->get_bool("video_thumb_ignore_aspect_ratio") == true) - { - $cmd = escapeshellcmd("{$ffmpeg} -y -i {$inname} -ss 00:00:00.0 -f image2 -vframes 1 {$outname}"); - } - else - { - $scale = 'scale="' . escapeshellarg("if(gt(a,{$w}/{$h}),{$w},-1)") . ':' . escapeshellarg("if(gt(a,{$w}/{$h}),-1,{$h})") . '"'; - $cmd = "{$ffmpeg} -y -i {$inname} -vf {$scale} -ss 00:00:00.0 -f image2 -vframes 1 {$outname}"; - } - - exec($cmd, $output, $returnValue); - - if ((int)$returnValue == (int)1) - { - $ok = true; - } - - log_debug('handle_video', "Generating thumbnail with command `$cmd`, returns $returnValue"); - break; - } - - return $ok; - } - - /** - * @param string $ext - * @return bool - */ - protected function supported_ext($ext) { - $exts = array("flv", "mp4", "m4v", "ogv", "webm"); - return in_array(strtolower($ext), $exts); - } - - /** - * @param string $filename - * @param mixed[] $metadata - * @return Image - */ - protected function create_image_from_data($filename, $metadata) { - $image = new Image(); - - //NOTE: No need to set width/height as we don't use it. - $image->width = 1; - $image->height = 1; - - switch (mime_content_type($filename)) { - case "video/webm": - $image->ext = "webm"; - break; - case "video/mp4": - $image->ext = "mp4"; - break; - case "video/ogg": - $image->ext = "ogv"; - break; - case "video/flv": - $image->ext = "flv"; - break; - case "video/x-flv": - $image->ext = "flv"; - break; - } - - $image->filesize = $metadata['size']; - $image->hash = $metadata['hash']; - $image->filename = $metadata['filename']; - $image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']); - $image->source = $metadata['source']; - - return $image; - } - - /** - * @param string $file - * @return bool - */ - protected function check_contents($file) { - $success = FALSE; - if (file_exists($file)) { - $mimeType = mime_content_type($file); - - $success = in_array($mimeType, [ - 'video/webm', - 'video/mp4', - 'video/ogg', - 'video/flv', - 'video/x-flv' - ]); - } - - return $success; - } + protected function check_contents(string $tmpname): bool + { + return ( + file_exists($tmpname) && + in_array(getMimeType($tmpname), self::SUPPORTED_MIME) + ); + } } - diff --git a/ext/handle_video/theme.php b/ext/handle_video/theme.php index 21d4ac61..eb4a8664 100644 --- a/ext/handle_video/theme.php +++ b/ext/handle_video/theme.php @@ -1,28 +1,39 @@ get_image_link(); - $thumb_url = make_http($image->get_thumb_link()); //used as fallback image - $ext = strtolower($image->get_ext()); - $full_url = make_http($ilink); - $autoplay = $config->get_bool("video_playback_autoplay"); - $loop = $config->get_bool("video_playback_loop"); - $player = make_link('lib/vendor/swf/flashmediaelement.swf'); +class VideoFileHandlerTheme extends Themelet +{ + public function display_image(Page $page, Image $image) + { + global $config; + $ilink = $image->get_image_link(); + $thumb_url = make_http($image->get_thumb_link()); //used as fallback image + $ext = strtolower($image->get_ext()); + $full_url = make_http($ilink); + $autoplay = $config->get_bool("video_playback_autoplay"); + $loop = $config->get_bool("video_playback_loop"); + $player = make_link('vendor/bower-asset/mediaelement/build/flashmediaelement.swf'); - $html = "Video not playing? Click here to download the file.
    "; + $width="auto"; + if ($image->width>1) { + $width = $image->width."px"; + } + $height="auto"; + if ($image->height>1) { + $height = $image->height."px"; + } - //Browser media format support: https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats - $supportedExts = ['mp4' => 'video/mp4', 'm4v' => 'video/mp4', 'ogv' => 'video/ogg', 'webm' => 'video/webm', 'flv' => 'video/flv']; - if(array_key_exists($ext, $supportedExts)) { - //FLV isn't supported by
    "; - } + } } - diff --git a/ext/image/config.php b/ext/image/config.php new file mode 100644 index 00000000..4a4a4cc3 --- /dev/null +++ b/ext/image/config.php @@ -0,0 +1,21 @@ + + * Modified by: jgen + * Link: http://code.shishnet.org/shimmie2/ + * Description: Handle the image database + * Visibility: admin + */ + +class ImageIOInfo extends ExtensionInfo +{ + public const KEY = "image"; + + public $key = self::KEY; + public $name = "Image Manager"; + public $url = self::SHIMMIE_URL; + public $authors = [self::SHISH_NAME=> self::SHISH_EMAIL, "jgen"=>"jgen.tech@gmail.com"]; + public $license = self::LICENSE_GPLV2; + public $description = "Handle the image database"; + public $visibility = self::VISIBLE_ADMIN; + public $core = true; +} diff --git a/ext/image/main.php b/ext/image/main.php index fde7b727..48a838ec 100644 --- a/ext/image/main.php +++ b/ext/image/main.php @@ -1,489 +1,372 @@ - * Modified by: jgen - * Link: http://code.shishnet.org/shimmie2/ - * Description: Handle the image database - * Visibility: admin - */ - - /** - * An image is being added to the database. - */ -class ImageAdditionEvent extends Event { - /** @var User */ - public $user; - - /** @var Image */ - public $image; - - /** - * Inserts a new image into the database with its associated - * information. Also calls TagSetEvent to set the tags for - * this new image. - * - * @see TagSetEvent - * @param Image $image The new image to add. - */ - public function __construct(Image $image) { - $this->image = $image; - } -} - -class ImageAdditionException extends SCoreException { - public $error; - - /** - * @param string $error - */ - public function __construct($error) { - $this->error = $error; - } -} - -/** - * An image is being deleted. - */ -class ImageDeletionEvent extends Event { - /** @var \Image */ - public $image; - - /** - * Deletes an image. - * - * Used by things like tags and comments handlers to - * clean out related rows in their tables. - * - * @param Image $image The image being deleted. - */ - public function __construct(Image $image) { - $this->image = $image; - } -} - -/** - * 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. - * - * @param int $id The ID of the image to replace. - * @param Image $image The image object of the new image to use. - */ - public function __construct(/*int*/ $id, Image $image) { - $this->id = $id; - $this->image = $image; - } -} - -class ImageReplaceException extends SCoreException { - /** @var string */ - public $error; - - /** - * @param string $error - */ - public function __construct(/*string*/ $error) { - $this->error = $error; - } -} - -/** - * 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; - - /** - * Request a thumbnail be made for an image object - * - * @param string $hash The unique hash of the image - * @param string $type The type of the image - * @param bool $force Regenerate the thumbnail even if one already exists - */ - public function __construct($hash, $type, $force=false) { - $this->hash = $hash; - $this->type = $type; - $this->force = $force; - } -} - - -/* - * ParseLinkTemplateEvent: - * $link -- the formatted link - * $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 $original; - /** @var \Image */ - public $image; - - /** - * @param string $link The formatted link - * @param Image $image The image who's link is being parsed - */ - public function __construct($link, Image $image) { - $this->link = $link; - $this->original = $link; - $this->image = $image; - } - - /** - * @param string $needle - * @param string $replace - */ - public function replace($needle, $replace) { - $this->link = str_replace($needle, $replace, $this->link); - } -} +require_once "config.php"; /** * A class to handle adding / getting / removing image files from the disk. */ -class ImageIO extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config; - $config->set_default_int('thumb_width', 192); - $config->set_default_int('thumb_height', 192); - $config->set_default_int('thumb_quality', 75); - $config->set_default_int('thumb_mem_limit', parse_shorthand_int('8MB')); - $config->set_default_string('thumb_convert_path', 'convert'); +class ImageIO extends Extension +{ + const COLLISION_OPTIONS = ['Error'=>ImageConfig::COLLISION_ERROR, 'Merge'=>ImageConfig::COLLISION_MERGE]; - if(function_exists("exif_read_data")) { - $config->set_default_bool('image_show_meta', false); - } - $config->set_default_string('image_ilink', ''); - $config->set_default_string('image_tlink', ''); - $config->set_default_string('image_tip', '$tags // $size // $filesize'); - $config->set_default_string('upload_collision_handler', 'error'); - $config->set_default_int('image_expires', (60*60*24*31) ); // defaults to one month - } - - public function onPageRequest(PageRequestEvent $event) { - if($event->page_matches("image/delete")) { - global $page, $user; - if($user->can("delete_image") && isset($_POST['image_id']) && $user->check_auth_token()) { - $image = Image::by_id($_POST['image_id']); - if($image) { - send_event(new ImageDeletionEvent($image)); - $page->set_mode("redirect"); - if(isset($_SERVER['HTTP_REFERER']) && !strstr($_SERVER['HTTP_REFERER'], 'post/view')) { - $page->set_redirect($_SERVER['HTTP_REFERER']); - } - else { - $page->set_redirect(make_link("post/list")); - } - } - } - } - else if($event->page_matches("image/replace")) { - global $page, $user; - if($user->can("replace_image") && isset($_POST['image_id']) && $user->check_auth_token()) { - $image = Image::by_id($_POST['image_id']); - if($image) { - $page->set_mode("redirect"); - $page->set_redirect(make_link('upload/replace/'.$image->id)); - } else { - /* Invalid image ID */ - throw new ImageReplaceException("Image to replace does not exist."); - } - } - } - else if($event->page_matches("image")) { - $num = int_escape($event->get_arg(0)); - $this->send_file($num, "image"); - } - else if($event->page_matches("thumb")) { - $num = int_escape($event->get_arg(0)); - $this->send_file($num, "thumb"); - } - } - - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { - global $user; - - if($user->can("delete_image")) { - $event->add_part($this->theme->get_deleter_html($event->image->id)); - } - /* In the future, could perhaps allow users to replace images that they own as well... */ - if ($user->can("replace_image")) { - $event->add_part($this->theme->get_replace_html($event->image->id)); - } - } - - public function onImageAddition(ImageAdditionEvent $event) { - try { - $this->add_image($event->image); - } - catch(ImageAdditionException $e) { - throw new UploadException($e->error); - } - } - - public function onImageDeletion(ImageDeletionEvent $event) { - $event->image->delete(); - } - - public function onImageReplace(ImageReplaceEvent $event) { - try { - $this->replace_image($event->id, $event->image); - } - catch(ImageReplaceException $e) { - throw new UploadException($e->error); - } - } - - public function onUserPageBuilding(UserPageBuildingEvent $event) { - $u_id = url_escape($event->display_user->id); - $i_image_count = Image::count_images(array("user_id={$event->display_user->id}")); - $i_days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1; - $h_image_rate = sprintf("%.1f", ($i_image_count / $i_days_old)); - $images_link = make_link("post/list/user_id=$u_id/1"); - $event->add_stats("Images uploaded: $i_image_count, $h_image_rate per day"); - } - - public function onSetupBuilding(SetupBuildingEvent $event) { - global $config; - - $sb = new SetupBlock("Image Options"); - $sb->position = 30; - // advanced only - //$sb->add_text_option("image_ilink", "Image link: "); - //$sb->add_text_option("image_tlink", "
    Thumbnail link: "); - $sb->add_text_option("image_tip", "Image tooltip: "); - $sb->add_choice_option("upload_collision_handler", array('Error'=>'error', 'Merge'=>'merge'), "
    Upload collision handler: "); - if(function_exists("exif_read_data")) { - $sb->add_bool_option("image_show_meta", "
    Show metadata: "); - } - - $event->panel->add_block($sb); - - $thumbers = array(); - $thumbers['Built-in GD'] = "gd"; - $thumbers['ImageMagick'] = "convert"; - - $sb = new SetupBlock("Thumbnailing"); - $sb->add_choice_option("thumb_engine", $thumbers, "Engine: "); - - $sb->add_label("
    Size "); - $sb->add_int_option("thumb_width"); - $sb->add_label(" x "); - $sb->add_int_option("thumb_height"); - $sb->add_label(" px at "); - $sb->add_int_option("thumb_quality"); - $sb->add_label(" % quality "); - - if($config->get_string("thumb_engine") == "convert") { - $sb->add_label("
    ImageMagick Binary: "); - $sb->add_text_option("thumb_convert_path"); - } - - if($config->get_string("thumb_engine") == "gd") { - $sb->add_shorthand_int_option("thumb_mem_limit", "
    Max memory use: "); - } - - $event->panel->add_block($sb); - } + const EXIF_READ_FUNCTION = "exif_read_data"; -// add image {{{ - /** - * @param Image $image - * @return null - * @throws ImageAdditionException - */ - private function add_image(Image $image) { - global $user, $database, $config; + const THUMBNAIL_ENGINES = [ + 'Built-in GD' => MediaEngine::GD, + 'ImageMagick' => MediaEngine::IMAGICK + ]; - /* - * Validate things - */ - if(strlen(trim($image->source)) == 0) { - $image->source = null; - } + const THUMBNAIL_TYPES = [ + 'JPEG' => "jpg", + 'WEBP (Not IE/Safari compatible)' => "webp" + ]; - /* - * Check for an existing image - */ - $existing = Image::by_hash($image->hash); - if(!is_null($existing)) { - $handler = $config->get_string("upload_collision_handler"); - if($handler == "merge" || isset($_GET['update'])) { - $merged = array_merge($image->get_tag_array(), $existing->get_tag_array()); - send_event(new TagSetEvent($existing, $merged)); - if(isset($_GET['rating']) && isset($_GET['update']) && ext_is_live("Ratings")){ - send_event(new RatingSetEvent($existing, $_GET['rating'])); - } - if(isset($_GET['source']) && isset($_GET['update'])){ - send_event(new SourceSetEvent($existing, $_GET['source'])); - } - return null; - } - else { - $error = "Image {$existing->id} ". - "already has hash {$image->hash}:

    ".$this->theme->build_thumb_html($existing); - throw new ImageAdditionException($error); - } - } + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_int(ImageConfig::THUMB_WIDTH, 192); + $config->set_default_int(ImageConfig::THUMB_HEIGHT, 192); + $config->set_default_int(ImageConfig::THUMB_SCALING, 100); + $config->set_default_int(ImageConfig::THUMB_QUALITY, 75); + $config->set_default_string(ImageConfig::THUMB_TYPE, 'jpg'); - // actually insert the info - $database->Execute( - "INSERT INTO images( + if (function_exists(self::EXIF_READ_FUNCTION)) { + $config->set_default_bool(ImageConfig::SHOW_META, false); + } + $config->set_default_string(ImageConfig::ILINK, ''); + $config->set_default_string(ImageConfig::TLINK, ''); + $config->set_default_string(ImageConfig::TIP, '$tags // $size // $filesize'); + $config->set_default_string(ImageConfig::UPLOAD_COLLISION_HANDLER, ImageConfig::COLLISION_ERROR); + $config->set_default_int(ImageConfig::EXPIRES, (60*60*24*31)); // defaults to one month + } + + public function onPageRequest(PageRequestEvent $event) + { + if ($event->page_matches("image/delete")) { + global $page, $user; + if ($user->can(Permissions::DELETE_IMAGE) && isset($_POST['image_id']) && $user->check_auth_token()) { + $image = Image::by_id($_POST['image_id']); + if ($image) { + send_event(new ImageDeletionEvent($image)); + $page->set_mode(PageMode::REDIRECT); + if (isset($_SERVER['HTTP_REFERER']) && !strstr($_SERVER['HTTP_REFERER'], 'post/view')) { + $page->set_redirect($_SERVER['HTTP_REFERER']); + } else { + $page->set_redirect(make_link("post/list")); + } + } + } + } elseif ($event->page_matches("image/replace")) { + global $page, $user; + if ($user->can(Permissions::REPLACE_IMAGE) && isset($_POST['image_id']) && $user->check_auth_token()) { + $image = Image::by_id($_POST['image_id']); + if ($image) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link('upload/replace/'.$image->id)); + } else { + /* Invalid image ID */ + throw new ImageReplaceException("Image to replace does not exist."); + } + } + } elseif ($event->page_matches("image")) { + $num = int_escape($event->get_arg(0)); + $this->send_file($num, "image"); + } elseif ($event->page_matches("thumb")) { + $num = int_escape($event->get_arg(0)); + $this->send_file($num, "thumb"); + } + } + + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) + { + global $user; + + if ($user->can(Permissions::DELETE_IMAGE)) { + $event->add_part($this->theme->get_deleter_html($event->image->id)); + } + /* In the future, could perhaps allow users to replace images that they own as well... */ + if ($user->can(Permissions::REPLACE_IMAGE)) { + $event->add_part($this->theme->get_replace_html($event->image->id)); + } + } + + public function onImageAddition(ImageAdditionEvent $event) + { + try { + $this->add_image($event); + } catch (ImageAdditionException $e) { + throw new UploadException($e->error); + } + } + + public function onImageDeletion(ImageDeletionEvent $event) + { + $event->image->delete(); + } + + public function onImageReplace(ImageReplaceEvent $event) + { + try { + $this->replace_image($event->id, $event->image); + } catch (ImageReplaceException $e) { + throw new UploadException($e->error); + } + } + + public function onUserPageBuilding(UserPageBuildingEvent $event) + { + $u_id = url_escape($event->display_user->id); + $i_image_count = Image::count_images(["user_id={$event->display_user->id}"]); + $i_days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1; + $h_image_rate = sprintf("%.1f", ($i_image_count / $i_days_old)); + $images_link = make_link("post/list/user_id=$u_id/1"); + $event->add_stats("Images uploaded: $i_image_count, $h_image_rate per day"); + } + + public function onSetupBuilding(SetupBuildingEvent $event) + { + global $config; + + $sb = new SetupBlock("Image Options"); + $sb->position = 30; + // advanced only + //$sb->add_text_option(ImageConfig::ILINK, "Image link: "); + //$sb->add_text_option(ImageConfig::TLINK, "
    Thumbnail link: "); + $sb->add_text_option(ImageConfig::TIP, "Image tooltip: "); + $sb->add_choice_option(ImageConfig::UPLOAD_COLLISION_HANDLER, self::COLLISION_OPTIONS, "
    Upload collision handler: "); + if (function_exists(self::EXIF_READ_FUNCTION)) { + $sb->add_bool_option(ImageConfig::SHOW_META, "
    Show metadata: "); + } + + $event->panel->add_block($sb); + + + + + $sb = new SetupBlock("Thumbnailing"); + $sb->add_choice_option(ImageConfig::THUMB_ENGINE, self::THUMBNAIL_ENGINES, "Engine: "); + $sb->add_label("
    "); + $sb->add_choice_option(ImageConfig::THUMB_TYPE, self::THUMBNAIL_TYPES, "Filetype: "); + + $sb->add_label("
    Size "); + $sb->add_int_option(ImageConfig::THUMB_WIDTH); + $sb->add_label(" x "); + $sb->add_int_option(ImageConfig::THUMB_HEIGHT); + $sb->add_label(" px at "); + $sb->add_int_option(ImageConfig::THUMB_QUALITY); + $sb->add_label(" % quality "); + + $sb->add_label("
    High-DPI scaling "); + $sb->add_int_option(ImageConfig::THUMB_SCALING); + $sb->add_label("%"); + + + $event->panel->add_block($sb); + } + + + // add image {{{ + private function add_image(ImageAdditionEvent $event) + { + global $user, $database, $config; + + $image = $event->image; + + /* + * Validate things + */ + if (strlen(trim($image->source)) == 0) { + $image->source = null; + } + + /* + * Check for an existing image + */ + $existing = Image::by_hash($image->hash); + if (!is_null($existing)) { + $handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER); + if ($handler == ImageConfig::COLLISION_MERGE || isset($_GET['update'])) { + $merged = array_merge($image->get_tag_array(), $existing->get_tag_array()); + send_event(new TagSetEvent($existing, $merged)); + if (isset($_GET['rating']) && isset($_GET['update']) && Extension::is_enabled(RatingsInfo::KEY)) { + send_event(new RatingSetEvent($existing, $_GET['rating'])); + } + if (isset($_GET['source']) && isset($_GET['update'])) { + send_event(new SourceSetEvent($existing, $_GET['source'])); + } + $event->merged = true; + $event->image = Image::by_id($existing->id); + return; + } else { + $error = "Image {$existing->id} ". + "already has hash {$image->hash}:

    ".$this->theme->build_thumb_html($existing); + throw new ImageAdditionException($error); + } + } + + // actually insert the info + $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, :width, :height, now(), :source + :hash, :ext, 0, 0, now(), :source )", - array( - "owner_id"=>$user->id, "owner_ip"=>$_SERVER['REMOTE_ADDR'], "filename"=>substr($image->filename, 0, 60), "filesize"=>$image->filesize, - "hash"=>$image->hash, "ext"=>strtolower($image->ext), "width"=>$image->width, "height"=>$image->height, "source"=>$image->source - ) - ); - $image->id = $database->get_last_insert_id('images_id_seq'); + [ + "owner_id" => $user->id, "owner_ip" => $_SERVER['REMOTE_ADDR'], "filename" => substr($image->filename, 0, 255), "filesize" => $image->filesize, + "hash" => $image->hash, "ext" => strtolower($image->ext), "source" => $image->source + ] + ); + $image->id = $database->get_last_insert_id('images_id_seq'); - log_info("image", "Uploaded Image #{$image->id} ({$image->hash})"); + log_info("image", "Uploaded Image #{$image->id} ({$image->hash})"); - # at this point in time, the image's tags haven't really been set, - # and so, having $image->tag_array set to something is a lie (but - # a useful one, as we want to know what the tags are /supposed/ to - # be). Here we correct the lie, by first nullifying the wrong tags - # then using the standard mechanism to set them properly. - $tags_to_set = $image->get_tag_array(); - $image->tag_array = array(); - send_event(new TagSetEvent($image, $tags_to_set)); + # at this point in time, the image's tags haven't really been set, + # and so, having $image->tag_array set to something is a lie (but + # a useful one, as we want to know what the tags are /supposed/ to + # be). Here we correct the lie, by first nullifying the wrong tags + # then using the standard mechanism to set them properly. + $tags_to_set = $image->get_tag_array(); + $image->tag_array = []; + send_event(new TagSetEvent($image, $tags_to_set)); - if($image->source !== null) { - log_info("core-image", "Source for Image #{$image->id} set to: {$image->source}"); - } - } -// }}} end add + if ($image->source !== null) { + log_info("core-image", "Source for Image #{$image->id} set to: {$image->source}"); + } -// fetch image {{{ - /** - * @param int $image_id - * @param string $type - */ - private function send_file($image_id, $type) { - global $config; - $image = Image::by_id($image_id); + try { + Media::update_image_media_properties($image->hash, strtolower($image->ext)); + } catch (MediaException $e) { + log_warning("add_image", "Error while running update_image_media_properties: ".$e->getMessage()); + } + } + // }}} end add - global $page; - if(!is_null($image)) { - $page->set_mode("data"); - if($type == "thumb") { - $page->set_type("image/jpeg"); - $file = $image->get_thumb_filename(); - } - else { - $page->set_type($image->get_mime_type()); - $file = $image->get_image_filename(); - } + // fetch image {{{ + private function send_file(int $image_id, string $type) + { + global $config; + $image = Image::by_id($image_id); - if(isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])) { - $if_modified_since = preg_replace('/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"]); - } - else { - $if_modified_since = ""; - } - $gmdate_mod = gmdate('D, d M Y H:i:s', filemtime($file)) . ' GMT'; + global $page; + if (!is_null($image)) { + if ($type == "thumb") { + $ext = $config->get_string(ImageConfig::THUMB_TYPE); + if (array_key_exists($ext, MIME_TYPE_MAP)) { + $page->set_type(MIME_TYPE_MAP[$ext]); + } else { + $page->set_type("image/jpeg"); + } - if($if_modified_since == $gmdate_mod) { - $page->set_code(304); - $page->set_data(""); - } - else { - $page->add_http_header("Last-Modified: $gmdate_mod"); - $page->set_data(file_get_contents($file)); - - if ( $config->get_int("image_expires") ) { - $expires = date(DATE_RFC1123, time() + $config->get_int("image_expires")); - } else { - $expires = 'Fri, 2 Sep 2101 12:42:42 GMT'; // War was beginning - } - $page->add_http_header('Expires: '.$expires); - } - } - else { - $page->set_title("Not Found"); - $page->set_heading("Not Found"); - $page->add_block(new Block("Navigation", "Index", "left", 0)); - $page->add_block(new Block("Image not in database", - "The requested image was not found in the database")); - } - } -// }}} end fetch + $file = $image->get_thumb_filename(); + } else { + $page->set_type($image->get_mime_type()); + $file = $image->get_image_filename(); + } -// replace image {{{ - /** - * @param int $id - * @param Image $image - * @throws ImageReplaceException - */ - private function replace_image($id, $image) { - global $database; + if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])) { + $if_modified_since = preg_replace('/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"]); + } else { + $if_modified_since = ""; + } + $gmdate_mod = gmdate('D, d M Y H:i:s', filemtime($file)) . ' GMT'; - /* Check to make sure the image exists. */ - $existing = Image::by_id($id); - - if(is_null($existing)) { - throw new ImageReplaceException("Image to replace does not exist!"); - } - - if(strlen(trim($image->source)) == 0) { - $image->source = $existing->get_source(); - } - - /* - This step could be optional, ie: perhaps move the image somewhere - and have it stored in a 'replaced images' list that could be - inspected later by an admin? - */ - log_debug("image", "Removing image with hash ".$existing->hash); - $existing->remove_image_only(); // Actually delete the old image file from disk - - // Update the data in the database. - $database->Execute( - "UPDATE images SET + if ($if_modified_since == $gmdate_mod) { + $page->set_mode(PageMode::DATA); + $page->set_code(304); + $page->set_data(""); + } else { + $page->set_mode(PageMode::FILE); + $page->add_http_header("Last-Modified: $gmdate_mod"); + if ($type != "thumb") { + $page->set_filename($image->get_nice_image_name(), 'inline'); + } + + $page->set_file($file); + + if ($config->get_int(ImageConfig::EXPIRES)) { + $expires = date(DATE_RFC1123, time() + $config->get_int(ImageConfig::EXPIRES)); + } else { + $expires = 'Fri, 2 Sep 2101 12:42:42 GMT'; // War was beginning + } + $page->add_http_header('Expires: ' . $expires); + } + } else { + $page->set_title("Not Found"); + $page->set_heading("Not Found"); + $page->add_block(new Block("Navigation", "Index", "left", 0)); + $page->add_block(new Block( + "Image not in database", + "The requested image was not found in the database" + )); + } + } + // }}} end fetch + + // replace image {{{ + private function replace_image(int $id, Image $image) + { + global $database; + + /* Check to make sure the image exists. */ + $existing = Image::by_id($id); + + if (is_null($existing)) { + throw new ImageReplaceException("Image to replace does not exist!"); + } + + $duplicate = Image::by_hash($image->hash); + + if (!is_null($duplicate) && $duplicate->id!=$id) { + $error = "Image {$duplicate->id} " . + "already has hash {$image->hash}:

    " . $this->theme->build_thumb_html($duplicate); + throw new ImageReplaceException($error); + } + + if (strlen(trim($image->source)) == 0) { + $image->source = $existing->get_source(); + } + + // Update the data in the database. + $database->Execute( + "UPDATE images SET filename = :filename, filesize = :filesize, hash = :hash, - ext = :ext, width = :width, height = :height, source = :source - WHERE + ext = :ext, width = 0, height = 0, source = :source + WHERE id = :id ", - array( - "filename"=>$image->filename, "filesize"=>$image->filesize, "hash"=>$image->hash, - "ext"=>strtolower($image->ext), "width"=>$image->width, "height"=>$image->height, "source"=>$image->source, - "id"=>$id - ) - ); + [ + "filename" => substr($image->filename, 0, 255), + "filesize" => $image->filesize, + "hash" => $image->hash, + "ext" => strtolower($image->ext), + "source" => $image->source, + "id" => $id, + ] + ); - log_info("image", "Replaced Image #{$id} with ({$image->hash})"); - } -// }}} end replace + /* + This step could be optional, ie: perhaps move the image somewhere + and have it stored in a 'replaced images' list that could be + inspected later by an admin? + */ + log_debug("image", "Removing image with hash " . $existing->hash); + $existing->remove_image_only(); // Actually delete the old image file from disk + try { + Media::update_image_media_properties($image->hash, $image->ext); + } catch (MediaException $e) { + log_warning("image_replace", "Error while running update_image_media_properties: ".$e->getMessage()); + } + + /* Generate new thumbnail */ + send_event(new ThumbnailGenerationEvent($image->hash, strtolower($image->ext))); + + log_info("image", "Replaced Image #{$id} with ({$image->hash})"); + } + // }}} end replace } // end of class ImageIO - diff --git a/ext/image/test.php b/ext/image/test.php index d5034175..a22dfca0 100644 --- a/ext/image/test.php +++ b/ext/image/test.php @@ -1,18 +1,20 @@ log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test"); +class ImageIOTest extends ShimmiePHPUnitTestCase +{ + public function testUserStats() + { + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test"); - // broken with sqlite? - //$this->get_page("user/test"); - //$this->assert_text("Images uploaded: 1"); + // broken with sqlite? + //$this->get_page("user/test"); + //$this->assert_text("Images uploaded: 1"); - //$this->click("Images uploaded"); - //$this->assert_title("Image $image_id: test"); + //$this->click("Images uploaded"); + //$this->assert_title("Image $image_id: test"); - # test that serving manually doesn't cause errors - $this->get_page("image/$image_id/moo.jpg"); - $this->get_page("thumb/$image_id/moo.jpg"); - } + # test that serving manually doesn't cause errors + $this->get_page("image/$image_id/moo.jpg"); + $this->get_page("thumb/$image_id/moo.jpg"); + } } diff --git a/ext/image/theme.php b/ext/image/theme.php index 96f23501..b20e0164 100644 --- a/ext/image/theme.php +++ b/ext/image/theme.php @@ -1,35 +1,31 @@ "; - - return $html; - } + + return $html; + } - /** - * Display link to replace the image - * - * @param $image_id integer The image to replace - * @return string - */ - public function get_replace_html(/*int*/ $image_id) { - $html = make_form(make_link("image/replace"))." + /** + * Display link to replace the image + */ + public function get_replace_html(int $image_id): string + { + $html = make_form(make_link("image/replace"))." "; - return $html; - } + return $html; + } } - diff --git a/ext/image_hash_ban/info.php b/ext/image_hash_ban/info.php new file mode 100644 index 00000000..d165286f --- /dev/null +++ b/ext/image_hash_ban/info.php @@ -0,0 +1,26 @@ + + * Link: http://atravelinggeek.com/ + * License: GPLv2 + * Description: Ban images based on their hash + * Based on the ResolutionLimit and IPban extensions by Shish + * Version 0.1, October 21, 2007 + */ + +class ImageBanInfo extends ExtensionInfo +{ + public const KEY = "image_hash_ban"; + + public $key = self::KEY; + public $name = "Image Hash Ban"; + public $url = "http://atravelinggeek.com/"; + public $authors = ["ATravelingGeek"=>"atg@atravelinggeek.com"]; + public $license = self::LICENSE_GPLV2; + public $description = "Ban images based on their hash"; + public $version = "0.1, October 21, 2007"; + public $documentation = +"Based on the ResolutionLimit and IPban extensions by Shish"; +} diff --git a/ext/image_hash_ban/main.php b/ext/image_hash_ban/main.php index 431d2cf7..33ff65d5 100644 --- a/ext/image_hash_ban/main.php +++ b/ext/image_hash_ban/main.php @@ -1,161 +1,163 @@ - * Link: http://atravelinggeek.com/ - * License: GPLv2 - * Description: Ban images based on their hash - * Based on the ResolutionLimit and IPban extensions by Shish - * Version 0.1, October 21, 2007 - */ // RemoveImageHashBanEvent {{{ -class RemoveImageHashBanEvent extends Event { - public $hash; +class RemoveImageHashBanEvent extends Event +{ + public $hash; - /** - * @param string $hash - */ - public function __construct($hash) { - $this->hash = $hash; - } + public function __construct(string $hash) + { + $this->hash = $hash; + } } // }}} // AddImageHashBanEvent {{{ -class AddImageHashBanEvent extends Event { - public $hash; - public $reason; +class AddImageHashBanEvent extends Event +{ + public $hash; + public $reason; - /** - * @param string $hash - * @param string $reason - */ - public function __construct($hash, $reason) { - $this->hash = $hash; - $this->reason = $reason; - } + public function __construct(string $hash, string $reason) + { + $this->hash = $hash; + $this->reason = $reason; + } } // }}} -class ImageBan extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config, $database; - if($config->get_int("ext_imageban_version") < 1) { - $database->create_table("image_bans", " +class ImageBan extends Extension +{ + public function onInitExt(InitExtEvent $event) + { + global $config, $database; + if ($config->get_int("ext_imageban_version") < 1) { + $database->create_table("image_bans", " id SCORE_AIPK, hash CHAR(32) NOT NULL, date SCORE_DATETIME DEFAULT SCORE_NOW, reason TEXT NOT NULL "); - $config->set_int("ext_imageban_version", 1); - } - } + $config->set_int("ext_imageban_version", 1); + } + } - public function onDataUpload(DataUploadEvent $event) { - global $database; - $row = $database->get_row("SELECT * FROM image_bans WHERE hash = :hash", array("hash"=>$event->hash)); - if($row) { - log_info("image_hash_ban", "Attempted to upload a blocked image ({$event->hash} - {$row['reason']})"); - throw new UploadException("Image ".html_escape($row["hash"])." has been banned, reason: ".format_text($row["reason"])); - } - } + public function onDataUpload(DataUploadEvent $event) + { + global $database; + $row = $database->get_row("SELECT * FROM image_bans WHERE hash = :hash", ["hash"=>$event->hash]); + if ($row) { + log_info("image_hash_ban", "Attempted to upload a blocked image ({$event->hash} - {$row['reason']})"); + throw new UploadException("Image ".html_escape($row["hash"])." has been banned, reason: ".format_text($row["reason"])); + } + } - public function onPageRequest(PageRequestEvent $event) { - global $database, $page, $user; + public function onPageRequest(PageRequestEvent $event) + { + global $database, $page, $user; - if($event->page_matches("image_hash_ban")) { - if($user->can("ban_image")) { - if($event->get_arg(0) == "add") { - $image = isset($_POST['image_id']) ? Image::by_id(int_escape($_POST['image_id'])) : null; - $hash = isset($_POST["hash"]) ? $_POST["hash"] : $image->hash; - $reason = isset($_POST['reason']) ? $_POST['reason'] : "DNP"; + if ($event->page_matches("image_hash_ban")) { + if ($user->can(Permissions::BAN_IMAGE)) { + if ($event->get_arg(0) == "add") { + $image = isset($_POST['image_id']) ? Image::by_id(int_escape($_POST['image_id'])) : null; + $hash = isset($_POST["hash"]) ? $_POST["hash"] : $image->hash; + $reason = isset($_POST['reason']) ? $_POST['reason'] : "DNP"; - if($hash) { - send_event(new AddImageHashBanEvent($hash, $reason)); - flash_message("Image ban added"); + if ($hash) { + send_event(new AddImageHashBanEvent($hash, $reason)); + flash_message("Image ban added"); - if($image) { - send_event(new ImageDeletionEvent($image)); - flash_message("Image deleted"); - } + if ($image) { + send_event(new ImageDeletionEvent($image)); + flash_message("Image deleted"); + } - $page->set_mode("redirect"); - $page->set_redirect($_SERVER['HTTP_REFERER']); - } - } - else if($event->get_arg(0) == "remove") { - if(isset($_POST['hash'])) { - send_event(new RemoveImageHashBanEvent($_POST['hash'])); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect($_SERVER['HTTP_REFERER']); + } + } elseif ($event->get_arg(0) == "remove") { + if (isset($_POST['hash'])) { + send_event(new RemoveImageHashBanEvent($_POST['hash'])); - flash_message("Image ban removed"); - $page->set_mode("redirect"); - $page->set_redirect($_SERVER['HTTP_REFERER']); - } - } - else if($event->get_arg(0) == "list") { - $page_num = 0; - if($event->count_args() == 2) { - $page_num = int_escape($event->get_arg(1)); - } - $page_size = 100; - $page_count = ceil($database->get_one("SELECT COUNT(id) FROM image_bans")/$page_size); - $this->theme->display_Image_hash_Bans($page, $page_num, $page_count, $this->get_image_hash_bans($page_num, $page_size)); - } - } - } - } + flash_message("Image ban removed"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect($_SERVER['HTTP_REFERER']); + } + } elseif ($event->get_arg(0) == "list") { + $page_num = 0; + if ($event->count_args() == 2) { + $page_num = int_escape($event->get_arg(1)); + } + $page_size = 100; + $page_count = ceil($database->get_one("SELECT COUNT(id) FROM image_bans")/$page_size); + $this->theme->display_Image_hash_Bans($page, $page_num, $page_count, $this->get_image_hash_bans($page_num, $page_size)); + } + } + } + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("ban_image")) { - $event->add_link("Image Bans", make_link("image_hash_ban/list/1")); - } - } + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::BAN_IMAGE)) { + $event->add_nav_link("image_bans", new Link('image_hash_ban/list/1'), "Image Bans", NavLink::is_active(["image_hash_ban"])); + } + } + } - public function onAddImageHashBan(AddImageHashBanEvent $event) { - global $database; - $database->Execute( - "INSERT INTO image_bans (hash, reason, date) VALUES (?, ?, now())", - array($event->hash, $event->reason)); - log_info("image_hash_ban", "Banned hash {$event->hash} because '{$event->reason}'"); - } - public function onRemoveImageHashBan(RemoveImageHashBanEvent $event) { - global $database; - $database->Execute("DELETE FROM image_bans WHERE hash = ?", array($event->hash)); - } + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::BAN_IMAGE)) { + $event->add_link("Image Bans", make_link("image_hash_ban/list/1")); + } + } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { - global $user; - if($user->can("ban_image")) { - $event->add_part($this->theme->get_buttons_html($event->image)); - } - } + public function onAddImageHashBan(AddImageHashBanEvent $event) + { + global $database; + $database->Execute( + "INSERT INTO image_bans (hash, reason, date) VALUES (?, ?, now())", + [$event->hash, $event->reason] + ); + log_info("image_hash_ban", "Banned hash {$event->hash} because '{$event->reason}'"); + } - // DB funness + public function onRemoveImageHashBan(RemoveImageHashBanEvent $event) + { + global $database; + $database->Execute("DELETE FROM image_bans WHERE hash = ?", [$event->hash]); + } - /** - * @param int $page - * @param int $size - * @return array - */ - public function get_image_hash_bans($page, $size=100) { - global $database; + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::BAN_IMAGE)) { + $event->add_part($this->theme->get_buttons_html($event->image)); + } + } - // FIXME: many - $size_i = int_escape($size); - $offset_i = int_escape($page-1)*$size_i; - $where = array("(1=1)"); - $args = array(); - if(!empty($_GET['hash'])) { - $where[] = 'hash = ?'; - $args[] = $_GET['hash']; - } - if(!empty($_GET['reason'])) { - $where[] = 'reason SCORE_ILIKE ?'; - $args[] = "%".$_GET['reason']."%"; - } - $where = implode(" AND ", $where); - $bans = $database->get_all($database->scoreql_to_sql(" + // DB funness + + public function get_image_hash_bans(int $page, int $size=100): array + { + global $database; + + // FIXME: many + $size_i = int_escape($size); + $offset_i = int_escape($page-1)*$size_i; + $where = ["(1=1)"]; + $args = []; + if (!empty($_GET['hash'])) { + $where[] = 'hash = ?'; + $args[] = $_GET['hash']; + } + if (!empty($_GET['reason'])) { + $where[] = 'reason SCORE_ILIKE ?'; + $args[] = "%".$_GET['reason']."%"; + } + $where = implode(" AND ", $where); + $bans = $database->get_all($database->scoreql_to_sql(" SELECT * FROM image_bans WHERE $where @@ -163,11 +165,16 @@ class ImageBan extends Extension { LIMIT $size_i OFFSET $offset_i "), $args); - if($bans) {return $bans;} - else {return array();} - } + if ($bans) { + return $bans; + } else { + return []; + } + } - // in before resolution limit plugin - public function get_priority() {return 30;} + // in before resolution limit plugin + public function get_priority(): int + { + return 30; + } } - diff --git a/ext/image_hash_ban/test.php b/ext/image_hash_ban/test.php index 4e22fe71..178dc50d 100644 --- a/ext/image_hash_ban/test.php +++ b/ext/image_hash_ban/test.php @@ -1,33 +1,34 @@ log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - $this->log_out(); +class HashBanTest extends ShimmiePHPUnitTestCase +{ + public function testBan() + { + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + $this->log_out(); - $this->log_in_as_admin(); - $this->get_page("post/view/$image_id"); + $this->log_in_as_admin(); + $this->get_page("post/view/$image_id"); - $this->markTestIncomplete(); + $this->markTestIncomplete(); - $this->click("Ban and Delete"); - $this->log_out(); + $this->click("Ban and Delete"); + $this->log_out(); - $this->log_in_as_user(); - $this->get_page("post/view/$image_id"); - $this->assert_response(404); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - $this->get_page("post/view/$image_id"); - $this->assert_response(404); + $this->log_in_as_user(); + $this->get_page("post/view/$image_id"); + $this->assert_response(404); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + $this->get_page("post/view/$image_id"); + $this->assert_response(404); - $this->log_in_as_admin(); - $this->get_page("image_hash_ban/list/1"); - $this->click("Remove"); + $this->log_in_as_admin(); + $this->get_page("image_hash_ban/list/1"); + $this->click("Remove"); - $this->log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - $this->get_page("post/view/$image_id"); - $this->assert_response(200); - } + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + $this->get_page("post/view/$image_id"); + $this->assert_response(200); + } } - diff --git a/ext/image_hash_ban/theme.php b/ext/image_hash_ban/theme.php index 0c759a4a..53bf6139 100644 --- a/ext/image_hash_ban/theme.php +++ b/ext/image_hash_ban/theme.php @@ -10,20 +10,22 @@ * October 21, 2007 */ -class ImageBanTheme extends Themelet { - /* - * Show all the bans - * - * $bans = an array of ( - * 'hash' => the banned hash - * 'reason' => why the hash was banned - * 'date' => when the ban started - * ) - */ - public function display_image_hash_bans(Page $page, $page_number, $page_count, $bans) { - $h_bans = ""; - foreach($bans as $ban) { - $h_bans .= " +class ImageBanTheme extends Themelet +{ + /* + * Show all the bans + * + * $bans = an array of ( + * 'hash' => the banned hash + * 'reason' => why the hash was banned + * 'date' => when the ban started + * ) + */ + public function display_image_hash_bans(Page $page, $page_number, $page_count, $bans) + { + $h_bans = ""; + foreach ($bans as $ban) { + $h_bans .= " ".make_form(make_link("image_hash_ban/remove"))." {$ban['hash']} @@ -35,8 +37,8 @@ class ImageBanTheme extends Themelet { "; - } - $html = " + } + $html = " @@ -59,29 +61,30 @@ class ImageBanTheme extends Themelet {
    HashReasonAction
    "; - $prev = $page_number - 1; - $next = $page_number + 1; + $prev = $page_number - 1; + $next = $page_number + 1; - $h_prev = ($page_number <= 1) ? "Prev" : "Prev"; - $h_index = "Index"; - $h_next = ($page_number >= $page_count) ? "Next" : "Next"; + $h_prev = ($page_number <= 1) ? "Prev" : "Prev"; + $h_index = "Index"; + $h_next = ($page_number >= $page_count) ? "Next" : "Next"; - $nav = "$h_prev | $h_index | $h_next"; + $nav = "$h_prev | $h_index | $h_next"; - $page->set_title("Image Bans"); - $page->set_heading("Image Bans"); - $page->add_block(new Block("Edit Image Bans", $html)); - $page->add_block(new Block("Navigation", $nav, "left", 0)); - $this->display_paginator($page, "image_hash_ban/list", null, $page_number, $page_count); - } + $page->set_title("Image Bans"); + $page->set_heading("Image Bans"); + $page->add_block(new Block("Edit Image Bans", $html)); + $page->add_block(new Block("Navigation", $nav, "left", 0)); + $this->display_paginator($page, "image_hash_ban/list", null, $page_number, $page_count); + } - /* - * Display a link to delete an image - * - * $image_id = the image to delete - */ - public function get_buttons_html(Image $image) { - $html = " + /* + * Display a link to delete an image + * + * $image_id = the image to delete + */ + public function get_buttons_html(Image $image) + { + $html = " ".make_form(make_link("image_hash_ban/add"))." @@ -89,7 +92,6 @@ class ImageBanTheme extends Themelet { "; - return $html; - } + return $html; + } } - diff --git a/ext/image_view_counter/info.php b/ext/image_view_counter/info.php new file mode 100644 index 00000000..2cea0069 --- /dev/null +++ b/ext/image_view_counter/info.php @@ -0,0 +1,27 @@ + + * Link: http://www.drudexsoftware.com/ + * License: GPLv2 + * Description: Tracks & displays how many times an image is viewed + * Documentation: + * + */ +class ImageViewCounterInfo extends ExtensionInfo +{ + public const KEY = "image_view_counter"; + + public $key = self::KEY; + public $name = "Image View Counter"; + public $url = "http://www.drudexsoftware.com/"; + public $authors = ["Drudex Software"=>"support@drudexsoftware.com"]; + public $license = self::LICENSE_GPLV2; + public $description = "Tracks & displays how many times an image is viewed"; + public $documentation = +"Whenever anyone views an image, a view will be added to that image. +This extension will also track any username & the IP address. +This is done to prevent duplicate views. +A person can only count as a view again 1 hour after viewing the image initially."; +} diff --git a/ext/image_view_counter/main.php b/ext/image_view_counter/main.php index 95613632..43250002 100644 --- a/ext/image_view_counter/main.php +++ b/ext/image_view_counter/main.php @@ -1,134 +1,133 @@ - * Link: http://www.drudexsoftware.com/ - * License: GPLv2 - * Description: Tracks & displays how many times an image is viewed - * Documentation: - * Whenever anyone views an image, a view will be added to that image. - * This extension will also track any username & the IP adress. - * This is done to prevent duplicate views. - * A person can only count as a view again 1 hour after viewing the image initially. - */ -class ImageViewCounter extends Extension { - private $view_interval = 3600; # allows views to be added each hour - - # Add Setup Block with options for view counter - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Image View Counter"); - $sb->add_bool_option("image_viewcounter_adminonly", "Display view counter only to admin"); - $event->panel->add_block($sb); - } - - # Adds view to database if needed - public function onDisplayingImage(DisplayingImageEvent $event) { - $imgid = $event->image->id; // determines image id - $this->addview($imgid); // adds a view - } - - # display views to user or admin below image if allowed - public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) { - global $user, $config; +class ImageViewCounter extends Extension +{ + private $view_interval = 3600; # allows views to be added each hour - $adminonly = $config->get_bool("image_viewcounter_adminonly"); // todo - if ($adminonly == false || ($adminonly && $user->is_admin())) - $event->add_part( - "Views:". - $this->get_view_count($event->image->id) . - "", 38); - } + # Add Setup Block with options for view counter + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Image View Counter"); + $sb->add_bool_option("image_viewcounter_adminonly", "Display view counter only to admin"); - # Installs DB table - public function onInitExt(InitExtEvent $event) { - global $database, $config; + $event->panel->add_block($sb); + } - // if the sql table doesn't exist yet, create it - if($config->get_bool("image_viewcounter_installed") == false) { //todo - $database->create_table("image_views"," + # Adds view to database if needed + public function onDisplayingImage(DisplayingImageEvent $event) + { + $imgid = $event->image->id; // determines image id + $this->addview($imgid); // adds a view + } + + # display views to user or admin below image if allowed + public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) + { + global $user, $config; + + $adminonly = $config->get_bool("image_viewcounter_adminonly"); // todo + if ($adminonly == false || ($adminonly && $user->can(Permissions::SEE_IMAGE_VIEW_COUNTS))) { + $event->add_part( + "Views:". + $this->get_view_count($event->image->id) . + "", + 38 + ); + } + } + + # Installs DB table + public function onInitExt(InitExtEvent $event) + { + global $database, $config; + + // if the sql table doesn't exist yet, create it + if ($config->get_bool("image_viewcounter_installed") == false) { //todo + $database->create_table("image_views", " id SCORE_AIPK, image_id INTEGER NOT NULL, user_id INTEGER NOT NULL, timestamp INTEGER NOT NULL, ipaddress SCORE_INET NOT NULL"); - $config->set_bool("image_viewcounter_installed", true); - } - } + $config->set_bool("image_viewcounter_installed", true); + } + } - /** - * Adds a view to the item if needed - * @param int $imgid - */ - private function addview($imgid) - { - global $database, $user; + /** + * Adds a view to the item if needed + */ + private function addview(int $imgid) + { + global $database, $user; - // don't add view if person already viewed recently - if ($this->can_add_view($imgid) == false) return; + // don't add view if person already viewed recently + if ($this->can_add_view($imgid) == false) { + return; + } - // Add view for current IP - $database->execute( - " + // Add view for current IP + $database->execute( + " INSERT INTO image_views (image_id, user_id, timestamp, ipaddress) VALUES (:image_id, :user_id, :timestamp, :ipaddress) ", - array( - "image_id" => $imgid, - "user_id" => $user->id, - "timestamp" => time(), - "ipaddress" => $_SERVER['REMOTE_ADDR'], - ) - ); - } + [ + "image_id" => $imgid, + "user_id" => $user->id, + "timestamp" => time(), + "ipaddress" => $_SERVER['REMOTE_ADDR'], + ] + ); + } - /** - * Returns true if this IP hasn't recently viewed this image - * @param int $imgid - */ - private function can_add_view($imgid) - { - global $database; + /** + * Returns true if this IP hasn't recently viewed this image + */ + private function can_add_view(int $imgid) + { + global $database; - // counts views from current IP in the last hour - $recent_from_ip = (int)$database->get_one( - " + // counts views from current IP in the last hour + $recent_from_ip = (int)$database->get_one( + " SELECT COUNT(*) FROM image_views WHERE ipaddress=:ipaddress AND timestamp >:lasthour AND image_id =:image_id ", - array( - "ipaddress" => $_SERVER['REMOTE_ADDR'], - "lasthour" => time() - $this->view_interval, - "image_id" => $imgid - ) - ); + [ + "ipaddress" => $_SERVER['REMOTE_ADDR'], + "lasthour" => time() - $this->view_interval, + "image_id" => $imgid + ] + ); - // if no views were found with the set criteria, return true - if($recent_from_ip == 0) return true; - else return false; - } + // if no views were found with the set criteria, return true + if ($recent_from_ip == 0) { + return true; + } else { + return false; + } + } - /** - * Returns the int of the view count from the given image id - * @param int $imgid - if not set or 0, return views of all images - */ - private function get_view_count($imgid = 0) - { - global $database; + /** + * Returns the int of the view count from the given image id + */ + private function get_view_count(int $imgid = 0) + { + global $database; - if ($imgid == 0) // return view count of all images - $view_count = (int)$database->get_one( - "SELECT COUNT(*) FROM image_views" - ); - else // return view count of specified image - $view_count = (int)$database->get_one( - "SELECT COUNT(*) FROM image_views WHERE image_id =:image_id", - array("image_id" => $imgid) - ); + if ($imgid == 0) { // return view count of all images + $view_count = (int)$database->get_one( + "SELECT COUNT(*) FROM image_views" + ); + } else { // return view count of specified image + $view_count = (int)$database->get_one( + "SELECT COUNT(*) FROM image_views WHERE image_id =:image_id", + ["image_id" => $imgid] + ); + } - // returns the count as int - return $view_count; - } + // returns the count as int + return $view_count; + } } - diff --git a/ext/index/info.php b/ext/index/info.php new file mode 100644 index 00000000..9987f569 --- /dev/null +++ b/ext/index/info.php @@ -0,0 +1,169 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: Show a list of uploaded images + * Documentation: + * Here is a list of the search methods available out of the box; + * Shimmie extensions may provide other filters: + *

      + *
    • by tag, eg + *
        + *
      • cat + *
      • pie + *
      • somethi* -- wildcards are supported + *
      + *
    • size (=, <, >, <=, >=) width x height, eg + *
        + *
      • size=1024x768 -- a specific wallpaper size + *
      • size>=500x500 -- no small images + *
      • size<1000x1000 -- no large images + *
      + *
    • width (=, <, >, <=, >=) width, eg + *
        + *
      • width=1024 -- find images with 1024 width + *
      • width>2000 -- find images bigger than 2000 width + *
      + *
    • height (=, <, >, <=, >=) height, eg + *
        + *
      • height=768 -- find images with 768 height + *
      • height>1000 -- find images bigger than 1000 height + *
      + *
    • ratio (=, <, >, <=, >=) width : height, eg + *
        + *
      • ratio=4:3, ratio=16:9 -- standard wallpaper + *
      • ratio=1:1 -- square images + *
      • ratio<1:1 -- tall images + *
      • ratio>1:1 -- wide images + *
      + *
    • filesize (=, <, >, <=, >=) size, eg + *
        + *
      • filesize>1024 -- no images under 1KB + *
      • filesize<=3MB -- shorthand filesizes are supported too + *
      + *
    • id (=, <, >, <=, >=) number, eg + *
        + *
      • id<20 -- search only the first few images + *
      • id>=500 -- search later images + *
      + *
    • user=Username & poster=Username, eg + *
        + *
      • user=Shish -- find all of Shish's posts + *
      • poster=Shish -- same as above + *
      + *
    • user_id=userID & poster_id=userID, eg + *
        + *
      • user_id=2 -- find all posts by user id 2 + *
      • poster_id=2 -- same as above + *
      + *
    • hash=md5sum & md5=md5sum, eg + *
        + *
      • hash=bf5b59173f16b6937a4021713dbfaa72 -- find the "Taiga want up!" image + *
      • md5=bf5b59173f16b6937a4021713dbfaa72 -- same as above + *
      + *
    • filetype=type & ext=type, eg + *
        + *
      • filetype=png -- find all PNG images + *
      • ext=png -- same as above + *
      + *
    • filename=blah & name=blah, eg + *
        + *
      • filename=kitten -- find all images with "kitten" in the original filename + *
      • name=kitten -- same as above + *
      + *
    • posted (=, <, >, <=, >=) date, eg + *
        + *
      • posted>=2009-12-25 posted<=2010-01-01 -- find images posted between christmas and new year + *
      + *
    • tags (=, <, >, <=, >=) count, eg + *
        + *
      • tags=1 -- search for images with only 1 tag + *
      • tags>=10 -- search for images with 10 or more tags + *
      • tags<25 -- search for images with less than 25 tags + *
      + *
    • source=(URL, any, none) eg + *
        + *
      • source=http://example.com -- find all images with "http://example.com" in the source + *
      • source=any -- find all images with a source + *
      • source=none -- find all images without a source + *
      + *
    • order=(id, width, height, filesize, filename)_(ASC, DESC), eg + *
        + *
      • order=width -- find all images sorted from highest > lowest width + *
      • order=filesize_asc -- find all images sorted from lowest > highest filesize + *
      + *
    • order=random_####, eg + *
        + *
      • order=random_8547 -- find all images sorted randomly using 8547 as a seed + *
      + *
    + *

    Search items can be combined to search for images which match both, + * or you can stick "-" in front of an item to search for things that don't + * match it. + *

    Metatags can be followed by ":" rather than "=" if you prefer. + *
    I.E: "posted:2014-01-01", "id:>=500" etc. + *

    Some search methods provided by extensions: + *

      + *
    • Numeric Score + *
        + *
      • score (=, <, >, <=, >=) number -- seach by score + *
      • upvoted_by=Username -- search for a user's likes + *
      • downvoted_by=Username -- search for a user's dislikes + *
      • upvoted_by_id=UserID -- search for a user's likes by user ID + *
      • downvoted_by_id=UserID -- search for a user's dislikes by user ID + *
      • order=score_(ASC, DESC) -- find all images sorted from by score + *
      + *
    • Image Rating + *
        + *
      • rating=se -- find safe and explicit images, ignore questionable and unknown + *
      + *
    • Favorites + *
        + *
      • favorites (=, <, >, <=, >=) number -- search for images favourited a certain number of times + *
      • favourited_by=Username -- search for a user's choices by username + *
      • favorited_by_userno=UserID -- search for a user's choice by userID + *
      + *
    • Notes + *
        + *
      • notes (=, <, >, <=, >=) number -- search by the number of notes an image has + *
      • notes_by=Username -- search for images containing notes created by username + *
      • notes_by_userno=UserID -- search for images containing notes created by userID + *
      + *
    • Artists + *
        + *
      • author=ArtistName -- search for images by artist + *
      + *
    • Image Comments + *
        + *
      • comments (=, <, >, <=, >=) number -- search for images by number of comments + *
      • commented_by=Username -- search for images containing user's comments by username + *
      • commented_by_userno=UserID -- search for images containing user's comments by userID + *
      + *
    • Pools + *
        + *
      • pool=(PoolID, any, none) -- search for images in a pool by PoolID. + *
      • pool_by_name=PoolName -- search for images in a pool by PoolName. underscores are replaced with spaces + *
      + *
    • Post Relationships + *
        + *
      • parent=(parentID, any, none) -- search for images by parentID / if they have, do not have a parent + *
      • child=(any, none) -- search for images which have, or do not have children + *
      + *
    + */ + +class IndexInfo extends ExtensionInfo +{ + public const KEY = "index"; + + public $key = self::KEY; + public $name = "Image List"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Show a list of uploaded images"; + public $core = true; +} diff --git a/ext/index/main.php b/ext/index/main.php index 29d225ea..5d136b20 100644 --- a/ext/index/main.php +++ b/ext/index/main.php @@ -1,332 +1,218 @@ - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Show a list of uploaded images - * Documentation: - * Here is a list of the search methods available out of the box; - * Shimmie extensions may provide other filters: - *
      - *
    • by tag, eg - *
        - *
      • cat - *
      • pie - *
      • somethi* -- wildcards are supported - *
      - *
    • size (=, <, >, <=, >=) width x height, eg - *
        - *
      • size=1024x768 -- a specific wallpaper size - *
      • size>=500x500 -- no small images - *
      • size<1000x1000 -- no large images - *
      - *
    • width (=, <, >, <=, >=) width, eg - *
        - *
      • width=1024 -- find images with 1024 width - *
      • width>2000 -- find images bigger than 2000 width - *
      - *
    • height (=, <, >, <=, >=) height, eg - *
        - *
      • height=768 -- find images with 768 height - *
      • height>1000 -- find images bigger than 1000 height - *
      - *
    • ratio (=, <, >, <=, >=) width : height, eg - *
        - *
      • ratio=4:3, ratio=16:9 -- standard wallpaper - *
      • ratio=1:1 -- square images - *
      • ratio<1:1 -- tall images - *
      • ratio>1:1 -- wide images - *
      - *
    • filesize (=, <, >, <=, >=) size, eg - *
        - *
      • filesize>1024 -- no images under 1KB - *
      • filesize<=3MB -- shorthand filesizes are supported too - *
      - *
    • id (=, <, >, <=, >=) number, eg - *
        - *
      • id<20 -- search only the first few images - *
      • id>=500 -- search later images - *
      - *
    • user=Username & poster=Username, eg - *
        - *
      • user=Shish -- find all of Shish's posts - *
      • poster=Shish -- same as above - *
      - *
    • user_id=userID & poster_id=userID, eg - *
        - *
      • user_id=2 -- find all posts by user id 2 - *
      • poster_id=2 -- same as above - *
      - *
    • hash=md5sum & md5=md5sum, eg - *
        - *
      • hash=bf5b59173f16b6937a4021713dbfaa72 -- find the "Taiga want up!" image - *
      • md5=bf5b59173f16b6937a4021713dbfaa72 -- same as above - *
      - *
    • filetype=type & ext=type, eg - *
        - *
      • filetype=png -- find all PNG images - *
      • ext=png -- same as above - *
      - *
    • filename=blah & name=blah, eg - *
        - *
      • filename=kitten -- find all images with "kitten" in the original filename - *
      • name=kitten -- same as above - *
      - *
    • posted (=, <, >, <=, >=) date, eg - *
        - *
      • posted>=2009-12-25 posted<=2010-01-01 -- find images posted between christmas and new year - *
      - *
    • tags (=, <, >, <=, >=) count, eg - *
        - *
      • tags=1 -- search for images with only 1 tag - *
      • tags>=10 -- search for images with 10 or more tags - *
      • tags<25 -- search for images with less than 25 tags - *
      - *
    • source=(URL, any, none) eg - *
        - *
      • source=http://example.com -- find all images with "http://example.com" in the source - *
      • source=any -- find all images with a source - *
      • source=none -- find all images without a source - *
      - *
    • order=(id, width, height, filesize, filename)_(ASC, DESC), eg - *
        - *
      • order=width -- find all images sorted from highest > lowest width - *
      • order=filesize_asc -- find all images sorted from lowest > highest filesize - *
      - *
    • order=random_####, eg - *
        - *
      • order=random_8547 -- find all images sorted randomly using 8547 as a seed - *
      - *
    - *

    Search items can be combined to search for images which match both, - * or you can stick "-" in front of an item to search for things that don't - * match it. - *

    Metatags can be followed by ":" rather than "=" if you prefer. - *
    I.E: "posted:2014-01-01", "id:>=500" etc. - *

    Some search methods provided by extensions: - *

      - *
    • Numeric Score - *
        - *
      • score (=, <, >, <=, >=) number -- seach by score - *
      • upvoted_by=Username -- search for a user's likes - *
      • downvoted_by=Username -- search for a user's dislikes - *
      • upvoted_by_id=UserID -- search for a user's likes by user ID - *
      • downvoted_by_id=UserID -- search for a user's dislikes by user ID - *
      • order=score_(ASC, DESC) -- find all images sorted from by score - *
      - *
    • Image Rating - *
        - *
      • rating=se -- find safe and explicit images, ignore questionable and unknown - *
      - *
    • Favorites - *
        - *
      • favorites (=, <, >, <=, >=) number -- search for images favourited a certain number of times - *
      • favourited_by=Username -- search for a user's choices by username - *
      • favorited_by_userno=UserID -- search for a user's choice by userID - *
      - *
    • Notes - *
        - *
      • notes (=, <, >, <=, >=) number -- search by the number of notes an image has - *
      • notes_by=Username -- search for images containing notes created by username - *
      • notes_by_userno=UserID -- search for images containing notes created by userID - *
      - *
    • Artists - *
        - *
      • author=ArtistName -- search for images by artist - *
      - *
    • Image Comments - *
        - *
      • comments (=, <, >, <=, >=) number -- search for images by number of comments - *
      • commented_by=Username -- search for images containing user's comments by username - *
      • commented_by_userno=UserID -- search for images containing user's comments by userID - *
      - *
    • Pools - *
        - *
      • pool=(PoolID, any, none) -- search for images in a pool by PoolID. - *
      • pool_by_name=PoolName -- search for images in a pool by PoolName. underscores are replaced with spaces - *
      - *
    • Post Relationships - *
        - *
      • parent=(parentID, any, none) -- search for images by parentID / if they have, do not have a parent - *
      • child=(any, none) -- search for images which have, or do not have children - *
      - *
    - */ /* * SearchTermParseEvent: * Signal that a search term needs parsing */ -class SearchTermParseEvent extends Event { - /** @var null|string */ - public $term = null; - /** @var string[] */ - public $context = array(); - /** @var \Querylet[] */ - public $querylets = array(); +class SearchTermParseEvent extends Event +{ + /** @var null|string */ + public $term = null; + /** @var string[] */ + public $context = []; + /** @var Querylet[] */ + public $querylets = []; - /** - * @param string|null $term - * @param string[] $context - */ - public function __construct($term, array $context) { - $this->term = $term; - $this->context = $context; - } + public function __construct(string $term=null, array $context=[]) + { + $this->term = $term; + $this->context = $context; + } - /** - * @return bool - */ - public function is_querylet_set() { - return (count($this->querylets) > 0); - } + public function is_querylet_set(): bool + { + return (count($this->querylets) > 0); + } - /** - * @return \Querylet[] - */ - public function get_querylets() { - return $this->querylets; - } + public function get_querylets(): array + { + return $this->querylets; + } - /** - * @param \Querylet $q - */ - public function add_querylet($q) { - $this->querylets[] = $q; - } + public function add_querylet(Querylet $q) + { + $this->querylets[] = $q; + } } -class SearchTermParseException extends SCoreException { +class SearchTermParseException extends SCoreException +{ } -class PostListBuildingEvent extends Event { - /** @var array */ - public $search_terms = array(); +class PostListBuildingEvent extends Event +{ + /** @var array */ + public $search_terms = []; - /** @var array */ - public $parts = array(); + /** @var array */ + public $parts = []; - /** - * @param string[] $search - */ - public function __construct(array $search) { - $this->search_terms = $search; - } + /** + * #param string[] $search + */ + public function __construct(array $search) + { + $this->search_terms = $search; + } - /** - * @param string $html - * @param int $position - */ - public function add_control(/*string*/ $html, /*int*/ $position=50) { - while(isset($this->parts[$position])) $position++; - $this->parts[$position] = $html; - } + public function add_control(string $html, int $position=50) + { + while (isset($this->parts[$position])) { + $position++; + } + $this->parts[$position] = $html; + } } -class Index extends Extension { +class Index extends Extension +{ /** @var int */ - private $stpen = 0; // search term parse event number + private $stpen = 0; // search term parse event number - public function onInitExt(InitExtEvent $event) { - global $config; - $config->set_default_int("index_images", 24); - $config->set_default_bool("index_tips", true); - $config->set_default_string("index_order", "id DESC"); - } + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_int("index_images", 24); + $config->set_default_bool("index_tips", true); + $config->set_default_string("index_order", "id DESC"); + } - public function onPageRequest(PageRequestEvent $event) { - global $database, $page; - if($event->page_matches("post/list")) { - if(isset($_GET['search'])) { - // implode(explode()) to resolve aliases and sanitise - $search = url_escape(Tag::implode(Tag::explode($_GET['search'], false))); - if(empty($search)) { - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/list/1")); - } - else { - $page->set_mode("redirect"); - $page->set_redirect(make_link('post/list/'.$search.'/1')); - } - return; - } + public function onPageRequest(PageRequestEvent $event) + { + global $database, $page, $user; + if ($event->page_matches("post/list")) { + if (isset($_GET['search'])) { + // implode(explode()) to resolve aliases and sanitise + $search = url_escape(Tag::implode(Tag::explode($_GET['search'], false))); + if (empty($search)) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/list/1")); + } else { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link('post/list/'.$search.'/1')); + } + return; + } - $search_terms = $event->get_search_terms(); - $page_number = $event->get_page_number(); - $page_size = $event->get_page_size(); + $search_terms = $event->get_search_terms(); + $page_number = $event->get_page_number(); + $page_size = $event->get_page_size(); - $count_search_terms = count($search_terms); + $count_search_terms = count($search_terms); - try { - #log_debug("index", "Search for ".implode(" ", $search_terms), false, array("terms"=>$search_terms)); - $total_pages = Image::count_pages($search_terms); - if(SPEED_HAX && $count_search_terms === 0 && ($page_number < 10)) { // extra caching for the first few post/list pages - $images = $database->cache->get("post-list:$page_number"); - if(!$images) { - $images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms); - $database->cache->set("post-list:$page_number", $images, 60); - } - } - else { - $images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms); - } - } - catch(SearchTermParseException $stpe) { - // FIXME: display the error somewhere - $total_pages = 0; - $images = array(); - } + try { + #log_debug("index", "Search for ".Tag::implode($search_terms), false, array("terms"=>$search_terms)); + $total_pages = Image::count_pages($search_terms); + $images = []; - $count_images = count($images); + if (SPEED_HAX) { + if (!$user->can("big_search")) { + $fast_page_limit = 500; + if ($total_pages > $fast_page_limit) { + $total_pages = $fast_page_limit; + } + if ($page_number > $fast_page_limit) { + $this->theme->display_error( + 404, + "Search limit hit", + "Only $fast_page_limit pages of results are searchable - " . + "if you want to find older results, use more specific search terms" + ); + return; + } + } + if ($count_search_terms === 0 && ($page_number < 10)) { + // extra caching for the first few post/list pages + $images = $database->cache->get("post-list:$page_number"); + if (!$images) { + $images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms); + $database->cache->set("post-list:$page_number", $images, 60); + } + } + } - if($count_search_terms === 0 && $count_images === 0 && $page_number === 1) { - $this->theme->display_intro($page); - send_event(new PostListBuildingEvent($search_terms)); - } - else if($count_search_terms > 0 && $count_images === 1 && $page_number === 1) { - $page->set_mode("redirect"); - $page->set_redirect(make_link('post/view/'.$images[0]->id)); - } - else { - $plbe = new PostListBuildingEvent($search_terms); - send_event($plbe); + if (!$images) { + $images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms); + } + } catch (SearchTermParseException $stpe) { + // FIXME: display the error somewhere + $total_pages = 0; + $images = []; + } - $this->theme->set_page($page_number, $total_pages, $search_terms); - $this->theme->display_page($page, $images); - if(count($plbe->parts) > 0) { - $this->theme->display_admin_block($plbe->parts); - } - } - } - } + $count_images = count($images); - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Index Options"); - $sb->position = 20; + if ($count_search_terms === 0 && $count_images === 0 && $page_number === 1) { + $this->theme->display_intro($page); + send_event(new PostListBuildingEvent($search_terms)); + } elseif ($count_search_terms > 0 && $count_images === 1 && $page_number === 1) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link('post/view/'.$images[0]->id)); + } else { + $plbe = new PostListBuildingEvent($search_terms); + send_event($plbe); - $sb->add_label("Show "); - $sb->add_int_option("index_images"); - $sb->add_label(" images on the post list"); + $this->theme->set_page($page_number, $total_pages, $search_terms); + $this->theme->display_page($page, $images); + if (count($plbe->parts) > 0) { + $this->theme->display_admin_block($plbe->parts); + } + } + } + } - $event->panel->add_block($sb); - } + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Index Options"); + $sb->position = 20; - public function onImageInfoSet(ImageInfoSetEvent $event) { - global $database; - if(SPEED_HAX) { - $database->cache->delete("thumb-block:{$event->image->id}"); - } - } + $sb->add_label("Show "); + $sb->add_int_option("index_images"); + $sb->add_label(" images on the post list"); - public function onSearchTermParse(SearchTermParseEvent $event) { - $matches = array(); - // check for tags first as tag based searches are more common. - if(preg_match("/^tags([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { - $cmp = ltrim($matches[1], ":") ?: "="; - $count = $matches[2]; - $event->add_querylet( - new Querylet("EXISTS ( + $event->panel->add_block($sb); + } + + public function onImageInfoSet(ImageInfoSetEvent $event) + { + global $database; + if (SPEED_HAX) { + $database->cache->delete("thumb-block:{$event->image->id}"); + } + } + + public function onPageNavBuilding(PageNavBuildingEvent $event) + { + $event->add_nav_link("posts", new Link('post/list'), "Posts", NavLink::is_active(["post","view"]), 20); + } + + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + if ($event->parent=="posts") { + $event->add_nav_link("posts_all", new Link('post/list'), "All"); + } + } + + public function onHelpPageBuilding(HelpPageBuildingEvent $event) + { + if ($event->key===HelpPages::SEARCH) { + $block = new Block(); + $block->header = "General"; + $block->body = $this->theme->get_help_html(); + $event->add_block($block, 0); + } + } + + + public function onSearchTermParse(SearchTermParseEvent $event) + { + $matches = []; + // check for tags first as tag based searches are more common. + if (preg_match("/^tags([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { + $cmp = ltrim($matches[1], ":") ?: "="; + $count = $matches[2]; + $event->add_querylet( + new Querylet("EXISTS ( SELECT 1 FROM image_tags it LEFT JOIN tags t ON it.tag_id = t.id @@ -334,74 +220,66 @@ class Index extends Extension { GROUP BY image_id HAVING COUNT(*) $cmp $count )") - ); - } - else if(preg_match("/^ratio([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+):(\d+)$/i", $event->term, $matches)) { - $cmp = preg_replace('/^:/', '=', $matches[1]); - $args = array("width{$this->stpen}"=>int_escape($matches[2]), "height{$this->stpen}"=>int_escape($matches[3])); - $event->add_querylet(new Querylet("width / height $cmp :width{$this->stpen} / :height{$this->stpen}", $args)); - } - else if(preg_match("/^(filesize|id)([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+[kmg]?b?)$/i", $event->term, $matches)) { - $col = $matches[1]; - $cmp = ltrim($matches[2], ":") ?: "="; - $val = parse_shorthand_int($matches[3]); - $event->add_querylet(new Querylet("images.$col $cmp :val{$this->stpen}", array("val{$this->stpen}"=>$val))); - } - else if(preg_match("/^(hash|md5)[=|:]([0-9a-fA-F]*)$/i", $event->term, $matches)) { - $hash = strtolower($matches[2]); - $event->add_querylet(new Querylet('images.hash = :hash', array("hash" => $hash))); - } - else if(preg_match("/^(filetype|ext)[=|:]([a-zA-Z0-9]*)$/i", $event->term, $matches)) { - $ext = strtolower($matches[2]); - $event->add_querylet(new Querylet('images.ext = :ext', array("ext" => $ext))); - } - else if(preg_match("/^(filename|name)[=|:]([a-zA-Z0-9]*)$/i", $event->term, $matches)) { - $filename = strtolower($matches[2]); - $event->add_querylet(new Querylet("images.filename LIKE :filename{$this->stpen}", array("filename{$this->stpen}"=>"%$filename%"))); - } - else if(preg_match("/^(source)[=|:](.*)$/i", $event->term, $matches)) { - $source = strtolower($matches[2]); + ); + } elseif (preg_match("/^ratio([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+):(\d+)$/i", $event->term, $matches)) { + $cmp = preg_replace('/^:/', '=', $matches[1]); + $args = ["width{$this->stpen}"=>int_escape($matches[2]), "height{$this->stpen}"=>int_escape($matches[3])]; + $event->add_querylet(new Querylet("width / height $cmp :width{$this->stpen} / :height{$this->stpen}", $args)); + } elseif (preg_match("/^(filesize|id)([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+[kmg]?b?)$/i", $event->term, $matches)) { + $col = $matches[1]; + $cmp = ltrim($matches[2], ":") ?: "="; + $val = parse_shorthand_int($matches[3]); + $event->add_querylet(new Querylet("images.$col $cmp :val{$this->stpen}", ["val{$this->stpen}"=>$val])); + } elseif (preg_match("/^(hash|md5)[=|:]([0-9a-fA-F]*)$/i", $event->term, $matches)) { + $hash = strtolower($matches[2]); + $event->add_querylet(new Querylet('images.hash = :hash', ["hash" => $hash])); + } elseif (preg_match("/^(phash)[=|:]([0-9a-fA-F]*)$/i", $event->term, $matches)) { + $phash = strtolower($matches[2]); + $event->add_querylet(new Querylet('images.phash = :phash', ["phash" => $phash])); + } elseif (preg_match("/^(filetype|ext)[=|:]([a-zA-Z0-9]*)$/i", $event->term, $matches)) { + $ext = strtolower($matches[2]); + $event->add_querylet(new Querylet('images.ext = :ext', ["ext" => $ext])); + } elseif (preg_match("/^(filename|name)[=|:]([a-zA-Z0-9]*)$/i", $event->term, $matches)) { + $filename = strtolower($matches[2]); + $event->add_querylet(new Querylet("images.filename LIKE :filename{$this->stpen}", ["filename{$this->stpen}"=>"%$filename%"])); + } elseif (preg_match("/^(source)[=|:](.*)$/i", $event->term, $matches)) { + $source = strtolower($matches[2]); - if(preg_match("/^(any|none)$/i", $source)){ - $not = ($source == "any" ? "NOT" : ""); - $event->add_querylet(new Querylet("images.source IS $not NULL")); - }else{ - $event->add_querylet(new Querylet('images.source LIKE :src', array("src"=>"%$source%"))); - } - } - else if(preg_match("/^posted([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])([0-9-]*)$/i", $event->term, $matches)) { - $cmp = ltrim($matches[1], ":") ?: "="; - $val = $matches[2]; - $event->add_querylet(new Querylet("images.posted $cmp :posted{$this->stpen}", array("posted{$this->stpen}"=>$val))); - } - else if(preg_match("/^size([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)x(\d+)$/i", $event->term, $matches)) { - $cmp = ltrim($matches[1], ":") ?: "="; - $args = array("width{$this->stpen}"=>int_escape($matches[2]), "height{$this->stpen}"=>int_escape($matches[3])); - $event->add_querylet(new Querylet("width $cmp :width{$this->stpen} AND height $cmp :height{$this->stpen}", $args)); - } - else if(preg_match("/^width([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { - $cmp = ltrim($matches[1], ":") ?: "="; - $event->add_querylet(new Querylet("width $cmp :width{$this->stpen}", array("width{$this->stpen}"=>int_escape($matches[2])))); - } - else if(preg_match("/^height([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { - $cmp = ltrim($matches[1], ":") ?: "="; - $event->add_querylet(new Querylet("height $cmp :height{$this->stpen}",array("height{$this->stpen}"=>int_escape($matches[2])))); - } - else if(preg_match("/^order[=|:](id|width|height|filesize|filename)[_]?(desc|asc)?$/i", $event->term, $matches)){ - $ord = strtolower($matches[1]); - $default_order_for_column = preg_match("/^(id|filename)$/", $matches[1]) ? "ASC" : "DESC"; - $sort = isset($matches[2]) ? strtoupper($matches[2]) : $default_order_for_column; - Image::$order_sql = "images.$ord $sort"; - $event->add_querylet(new Querylet("1=1")); //small hack to avoid metatag being treated as normal tag - } - else if(preg_match("/^order[=|:]random[_]([0-9]{1,4})$/i", $event->term, $matches)){ - //order[=|:]random requires a seed to avoid duplicates - //since the tag can't be changed during the parseevent, we instead generate the seed during submit using js - $seed = $matches[1]; - Image::$order_sql = "RAND($seed)"; - $event->add_querylet(new Querylet("1=1")); //small hack to avoid metatag being treated as normal tag - } + if (preg_match("/^(any|none)$/i", $source)) { + $not = ($source == "any" ? "NOT" : ""); + $event->add_querylet(new Querylet("images.source IS $not NULL")); + } else { + $event->add_querylet(new Querylet('images.source LIKE :src', ["src"=>"%$source%"])); + } + } elseif (preg_match("/^posted([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])([0-9-]*)$/i", $event->term, $matches)) { + // TODO Make this able to search = without needing a time component. + $cmp = ltrim($matches[1], ":") ?: "="; + $val = $matches[2]; + $event->add_querylet(new Querylet("images.posted $cmp :posted{$this->stpen}", ["posted{$this->stpen}"=>$val])); + } elseif (preg_match("/^size([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)x(\d+)$/i", $event->term, $matches)) { + $cmp = ltrim($matches[1], ":") ?: "="; + $args = ["width{$this->stpen}"=>int_escape($matches[2]), "height{$this->stpen}"=>int_escape($matches[3])]; + $event->add_querylet(new Querylet("width $cmp :width{$this->stpen} AND height $cmp :height{$this->stpen}", $args)); + } elseif (preg_match("/^width([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { + $cmp = ltrim($matches[1], ":") ?: "="; + $event->add_querylet(new Querylet("width $cmp :width{$this->stpen}", ["width{$this->stpen}"=>int_escape($matches[2])])); + } elseif (preg_match("/^height([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { + $cmp = ltrim($matches[1], ":") ?: "="; + $event->add_querylet(new Querylet("height $cmp :height{$this->stpen}", ["height{$this->stpen}"=>int_escape($matches[2])])); + } elseif (preg_match("/^order[=|:](id|width|height|filesize|filename)[_]?(desc|asc)?$/i", $event->term, $matches)) { + $ord = strtolower($matches[1]); + $default_order_for_column = preg_match("/^(id|filename)$/", $matches[1]) ? "ASC" : "DESC"; + $sort = isset($matches[2]) ? strtoupper($matches[2]) : $default_order_for_column; + Image::$order_sql = "images.$ord $sort"; + $event->add_querylet(new Querylet("1=1")); //small hack to avoid metatag being treated as normal tag + } elseif (preg_match("/^order[=|:]random[_]([0-9]{1,4})$/i", $event->term, $matches)) { + //order[=|:]random requires a seed to avoid duplicates + //since the tag can't be changed during the parseevent, we instead generate the seed during submit using js + $seed = $matches[1]; + Image::$order_sql = "RAND($seed)"; + $event->add_querylet(new Querylet("1=1")); //small hack to avoid metatag being treated as normal tag + } - $this->stpen++; - } + $this->stpen++; + } } diff --git a/ext/index/script.js b/ext/index/script.js index 5f1ab8e2..02113695 100644 --- a/ext/index/script.js +++ b/ext/index/script.js @@ -31,6 +31,21 @@ $(function() { input.val(tagArr.join(" ")); } }); + + /* + * 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); + }); + } + }); }); function select_blocked_tags() { diff --git a/ext/index/style.css b/ext/index/style.css index a709965f..c23a0c12 100644 --- a/ext/index/style.css +++ b/ext/index/style.css @@ -1,4 +1,5 @@ +/*noinspection CssRedundantUnit*/ #image-list .blockbody { background: none; border: none; diff --git a/ext/index/test.php b/ext/index/test.php index 682fb1cc..5d54fd42 100644 --- a/ext/index/test.php +++ b/ext/index/test.php @@ -1,204 +1,219 @@ log_in_as_user(); - $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "thing computer screenshot pbx phone"); - $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "thing computer computing bedroom workshop"); - $this->log_out(); +class IndexTest extends ShimmiePHPUnitTestCase +{ + private function upload() + { + $this->log_in_as_user(); + $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "thing computer screenshot pbx phone"); + $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "thing computer computing bedroom workshop"); + $this->log_out(); - # make sure both uploads were ok - $this->assertTrue($image_id_1 > 0); - $this->assertTrue($image_id_2 > 0); + # make sure both uploads were ok + $this->assertTrue($image_id_1 > 0); + $this->assertTrue($image_id_2 > 0); - return array($image_id_1, $image_id_2); - } + return [$image_id_1, $image_id_2]; + } - public function testIndexPage() { - $this->get_page('post/list'); - $this->assert_title("Welcome to Shimmie ".VERSION); - $this->assert_no_text("Prev | Index | Next"); + public function testIndexPage() + { + $this->get_page('post/list'); + $this->assert_title("Welcome to Shimmie"); + $this->assert_no_text("Prev | Index | Next"); - $this->log_in_as_user(); - $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - $this->log_out(); + $this->log_in_as_user(); + $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + $this->log_out(); - $this->get_page('post/list'); - $this->assert_title("Shimmie"); - // FIXME - //$this->assert_text("Prev | Index | Next"); + $this->get_page('post/list'); + $this->assert_title("Shimmie"); + // FIXME + //$this->assert_text("Prev | Index | Next"); - $this->get_page('post/list/-1'); - $this->assert_title("Shimmie"); + $this->get_page('post/list/-1'); + $this->assert_title("Shimmie"); - $this->get_page('post/list/0'); - $this->assert_title("Shimmie"); + $this->get_page('post/list/0'); + $this->assert_title("Shimmie"); - $this->get_page('post/list/1'); - $this->assert_title("Shimmie"); + $this->get_page('post/list/1'); + $this->assert_title("Shimmie"); - $this->get_page('post/list/99999'); - $this->assert_response(404); - } + $this->get_page('post/list/99999'); + $this->assert_response(404); + } - /* * * * * * * * * * * - * Tag Search * - * * * * * * * * * * */ - public function testTagSearchNoResults() { - $image_ids = $this->upload(); + /* * * * * * * * * * * + * Tag Search * + * * * * * * * * * * */ + public function testTagSearchNoResults() + { + $image_ids = $this->upload(); - $this->get_page('post/list/maumaumau/1'); - $this->assert_response(404); - } + $this->get_page('post/list/maumaumau/1'); + $this->assert_response(404); + } - public function testTagSearchOneResult() { - $image_ids = $this->upload(); + public function testTagSearchOneResult() + { + $image_ids = $this->upload(); - $this->get_page("post/list/pbx/1"); - $this->assert_response(302); - } + $this->get_page("post/list/pbx/1"); + $this->assert_response(302); + } - public function testTagSearchManyResults() { - $image_ids = $this->upload(); + public function testTagSearchManyResults() + { + $image_ids = $this->upload(); - $this->get_page('post/list/computer/1'); - $this->assert_response(200); - $this->assert_title("computer"); - } + $this->get_page('post/list/computer/1'); + $this->assert_response(200); + $this->assert_title("computer"); + } - /* * * * * * * * * * * - * Multi-Tag Search * - * * * * * * * * * * */ - public function testMultiTagSearchNoResults() { - $image_ids = $this->upload(); + /* * * * * * * * * * * + * Multi-Tag Search * + * * * * * * * * * * */ + public function testMultiTagSearchNoResults() + { + $image_ids = $this->upload(); - # multiple tags, one of which doesn't exist - # (test the "one tag doesn't exist = no hits" path) - $this->get_page('post/list/computer asdfasdfwaffle/1'); - $this->assert_response(404); - } + # multiple tags, one of which doesn't exist + # (test the "one tag doesn't exist = no hits" path) + $this->get_page('post/list/computer asdfasdfwaffle/1'); + $this->assert_response(404); + } - public function testMultiTagSearchOneResult() { - $image_ids = $this->upload(); + public function testMultiTagSearchOneResult() + { + $image_ids = $this->upload(); - $this->get_page('post/list/computer screenshot/1'); - $this->assert_response(302); - } + $this->get_page('post/list/computer screenshot/1'); + $this->assert_response(302); + } - public function testMultiTagSearchManyResults() { - $image_ids = $this->upload(); + public function testMultiTagSearchManyResults() + { + $image_ids = $this->upload(); - $this->get_page('post/list/computer thing/1'); - $this->assert_response(200); - } + $this->get_page('post/list/computer thing/1'); + $this->assert_response(200); + } - /* * * * * * * * * * * - * Meta Search * - * * * * * * * * * * */ - public function testMetaSearchNoResults() { - $this->get_page('post/list/hash=1234567890/1'); - $this->assert_response(404); - } + /* * * * * * * * * * * + * Meta Search * + * * * * * * * * * * */ + public function testMetaSearchNoResults() + { + $this->get_page('post/list/hash=1234567890/1'); + $this->assert_response(404); + } - public function testMetaSearchOneResult() { - $image_ids = $this->upload(); + public function testMetaSearchOneResult() + { + $image_ids = $this->upload(); - $this->get_page("post/list/hash=feb01bab5698a11dd87416724c7a89e3/1"); - $this->assert_response(302); + $this->get_page("post/list/hash=feb01bab5698a11dd87416724c7a89e3/1"); + $this->assert_response(302); - $this->get_page("post/list/md5=feb01bab5698a11dd87416724c7a89e3/1"); - $this->assert_response(302); + $this->get_page("post/list/md5=feb01bab5698a11dd87416724c7a89e3/1"); + $this->assert_response(302); - $this->get_page("post/list/id={$image_ids[1]}/1"); - $this->assert_response(302); + $this->get_page("post/list/id={$image_ids[1]}/1"); + $this->assert_response(302); - $this->get_page("post/list/filename=screenshot/1"); - $this->assert_response(302); + $this->get_page("post/list/filename=screenshot/1"); + $this->assert_response(302); + } - } + public function testMetaSearchManyResults() + { + $image_ids = $this->upload(); - public function testMetaSearchManyResults() { - $image_ids = $this->upload(); + $this->get_page('post/list/size=640x480/1'); + $this->assert_response(200); - $this->get_page('post/list/size=640x480/1'); - $this->assert_response(200); + $this->get_page("post/list/tags=5/1"); + $this->assert_response(200); - $this->get_page("post/list/tags=5/1"); - $this->assert_response(200); + $this->get_page("post/list/ext=jpg/1"); + $this->assert_response(200); + } - $this->get_page("post/list/ext=jpg/1"); - $this->assert_response(200); - } + /* * * * * * * * * * * + * Wildcards * + * * * * * * * * * * */ + public function testWildSearchNoResults() + { + $image_ids = $this->upload(); - /* * * * * * * * * * * - * Wildcards * - * * * * * * * * * * */ - public function testWildSearchNoResults() { - $image_ids = $this->upload(); + $this->get_page("post/list/asdfasdf*/1"); + $this->assert_response(404); + } - $this->get_page("post/list/asdfasdf*/1"); - $this->assert_response(404); - } + public function testWildSearchOneResult() + { + $image_ids = $this->upload(); - public function testWildSearchOneResult() { - $image_ids = $this->upload(); + global $database; + $db = $database->get_driver_name(); + if ($db == DatabaseDriver::PGSQL || $db == DatabaseDriver::SQLITE) { + $this->markTestIncomplete(); + } - global $database; - $db = $database->get_driver_name(); - if($db == "pgsql" || $db == "sqlite") { - $this->markTestIncomplete(); - } + // Only the first image matches both the wildcard and the tag. + // This checks for https://github.com/shish/shimmie2/issues/547 + // (comp* is expanded to "computer computing", then we searched + // for images which match two or more of the tags in + // "computer computing screenshot") + $this->get_page("post/list/comp* screenshot/1"); + $this->assert_response(302); + } - // Only the first image matches both the wildcard and the tag. - // This checks for https://github.com/shish/shimmie2/issues/547 - // (comp* is expanded to "computer computing", then we searched - // for images which match two or more of the tags in - // "computer computing screenshot") - $this->get_page("post/list/comp* screenshot/1"); - $this->assert_response(302); - } + public function testWildSearchManyResults() + { + $image_ids = $this->upload(); - public function testWildSearchManyResults() { - $image_ids = $this->upload(); - - // two images match comp* - one matches it once, - // one matches it twice - $this->get_page("post/list/comp*/1"); - $this->assert_response(200); - } + // two images match comp* - one matches it once, + // one matches it twice + $this->get_page("post/list/comp*/1"); + $this->assert_response(200); + } - /* * * * * * * * * * * - * Mixed * - * * * * * * * * * * */ - public function testMixedSearchTagMeta() { - $image_ids = $this->upload(); + /* * * * * * * * * * * + * Mixed * + * * * * * * * * * * */ + public function testMixedSearchTagMeta() + { + $image_ids = $this->upload(); - # multiple tags, many results - $this->get_page('post/list/computer size=640x480/1'); - $this->assert_response(200); - } - // tag + negative - // wildcards + ??? + # multiple tags, many results + $this->get_page('post/list/computer size=640x480/1'); + $this->assert_response(200); + } + // tag + negative + // wildcards + ??? - /* * * * * * * * * * * - * Other * - * - negative tags * - * - wildcards * - * * * * * * * * * * */ - public function testOther() { - $this->markTestIncomplete(); + /* * * * * * * * * * * + * Other * + * - negative tags * + * - wildcards * + * * * * * * * * * * */ + public function testOther() + { + $this->markTestIncomplete(); - # negative tag, should have one result - $this->get_page('post/list/computer -pbx/1'); - $this->assert_response(302); + # negative tag, should have one result + $this->get_page('post/list/computer -pbx/1'); + $this->assert_response(302); - # negative tag alone, should work - # FIXME: known broken in mysql - //$this->get_page('post/list/-pbx/1'); - //$this->assert_response(302); + # negative tag alone, should work + # FIXME: known broken in mysql + //$this->get_page('post/list/-pbx/1'); + //$this->assert_response(302); - # test various search methods - $this->get_page("post/list/bedroo*/1"); - $this->assert_response(302); - } + # test various search methods + $this->get_page("post/list/bedroo*/1"); + $this->assert_response(302); + } } - diff --git a/ext/index/theme.php b/ext/index/theme.php index ecc0b496..8c881afd 100644 --- a/ext/index/theme.php +++ b/ext/index/theme.php @@ -1,21 +1,21 @@ page_number = $page_number; - $this->total_pages = $total_pages; - $this->search_terms = $search_terms; - } + public function set_page(int $page_number, int $total_pages, array $search_terms) + { + $this->page_number = $page_number; + $this->total_pages = $total_pages; + $this->search_terms = $search_terms; + } - public function display_intro(Page $page) { - $text = " + public function display_intro(Page $page) + { + $text = "

    The first thing you'll probably want to do is create a new account; note that the first account you create will by default be marked as the board's @@ -27,59 +27,57 @@ and of course start organising your images :-)

    This message will go away once your first image is uploaded~

    "; - $page->set_title("Welcome to Shimmie ".VERSION); - $page->set_heading("Welcome to Shimmie"); - $page->add_block(new Block("Installation Succeeded!", $text, "main", 0)); - } + $page->set_title("Welcome to Shimmie ".VERSION); + $page->set_heading("Welcome to Shimmie"); + $page->add_block(new Block("Installation Succeeded!", $text, "main", 0)); + } - /** - * @param Page $page - * @param Image[] $images - */ - public function display_page(Page $page, $images) { - $this->display_page_header($page, $images); + /** + * #param Image[] $images + */ + public function display_page(Page $page, array $images) + { + $this->display_page_header($page, $images); - $nav = $this->build_navigation($this->page_number, $this->total_pages, $this->search_terms); - $page->add_block(new Block("Navigation", $nav, "left", 0)); + $nav = $this->build_navigation($this->page_number, $this->total_pages, $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"); - } - } + 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 string[] $parts - */ - public function display_admin_block($parts) { - global $page; - $page->add_block(new Block("List Controls", join("
    ", $parts), "left", 50)); - } + /** + * #param string[] $parts + */ + public function display_admin_block(array $parts) + { + global $page; + $page->add_block(new Block("List Controls", join("
    ", $parts), "left", 50)); + } - /** - * @param int $page_number - * @param int $total_pages - * @param string[] $search_terms - * @return string - */ - protected function build_navigation($page_number, $total_pages, $search_terms) { - $prev = $page_number - 1; - $next = $page_number + 1; + /** + * #param string[] $search_terms + */ + protected function build_navigation(int $page_number, int $total_pages, array $search_terms): string + { + $prev = $page_number - 1; + $next = $page_number + 1; - $u_tags = url_escape(implode(" ", $search_terms)); - $query = empty($u_tags) ? "" : '/'.$u_tags; + $u_tags = url_escape(Tag::implode($search_terms)); + $query = empty($u_tags) ? "" : '/'.$u_tags; - $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'; - $h_search_string = html_escape(implode(" ", $search_terms)); - $h_search_link = make_link(); - $h_search = " + $h_search_string = html_escape(Tag::implode($search_terms)); + $h_search_link = make_link(); + $h_search = "

    @@ -87,61 +85,259 @@ and of course start organising your images :-)
    "; - return $h_prev.' | '.$h_index.' | '.$h_next.'
    '.$h_search; - } + return $h_prev.' | '.$h_index.' | '.$h_next.'
    '.$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 .= $this->build_thumb_html($image); - } - $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 .= $this->build_thumb_html($image); + } + $table .= "
    "; + return $table; + } - /** - * @param Page $page - * @param Image[] $images - */ - protected function display_page_header(Page $page, $images) { - global $config; + /** + * #param Image[] $images + */ + protected function display_page_header(Page $page, array $images) + { + global $config; - if (count($this->search_terms) == 0) { - $page_title = $config->get_string('title'); - } else { - $search_string = implode(' ', $this->search_terms); - $page_title = html_escape($search_string); - if (count($images) > 0) { - $page->set_subheading("Page {$this->page_number} / {$this->total_pages}"); - } - } - if ($this->page_number > 1 || count($this->search_terms) > 0) { - // $page_title .= " / $page_number"; - } + if (count($this->search_terms) == 0) { + $page_title = $config->get_string(SetupConfig::TITLE); + } else { + $search_string = implode(' ', $this->search_terms); + $page_title = html_escape($search_string); + if (count($images) > 0) { + $page->set_subheading("Page {$this->page_number} / {$this->total_pages}"); + } + } + if ($this->page_number > 1 || count($this->search_terms) > 0) { + // $page_title .= " / $page_number"; + } - $page->set_title($page_title); - $page->set_heading($page_title); - } + $page->set_title($page_title); + $page->set_heading($page_title); + } - /** - * @param Page $page - * @param Image[] $images - */ - protected function display_page_images(Page $page, $images) { - if (count($this->search_terms) > 0) { - $query = url_escape(implode(' ', $this->search_terms)); - $page->add_block(new Block("Images", $this->build_table($images, "#search=$query"), "main", 10, "image-list")); - $this->display_paginator($page, "post/list/$query", null, $this->page_number, $this->total_pages, TRUE); - } else { - $page->add_block(new Block("Images", $this->build_table($images, null), "main", 10, "image-list")); - $this->display_paginator($page, "post/list", null, $this->page_number, $this->total_pages, TRUE); - } - } + /** + * #param Image[] $images + */ + protected function display_page_images(Page $page, array $images) + { + if (count($this->search_terms) > 0) { + if ($this->page_number > 3) { + // only index the first pages of each term + $page->add_html_header(''); + } + $query = url_escape(implode(' ', $this->search_terms)); + $page->add_block(new Block("Images", $this->build_table($images, "#search=$query"), "main", 10, "image-list")); + $this->display_paginator($page, "post/list/$query", null, $this->page_number, $this->total_pages, true); + } else { + $page->add_block(new Block("Images", $this->build_table($images, null), "main", 10, "image-list")); + $this->display_paginator($page, "post/list", null, $this->page_number, $this->total_pages, true); + } + } + + public function get_help_html() + { + return '

    Searching is largely based on tags, with a number of special keywords available that allow searching based on properties of the images.

    + +
    +
    tagname
    +

    Returns images that are tagged with "tagname".

    +
    + +
    +
    tagname othertagname
    +

    Returns images that are tagged with "tagname" and "othertagname".

    +
    + +

    Most tags and keywords can be prefaced with a negative sign (-) to indicate that you want to search for images that do not match something.

    + +
    +
    -tagname
    +

    Returns images that are not tagged with "tagname".

    +
    + +
    +
    -tagname -othertagname
    +

    Returns images that are not tagged with "tagname" and "othertagname". This is different than without the negative sign, as images with "tagname" or "othertagname" can still be returned as long as the other one is not present.

    +
    + +
    +
    tagname -othertagname
    +

    Returns images that are tagged with "tagname", but are not tagged with "othertagname".

    +
    + +

    Wildcard searches are possible as well using * for "any one, more, or none" and ? for "any one".

    + +
    +
    tagn*
    +

    Returns images that are tagged with "tagname", "tagnot", or anything else that starts with "tagn".

    +
    + +
    +
    tagn?me
    +

    Returns images that are tagged with "tagname", "tagnome", or anything else that starts with "tagn", has one character, and ends with "me".

    +
    + +
    +
    tags=1
    +

    Returns images with exactly 1 tag.

    +
    + +
    +
    tags>0
    +

    Returns images with 1 or more tags.

    +
    + +

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

    + +
    + +

    Search for images by aspect ratio

    + +
    +
    ratio=4:3
    +

    Returns images with an aspect ratio of 4:3.

    +
    + +
    +
    ratio>16:9
    +

    Returns images with an aspect ratio greater than 16:9.

    +
    + +

    Can use <, <=, >, >=, or =. The relation is calculated by dividing width by height.

    + +
    + +

    Search for images by file size

    + +
    +
    filesize=1
    +

    Returns images exactly 1 byte in size.

    +
    + +
    +
    filesize>100mb
    +

    Returns images greater than 100 megabytes in size.

    +
    + +

    Can use <, <=, >, >=, or =. Supported suffixes are kb, mb, and gb. Uses multiples of 1024.

    + +
    + +

    Search for images by MD5 hash

    + +
    +
    hash=0D3512CAA964B2BA5D7851AF5951F33B
    +

    Returns image with an MD5 hash 0D3512CAA964B2BA5D7851AF5951F33B.

    +
    + +
    + +

    Search for images by file type

    + +
    +
    filetype=jpg
    +

    Returns images that are of type "jpg".

    +
    + +
    + +

    Search for images by file name

    + +
    +
    filename=picasso.jpg
    +

    Returns images that are named "picasso.jpg".

    +
    + +
    + +

    Search for images by source

    + +
    +
    source=http://google.com/
    +

    Returns images with a source of "http://google.com/".

    +
    + +
    +
    source=any
    +

    Returns images with a source set.

    +
    + +
    +
    source=none
    +

    Returns images without a source set.

    +
    + +
    + +

    Search for images by date posted.

    + +
    +
    posted>=07-19-2019
    +

    Returns images posted on or after 07-19-2019.

    +
    + +

    Can use <, <=, >, >=, or =. Date format is mm-dd-yyyy. Date posted includes time component, so = will not work unless the time is exact.

    + +
    + +

    Search for images by image dimensions

    + +
    +
    size=640x480
    +

    Returns images exactly 640 pixels wide by 480 pixels high.

    +
    + +
    +
    size>1920x1080
    +

    Returns images with a width larger than 1920 and a height larger than 1080.

    +
    + +
    +
    width=1000
    +

    Returns images exactly 1000 pixels wide.

    +
    + +
    +
    height=1000
    +

    Returns images exactly 1000 pixels high.

    +
    + +

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

    + +
    + +

    Sorting search results can be done using the pattern order:field_direction. _direction can be either _asc or _desc, indicating ascending (123) or descending (321) order.

    + +
    +
    order:id_asc
    +

    Returns images sorted by ID, smallest first.

    +
    + +
    +
    order:width_desc
    +

    Returns images sorted by width, largest first.

    +
    + +

    These fields are supported: +

      +
    • id
    • +
    • width
    • +
    • height
    • +
    • filesize
    • +
    • filename
    • +
    +

    + '; + } } - diff --git a/ext/ipban/info.php b/ext/ipban/info.php new file mode 100644 index 00000000..83e595f8 --- /dev/null +++ b/ext/ipban/info.php @@ -0,0 +1,28 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: Ban IP addresses + * Documentation: + * + */ + +class IPBanInfo extends ExtensionInfo +{ + public const KEY = "ipban"; + + public $key = self::KEY; + public $name = "IP Ban"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Ban IP addresses"; + public $documentation = +"Adding a Ban +
    IP: Can be a single IP (eg. 123.234.210.21), or a CIDR block (eg. 152.23.43.0/24) +
    Reason: Any text, for the admin to remember why the ban was put in place +
    Until: Either a date in YYYY-MM-DD format, or an offset like \"3 days\""; +} diff --git a/ext/ipban/main.php b/ext/ipban/main.php index 0469e593..6b9dad94 100644 --- a/ext/ipban/main.php +++ b/ext/ipban/main.php @@ -1,119 +1,145 @@ - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Ban IP addresses - * Documentation: - * Adding a Ban - *
    IP: Can be a single IP (eg. 123.234.210.21), or a CIDR block (eg. 152.23.43.0/24) - *
    Reason: Any text, for the admin to remember why the ban was put in place - *
    Until: Either a date in YYYY-MM-DD format, or an offset like "3 days" - */ // RemoveIPBanEvent {{{ -class RemoveIPBanEvent extends Event { - public $id; +class RemoveIPBanEvent extends Event +{ + public $id; - public function __construct($id) { - $this->id = $id; - } + public function __construct(int $id) + { + $this->id = $id; + } } // }}} // AddIPBanEvent {{{ -class AddIPBanEvent extends Event { - public $ip; - public $reason; - public $end; +class AddIPBanEvent extends Event +{ + public $ip; + public $reason; + public $end; - public function __construct(/*string(ip)*/ $ip, /*string*/ $reason, /*string*/ $end) { - $this->ip = trim($ip); - $this->reason = trim($reason); - $this->end = trim($end); - } + public function __construct(string $ip, string $reason, string $end) + { + $this->ip = trim($ip); + $this->reason = trim($reason); + $this->end = trim($end); + } } // }}} -class IPBan extends Extension { - public function get_priority() {return 10;} +class IPBan extends Extension +{ + public function get_priority(): int + { + return 10; + } - public function onInitExt(InitExtEvent $event) { - global $config; - if($config->get_int("ext_ipban_version") < 8) { - $this->install(); - } - $this->check_ip_ban(); - } + public function onInitExt(InitExtEvent $event) + { + global $config; + if ($config->get_int("ext_ipban_version") < 8) { + $this->install(); + } + $config->set_default_string( + "ipban_message", + '

    IP $IP has been banned until $DATE by $ADMIN because of $REASON +

    If you couldn\'t possibly be guilty of what you\'re banned for, the person we banned probably had a dynamic IP address and so do you. +

    See http://whatismyipaddress.com/dynamic-static for more information. +

    $CONTACT' + ); + $this->check_ip_ban(); + } - public function onPageRequest(PageRequestEvent $event) { - if($event->page_matches("ip_ban")) { - global $page, $user; - if($user->can("ban_ip")) { - if($event->get_arg(0) == "add" && $user->check_auth_token()) { - if(isset($_POST['ip']) && isset($_POST['reason']) && isset($_POST['end'])) { - if(empty($_POST['end'])) $end = null; - else $end = $_POST['end']; - send_event(new AddIPBanEvent($_POST['ip'], $_POST['reason'], $end)); + public function onPageRequest(PageRequestEvent $event) + { + if ($event->page_matches("ip_ban")) { + global $page, $user; + if ($user->can(Permissions::BAN_IP)) { + if ($event->get_arg(0) == "add" && $user->check_auth_token()) { + if (isset($_POST['ip']) && isset($_POST['reason']) && isset($_POST['end'])) { + if (empty($_POST['end'])) { + $end = null; + } else { + $end = $_POST['end']; + } + send_event(new AddIPBanEvent($_POST['ip'], $_POST['reason'], $end)); - flash_message("Ban for {$_POST['ip']} added"); - $page->set_mode("redirect"); - $page->set_redirect(make_link("ip_ban/list")); - } - } - else if($event->get_arg(0) == "remove" && $user->check_auth_token()) { - if(isset($_POST['id'])) { - send_event(new RemoveIPBanEvent($_POST['id'])); + flash_message("Ban for {$_POST['ip']} added"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("ip_ban/list")); + } + } elseif ($event->get_arg(0) == "remove" && $user->check_auth_token()) { + if (isset($_POST['id'])) { + send_event(new RemoveIPBanEvent($_POST['id'])); - flash_message("Ban removed"); - $page->set_mode("redirect"); - $page->set_redirect(make_link("ip_ban/list")); - } - } - else if($event->get_arg(0) == "list") { - $bans = (isset($_GET["all"])) ? $this->get_bans() : $this->get_active_bans(); - $this->theme->display_bans($page, $bans); - } - } - else { - $this->theme->display_permission_denied(); - } - } - } + flash_message("Ban removed"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("ip_ban/list")); + } + } elseif ($event->get_arg(0) == "list") { + $bans = (isset($_GET["all"])) ? $this->get_bans() : $this->get_active_bans(); + $this->theme->display_bans($page, $bans); + } + } else { + $this->theme->display_permission_denied(); + } + } + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("ban_ip")) { - $event->add_link("IP Bans", make_link("ip_ban/list")); - } - } + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("IP Ban"); + $sb->add_longtext_option("ipban_message", 'Message to show to banned users:
    (with $IP, $DATE, $ADMIN, $REASON, and $CONTACT)'); + $event->panel->add_block($sb); + } - public function onAddIPBan(AddIPBanEvent $event) { - global $user, $database; - $sql = "INSERT INTO bans (ip, reason, end_timestamp, banner_id) VALUES (:ip, :reason, :end, :admin_id)"; - $database->Execute($sql, array("ip"=>$event->ip, "reason"=>$event->reason, "end"=>strtotime($event->end), "admin_id"=>$user->id)); - $database->cache->delete("ip_bans_sorted"); - log_info("ipban", "Banned {$event->ip} because '{$event->reason}' until {$event->end}"); - } + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::BAN_IP)) { + $event->add_nav_link("ip_bans", new Link('ip_ban/list'), "IP Bans", NavLink::is_active(["ip_ban"])); + } + } + } - public function onRemoveIPBan(RemoveIPBanEvent $event) { - global $database; - $ban = $database->get_row("SELECT * FROM bans WHERE id = :id", array("id"=>$event->id)); - if($ban) { - $database->Execute("DELETE FROM bans WHERE id = :id", array("id"=>$event->id)); - $database->cache->delete("ip_bans_sorted"); - log_info("ipban", "Removed {$ban['ip']}'s ban"); - } - } + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::BAN_IP)) { + $event->add_link("IP Bans", make_link("ip_ban/list")); + } + } -// installer {{{ - protected function install() { - global $database; - global $config; + public function onAddIPBan(AddIPBanEvent $event) + { + global $user, $database; + $sql = "INSERT INTO bans (ip, reason, end_timestamp, banner_id) VALUES (:ip, :reason, :end, :admin_id)"; + $database->Execute($sql, ["ip"=>$event->ip, "reason"=>$event->reason, "end"=>strtotime($event->end), "admin_id"=>$user->id]); + $database->cache->delete("ip_bans_sorted"); + log_info("ipban", "Banned {$event->ip} because '{$event->reason}' until {$event->end}"); + } - // shortcut to latest - if($config->get_int("ext_ipban_version") < 1) { - $database->create_table("bans", " + public function onRemoveIPBan(RemoveIPBanEvent $event) + { + global $database; + $ban = $database->get_row("SELECT * FROM bans WHERE id = :id", ["id"=>$event->id]); + if ($ban) { + $database->Execute("DELETE FROM bans WHERE id = :id", ["id"=>$event->id]); + $database->cache->delete("ip_bans_sorted"); + log_info("ipban", "Removed {$ban['ip']}'s ban"); + } + } + + // installer {{{ + protected function install() + { + global $database; + global $config; + + // shortcut to latest + if ($config->get_int("ext_ipban_version") < 1) { + $database->create_table("bans", " id SCORE_AIPK, banner_id INTEGER NOT NULL, ip SCORE_INET NOT NULL, @@ -122,14 +148,14 @@ class IPBan extends Extension { added SCORE_DATETIME NOT NULL DEFAULT SCORE_NOW, FOREIGN KEY (banner_id) REFERENCES users(id) ON DELETE CASCADE, "); - $database->execute("CREATE INDEX bans__end_timestamp ON bans(end_timestamp)"); - $config->set_int("ext_ipban_version", 8); - } + $database->execute("CREATE INDEX bans__end_timestamp ON bans(end_timestamp)"); + $config->set_int("ext_ipban_version", 8); + } - // === + // === - if($config->get_int("ext_ipban_version") < 1) { - $database->Execute("CREATE TABLE bans ( + if ($config->get_int("ext_ipban_version") < 1) { + $database->Execute("CREATE TABLE bans ( id int(11) NOT NULL auto_increment, ip char(15) default NULL, date SCORE_DATETIME default NULL, @@ -137,153 +163,170 @@ class IPBan extends Extension { reason varchar(255) default NULL, PRIMARY KEY (id) )"); - $config->set_int("ext_ipban_version", 1); - } + $config->set_int("ext_ipban_version", 1); + } - if($config->get_int("ext_ipban_version") == 1) { - $database->execute("ALTER TABLE bans ADD COLUMN banner_id INTEGER NOT NULL AFTER id"); - $config->set_int("ext_ipban_version", 2); - } + if ($config->get_int("ext_ipban_version") == 1) { + $database->execute("ALTER TABLE bans ADD COLUMN banner_id INTEGER NOT NULL AFTER id"); + $config->set_int("ext_ipban_version", 2); + } - if($config->get_int("ext_ipban_version") == 2) { - $database->execute("ALTER TABLE bans DROP COLUMN date"); - $database->execute("ALTER TABLE bans CHANGE ip ip CHAR(20) NOT NULL"); - $database->execute("ALTER TABLE bans CHANGE reason reason TEXT NOT NULL"); - $database->execute("CREATE INDEX bans__end ON bans(end)"); - $config->set_int("ext_ipban_version", 3); - } + if ($config->get_int("ext_ipban_version") == 2) { + $database->execute("ALTER TABLE bans DROP COLUMN date"); + $database->execute("ALTER TABLE bans CHANGE ip ip CHAR(20) NOT NULL"); + $database->execute("ALTER TABLE bans CHANGE reason reason TEXT NOT NULL"); + $database->execute("CREATE INDEX bans__end ON bans(end)"); + $config->set_int("ext_ipban_version", 3); + } - if($config->get_int("ext_ipban_version") == 3) { - $database->execute("ALTER TABLE bans CHANGE end old_end DATE NOT NULL"); - $database->execute("ALTER TABLE bans ADD COLUMN end INTEGER"); - $database->execute("UPDATE bans SET end = UNIX_TIMESTAMP(old_end)"); - $database->execute("ALTER TABLE bans DROP COLUMN old_end"); - $database->execute("CREATE INDEX bans__end ON bans(end)"); - $config->set_int("ext_ipban_version", 4); - } + if ($config->get_int("ext_ipban_version") == 3) { + $database->execute("ALTER TABLE bans CHANGE end old_end DATE NOT NULL"); + $database->execute("ALTER TABLE bans ADD COLUMN end INTEGER"); + $database->execute("UPDATE bans SET end = UNIX_TIMESTAMP(old_end)"); + $database->execute("ALTER TABLE bans DROP COLUMN old_end"); + $database->execute("CREATE INDEX bans__end ON bans(end)"); + $config->set_int("ext_ipban_version", 4); + } - if($config->get_int("ext_ipban_version") == 4) { - $database->execute("ALTER TABLE bans CHANGE end end_timestamp INTEGER"); - $config->set_int("ext_ipban_version", 5); - } + if ($config->get_int("ext_ipban_version") == 4) { + $database->execute("ALTER TABLE bans CHANGE end end_timestamp INTEGER"); + $config->set_int("ext_ipban_version", 5); + } - if($config->get_int("ext_ipban_version") == 5) { - $database->execute("ALTER TABLE bans CHANGE ip ip VARCHAR(15)"); - $config->set_int("ext_ipban_version", 6); - } + if ($config->get_int("ext_ipban_version") == 5) { + $database->execute("ALTER TABLE bans CHANGE ip ip VARCHAR(15)"); + $config->set_int("ext_ipban_version", 6); + } - if($config->get_int("ext_ipban_version") == 6) { - $database->Execute("ALTER TABLE bans ADD FOREIGN KEY (banner_id) REFERENCES users(id) ON DELETE CASCADE"); - $config->set_int("ext_ipban_version", 7); - } + if ($config->get_int("ext_ipban_version") == 6) { + $database->Execute("ALTER TABLE bans ADD FOREIGN KEY (banner_id) REFERENCES users(id) ON DELETE CASCADE"); + $config->set_int("ext_ipban_version", 7); + } - if($config->get_int("ext_ipban_version") == 7) { - $database->execute($database->scoreql_to_sql("ALTER TABLE bans CHANGE ip ip SCORE_INET")); - $database->execute($database->scoreql_to_sql("ALTER TABLE bans ADD COLUMN added SCORE_DATETIME NOT NULL DEFAULT SCORE_NOW")); - $config->set_int("ext_ipban_version", 8); - } - } -// }}} -// deal with banned person {{{ - private function check_ip_ban() { - $remote = $_SERVER['REMOTE_ADDR']; - $bans = $this->get_active_bans_sorted(); + if ($config->get_int("ext_ipban_version") == 7) { + $database->execute($database->scoreql_to_sql("ALTER TABLE bans CHANGE ip ip SCORE_INET")); + $database->execute($database->scoreql_to_sql("ALTER TABLE bans ADD COLUMN added SCORE_DATETIME NOT NULL DEFAULT SCORE_NOW")); + $config->set_int("ext_ipban_version", 8); + } + } + // }}} + // deal with banned person {{{ + private function check_ip_ban() + { + $remote = $_SERVER['REMOTE_ADDR']; + $bans = $this->get_active_bans_sorted(); - // bans[0] = IPs - if(isset($bans[0][$remote])) { - $this->block($remote); // never returns - } + // bans[0] = IPs + if (isset($bans[0][$remote])) { + $this->block($remote); // never returns + } - // bans[1] = CIDR nets - foreach($bans[1] as $ip => $true) { - if(ip_in_range($remote, $ip)) { - $this->block($remote); // never returns - } - } - } + // bans[1] = CIDR nets + foreach ($bans[1] as $ip => $true) { + if (ip_in_range($remote, $ip)) { + $this->block($remote); // never returns + } + } + } - private function block(/*string*/ $remote) { - global $config, $database; + private function block(string $remote) + { + global $config, $database; - $prefix = ($database->get_driver_name() == "sqlite" ? "bans." : ""); + $prefix = ($database->get_driver_name() == DatabaseDriver::SQLITE ? "bans." : ""); - $bans = $this->get_active_bans(); + $bans = $this->get_active_bans(); - foreach($bans as $row) { - $ip = $row[$prefix."ip"]; - if( - (strstr($ip, '/') && ip_in_range($remote, $ip)) || - ($ip == $remote) - ) { - $reason = $row[$prefix.'reason']; - $admin = User::by_id($row[$prefix.'banner_id']); - $date = date("Y-m-d", $row[$prefix.'end_timestamp']); - header("HTTP/1.0 403 Forbidden"); - print "IP $ip has been banned until $date by {$admin->name} because of $reason\n"; - print "

    If you couldn't possibly be guilty of what you're banned for, the person we banned probably had a dynamic IP address and so do you. See http://whatismyipaddress.com/dynamic-static for more information.\n"; + foreach ($bans as $row) { + $ip = $row[$prefix."ip"]; + if ( + (strstr($ip, '/') && ip_in_range($remote, $ip)) || + ($ip == $remote) + ) { + $reason = $row[$prefix.'reason']; + $admin = User::by_id($row[$prefix.'banner_id']); + $date = date("Y-m-d", $row[$prefix.'end_timestamp']); + $msg = $config->get_string("ipban_message"); + $msg = str_replace('$IP', $ip, $msg); + $msg = str_replace('$DATE', $date, $msg); + $msg = str_replace('$ADMIN', $admin->name, $msg); + $msg = str_replace('$REASON', $reason, $msg); + $contact_link = contact_link(); + if (!empty($contact_link)) { + $msg = str_replace('$CONTACT', "Contact the staff (be sure to include this message)", $msg); + } else { + $msg = str_replace('$CONTACT', "", $msg); + } + header("HTTP/1.0 403 Forbidden"); + print "$msg"; - $contact_link = contact_link(); - if(!empty($contact_link)) { - print "

    Contact the staff (be sure to include this message)"; - } - exit; - } - } - log_error("ipban", "block($remote) called but no bans matched"); - exit; - } -// }}} -// database {{{ - private function get_bans() { - global $database; - $bans = $database->get_all(" + exit; + } + } + log_error("ipban", "block($remote) called but no bans matched"); + exit; + } + // }}} + // database {{{ + private function get_bans() + { + global $database; + $bans = $database->get_all(" SELECT bans.*, users.name as banner_name FROM bans JOIN users ON banner_id = users.id ORDER BY added, end_timestamp, bans.id "); - if($bans) {return $bans;} - else {return array();} - } + if ($bans) { + return $bans; + } else { + return []; + } + } - private function get_active_bans() { - global $database; + private function get_active_bans() + { + global $database; - $bans = $database->get_all(" + $bans = $database->get_all(" SELECT bans.*, users.name as banner_name FROM bans JOIN users ON banner_id = users.id WHERE (end_timestamp > :end_timestamp) OR (end_timestamp IS NULL) ORDER BY end_timestamp, bans.id - ", array("end_timestamp"=>time())); + ", ["end_timestamp"=>time()]); - if($bans) {return $bans;} - else {return array();} - } + if ($bans) { + return $bans; + } else { + return []; + } + } - // returns [ips, nets] - private function get_active_bans_sorted() { - global $database; + // returns [ips, nets] + private function get_active_bans_sorted() + { + global $database; - $cached = $database->cache->get("ip_bans_sorted"); - if($cached) return $cached; + $cached = $database->cache->get("ip_bans_sorted"); + if ($cached) { + return $cached; + } - $bans = $this->get_active_bans(); - $ips = array(); # "0.0.0.0" => false); - $nets = array(); # "0.0.0.0/32" => false); - foreach($bans as $row) { - if(strstr($row['ip'], '/')) { - $nets[$row['ip']] = true; - } - else { - $ips[$row['ip']] = true; - } - } + $bans = $this->get_active_bans(); + $ips = []; # "0.0.0.0" => false); + $nets = []; # "0.0.0.0/32" => false); + foreach ($bans as $row) { + if (strstr($row['ip'], '/')) { + $nets[$row['ip']] = true; + } else { + $ips[$row['ip']] = true; + } + } - $sorted = array($ips, $nets); - $database->cache->set("ip_bans_sorted", $sorted, 600); - return $sorted; - } -// }}} + $sorted = [$ips, $nets]; + $database->cache->set("ip_bans_sorted", $sorted, 600); + return $sorted; + } + // }}} } - diff --git a/ext/ipban/test.php b/ext/ipban/test.php index ec0dfe0b..12816dab 100644 --- a/ext/ipban/test.php +++ b/ext/ipban/test.php @@ -1,29 +1,30 @@ get_page('ip_ban/list'); - $this->assert_response(403); - $this->assert_title("Permission Denied"); +class IPBanTest extends ShimmiePHPUnitTestCase +{ + public function testIPBan() + { + $this->get_page('ip_ban/list'); + $this->assert_response(403); + $this->assert_title("Permission Denied"); - $this->log_in_as_admin(); + $this->log_in_as_admin(); - $this->get_page('ip_ban/list'); - $this->assert_no_text("42.42.42.42"); + $this->get_page('ip_ban/list'); + $this->assert_no_text("42.42.42.42"); - $this->markTestIncomplete(); + $this->markTestIncomplete(); - $this->set_field('ip', '42.42.42.42'); - $this->set_field('reason', 'unit testing'); - $this->set_field('end', '1 week'); - $this->click("Ban"); + $this->set_field('ip', '42.42.42.42'); + $this->set_field('reason', 'unit testing'); + $this->set_field('end', '1 week'); + $this->click("Ban"); - $this->assert_text("42.42.42.42"); - $this->click("Remove"); // FIXME: remove which ban? :S - $this->assert_no_text("42.42.42.42"); + $this->assert_text("42.42.42.42"); + $this->click("Remove"); // FIXME: remove which ban? :S + $this->assert_no_text("42.42.42.42"); - $this->get_page('ip_ban/list?all=on'); // just test it doesn't crash for now + $this->get_page('ip_ban/list?all=on'); // just test it doesn't crash for now - # FIXME: test that the IP is actually banned - } + # FIXME: test that the IP is actually banned + } } - diff --git a/ext/ipban/theme.php b/ext/ipban/theme.php index a2f14f21..0373c94d 100644 --- a/ext/ipban/theme.php +++ b/ext/ipban/theme.php @@ -1,23 +1,25 @@ the banned IP - * 'reason' => why the IP was banned - * 'date' => when the ban started - * 'end' => when the ban will end - * ) - */ - public function display_bans(Page $page, $bans) { - global $database, $user; - $h_bans = ""; - $prefix = ($database->get_driver_name() == "sqlite" ? "bans." : ""); - foreach($bans as $ban) { - $end_human = date('Y-m-d', $ban[$prefix.'end_timestamp']); - $h_bans .= " +class IPBanTheme extends Themelet +{ + /* + * Show all the bans + * + * $bans = an array of ( + * 'ip' => the banned IP + * 'reason' => why the IP was banned + * 'date' => when the ban started + * 'end' => when the ban will end + * ) + */ + public function display_bans(Page $page, $bans) + { + global $database, $user; + $h_bans = ""; + $prefix = ($database->get_driver_name() == DatabaseDriver::SQLITE ? "bans." : ""); + foreach ($bans as $ban) { + $end_human = date('Y-m-d', $ban[$prefix.'end_timestamp']); + $h_bans .= " {$ban[$prefix.'ip']} {$ban[$prefix.'reason']} @@ -32,8 +34,8 @@ class IPBanTheme extends Themelet { "; - } - $html = " + } + $html = " Show All

    @@ -50,10 +52,9 @@ class IPBanTheme extends Themelet {
    IPReasonByFromUntilAction
    "; - $page->set_title("IP Bans"); - $page->set_heading("IP Bans"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Edit IP Bans", $html)); - } + $page->set_title("IP Bans"); + $page->set_heading("IP Bans"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Edit IP Bans", $html)); + } } - diff --git a/ext/link_image/info.php b/ext/link_image/info.php new file mode 100644 index 00000000..cbc46255 --- /dev/null +++ b/ext/link_image/info.php @@ -0,0 +1,17 @@ + + * Description: Show various forms of link to each image, for copy & paste + */ + +class LinkImageInfo extends ExtensionInfo +{ + public const KEY = "link_image"; + + public $key = self::KEY; + public $name = "Link to Image"; + public $authors = ["Artanis"=>"artanis.00@gmail.com"]; + public $description = "Show various forms of link to each image, for copy & paste"; +} diff --git a/ext/link_image/main.php b/ext/link_image/main.php index 859d510f..c7c73ec1 100644 --- a/ext/link_image/main.php +++ b/ext/link_image/main.php @@ -1,37 +1,37 @@ - * Description: Show various forms of link to each image, for copy & paste - */ -class LinkImage extends Extension { - public function onDisplayingImage(DisplayingImageEvent $event) { - global $page; - $this->theme->links_block($page, $this->data($event->image)); - } - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Link to Image"); - $sb->add_text_option("ext_link-img_text-link_format", "Text Link Format: "); - $event->panel->add_block($sb); - } +class LinkImage extends Extension +{ + public function onDisplayingImage(DisplayingImageEvent $event) + { + global $page; + $this->theme->links_block($page, $this->data($event->image)); + } - public function onInitExt(InitExtEvent $event) { - global $config; - $config->set_default_string("ext_link-img_text-link_format", '$title - $id ($ext $size $filesize)'); - } + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Link to Image"); + $sb->add_text_option("ext_link-img_text-link_format", "Text Link Format: "); + $event->panel->add_block($sb); + } - private function data(Image $image) { - global $config; + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_string("ext_link-img_text-link_format", '$title - $id ($ext $size $filesize)'); + } - $text_link = $image->parse_link_template($config->get_string("ext_link-img_text-link_format")); - $text_link = trim($text_link) == "" ? null : $text_link; // null blank setting so the url gets filled in on the text links. + private function data(Image $image) + { + global $config; - return array( - 'thumb_src' => make_http($image->get_thumb_link()), - 'image_src' => make_http($image->get_image_link()), - 'post_link' => make_http(make_link("post/view/{$image->id}")), - 'text_link' => $text_link); - } + $text_link = $image->parse_link_template($config->get_string("ext_link-img_text-link_format")); + $text_link = trim($text_link) == "" ? null : $text_link; // null blank setting so the url gets filled in on the text links. + + return [ + 'thumb_src' => make_http($image->get_thumb_link()), + 'image_src' => make_http($image->get_image_link()), + 'post_link' => make_http(make_link("post/view/{$image->id}")), + 'text_link' => $text_link]; + } } - diff --git a/ext/link_image/test.php b/ext/link_image/test.php index ca8e6f9b..c0ce6ae3 100644 --- a/ext/link_image/test.php +++ b/ext/link_image/test.php @@ -1,24 +1,25 @@ log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pie"); +class LinkImageTest extends ShimmiePHPUnitTestCase +{ + public function testLinkImage() + { + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pie"); - # FIXME - # look in the "plain text link to post" box, follow the link - # in there, see if it takes us to the right page - $this->get_page("post/view/$image_id"); + # FIXME + # look in the "plain text link to post" box, follow the link + # in there, see if it takes us to the right page + $this->get_page("post/view/$image_id"); - $this->markTestIncomplete(); + $this->markTestIncomplete(); - // FIXME - $matches = array(); - preg_match("#value='(http://.*(/|%2F)post(/|%2F)view(/|%2F)[0-9]+)'#", $raw, $matches); - $this->assertTrue(count($matches) > 0); - if($matches) { - $this->get($matches[1]); - $this->assert_title("Image $image_id: pie"); - } - } + // FIXME + $matches = []; + preg_match("#value='(http://.*(/|%2F)post(/|%2F)view(/|%2F)[0-9]+)'#", $this->page_to_text(), $matches); + $this->assertTrue(count($matches) > 0); + if ($matches) { + $this->get($matches[1]); + $this->assert_title("Image $image_id: pie"); + } + } } - diff --git a/ext/link_image/theme.php b/ext/link_image/theme.php index 213311dc..baf5c62f 100644 --- a/ext/link_image/theme.php +++ b/ext/link_image/theme.php @@ -1,25 +1,27 @@ add_block( new Block( - "Link to Image", - " + $page->add_block(new Block( + "Link to Image", + " @@ -27,10 +29,10 @@ class LinkImageTheme extends Themelet { HTML
    BBCode ". - $this->link_code("Link",$this->url($post_link, $text_link,"ubb"),"ubb_text-link"). - $this->link_code("Thumb",$this->url($post_link, $this->img($thumb_src,"ubb"),"ubb"),"ubb_thumb-link"). - $this->link_code("Image", $this->img($image_src,"ubb"), "ubb_full-img"). - " + $this->link_code("Link", $this->url($post_link, $text_link, "ubb"), "ubb_text-link"). + $this->link_code("Thumb", $this->url($post_link, $this->img($thumb_src, "ubb"), "ubb"), "ubb_thumb-link"). + $this->link_code("Image", $this->img($image_src, "ubb"), "ubb_full-img"). + "
    ". - $this->link_code("Link", $this->url($post_link, $text_link,"html"), "html_text-link"). - $this->link_code("Thumb", $this->url($post_link,$this->img($thumb_src,"html"),"html"), "html_thumb-link"). - $this->link_code("Image", $this->img($image_src,"html"), "html_full-image"). - " + $this->link_code("Link", $this->url($post_link, $text_link, "html"), "html_text-link"). + $this->link_code("Thumb", $this->url($post_link, $this->img($thumb_src, "html"), "html"), "html_thumb-link"). + $this->link_code("Image", $this->img($image_src, "html"), "html_full-image"). + "
    @@ -38,56 +40,61 @@ class LinkImageTheme extends Themelet { Plain Text ". - $this->link_code("Link",$post_link,"text_post-link"). - $this->link_code("Thumb",$thumb_src,"text_thumb-url"). - $this->link_code("Image",$image_src,"text_image-src"). - " + $this->link_code("Link", $post_link, "text_post-link"). + $this->link_code("Thumb", $thumb_src, "text_thumb-url"). + $this->link_code("Image", $image_src, "text_image-src"). + "
    ", - "main", - 50)); - } + "main", + 50 + )); + } - protected function url (/*string*/ $url, /*string*/ $content, /*string*/ $type) { - if ($content == NULL) {$content=$url;} + protected function url(string $url, string $content, string $type) + { + if ($content == null) { + $content=$url; + } - switch ($type) { - case "html": - $text = "".$content.""; - break; - case "ubb": - $text = "[url=".$url."]".$content."[/url]"; - break; - default: - $text = $url." - ".$content; - } - return $text; - } + switch ($type) { + case "html": + $text = "".$content.""; + break; + case "ubb": + $text = "[url=".$url."]".$content."[/url]"; + break; + default: + $text = $url." - ".$content; + } + return $text; + } - protected function img (/*string*/ $src, /*string*/ $type) { - switch ($type) { - case "html": - $text = ""; - break; - case "ubb": - $text = "[img]".$src."[/img]"; - break; - default: - $text = $src; - } - return $text; - } + protected function img(string $src, string $type) + { + switch ($type) { + case "html": + $text = ""; + break; + case "ubb": + $text = "[img]".$src."[/img]"; + break; + default: + $text = $src; + } + return $text; + } - protected function link_code(/*string*/ $label, /*string*/ $content, $id=NULL) { - return " + protected function link_code(string $label, string $content, $id=null) + { + return " "; - } + } } - diff --git a/ext/livefeed/info.php b/ext/livefeed/info.php new file mode 100644 index 00000000..5aeb4ea4 --- /dev/null +++ b/ext/livefeed/info.php @@ -0,0 +1,22 @@ + +* License: GPLv2 +* Visibility: admin +* Description: Logs user-safe (no IPs) data to a UDP socket, eg IRCCat +* Documentation: +*/ + +class LiveFeedInfo extends ExtensionInfo +{ + public const KEY = "livefeed"; + + public $key = self::KEY; + public $name = "Live Feed"; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $visibility = self::VISIBLE_ADMIN; + public $description = "Logs user-safe (no IPs) data to a UDP socket, eg IRCCat"; +} diff --git a/ext/livefeed/main.php b/ext/livefeed/main.php index a6c281ad..aebf1de6 100644 --- a/ext/livefeed/main.php +++ b/ext/livefeed/main.php @@ -1,71 +1,74 @@ -* License: GPLv2 -* Visibility: admin -* Description: Logs user-safe (no IPs) data to a UDP socket, eg IRCCat -* Documentation: -*/ -class LiveFeed extends Extension { - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Live Feed"); - $sb->add_text_option("livefeed_host", "IP:port to send events to: "); - $event->panel->add_block($sb); - } +class LiveFeed extends Extension +{ + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Live Feed"); + $sb->add_text_option("livefeed_host", "IP:port to send events to: "); + $event->panel->add_block($sb); + } - public function onUserCreation(UserCreationEvent $event) { - $this->msg("New user created: {$event->username}"); - } + public function onUserCreation(UserCreationEvent $event) + { + $this->msg("New user created: {$event->username}"); + } - public function onImageAddition(ImageAdditionEvent $event) { - global $user; - $this->msg( - make_http(make_link("post/view/".$event->image->id))." - ". - "new post by ".$user->name - ); - } + public function onImageAddition(ImageAdditionEvent $event) + { + global $user; + $this->msg( + make_http(make_link("post/view/".$event->image->id))." - ". + "new post by ".$user->name + ); + } - public function onTagSet(TagSetEvent $event) { - $this->msg( - make_http(make_link("post/view/".$event->image->id))." - ". - "tags set to: ".Tag::implode($event->tags) - ); - } + public function onTagSet(TagSetEvent $event) + { + $this->msg( + make_http(make_link("post/view/".$event->image->id))." - ". + "tags set to: ".Tag::implode($event->tags) + ); + } - public function onCommentPosting(CommentPostingEvent $event) { - global $user; - $this->msg( - make_http(make_link("post/view/".$event->image_id))." - ". - $user->name . ": " . str_replace("\n", " ", $event->comment) - ); - } + public function onCommentPosting(CommentPostingEvent $event) + { + global $user; + $this->msg( + make_http(make_link("post/view/".$event->image_id))." - ". + $user->name . ": " . str_replace("\n", " ", $event->comment) + ); + } - public function onImageInfoSet(ImageInfoSetEvent $event) { -# $this->msg("Image info set"); - } + public function onImageInfoSet(ImageInfoSetEvent $event) + { + # $this->msg("Image info set"); + } - public function get_priority() {return 99;} + public function get_priority(): int + { + return 99; + } - /** - * @param string $data - */ - private function msg($data) { - global $config; - assert('is_string($data)'); + private function msg(string $data) + { + global $config; - $host = $config->get_string("livefeed_host", "127.0.0.1:25252"); + $host = $config->get_string("livefeed_host", "127.0.0.1:25252"); - if(!$host) { return; } + if (!$host) { + return; + } try { - $parts = explode(":", $host); + $parts = explode(":", $host); $host = $parts[0]; $port = $parts[1]; $fp = fsockopen("udp://$host", $port, $errno, $errstr); - if (! $fp) { return; } - fwrite($fp, "$data\n"); + if (! $fp) { + return; + } + fwrite($fp, "$data\n"); fclose($fp); } catch (Exception $e) { /* logging errors shouldn't break everything */ diff --git a/ext/log_db/info.php b/ext/log_db/info.php new file mode 100644 index 00000000..0757b4df --- /dev/null +++ b/ext/log_db/info.php @@ -0,0 +1,21 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * Description: Keep a record of SCore events (in the database). + * Visibility: admin + */ + +class LogDatabaseInfo extends ExtensionInfo +{ + public const KEY = "log_db"; + + public $key = self::KEY; + public $name = "Logging (Database)"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $description = "Keep a record of SCore events (in the database)."; + public $visibility = self::VISIBLE_ADMIN; +} diff --git a/ext/log_db/main.php b/ext/log_db/main.php index 3b1bab40..a60567f3 100644 --- a/ext/log_db/main.php +++ b/ext/log_db/main.php @@ -1,19 +1,14 @@ - * Link: http://code.shishnet.org/shimmie2/ - * Description: Keep a record of SCore events (in the database). - * Visibility: admin - */ -class LogDatabase extends Extension { - public function onInitExt(InitExtEvent $event) { - global $database; - global $config; +class LogDatabase extends Extension +{ + public function onInitExt(InitExtEvent $event) + { + global $database; + global $config; - if($config->get_int("ext_log_database_version") < 1) { - $database->create_table("score_log", " + if ($config->get_int("ext_log_database_version") < 1) { + $database->create_table("score_log", " id SCORE_AIPK, date_sent SCORE_DATETIME NOT NULL, section VARCHAR(32) NOT NULL, @@ -22,117 +17,139 @@ class LogDatabase extends Extension { priority INT NOT NULL, message TEXT NOT NULL "); - //INDEX(section) - $config->set_int("ext_log_database_version", 1); - } + //INDEX(section) + $config->set_int("ext_log_database_version", 1); + } - $config->set_default_int("log_db_priority", SCORE_LOG_INFO); - } + $config->set_default_int("log_db_priority", SCORE_LOG_INFO); + } - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Logging (Database)"); - $sb->add_choice_option("log_db_priority", array( - "Debug" => SCORE_LOG_DEBUG, - "Info" => SCORE_LOG_INFO, - "Warning" => SCORE_LOG_WARNING, - "Error" => SCORE_LOG_ERROR, - "Critical" => SCORE_LOG_CRITICAL, - ), "Debug Level: "); - $event->panel->add_block($sb); - } + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Logging (Database)"); + $sb->add_choice_option("log_db_priority", [ + "Debug" => SCORE_LOG_DEBUG, + "Info" => SCORE_LOG_INFO, + "Warning" => SCORE_LOG_WARNING, + "Error" => SCORE_LOG_ERROR, + "Critical" => SCORE_LOG_CRITICAL, + ], "Debug Level: "); + $event->panel->add_block($sb); + } - public function onPageRequest(PageRequestEvent $event) { - global $database, $user; - if($event->page_matches("log/view")) { - if($user->can("view_eventlog")) { - $wheres = array(); - $args = array(); - $page_num = int_escape($event->get_arg(0)); - if($page_num <= 0) $page_num = 1; - if(!empty($_GET["time"])) { - $wheres[] = "date_sent LIKE :time"; - $args["time"] = $_GET["time"]."%"; - } - if(!empty($_GET["module"])) { - $wheres[] = "section = :module"; - $args["module"] = $_GET["module"]; - } - if(!empty($_GET["user"])) { - if($database->get_driver_name() == "pgsql") { - if(preg_match("#\d+\.\d+\.\d+\.\d+(/\d+)?#", $_GET["user"])) { - $wheres[] = "(username = :user1 OR text(address) = :user2)"; - $args["user1"] = $_GET["user"]; - $args["user2"] = $_GET["user"] . "/32"; - } - else { - $wheres[] = "lower(username) = lower(:user)"; - $args["user"] = $_GET["user"]; - } - } - else { - $wheres[] = "(username = :user1 OR address = :user2)"; - $args["user1"] = $_GET["user"]; - $args["user2"] = $_GET["user"]; - } - } - if(!empty($_GET["priority"])) { - $wheres[] = "priority >= :priority"; - $args["priority"] = int_escape($_GET["priority"]); - } - else { - $wheres[] = "priority >= :priority"; - $args["priority"] = 20; - } - $where = ""; - if(count($wheres) > 0) { - $where = "WHERE "; - $where .= join(" AND ", $wheres); - } + public function onPageRequest(PageRequestEvent $event) + { + global $database, $user; + if ($event->page_matches("log/view")) { + if ($user->can(Permissions::VIEW_EVENTLOG)) { + $wheres = []; + $args = []; + $page_num = int_escape($event->get_arg(0)); + if ($page_num <= 0) { + $page_num = 1; + } + if (!empty($_GET["time-start"])) { + $wheres[] = "date_sent > :time_start"; + $args["time_start"] = $_GET["time-start"]; + } + if (!empty($_GET["time-end"])) { + $wheres[] = "date_sent < :time_end"; + $args["time_end"] = $_GET["time-end"]; + } + if (!empty($_GET["module"])) { + $wheres[] = "section = :module"; + $args["module"] = $_GET["module"]; + } + if (!empty($_GET["user"])) { + if ($database->get_driver_name() == DatabaseDriver::PGSQL) { + if (preg_match("#\d+\.\d+\.\d+\.\d+(/\d+)?#", $_GET["user"])) { + $wheres[] = "(username = :user1 OR text(address) = :user2)"; + $args["user1"] = $_GET["user"]; + $args["user2"] = $_GET["user"] . "/32"; + } else { + $wheres[] = "lower(username) = lower(:user)"; + $args["user"] = $_GET["user"]; + } + } else { + $wheres[] = "(username = :user1 OR address = :user2)"; + $args["user1"] = $_GET["user"]; + $args["user2"] = $_GET["user"]; + } + } + if (!empty($_GET["priority"])) { + $wheres[] = "priority >= :priority"; + $args["priority"] = int_escape($_GET["priority"]); + } else { + $wheres[] = "priority >= :priority"; + $args["priority"] = 20; + } + if (!empty($_GET["message"])) { + $wheres[] = $database->scoreql_to_sql("SCORE_STRNORM(message) LIKE SCORE_STRNORM(:message)"); + $args["message"] = "%" . $_GET["message"] . "%"; + } + $where = ""; + if (count($wheres) > 0) { + $where = "WHERE "; + $where .= join(" AND ", $wheres); + } - $limit = 50; - $offset = ($page_num-1) * $limit; - $page_total = $database->cache->get("event_log_length"); - if(!$page_total) { - $page_total = $database->get_one("SELECT count(*) FROM score_log $where", $args); - // don't cache a length of zero when the extension is first installed - if($page_total > 10) { - $database->cache->set("event_log_length", $page_total, 600); - } - } + $limit = 50; + $offset = ($page_num-1) * $limit; + $page_total = $database->cache->get("event_log_length"); + if (!$page_total) { + $page_total = $database->get_one("SELECT count(*) FROM score_log $where", $args); + // don't cache a length of zero when the extension is first installed + if ($page_total > 10) { + $database->cache->set("event_log_length", $page_total, 600); + } + } - $args["limit"] = $limit; - $args["offset"] = $offset; - $events = $database->get_all("SELECT * FROM score_log $where ORDER BY id DESC LIMIT :limit OFFSET :offset", $args); + $args["limit"] = $limit; + $args["offset"] = $offset; + $events = $database->get_all("SELECT * FROM score_log $where ORDER BY id DESC LIMIT :limit OFFSET :offset", $args); - $this->theme->display_events($events, $page_num, 100); - } - } - } + $this->theme->display_events($events, $page_num, 100); + } + } + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("view_eventlog")) { - $event->add_link("Event Log", make_link("log/view")); - } - } + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::VIEW_EVENTLOG)) { + $event->add_nav_link("event_log", new Link('log/view'), "Event Log"); + } + } + } - public function onLog(LogEvent $event) { - global $config, $database, $user; + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::VIEW_EVENTLOG)) { + $event->add_link("Event Log", make_link("log/view")); + } + } - $username = ($user && $user->name) ? $user->name : "null"; + public function onLog(LogEvent $event) + { + global $config, $database, $user; - // not installed yet... - if($config->get_int("ext_log_database_version") < 1) return; + $username = ($user && $user->name) ? $user->name : "null"; - if($event->priority >= $config->get_int("log_db_priority")) { - $database->execute(" + // not installed yet... + if ($config->get_int("ext_log_database_version") < 1) { + return; + } + + if ($event->priority >= $config->get_int("log_db_priority")) { + $database->execute(" INSERT INTO score_log(date_sent, section, priority, username, address, message) VALUES(now(), :section, :priority, :username, :address, :message) - ", array( - "section"=>$event->section, "priority"=>$event->priority, "username"=>$username, - "address"=>$_SERVER['REMOTE_ADDR'], "message"=>$event->message - )); - } - } + ", [ + "section"=>$event->section, "priority"=>$event->priority, "username"=>$username, + "address"=>$_SERVER['REMOTE_ADDR'], "message"=>$event->message + ]); + } + } } - diff --git a/ext/log_db/test.php b/ext/log_db/test.php index 042a4640..691feedf 100644 --- a/ext/log_db/test.php +++ b/ext/log_db/test.php @@ -1,11 +1,13 @@ log_in_as_admin(); - $this->get_page("log/view"); - $this->get_page("log/view?module=core-image"); - $this->get_page("log/view?time=2012-03-01"); - $this->get_page("log/view?user=demo"); - $this->get_page("log/view?priority=10"); - } +class LogDatabaseTest extends ShimmiePHPUnitTestCase +{ + public function testLog() + { + $this->log_in_as_admin(); + $this->get_page("log/view"); + $this->get_page("log/view?module=core-image"); + $this->get_page("log/view?time=2012-03-01"); + $this->get_page("log/view?user=demo"); + $this->get_page("log/view?priority=10"); + } } diff --git a/ext/log_db/theme.php b/ext/log_db/theme.php index 0f7ce961..8c1356fc 100644 --- a/ext/log_db/theme.php +++ b/ext/log_db/theme.php @@ -1,18 +1,28 @@ .sizedinputs TD INPUT { width: 100%; @@ -20,12 +30,14 @@ class LogDatabaseTheme extends Themelet { - + - + + \n"; - reset($events); // rewind to first element in array. - - foreach($events as $event) { - $c = $this->pri_to_col($event['priority']); - $table .= ""; - $table .= ""; - $table .= ""; - if($event['username'] == "Anonymous") { - $table .= ""; - } - else { - $table .= ""; - } - $table .= ""; - $table .= "\n"; - } - $table .= "
    TimeModuleUserMessage
    TimeModuleUserMessage
    +
    ".str_replace(" ", " ", substr($event['date_sent'], 0, 19))."".$event['section']."".$event['address']."". - "".html_escape($event['username'])."". - "".$this->scan_entities(html_escape($event['message']))."
    "; + reset($events); // rewind to first element in array. + + foreach ($events as $event) { + $c = $this->pri_to_col($event['priority']); + $table .= ""; + $table .= "".str_replace(" ", " ", substr($event['date_sent'], 0, 19)).""; + $table .= "".$event['section'].""; + if ($event['username'] == "Anonymous") { + $table .= "".$event['address'].""; + } else { + $table .= "". + "".html_escape($event['username'])."". + ""; + } + $table .= "".$this->scan_entities(html_escape($event['message'])).""; + $table .= "\n"; + } + $table .= ""; - global $page; - $page->set_title("Event Log"); - $page->set_heading("Event Log"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Events", $table)); + global $page; + $page->set_title("Event Log"); + $page->set_heading("Event Log"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Events", $table)); + $this->display_paginator($page, "log/view", $this->get_args(), $page_num, $page_total); + } - $args = ""; - // Check if each arg is actually empty and skip it if so - if(strlen($this->ueie("time"))) - $args .= $this->ueie("time")."&"; - if(strlen($this->ueie("module"))) - $args .= $this->ueie("module")."&"; - if(strlen($this->ueie("user"))) - $args .= $this->ueie("user")."&"; - if(strlen($this->ueie("priority"))) - $args .= $this->ueie("priority"); - // If there are no args at all, set $args to null to prevent an unnecessary ? at the end of the paginator url - if(strlen($args) == 0) - $args = null; - $this->display_paginator($page, "log/view", $args, $page_num, $page_total); - } + protected function get_args() + { + $args = ""; + // Check if each arg is actually empty and skip it if so + if (strlen($this->ueie("time-start"))) { + $args .= $this->ueie("time-start")."&"; + } + if (strlen($this->ueie("time-end"))) { + $args .= $this->ueie("time-end")."&"; + } + if (strlen($this->ueie("module"))) { + $args .= $this->ueie("module")."&"; + } + if (strlen($this->ueie("user"))) { + $args .= $this->ueie("user")."&"; + } + if (strlen($this->ueie("message"))) { + $args .= $this->ueie("message")."&"; + } + if (strlen($this->ueie("priority"))) { + $args .= $this->ueie("priority"); + } + // If there are no args at all, set $args to null to prevent an unnecessary ? at the end of the paginator url + if (strlen($args) == 0) { + $args = null; + } + return $args; + } - protected function pri_to_col($pri) { - switch($pri) { - case SCORE_LOG_DEBUG: return "#999"; - case SCORE_LOG_INFO: return "#000"; - case SCORE_LOG_WARNING: return "#800"; - case SCORE_LOG_ERROR: return "#C00"; - case SCORE_LOG_CRITICAL: return "#F00"; - default: return ""; - } - } + protected function pri_to_col($pri) + { + switch ($pri) { + case SCORE_LOG_DEBUG: return "#999"; + case SCORE_LOG_INFO: return "#000"; + case SCORE_LOG_WARNING: return "#800"; + case SCORE_LOG_ERROR: return "#C00"; + case SCORE_LOG_CRITICAL: return "#F00"; + default: return ""; + } + } - protected function scan_entities($line) { - $line = preg_replace_callback("/Image #(\d+)/s", array($this, "link_image"), $line); - return $line; - } + protected function scan_entities($line) + { + $line = preg_replace_callback("/Image #(\d+)/s", [$this, "link_image"], $line); + return $line; + } - protected function link_image($id) { - $iid = int_escape($id[1]); - return "Image #$iid"; - } + protected function link_image($id) + { + $iid = int_escape($id[1]); + return "Image #$iid"; + } } - diff --git a/ext/log_logstash/info.php b/ext/log_logstash/info.php new file mode 100644 index 00000000..6f35d526 --- /dev/null +++ b/ext/log_logstash/info.php @@ -0,0 +1,21 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * Description: Send log events to a network port. + * Visibility: admin + */ + +class LogLogstashInfo extends ExtensionInfo +{ + public const KEY = "log_logstash"; + + public $key = self::KEY; + public $name = "Logging (Logstash)"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $description = "Send log events to a network port."; + public $visibility = self::VISIBLE_ADMIN; +} diff --git a/ext/log_logstash/main.php b/ext/log_logstash/main.php index 43517add..c09d7e24 100644 --- a/ext/log_logstash/main.php +++ b/ext/log_logstash/main.php @@ -1,55 +1,54 @@ - * Link: http://code.shishnet.org/shimmie2/ - * Description: Send log events to a network port. - * Visibility: admin - */ -class LogLogstash extends Extension { - - public function onLog(LogEvent $event) { - global $user; +class LogLogstash extends Extension +{ + public function onLog(LogEvent $event) + { + global $user; - try { - $data = array( - "@type" => "shimmie", - "@message" => $event->message, - "@fields" => array( - "username" => ($user && $user->name) ? $user->name : "Anonymous", - "section" => $event->section, - "priority" => $event->priority, - "time" => $event->time, - "args" => $event->args, - ), - #"@request" => $_SERVER, - "@request" => array( - "UID" => get_request_id(), - "REMOTE_ADDR" => $_SERVER['REMOTE_ADDR'], - ), - ); + try { + $data = [ + "@type" => "shimmie", + "@message" => $event->message, + "@fields" => [ + "username" => ($user && $user->name) ? $user->name : "Anonymous", + "section" => $event->section, + "priority" => $event->priority, + "time" => $event->time, + "args" => $event->args, + ], + #"@request" => $_SERVER, + "@request" => [ + "UID" => get_request_id(), + "REMOTE_ADDR" => $_SERVER['REMOTE_ADDR'], + ], + ]; - $this->send_data($data); - } catch (Exception $e) { - } - } + $this->send_data($data); + } catch (Exception $e) { + } + } - private function send_data($data) { - global $config; + private function send_data($data) + { + global $config; - $host = $config->get_string("log_logstash_host"); - if(!$host) { return; } + $host = $config->get_string("log_logstash_host"); + if (!$host) { + return; + } - try { - $parts = explode(":", $host); - $host = $parts[0]; - $port = $parts[1]; - $fp = fsockopen("udp://$host", $port, $errno, $errstr); - if (! $fp) { return; } - fwrite($fp, json_encode($data)); - fclose($fp); - } catch (Exception $e) { - } - } + try { + $parts = explode(":", $host); + $host = $parts[0]; + $port = $parts[1]; + $fp = fsockopen("udp://$host", $port, $errno, $errstr); + if (! $fp) { + return; + } + fwrite($fp, json_encode($data)); + fclose($fp); + } catch (Exception $e) { + } + } } diff --git a/ext/log_net/info.php b/ext/log_net/info.php new file mode 100644 index 00000000..a053a994 --- /dev/null +++ b/ext/log_net/info.php @@ -0,0 +1,21 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * Description: Send log events to a network port. + * Visibility: admin + */ + +class LogNetInfo extends ExtensionInfo +{ + public const KEY = "log_net"; + + public $key = self::KEY; + public $name = "Logging (Network)"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $description = "Send log events to a network port."; + public $visibility = self::VISIBLE_ADMIN; +} diff --git a/ext/log_net/main.php b/ext/log_net/main.php index f5e07175..dcc341db 100644 --- a/ext/log_net/main.php +++ b/ext/log_net/main.php @@ -1,49 +1,47 @@ - * Link: http://code.shishnet.org/shimmie2/ - * Description: Send log events to a network port. - * Visibility: admin - */ -class LogNet extends Extension { - private $count = 0; +class LogNet extends Extension +{ + private $count = 0; - public function onLog(LogEvent $event) { - global $user; + public function onLog(LogEvent $event) + { + global $user; - if($event->priority > 10) { - $this->count++; - if($this->count < 10) { - // TODO: colour based on event->priority - $username = ($user && $user->name) ? $user->name : "Anonymous"; - $str = sprintf("%-15s %-10s: %s", $_SERVER['REMOTE_ADDR'], $username, $event->message); - $this->msg($str); - } - else if($this->count == 10) { - $this->msg('suppressing flood, check the web log'); - } - } - } + if ($event->priority > 10) { + $this->count++; + if ($this->count < 10) { + // TODO: colour based on event->priority + $username = ($user && $user->name) ? $user->name : "Anonymous"; + $str = sprintf("%-15s %-10s: %s", $_SERVER['REMOTE_ADDR'], $username, $event->message); + $this->msg($str); + } elseif ($this->count == 10) { + $this->msg('suppressing flood, check the web log'); + } + } + } - private function msg($data) { - global $config; - $host = $config->get_string("log_net_host", "127.0.0.1:35353"); + private function msg($data) + { + global $config; + $host = $config->get_string("log_net_host", "127.0.0.1:35353"); - if(!$host) { return; } + if (!$host) { + return; + } - try { - $parts = explode(":", $host); - $host = $parts[0]; - $port = $parts[1]; - $fp = fsockopen("udp://$host", $port, $errno, $errstr); - if (! $fp) { return; } - fwrite($fp, "$data\n"); - fclose($fp); - } catch (Exception $e) { - /* logging errors shouldn't break everything */ - } - } + try { + $parts = explode(":", $host); + $host = $parts[0]; + $port = $parts[1]; + $fp = fsockopen("udp://$host", $port, $errno, $errstr); + if (! $fp) { + return; + } + fwrite($fp, "$data\n"); + fclose($fp); + } catch (Exception $e) { + /* logging errors shouldn't break everything */ + } + } } - diff --git a/ext/mail/info.php b/ext/mail/info.php new file mode 100644 index 00000000..bb370eae --- /dev/null +++ b/ext/mail/info.php @@ -0,0 +1,22 @@ + + * Link: http://seemslegit.com + * License: GPLv2 + * Description: Provides an interface for sending and receiving mail. + */ + +class MailInfo extends ExtensionInfo +{ + public const KEY = "mail"; + + public $key = self::KEY; + public $name = "Mail System"; + public $url = "http://seemslegit.com"; + public $authors = ["Zach Hall"=>"zach@sosguy.net"]; + public $license = self::LICENSE_GPLV2; + public $core = true; + public $description = "Provides an interface for sending and receiving mail."; +} diff --git a/ext/mail/mail.css b/ext/mail/mail.css index 34b8b3c3..17ddccc1 100644 --- a/ext/mail/mail.css +++ b/ext/mail/mail.css @@ -6,4 +6,4 @@ .defaultText { font-size:12px; color:#000000; line-height:150%; font-family:trebuchet ms; } .footerRow { background-color:#FFFFCC; border-top:10px solid #FFFFFF; } .footerText { font-size:10px; color:#996600; line-height:100%; font-family:verdana; } -a { color:#FF6600; color:#FF6600; color:#FF6600; } \ No newline at end of file +a { color:#FF6600; } \ No newline at end of file diff --git a/ext/mail/main.php b/ext/mail/main.php index d4b8007f..f99084d9 100644 --- a/ext/mail/main.php +++ b/ext/mail/main.php @@ -1,45 +1,47 @@ -* Link: http://seemslegit.com -* License: GPLv2 -* Description: Provides an interface for sending and receiving mail. -*/ -class Mail extends Extension { - public function onSetupBuilding(SetupBuildingEvent $event) { - $sb = new SetupBlock("Mailing Options"); - $sb->add_text_option("mail_sub", "Subject prefix: "); - $sb->add_text_option("mail_img", "
    Banner Image URL: "); - $sb->add_text_option("mail_style", "
    Style URL: "); - $sb->add_longtext_option("mail_fot", "
    Footer (Use HTML)"); - $sb->add_label("
    Should measure 550x110px. Use an absolute URL"); - $event->panel->add_block($sb); - } - - public function onInitExt(InitExtEvent $event) { - global $config; - $config->set_default_string("mail_sub", $config->get_string("site_title")." - "); - $config->set_default_string("mail_img", make_http("ext/mail/banner.png")); - $config->set_default_string("mail_style", make_http("ext/mail/mail.css")); - $config->set_default_string("mail_fot", "".$config->get_string("site_title").""); - } -} -class MailTest extends Extension { - public function onPageRequest(PageRequestEvent $event) { - if($event->page_matches("mail/test")) { - global $page; - $page->set_mode("data"); - echo "Alert: uncomment this page's code on /ext/mail/main.php starting on line 33, and change the email address. Make sure you're using a server with a domain, not localhost."; - /* - echo "Preparing to send message:
    "; - echo "created new mail object. sending now... "; - $email = new Email("example@localhost.com", "hello", "hello world", "this is a test message."); - $email->send(); - echo "sent."; - */ - } - } -} +class Mail extends Extension +{ + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Mailing Options"); + $sb->add_text_option("mail_sub", "Subject prefix: "); + $sb->add_text_option("mail_img", "
    Banner Image URL: "); + $sb->add_text_option("mail_style", "
    Style URL: "); + $sb->add_longtext_option("mail_fot", "
    Footer (Use HTML)"); + $sb->add_label("
    Should measure 550x110px. Use an absolute URL"); + $event->panel->add_block($sb); + } + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_string("mail_sub", $config->get_string("site_title")." - "); + $config->set_default_string("mail_img", make_http("ext/mail/banner.png")); + $config->set_default_string("mail_style", make_http("ext/mail/mail.css")); + $config->set_default_string("mail_fot", "".$config->get_string("site_title").""); + } +} +class MailTest extends Extension +{ + public function __construct() + { + parent::__construct("Mail"); + } + + public function onPageRequest(PageRequestEvent $event) + { + if ($event->page_matches("mail/test")) { + global $page; + $page->set_mode(PageMode::DATA); + echo "Alert: uncomment this page's code on /ext/mail/main.php starting on line 33, and change the email address. Make sure you're using a server with a domain, not localhost."; + /* + echo "Preparing to send message:
    "; + echo "created new mail object. sending now... "; + $email = new Email("example@localhost.com", "hello", "hello world", "this is a test message."); + $email->send(); + echo "sent."; + */ + } + } +} diff --git a/ext/mass_tagger/main.php b/ext/mass_tagger/main.php deleted file mode 100644 index e46ec63c..00000000 --- a/ext/mass_tagger/main.php +++ /dev/null @@ -1,69 +0,0 @@ -, contributions by Shish and Agasa - * License: WTFPL - * Description: Tag a bunch of images at once - * Documentation: - * Once enabled, a new "Mass Tagger" box will appear on the left hand side of - * post listings, with a button to enable the mass tagger. Once clicked JS will - * add buttons to each image to mark them for tagging, and a field for - * inputting tags will appear. Once the "Tag" button is clicked, the tags in - * the text field will be added to marked images. - */ - -class MassTagger extends Extension { - public function onPostListBuilding(PostListBuildingEvent $event) { - global $config, $page, $user; - - if($user->is_admin()) { - $this->theme->display_mass_tagger( $page, $event, $config ); - } - } - - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; - if($event->page_matches("mass_tagger/tag") && $user->is_admin()) { - if( !isset($_POST['ids']) or !isset($_POST['tag']) ) return; - - $tags = Tag::explode($_POST['tag']); - - $pos_tag_array = array(); - $neg_tag_array = array(); - foreach($tags as $new_tag) { - if (strpos($new_tag, '-') === 0) - $neg_tag_array[] = substr($new_tag,1); - else - $pos_tag_array[] = $new_tag; - } - - $ids = explode( ':', $_POST['ids'] ); - $ids = array_filter ( $ids , 'is_numeric' ); - - $images = array_map( "Image::by_id", $ids ); - - if(isset($_POST['setadd']) && $_POST['setadd'] == 'set') { - foreach($images as $image) { - $image->set_tags($tags); - } - } - else { - foreach($images as $image) { - if (!empty($neg_tag_array)) { - $img_tags = array_merge($pos_tag_array, $image->get_tag_array()); - $img_tags = array_diff($img_tags, $neg_tag_array); - $image->set_tags($img_tags); - } - else { - $image->set_tags(array_merge($tags, $image->get_tag_array())); - } - } - } - - $page->set_mode("redirect"); - if(!isset($_SERVER['HTTP_REFERER'])) $_SERVER['HTTP_REFERER'] = make_link(); - $page->set_redirect($_SERVER['HTTP_REFERER']); - } - } -} - diff --git a/ext/mass_tagger/script.js b/ext/mass_tagger/script.js deleted file mode 100644 index 0e4dbcd9..00000000 --- a/ext/mass_tagger/script.js +++ /dev/null @@ -1,41 +0,0 @@ -/*jshint bitwise:true, curly:true, forin:false, noarg:true, noempty:true, nonew:true, undef:true, strict:false, browser:true, jquery:true */ - -function activate_mass_tagger ( image_link ) { - $(".shm-thumb").each( - function ( index, block ) { - add_mass_tag_button( $(block), image_link ); - } - ); - $('#mass_tagger_controls').show(); - $('#mass_tagger_activate').hide(); -} - -function add_mass_tag_button($block, image_link) { - - var c = function() { toggle_tag(this, $block.data("post-id")); return false; }; - - $block.find("A").click(c); - $block.click(c); // sometimes the thumbs *is* the A -} - -function toggle_tag( button, id ) { - id += ":"; - var list = $('#mass_tagger_ids'); - var string = list.val(); - - if( (string.indexOf(id) == 0) || (string.indexOf(":"+id) > -1) ) { - $(button).removeClass('mass-tagger-selected'); - string = string.replace(id, ''); - list.val(string); - } - else { - $(button).addClass('mass-tagger-selected'); - string += id; - list.val(string); - } -} - -$(function () { - // Clear the selection, in case it was autocompleted by the browser. - $('#mass_tagger_ids').val(""); -}); diff --git a/ext/mass_tagger/style.css b/ext/mass_tagger/style.css deleted file mode 100644 index c0a2ea6c..00000000 --- a/ext/mass_tagger/style.css +++ /dev/null @@ -1,3 +0,0 @@ -.mass-tagger-selected { - border: 3px solid blue; -} \ No newline at end of file diff --git a/ext/mass_tagger/theme.php b/ext/mass_tagger/theme.php deleted file mode 100644 index abce598b..00000000 --- a/ext/mass_tagger/theme.php +++ /dev/null @@ -1,27 +0,0 @@ - - -

    - - "; - $block = new Block("Mass Tagger", $body, "left", 50); - $page->add_block( $block ); - } -} - diff --git a/ext/mass_tagger/toggle.gif b/ext/mass_tagger/toggle.gif deleted file mode 100644 index 64c3c765..00000000 Binary files a/ext/mass_tagger/toggle.gif and /dev/null differ diff --git a/ext/media/info.php b/ext/media/info.php new file mode 100644 index 00000000..abafffbe --- /dev/null +++ b/ext/media/info.php @@ -0,0 +1,20 @@ + + * Description: Provides common functions and settings used for media operations. + */ + +class MediaInfo extends ExtensionInfo +{ + public const KEY = "media"; + + public $key = self::KEY; + public $name = "Media"; + public $url = self::SHIMMIE_URL; + public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"]; + public $license = self::LICENSE_WTFPL; + public $description = "Provides common functions and settings used for media operations."; + public $core = true; +} diff --git a/ext/media/main.php b/ext/media/main.php new file mode 100644 index 00000000..e11a6009 --- /dev/null +++ b/ext/media/main.php @@ -0,0 +1,1138 @@ + [ + "gif", + "jpg", + "png", + "webp", + Media::WEBP_LOSSY, + ], + MediaEngine::IMAGICK => [ + "gif", + "jpg", + "png", + "webp", + Media::WEBP_LOSSY, + Media::WEBP_LOSSLESS, + ], + MediaEngine::FFMPEG => [ + "jpg", + "webp", + "png" + ] + ]; + public const INPUT_SUPPORT = [ + MediaEngine::GD => [ + "bmp", + "gif", + "jpg", + "png", + "webp", + Media::WEBP_LOSSY, + Media::WEBP_LOSSLESS + ], + MediaEngine::IMAGICK => [ + "bmp", + "gif", + "jpg", + "png", + "psd", + "tiff", + "webp", + Media::WEBP_LOSSY, + Media::WEBP_LOSSLESS, + "ico", + ], + MediaEngine::FFMPEG => [ + "avi", + "mkv", + "webm", + "mp4", + "mov", + "flv" + ] + ]; +} + +class MediaException extends SCoreException +{ +} + +class MediaResizeEvent extends Event +{ + public $engine; + public $input_path; + public $input_type; + public $output_path; + public $target_format; + public $target_width; + public $target_height; + public $target_quality; + public $minimize; + public $ignore_aspect_ratio; + public $allow_upscale; + + public function __construct( + String $engine, + string $input_path, + string $input_type, + string $output_path, + int $target_width, + int $target_height, + bool $ignore_aspect_ratio = false, + string $target_format = null, + int $target_quality = 80, + bool $minimize = false, + bool $allow_upscale = true + ) { + assert(in_array($engine, MediaEngine::ALL)); + $this->engine = $engine; + $this->input_path = $input_path; + $this->input_type = $input_type; + $this->output_path = $output_path; + $this->target_height = $target_height; + $this->target_width = $target_width; + $this->target_format = $target_format; + $this->target_quality = $target_quality; + $this->minimize = $minimize; + $this->ignore_aspect_ratio = $ignore_aspect_ratio; + $this->allow_upscale = $allow_upscale; + } +} + +class MediaCheckPropertiesEvent extends Event +{ + public $file_name; + public $ext; + public $lossless = null; + public $audio = null; + public $video = null; + public $length = null; + public $height = null; + public $width = null; + + public function __construct(string $file_name, string $ext) + { + $this->file_name = $file_name; + $this->ext = $ext; + } +} + + +class Media extends Extension +{ + const WEBP_LOSSY = "webp-lossy"; + const WEBP_LOSSLESS = "webp-lossless"; + + const IMAGE_MEDIA_ENGINES = [ + "GD" => MediaEngine::GD, + "ImageMagick" => MediaEngine::IMAGICK, + ]; + + const LOSSLESS_FORMATS = [ + self::WEBP_LOSSLESS, + "png", + "psd", + "bmp", + "ico", + "cur", + "ani", + "gif" + + ]; + + const ALPHA_FORMATS = [ + self::WEBP_LOSSLESS, + self::WEBP_LOSSY, + "webp", + "png", + ]; + + const FORMAT_ALIASES = [ + "tif" => "tiff", + "jpeg" => "jpg", + ]; + + + //RIFF####WEBPVP8?..............ANIM + private const WEBP_ANIMATION_HEADER = + [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, 0x41, 0x4E, 0x49, 0x4D]; + + //RIFF####WEBPVP8L + private const WEBP_LOSSLESS_HEADER = + [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x4C]; + + + public static function imagick_available(): bool + { + return extension_loaded("imagick"); + } + + /** + * High priority just so that it can be early in the settings + */ + public function get_priority(): int + { + return 30; + } + + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_string(MediaConfig::FFPROBE_PATH, 'ffprobe'); + $config->set_default_int(MediaConfig::MEM_LIMIT, parse_shorthand_int('8MB')); + $config->set_default_string(MediaConfig::FFMPEG_PATH, 'ffmpeg'); + $config->set_default_string(MediaConfig::CONVERT_PATH, 'convert'); + + + if ($config->get_int(MediaConfig::VERSION) < 1) { + $current_value = $config->get_string("thumb_ffmpeg_path"); + if (!empty($current_value)) { + $config->set_string(MediaConfig::FFMPEG_PATH, $current_value); + } elseif ($ffmpeg = shell_exec((PHP_OS == 'WINNT' ? 'where' : 'which') . ' ffmpeg')) { + //ffmpeg exists in PATH, check if it's executable, and if so, default to it instead of static + if (is_executable(strtok($ffmpeg, PHP_EOL))) { + $config->set_default_string(MediaConfig::FFMPEG_PATH, 'ffmpeg'); + } + } + + if ($ffprobe = shell_exec((PHP_OS == 'WINNT' ? 'where' : 'which') . ' ffprobe')) { + //ffprobe exists in PATH, check if it's executable, and if so, default to it instead of static + if (is_executable(strtok($ffprobe, PHP_EOL))) { + $config->set_default_string(MediaConfig::FFPROBE_PATH, 'ffprobe'); + } + } + + $current_value = $config->get_string("thumb_convert_path"); + if (!empty($current_value)) { + $config->set_string(MediaConfig::CONVERT_PATH, $current_value); + } elseif ($convert = shell_exec((PHP_OS == 'WINNT' ? 'where' : 'which') . ' convert')) { + //ffmpeg exists in PATH, check if it's executable, and if so, default to it instead of static + if (is_executable(strtok($convert, PHP_EOL))) { + $config->set_default_string(MediaConfig::CONVERT_PATH, 'convert'); + } + } + + $current_value = $config->get_int("thumb_mem_limit"); + if (!empty($current_value)) { + $config->set_int(MediaConfig::MEM_LIMIT, $current_value); + } + + $config->set_int(MediaConfig::VERSION, 1); + log_info("media", "extension installed"); + } + } + + public function onPageRequest(PageRequestEvent $event) + { + global $database, $page, $user; + + if ($event->page_matches("media_rescan/") && $user->can(Permissions::RESCAN_MEDIA) && isset($_POST['image_id'])) { + $image = Image::by_id(int_escape($_POST['image_id'])); + + $this->update_image_media_properties($image->hash, $image->ext); + + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/$image->id")); + } + } + + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = new SetupBlock("Media Engines"); + +// if (self::imagick_available()) { +// try { +// $image = new Imagick(realpath('tests/favicon.png')); +// $image->clear(); +// $sb->add_label("ImageMagick detected"); +// } catch (ImagickException $e) { +// $sb->add_label("ImageMagick not detected"); +// } +// } else { + $sb->start_table(); + $sb->add_table_header("Commands"); + + $sb->add_text_option(MediaConfig::CONVERT_PATH, "convert", true); +// } + + $sb->add_text_option(MediaConfig::FFMPEG_PATH, "
    ffmpeg", true); + $sb->add_text_option(MediaConfig::FFPROBE_PATH, "
    ffprobe", true); + + $sb->add_shorthand_int_option(MediaConfig::MEM_LIMIT, "
    Mem limit: ", true); + $sb->end_table(); + + $event->panel->add_block($sb); + } + + public function onAdminBuilding(AdminBuildingEvent $event) + { + global $database; + $types = $database->get_all("SELECT ext, count(*) count FROM images group by ext"); + + $this->theme->display_form($types); + } + + public function onAdminAction(AdminActionEvent $event) + { + $action = $event->action; + if (method_exists($this, $action)) { + $event->redirect = $this->$action(); + } + } + + + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::DELETE_IMAGE)) { + $event->add_part($this->theme->get_buttons_html($event->image->id)); + } + } + + + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) + { + global $user; + + if ($user->can(Permissions::RESCAN_MEDIA)) { + $event->add_action("bulk_media_rescan", "Scan Media Properties"); + } + } + + public function onBulkAction(BulkActionEvent $event) + { + global $user; + + switch ($event->action) { + case "bulk_media_rescan": + if ($user->can(Permissions::RESCAN_MEDIA)) { + $total = 0; + foreach ($event->items as $image) { + try { + $this->update_image_media_properties($image->hash, $image->ext); + $total++; + } catch (MediaException $e) { + } + } + flash_message("Scanned media properties for $total items"); + } + break; + } + } + + /** + * @param MediaResizeEvent $event + * @throws MediaException + * @throws InsufficientMemoryException + */ + public function onMediaResize(MediaResizeEvent $event) + { + switch ($event->engine) { + case MediaEngine::GD: + $info = getimagesize($event->input_path); + if ($info === false) { + throw new MediaException("getimagesize failed for " . $event->input_path); + } + + self::image_resize_gd( + $event->input_path, + $info, + $event->target_width, + $event->target_height, + $event->output_path, + $event->target_format, + $event->ignore_aspect_ratio, + $event->target_quality, + $event->allow_upscale + ); + + break; + case MediaEngine::IMAGICK: +// if (self::imagick_available()) { +// } else { + self::image_resize_convert( + $event->input_path, + $event->input_type, + $event->target_width, + $event->target_height, + $event->output_path, + $event->target_format, + $event->ignore_aspect_ratio, + $event->target_quality, + $event->minimize, + $event->allow_upscale + ); + //} + break; + default: + throw new MediaException("Engine not supported for resize: " . $event->engine); + } + + // TODO: Get output optimization tools working better +// if ($config->get_bool("thumb_optim", false)) { +// exec("jpegoptim $outname", $output, $ret); +// } + } + + + const CONTENT_SEARCH_TERM_REGEX = "/^content[=|:]((video)|(audio))$/i"; + + + public function onSearchTermParse(SearchTermParseEvent $event) + { + global $database; + + $matches = []; + if (preg_match(self::CONTENT_SEARCH_TERM_REGEX, $event->term, $matches)) { + $field = $matches[2]; + $event->add_querylet(new Querylet($database->scoreql_to_sql("$field = SCORE_BOOL_Y"))); + } + } + + public function onHelpPageBuilding(HelpPageBuildingEvent $event) + { + if ($event->key===HelpPages::SEARCH) { + $block = new Block(); + $block->header = "Media"; + $block->body = $this->theme->get_help_html(); + $event->add_block($block); + } + } + + + public function onTagTermParse(TagTermParseEvent $event) + { + $matches = []; + + if (preg_match(self::CONTENT_SEARCH_TERM_REGEX, strtolower($event->term), $matches) && $event->parse) { + // Nothing to save, just helping filter out reserved tags + } + + if (!empty($matches)) { + $event->metatag = true; + } + } + + private function media_rescan(): bool + { + $ext = ""; + if (array_key_exists("media_rescan_type", $_POST)) { + $ext = $_POST["media_rescan_type"]; + } + + $results = $this->get_images($ext); + + foreach ($results as $result) { + $this->update_image_media_properties($result["hash"], $result["ext"]); + } + return true; + } + + public static function update_image_media_properties(string $hash, string $ext) + { + global $database; + + $path = warehouse_path(Image::IMAGE_DIR, $hash); + $mcpe = new MediaCheckPropertiesEvent($path, $ext); + send_event($mcpe); + + + $database->execute( + "UPDATE images SET + lossless = :lossless, video = :video, audio = :audio, + height = :height, width = :width, + length = :length WHERE hash = :hash", + [ + "hash" => $hash, + "width" => $mcpe->width ?? 0, + "height" => $mcpe->height ?? 0, + "lossless" => $database->scoresql_value_prepare($mcpe->lossless), + "video" => $database->scoresql_value_prepare($mcpe->video), + "audio" => $database->scoresql_value_prepare($mcpe->audio), + "length" => $mcpe->length + ] + ); + } + + public function get_images(String $ext = null) + { + global $database; + + $query = "SELECT id, hash, ext FROM images "; + $args = []; + if (!empty($ext)) { + $query .= " WHERE ext = :ext"; + $args["ext"] = $ext; + } + return $database->get_all($query, $args); + } + + /** + * Check Memory usage limits + * + * Old check: $memory_use = (filesize($image_filename)*2) + ($width*$height*4) + (4*1024*1024); + * New check: $memory_use = $width * $height * ($bits_per_channel) * channels * 2.5 + * + * It didn't make sense to compute the memory usage based on the NEW size for the image. ($width*$height*4) + * We need to consider the size that we are GOING TO instead. + * + * The factor of 2.5 is simply a rough guideline. + * http://stackoverflow.com/questions/527532/reasonable-php-memory-limit-for-image-resize + * + * @param array $info The output of getimagesize() for the source file in question. + * @return int The number of bytes an image resize operation is estimated to use. + */ + public static function calc_memory_use(array $info): int + { + if (isset($info['bits']) && isset($info['channels'])) { + $memory_use = ($info[0] * $info[1] * ($info['bits'] / 8) * $info['channels'] * 2.5) / 1024; + } else { + // If we don't have bits and channel info from the image then assume default values + // of 8 bits per color and 4 channels (R,G,B,A) -- ie: regular 24-bit color + $memory_use = ($info[0] * $info[1] * 1 * 4 * 2.5) / 1024; + } + return (int)$memory_use; + } + + + /** + * Creates a thumbnail using ffmpeg. + * + * @param $hash + * @return bool true if successful, false if not. + * @throws MediaException + */ + public static function create_thumbnail_ffmpeg($hash): bool + { + global $config; + + $ffmpeg = $config->get_string(MediaConfig::FFMPEG_PATH); + if ($ffmpeg == null || $ffmpeg == "") { + throw new MediaException("ffmpeg command configured"); + } + + $inname = warehouse_path(Image::IMAGE_DIR, $hash); + $outname = warehouse_path(Image::THUMBNAIL_DIR, $hash); + + $orig_size = self::video_size($inname); + $scaled_size = get_thumbnail_size($orig_size[0], $orig_size[1], true); + + $codec = "mjpeg"; + $quality = $config->get_int(ImageConfig::THUMB_QUALITY); + if ($config->get_string(ImageConfig::THUMB_TYPE) == "webp") { + $codec = "libwebp"; + } else { + // mjpeg quality ranges from 2-31, with 2 being the best quality. + $quality = floor(31 - (31 * ($quality / 100))); + if ($quality < 2) { + $quality = 2; + } + } + + $args = [ + escapeshellarg($ffmpeg), + "-y", "-i", escapeshellarg($inname), + "-vf", "thumbnail,scale={$scaled_size[0]}:{$scaled_size[1]}", + "-f", "image2", + "-vframes", "1", + "-c:v", $codec, + "-q:v", $quality, + escapeshellarg($outname), + ]; + + $cmd = escapeshellcmd(implode(" ", $args)); + + exec($cmd, $output, $ret); + + if ((int)$ret == (int)0) { + log_debug('Media', "Generating thumbnail with command `$cmd`, returns $ret"); + return true; + } else { + log_error('Media', "Generating thumbnail with command `$cmd`, returns $ret"); + return false; + } + } + + + public static function get_ffprobe_data($filename): array + { + global $config; + + $ffprobe = $config->get_string(MediaConfig::FFPROBE_PATH); + if ($ffprobe == null || $ffprobe == "") { + throw new MediaException("ffprobe command configured"); + } + + $args = [ + escapeshellarg($ffprobe), + "-print_format", "json", + "-v", "quiet", + "-show_format", + "-show_streams", + escapeshellarg($filename), + ]; + + $cmd = escapeshellcmd(implode(" ", $args)); + + exec($cmd, $output, $ret); + + if ((int)$ret == (int)0) { + log_debug('Media', "Getting media data `$cmd`, returns $ret"); + $output = implode($output); + $data = json_decode($output, true); + + return $data; + } else { + log_error('Media', "Getting media data `$cmd`, returns $ret"); + return []; + } + } + + public static function determine_ext(String $format): String + { + $format = self::normalize_format($format); + switch ($format) { + case self::WEBP_LOSSLESS: + case self::WEBP_LOSSY: + return "webp"; + default: + return $format; + } + } + +// private static function image_save_imagick(Imagick $image, string $path, string $format, int $output_quality = 80, bool $minimize) +// { +// switch ($format) { +// case "png": +// $result = $image->setOption('png:compression-level', 9); +// if ($result !== true) { +// throw new GraphicsException("Could not set png compression option"); +// } +// break; +// case Graphics::WEBP_LOSSLESS: +// $result = $image->setOption('webp:lossless', true); +// if ($result !== true) { +// throw new GraphicsException("Could not set lossless webp option"); +// } +// break; +// default: +// $result = $image->setImageCompressionQuality($output_quality); +// if ($result !== true) { +// throw new GraphicsException("Could not set compression quality for $path to $output_quality"); +// } +// break; +// } +// +// if (self::supports_alpha($format)) { +// $result = $image->setImageBackgroundColor(new \ImagickPixel('transparent')); +// } else { +// $result = $image->setImageBackgroundColor(new \ImagickPixel('black')); +// } +// if ($result !== true) { +// throw new GraphicsException("Could not set background color"); +// } +// +// +// if ($minimize) { +// $profiles = $image->getImageProfiles("icc", true); +// $result = $image->stripImage(); +// if ($result !== true) { +// throw new GraphicsException("Could not strip information from image"); +// } +// if (!empty($profiles)) { +// $image->profileImage("icc", $profiles['icc']); +// } +// } +// +// $ext = self::determine_ext($format); +// +// $result = $image->writeImage($ext . ":" . $path); +// if ($result !== true) { +// throw new GraphicsException("Could not write image to $path"); +// } +// } + +// public static function image_resize_imagick( +// String $input_path, +// String $input_type, +// int $new_width, +// int $new_height, +// string $output_filename, +// string $output_type = null, +// bool $ignore_aspect_ratio = false, +// int $output_quality = 80, +// bool $minimize = false, +// bool $allow_upscale = true +// ): void +// { +// global $config; +// +// if (!empty($input_type)) { +// $input_type = self::determine_ext($input_type); +// } +// +// try { +// $image = new Imagick($input_type . ":" . $input_path); +// try { +// $result = $image->flattenImages(); +// if ($result !== true) { +// throw new GraphicsException("Could not flatten image $input_path"); +// } +// +// $height = $image->getImageHeight(); +// $width = $image->getImageWidth(); +// if (!$allow_upscale && +// ($new_width > $width || $new_height > $height)) { +// $new_height = $height; +// $new_width = $width; +// } +// +// $result = $image->resizeImage($new_width, $new_width, Imagick::FILTER_LANCZOS, 0, !$ignore_aspect_ratio); +// if ($result !== true) { +// throw new GraphicsException("Could not perform image resize on $input_path"); +// } +// +// +// if (empty($output_type)) { +// $output_type = $input_type; +// } +// +// self::image_save_imagick($image, $output_filename, $output_type, $output_quality); +// +// } finally { +// $image->destroy(); +// } +// } catch (ImagickException $e) { +// throw new GraphicsException("Error while resizing with Imagick: " . $e->getMessage(), $e->getCode(), $e); +// } +// } + + public static function is_lossless(string $filename, string $format) + { + if (in_array($format, self::LOSSLESS_FORMATS)) { + return true; + } + switch ($format) { + case "webp": + return self::is_lossless_webp($filename); + break; + } + return false; + } + + public static function image_resize_convert( + String $input_path, + String $input_type, + int $new_width, + int $new_height, + string $output_filename, + string $output_type = null, + bool $ignore_aspect_ratio = false, + int $output_quality = 80, + bool $minimize = false, + bool $allow_upscale = true + ): void { + global $config; + + $convert = $config->get_string(MediaConfig::CONVERT_PATH); + + if (empty($convert)) { + throw new MediaException("convert command not configured"); + } + + if (empty($output_type)) { + $output_type = $input_type; + } + + if ($output_type=="webp" && self::is_lossless($input_path, $input_type)) { + $output_type = self::WEBP_LOSSLESS; + } + + $bg = "black"; + if (self::supports_alpha($output_type)) { + $bg = "none"; + } + if (!empty($input_type)) { + $input_type = $input_type . ":"; + } + + + $resize_args = ""; + if (!$allow_upscale) { + $resize_args .= "\>"; + } + if ($ignore_aspect_ratio) { + $resize_args .= "\!"; + } + + $args = ""; + switch ($output_type) { + case Media::WEBP_LOSSLESS: + $args .= '-define webp:lossless=true'; + break; + case "png": + $args .= '-define png:compression-level=9'; + break; + } + + if ($minimize) { + $args .= " -strip -thumbnail"; + } else { + $args .= " -resize"; + } + + + $output_ext = self::determine_ext($output_type); + + $format = '"%s" %s %ux%u%s -quality %u -background %s %s"%s[0]" %s:"%s" 2>&1'; + $cmd = sprintf($format, $convert, $args, $new_width, $new_height, $resize_args, $output_quality, $bg, $input_type, $input_path, $output_ext, $output_filename); + $cmd = str_replace("\"convert\"", "convert", $cmd); // quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27 + exec($cmd, $output, $ret); + if ($ret != 0) { + throw new MediaException("Resizing image with command `$cmd`, returns $ret, outputting " . implode("\r\n", $output)); + } else { + log_debug('Media', "Generating thumbnail with command `$cmd`, returns $ret"); + } + } + + /** + * Performs a resize operation on an image file using GD. + * + * @param String $image_filename The source file to be resized. + * @param array $info The output of getimagesize() for the source file. + * @param int $new_width + * @param int $new_height + * @param string $output_filename + * @param string|null $output_type If set to null, the output file type will be automatically determined via the $info parameter. Otherwise an exception will be thrown. + * @param int $output_quality Defaults to 80. + * @throws MediaException + * @throws InsufficientMemoryException if the estimated memory usage exceeds the memory limit. + */ + public static function image_resize_gd( + String $image_filename, + array $info, + int $new_width, + int $new_height, + string $output_filename, + string $output_type = null, + bool $ignore_aspect_ratio = false, + int $output_quality = 80, + bool $allow_upscale = true + ) { + $width = $info[0]; + $height = $info[1]; + + if ($output_type == null) { + /* If not specified, output to the same format as the original image */ + switch ($info[2]) { + case IMAGETYPE_GIF: + $output_type = "gif"; + break; + case IMAGETYPE_JPEG: + $output_type = "jpeg"; + break; + case IMAGETYPE_PNG: + $output_type = "png"; + break; + case IMAGETYPE_WEBP: + $output_type = "webp"; + break; + case IMAGETYPE_BMP: + $output_type = "bmp"; + break; + default: + throw new MediaException("Failed to save the new image - Unsupported image type."); + } + } + + $memory_use = self::calc_memory_use($info); + $memory_limit = get_memory_limit(); + if ($memory_use > $memory_limit) { + throw new InsufficientMemoryException("The image is too large to resize given the memory limits. ($memory_use > $memory_limit)"); + } + + if (!$ignore_aspect_ratio) { + list($new_width, $new_height) = get_scaled_by_aspect_ratio($width, $height, $new_width, $new_height); + } + if (!$allow_upscale && + ($new_width > $width || $new_height > $height)) { + $new_height = $height; + $new_width = $width; + } + + $image = imagecreatefromstring(file_get_contents($image_filename)); + $image_resized = imagecreatetruecolor($new_width, $new_height); + try { + if ($image === false) { + throw new MediaException("Could not load image: " . $image_filename); + } + if ($image_resized === false) { + throw new MediaException("Could not create output image with dimensions $new_width c $new_height "); + } + + // Handle transparent images + switch ($info[2]) { + case IMAGETYPE_GIF: + $transparency = imagecolortransparent($image); + $pallet_size = imagecolorstotal($image); + + // If we have a specific transparent color + if ($transparency >= 0 && $transparency < $pallet_size) { + // Get the original image's transparent color's RGB values + $transparent_color = imagecolorsforindex($image, $transparency); + + // Allocate the same color in the new image resource + $transparency = imagecolorallocate($image_resized, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); + if ($transparency === false) { + throw new MediaException("Unable to allocate transparent color"); + } + + // Completely fill the background of the new image with allocated color. + if (imagefill($image_resized, 0, 0, $transparency) === false) { + throw new MediaException("Unable to fill new image with transparent color"); + } + + // Set the background color for new image to transparent + imagecolortransparent($image_resized, $transparency); + } + break; + case IMAGETYPE_PNG: + case IMAGETYPE_WEBP: + // + // More info here: http://stackoverflow.com/questions/279236/how-do-i-resize-pngs-with-transparency-in-php + // + if (imagealphablending($image_resized, false) === false) { + throw new MediaException("Unable to disable image alpha blending"); + } + if (imagesavealpha($image_resized, true) === false) { + throw new MediaException("Unable to enable image save alpha"); + } + $transparent_color = imagecolorallocatealpha($image_resized, 255, 255, 255, 127); + if ($transparent_color === false) { + throw new MediaException("Unable to allocate transparent color"); + } + if (imagefilledrectangle($image_resized, 0, 0, $new_width, $new_height, $transparent_color) === false) { + throw new MediaException("Unable to fill new image with transparent color"); + } + break; + } + + // Actually resize the image. + if (imagecopyresampled( + $image_resized, + $image, + 0, + 0, + 0, + 0, + $new_width, + $new_height, + $width, + $height + ) === false) { + throw new MediaException("Unable to copy resized image data to new image"); + } + + switch ($output_type) { + case "bmp": + $result = imagebmp($image_resized, $output_filename, true); + break; + case "webp": + case Media::WEBP_LOSSY: + $result = imagewebp($image_resized, $output_filename, $output_quality); + break; + case "jpg": + case "jpeg": + $result = imagejpeg($image_resized, $output_filename, $output_quality); + break; + case "png": + $result = imagepng($image_resized, $output_filename, 9); + break; + case "gif": + $result = imagegif($image_resized, $output_filename); + break; + default: + throw new MediaException("Failed to save the new image - Unsupported image type: $output_type"); + } + if ($result === false) { + throw new MediaException("Failed to save the new image, function returned false when saving type: $output_type"); + } + } finally { + @imagedestroy($image); + @imagedestroy($image_resized); + } + } + + /** + * Determines if a file is an animated gif. + * + * @param String $image_filename The path of the file to check. + * @return bool true if the file is an animated gif, false if it is not. + */ + public static function is_animated_gif(String $image_filename): bool + { + $is_anim_gif = 0; + if (($fh = @fopen($image_filename, 'rb'))) { + try { + //check if gif is animated (via http://www.php.net/manual/en/function.imagecreatefromgif.php#104473) + while (!feof($fh) && $is_anim_gif < 2) { + $chunk = fread($fh, 1024 * 100); + $is_anim_gif += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches); + } + } finally { + @fclose($fh); + } + } + return ($is_anim_gif == 0); + } + + + private static function compare_file_bytes(String $file_name, array $comparison): bool + { + $size = filesize($file_name); + if ($size < count($comparison)) { + // Can't match because it's too small + return false; + } + + if (($fh = @fopen($file_name, 'rb'))) { + try { + $chunk = unpack("C*", fread($fh, count($comparison))); + + for ($i = 0; $i < count($comparison); $i++) { + $byte = $comparison[$i]; + if ($byte == null) { + continue; + } else { + $fileByte = $chunk[$i + 1]; + if ($fileByte != $byte) { + return false; + } + } + } + return true; + } finally { + @fclose($fh); + } + } else { + throw new MediaException("Unable to open file for byte check: $file_name"); + } + } + + public static function is_animated_webp(String $image_filename): bool + { + return self::compare_file_bytes($image_filename, self::WEBP_ANIMATION_HEADER); + } + + public static function is_lossless_webp(String $image_filename): bool + { + return self::compare_file_bytes($image_filename, self::WEBP_LOSSLESS_HEADER); + } + + public static function supports_alpha(string $format) + { + return in_array(self::normalize_format($format), self::ALPHA_FORMATS); + } + + public static function is_input_supported(string $engine, string $format, ?bool $lossless = null): bool + { + $format = self::normalize_format($format, $lossless); + if (!in_array($format, MediaEngine::INPUT_SUPPORT[$engine])) { + return false; + } + return true; + } + + public static function is_output_supported(string $engine, string $format, ?bool $lossless = false): bool + { + $format = self::normalize_format($format, $lossless); + if (!in_array($format, MediaEngine::OUTPUT_SUPPORT[$engine])) { + return false; + } + return true; + } + + /** + * Checks if a format (normally a file extension) is a variant name of another format (ie, jpg and jpeg). + * If one is found, then the maine name that the Media extension will recognize is returned, + * otherwise the incoming format is returned. + * + * @param $format + * @return string|null The format name that the media extension will recognize. + */ + public static function normalize_format(string $format, ?bool $lossless = null): ?string + { + if ($format == "webp") { + if ($lossless === true) { + $format = Media::WEBP_LOSSLESS; + } else { + $format = Media::WEBP_LOSSY; + } + } + + if (array_key_exists($format, Media::FORMAT_ALIASES)) { + return self::FORMAT_ALIASES[$format]; + } + return $format; + } + + + /** + * Determines the dimensions of a video file using ffmpeg. + * + * @param string $filename + * @return array [width, height] + */ + public static function video_size(string $filename): array + { + global $config; + $ffmpeg = $config->get_string(MediaConfig::FFMPEG_PATH); + $cmd = escapeshellcmd(implode(" ", [ + escapeshellarg($ffmpeg), + "-y", "-i", escapeshellarg($filename), + "-vstats" + ])); + $output = shell_exec($cmd . " 2>&1"); + // error_log("Getting size with `$cmd`"); + + $regex_sizes = "/Video: .* ([0-9]{1,4})x([0-9]{1,4})/"; + if (preg_match($regex_sizes, $output, $regs)) { + if (preg_match("/displaymatrix: rotation of (90|270).00 degrees/", $output)) { + $size = [$regs[2], $regs[1]]; + } else { + $size = [$regs[1], $regs[2]]; + } + } else { + $size = [1, 1]; + } + log_debug('Media', "Getting video size with `$cmd`, returns $output -- $size[0], $size[1]"); + return $size; + } +} diff --git a/ext/media/theme.php b/ext/media/theme.php new file mode 100644 index 00000000..15c3e03e --- /dev/null +++ b/ext/media/theme.php @@ -0,0 +1,46 @@ +"; + $html .= "Image Type"; + $html .= ""; + $html .= "\n"; + $page->add_block(new Block("Media Tools", $html)); + } + + public function get_buttons_html(int $image_id): string + { + return " + ".make_form(make_link("media_rescan/"))." + + + + "; + } + + public function get_help_html() + { + return '

    Search for items based on the type of media.

    +
    +
    content:audio
    +

    Returns items that contain audio, including videos and audio files.

    +
    +
    +
    content:video
    +

    Returns items that contain video, including animated GIFs.

    +
    +

    These search terms depend on the items being scanned for media content. Automatic scanning was implemented in mid-2019, so items uploaded before, or items uploaded on a system without ffmpeg, will require additional scanning before this will work.

    + '; + } +} diff --git a/ext/not_a_tag/info.php b/ext/not_a_tag/info.php new file mode 100644 index 00000000..ddd97f82 --- /dev/null +++ b/ext/not_a_tag/info.php @@ -0,0 +1,20 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: Redirect users to the rules if they use bad tags + */ +class NotATagInfo extends ExtensionInfo +{ + public const KEY = "not_a_tag"; + + public $key = self::KEY; + public $name = "Not A Tag"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Redirect users to the rules if they use bad tags"; +} diff --git a/ext/not_a_tag/main.php b/ext/not_a_tag/main.php index 29f4f502..cb59717c 100644 --- a/ext/not_a_tag/main.php +++ b/ext/not_a_tag/main.php @@ -1,121 +1,132 @@ - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Redirect users to the rules if they use bad tags - */ -class NotATag extends Extension { - public function get_priority() {return 30;} // before ImageUploadEvent and tag_history - public function onInitExt(InitExtEvent $event) { - global $config, $database; - if($config->get_int("ext_notatag_version") < 1) { - $database->create_table("untags", " +class NotATag extends Extension +{ + public function get_priority(): int + { + return 30; + } // before ImageUploadEvent and tag_history + + public function onInitExt(InitExtEvent $event) + { + global $config, $database; + if ($config->get_int("ext_notatag_version") < 1) { + $database->create_table("untags", " tag VARCHAR(128) NOT NULL PRIMARY KEY, redirect VARCHAR(255) NOT NULL "); - $config->set_int("ext_notatag_version", 1); - } - } + $config->set_int("ext_notatag_version", 1); + } + } - public function onImageAddition(ImageAdditionEvent $event) { - $this->scan($event->image->get_tag_array()); - } + public function onImageAddition(ImageAdditionEvent $event) + { + $this->scan($event->image->get_tag_array()); + } - public function onTagSet(TagSetEvent $event) { - $this->scan($event->tags); - } + public function onTagSet(TagSetEvent $event) + { + $this->scan($event->tags); + } - /** - * @param string[] $tags_mixed - */ - private function scan($tags_mixed) { - global $database; + /** + * #param string[] $tags_mixed + */ + private function scan(array $tags_mixed) + { + global $database; - $tags = array(); - foreach($tags_mixed as $tag) $tags[] = strtolower($tag); + $tags = []; + foreach ($tags_mixed as $tag) { + $tags[] = strtolower($tag); + } - $pairs = $database->get_all("SELECT * FROM untags"); - foreach($pairs as $tag_url) { - $tag = strtolower($tag_url[0]); - $url = $tag_url[1]; - if(in_array($tag, $tags)) { - header("Location: $url"); - exit; # FIXME: need a better way of aborting the tag-set or upload - } - } - } + $pairs = $database->get_all("SELECT * FROM untags"); + foreach ($pairs as $tag_url) { + $tag = strtolower($tag_url[0]); + $url = $tag_url[1]; + if (in_array($tag, $tags)) { + header("Location: $url"); + exit; # FIXME: need a better way of aborting the tag-set or upload + } + } + } - public function onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("ban_image")) { - $event->add_link("UnTags", make_link("untag/list/1")); - } - } + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="tags") { + if ($user->can(Permissions::BAN_IMAGE)) { + $event->add_nav_link("untags", new Link('untag/list/1'), "UnTags"); + } + } + } - public function onPageRequest(PageRequestEvent $event) { - global $database, $page, $user; + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::BAN_IMAGE)) { + $event->add_link("UnTags", make_link("untag/list/1")); + } + } - if($event->page_matches("untag")) { - if($user->can("ban_image")) { - if($event->get_arg(0) == "add") { - $tag = $_POST["tag"]; - $redirect = isset($_POST['redirect']) ? $_POST['redirect'] : "DNP"; + public function onPageRequest(PageRequestEvent $event) + { + global $database, $page, $user; - $database->Execute( - "INSERT INTO untags(tag, redirect) VALUES (?, ?)", - array($tag, $redirect)); + if ($event->page_matches("untag")) { + if ($user->can(Permissions::BAN_IMAGE)) { + if ($event->get_arg(0) == "add") { + $tag = $_POST["tag"]; + $redirect = isset($_POST['redirect']) ? $_POST['redirect'] : "DNP"; - $page->set_mode("redirect"); - $page->set_redirect($_SERVER['HTTP_REFERER']); - } - else if($event->get_arg(0) == "remove") { - if(isset($_POST['tag'])) { - $database->Execute("DELETE FROM untags WHERE tag = ?", array($_POST['tag'])); + $database->Execute( + "INSERT INTO untags(tag, redirect) VALUES (?, ?)", + [$tag, $redirect] + ); - flash_message("Image ban removed"); - $page->set_mode("redirect"); - $page->set_redirect($_SERVER['HTTP_REFERER']); - } - } - else if($event->get_arg(0) == "list") { - $page_num = 0; - if($event->count_args() == 2) { - $page_num = int_escape($event->get_arg(1)); - } - $page_size = 100; - $page_count = ceil($database->get_one("SELECT COUNT(tag) FROM untags")/$page_size); - $this->theme->display_untags($page, $page_num, $page_count, $this->get_untags($page_num, $page_size)); - } - } - } - } + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect($_SERVER['HTTP_REFERER']); + } elseif ($event->get_arg(0) == "remove") { + if (isset($_POST['tag'])) { + $database->Execute("DELETE FROM untags WHERE tag = ?", [$_POST['tag']]); - /** - * @param int $page - * @param int $size - * @return array - */ - public function get_untags($page, $size=100) { - global $database; + flash_message("Image ban removed"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect($_SERVER['HTTP_REFERER']); + } + } elseif ($event->get_arg(0) == "list") { + $page_num = 0; + if ($event->count_args() == 2) { + $page_num = int_escape($event->get_arg(1)); + } + $page_size = 100; + $page_count = ceil($database->get_one("SELECT COUNT(tag) FROM untags")/$page_size); + $this->theme->display_untags($page, $page_num, $page_count, $this->get_untags($page_num, $page_size)); + } + } + } + } - // FIXME: many - $size_i = int_escape($size); - $offset_i = int_escape($page-1)*$size_i; - $where = array("(1=1)"); - $args = array(); - if(!empty($_GET['tag'])) { - $where[] = 'tag SCORE_ILIKE ?'; - $args[] = "%".$_GET['tag']."%"; - } - if(!empty($_GET['redirect'])) { - $where[] = 'redirect SCORE_ILIKE ?'; - $args[] = "%".$_GET['redirect']."%"; - } - $where = implode(" AND ", $where); - $bans = $database->get_all($database->scoreql_to_sql(" + public function get_untags(int $page, int $size=100): array + { + global $database; + + // FIXME: many + $size_i = int_escape($size); + $offset_i = int_escape($page-1)*$size_i; + $where = ["(1=1)"]; + $args = []; + if (!empty($_GET['tag'])) { + $where[] = 'tag SCORE_ILIKE ?'; + $args[] = "%".$_GET['tag']."%"; + } + if (!empty($_GET['redirect'])) { + $where[] = 'redirect SCORE_ILIKE ?'; + $args[] = "%".$_GET['redirect']."%"; + } + $where = implode(" AND ", $where); + $bans = $database->get_all($database->scoreql_to_sql(" SELECT * FROM untags WHERE $where @@ -123,8 +134,10 @@ class NotATag extends Extension { LIMIT $size_i OFFSET $offset_i "), $args); - if($bans) {return $bans;} - else {return array();} - } + if ($bans) { + return $bans; + } else { + return []; + } + } } - diff --git a/ext/not_a_tag/theme.php b/ext/not_a_tag/theme.php index d3730456..535a1b36 100644 --- a/ext/not_a_tag/theme.php +++ b/ext/not_a_tag/theme.php @@ -1,9 +1,11 @@ ".make_form(make_link("untag/remove"))." {$ban['tag']} @@ -15,8 +17,8 @@ class NotATagTheme extends Themelet { "; - } - $html = " + } + $html = " @@ -39,20 +41,19 @@ class NotATagTheme extends Themelet {
    TagRedirectAction
    "; - $prev = $page_number - 1; - $next = $page_number + 1; + $prev = $page_number - 1; + $next = $page_number + 1; - $h_prev = ($page_number <= 1) ? "Prev" : "Prev"; - $h_index = "Index"; - $h_next = ($page_number >= $page_count) ? "Next" : "Next"; + $h_prev = ($page_number <= 1) ? "Prev" : "Prev"; + $h_index = "Index"; + $h_next = ($page_number >= $page_count) ? "Next" : "Next"; - $nav = "$h_prev | $h_index | $h_next"; + $nav = "$h_prev | $h_index | $h_next"; - $page->set_title("UnTags"); - $page->set_heading("UnTags"); - $page->add_block(new Block("Edit UnTags", $html)); - $page->add_block(new Block("Navigation", $nav, "left", 0)); - $this->display_paginator($page, "untag/list", null, $page_number, $page_count); - } + $page->set_title("UnTags"); + $page->set_heading("UnTags"); + $page->add_block(new Block("Edit UnTags", $html)); + $page->add_block(new Block("Navigation", $nav, "left", 0)); + $this->display_paginator($page, "untag/list", null, $page_number, $page_count); + } } - diff --git a/ext/notes/info.php b/ext/notes/info.php new file mode 100644 index 00000000..9296b7b8 --- /dev/null +++ b/ext/notes/info.php @@ -0,0 +1,20 @@ + + * License: GPLv2 + * Description: Annotate images + * Documentation: + */ + +class NotesInfo extends ExtensionInfo +{ + public const KEY = "notes"; + + public $key = self::KEY; + public $name = "Notes"; + public $authors = ["Sein Kraft"=>"mail@seinkraft.info"]; + public $license = self::LICENSE_GPLV2; + public $description = "Annotate images"; +} diff --git a/ext/notes/main.php b/ext/notes/main.php index b1706c03..9f4f9e72 100644 --- a/ext/notes/main.php +++ b/ext/notes/main.php @@ -1,20 +1,15 @@ - * License: GPLv2 - * Description: Annotate images - * Documentation: - */ -class Notes extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config, $database; +class Notes extends Extension +{ + public function onInitExt(InitExtEvent $event) + { + global $config, $database; - // shortcut to latest - if ($config->get_int("ext_notes_version") < 1) { - $database->Execute("ALTER TABLE images ADD COLUMN notes INTEGER NOT NULL DEFAULT 0"); - $database->create_table("notes", " + // shortcut to latest + if ($config->get_int("ext_notes_version") < 1) { + $database->Execute("ALTER TABLE images ADD COLUMN notes INTEGER NOT NULL DEFAULT 0"); + $database->create_table("notes", " id SCORE_AIPK, enable INTEGER NOT NULL, image_id INTEGER NOT NULL, @@ -29,9 +24,9 @@ class Notes extends Extension { FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE "); - $database->execute("CREATE INDEX notes_image_id_idx ON notes(image_id)", array()); + $database->execute("CREATE INDEX notes_image_id_idx ON notes(image_id)", []); - $database->create_table("note_request", " + $database->create_table("note_request", " id SCORE_AIPK, image_id INTEGER NOT NULL, user_id INTEGER NOT NULL, @@ -39,9 +34,9 @@ class Notes extends Extension { FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE "); - $database->execute("CREATE INDEX note_request_image_id_idx ON note_request(image_id)", array()); + $database->execute("CREATE INDEX note_request_image_id_idx ON note_request(image_id)", []); - $database->create_table("note_histories", " + $database->create_table("note_histories", " id SCORE_AIPK, note_enable INTEGER NOT NULL, note_id INTEGER NOT NULL, @@ -58,478 +53,515 @@ class Notes extends Extension { FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE "); - $database->execute("CREATE INDEX note_histories_image_id_idx ON note_histories(image_id)", array()); + $database->execute("CREATE INDEX note_histories_image_id_idx ON note_histories(image_id)", []); - $config->set_int("notesNotesPerPage", 20); - $config->set_int("notesRequestsPerPage", 20); - $config->set_int("notesHistoriesPerPage", 20); + $config->set_int("notesNotesPerPage", 20); + $config->set_int("notesRequestsPerPage", 20); + $config->set_int("notesHistoriesPerPage", 20); - $config->set_int("ext_notes_version", 1); - log_info("notes", "extension installed"); - } - } + $config->set_int("ext_notes_version", 1); + log_info("notes", "extension installed"); + } + } - public function onPageRequest(PageRequestEvent $event) { - global $page, $user; - if($event->page_matches("note")) { - switch($event->get_arg(0)) { - case "list": //index - $this->get_notes_list($event); // This should show images like post/list but i don't know how do that. - break; - case "requests": // The same as post/list but only for note_request table. - $this->get_notes_requests($event); // This should show images like post/list but i don't know how do that. - break; - case "search": - if(!$user->is_anonymous()) - $this->theme->search_notes_page($page); - break; - case "updated": //Thinking how to build this function. - $this->get_histories($event); - break; - case "history": //Thinking how to build this function. - $this->get_history($event); - break; - case "revert": - $noteID = $event->get_arg(1); - $reviewID = $event->get_arg(2); - if(!$user->is_anonymous()){ - $this->revert_history($noteID, $reviewID); - } + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; + if ($event->page_matches("note")) { + switch ($event->get_arg(0)) { + case "list": //index + $this->get_notes_list($event); // This should show images like post/list but i don't know how do that. + break; + case "requests": // The same as post/list but only for note_request table. + $this->get_notes_requests($event); // This should show images like post/list but i don't know how do that. + break; + case "search": + if (!$user->is_anonymous()) { + $this->theme->search_notes_page($page); + } + break; + case "updated": //Thinking how to build this function. + $this->get_histories($event); + break; + case "history": //Thinking how to build this function. + $this->get_history($event); + break; + case "revert": + $noteID = $event->get_arg(1); + $reviewID = $event->get_arg(2); + if (!$user->is_anonymous()) { + $this->revert_history($noteID, $reviewID); + } - $page->set_mode("redirect"); - $page->set_redirect(make_link("note/updated")); - break; - case "add_note": - if(!$user->is_anonymous()) - $this->add_new_note(); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("note/updated")); + break; + case "add_note": + if (!$user->is_anonymous()) { + $this->add_new_note(); + } - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/".$_POST["image_id"])); - break; - case "add_request": - if(!$user->is_anonymous()) - $this->add_note_request(); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$_POST["image_id"])); + break; + case "add_request": + if (!$user->is_anonymous()) { + $this->add_note_request(); + } - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/".$_POST["image_id"])); - break; - case "nuke_notes": - if($user->is_admin()) - $this->nuke_notes(); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$_POST["image_id"])); + break; + case "nuke_notes": + if ($user->can(Permissions::NOTES_ADMIN)) { + $this->nuke_notes(); + } - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/".$_POST["image_id"])); - break; - case "nuke_requests": - if($user->is_admin()) - $this->nuke_requests(); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$_POST["image_id"])); + break; + case "nuke_requests": + if ($user->can(Permissions::NOTES_ADMIN)) { + $this->nuke_requests(); + } - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/".$_POST["image_id"])); - break; - case "edit_note": - if (!$user->is_anonymous()) { - $this->update_note(); - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/" . $_POST["image_id"])); - } - break; - case "delete_note": - if ($user->is_admin()) { - $this->delete_note(); - $page->set_mode("redirect"); - $page->set_redirect(make_link("post/view/".$_POST["image_id"])); - } - break; - default: - $page->set_mode("redirect"); - $page->set_redirect(make_link("note/list")); - break; - } - } - } + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$_POST["image_id"])); + break; + case "edit_note": + if (!$user->is_anonymous()) { + $this->update_note(); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/" . $_POST["image_id"])); + } + break; + case "delete_note": + if ($user->can(Permissions::NOTES_ADMIN)) { + $this->delete_note(); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$_POST["image_id"])); + } + break; + default: + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("note/list")); + break; + } + } + } - /* - * HERE WE LOAD THE NOTES IN THE IMAGE - */ - public function onDisplayingImage(DisplayingImageEvent $event) { - global $page, $user; + /* + * HERE WE LOAD THE NOTES IN THE IMAGE + */ + public function onDisplayingImage(DisplayingImageEvent $event) + { + global $page, $user; - //display form on image event - $notes = $this->get_notes($event->image->id); - $this->theme->display_note_system($page, $event->image->id, $notes, $user->is_admin()); - } + //display form on image event + $notes = $this->get_notes($event->image->id); + $this->theme->display_note_system($page, $event->image->id, $notes, $user->can(Permissions::NOTES_ADMIN)); + } - /* - * HERE WE ADD THE BUTTONS ON SIDEBAR - */ - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { - global $user; - if(!$user->is_anonymous()) { - $event->add_part($this->theme->note_button($event->image->id)); - $event->add_part($this->theme->request_button($event->image->id)); - if($user->is_admin()) { - $event->add_part($this->theme->nuke_notes_button($event->image->id)); - $event->add_part($this->theme->nuke_requests_button($event->image->id)); - } - } - } + /* + * HERE WE ADD THE BUTTONS ON SIDEBAR + */ + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) + { + global $user; + if (!$user->is_anonymous()) { + $event->add_part($this->theme->note_button($event->image->id)); + $event->add_part($this->theme->request_button($event->image->id)); + if ($user->can(Permissions::NOTES_ADMIN)) { + $event->add_part($this->theme->nuke_notes_button($event->image->id)); + $event->add_part($this->theme->nuke_requests_button($event->image->id)); + } + } + } - /* - * HERE WE ADD QUERYLETS TO ADD SEARCH SYSTEM - */ - public function onSearchTermParse(SearchTermParseEvent $event) { - $matches = array(); - if(preg_match("/^note[=|:](.*)$/i", $event->term, $matches)) { - $notes = int_escape($matches[1]); - $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM notes WHERE note = $notes)")); - } - else if(preg_match("/^notes([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)%/i", $event->term, $matches)) { - $cmp = ltrim($matches[1], ":") ?: "="; - $notes = $matches[2]; - $event->add_querylet(new Querylet("images.id IN (SELECT id FROM images WHERE notes $cmp $notes)")); - } - else if(preg_match("/^notes_by[=|:](.*)$/i", $event->term, $matches)) { - $user = User::by_name($matches[1]); - if(!is_null($user)) { - $user_id = $user->id; - } else { - $user_id = -1; - } + /* + * HERE WE ADD QUERYLETS TO ADD SEARCH SYSTEM + */ + public function onSearchTermParse(SearchTermParseEvent $event) + { + $matches = []; + if (preg_match("/^note[=|:](.*)$/i", $event->term, $matches)) { + $notes = int_escape($matches[1]); + $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM notes WHERE note = $notes)")); + } elseif (preg_match("/^notes([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)%/i", $event->term, $matches)) { + $cmp = ltrim($matches[1], ":") ?: "="; + $notes = $matches[2]; + $event->add_querylet(new Querylet("images.id IN (SELECT id FROM images WHERE notes $cmp $notes)")); + } elseif (preg_match("/^notes_by[=|:](.*)$/i", $event->term, $matches)) { + $user = User::by_name($matches[1]); + if (!is_null($user)) { + $user_id = $user->id; + } else { + $user_id = -1; + } - $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM notes WHERE user_id = $user_id)")); - } - else if(preg_match("/^notes_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 notes WHERE user_id = $user_id)")); - } - } + $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM notes WHERE user_id = $user_id)")); + } elseif (preg_match("/^(notes_by_userno|notes_by_user_id)[=|:](\d+)$/i", $event->term, $matches)) { + $user_id = int_escape($matches[2]); + $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM notes WHERE user_id = $user_id)")); + } + } + + public function onHelpPageBuilding(HelpPageBuildingEvent $event) + { + if ($event->key===HelpPages::SEARCH) { + $block = new Block(); + $block->header = "Notes"; + $block->body = $this->theme->get_help_html(); + $event->add_block($block); + } + } - /** - * HERE WE GET ALL NOTES FOR DISPLAYED IMAGE. - * - * @param int $imageID - * @return array - */ - private function get_notes($imageID) { - global $database; + /** + * HERE WE GET ALL NOTES FOR DISPLAYED IMAGE. + */ + private function get_notes(int $imageID): array + { + global $database; - return $database->get_all( - "SELECT * ". - "FROM notes ". - "WHERE enable = ? AND image_id = ? ". - "ORDER BY date ASC", - array('1', $imageID)); - } + return $database->get_all( + "SELECT * ". + "FROM notes ". + "WHERE enable = ? AND image_id = ? ". + "ORDER BY date ASC", + ['1', $imageID] + ); + } - /* - * HERE WE ADD A NOTE TO DATABASE - */ - private function add_new_note() { - global $database, $user; + /* + * HERE WE ADD A NOTE TO DATABASE + */ + private function add_new_note() + { + global $database, $user; - $imageID = int_escape($_POST["image_id"]); - $user_id = $user->id; - $noteX1 = int_escape($_POST["note_x1"]); - $noteY1 = int_escape($_POST["note_y1"]); - $noteHeight = int_escape($_POST["note_height"]); - $noteWidth = int_escape($_POST["note_width"]); - $noteText = html_escape($_POST["note_text"]); + $imageID = int_escape($_POST["image_id"]); + $user_id = $user->id; + $noteX1 = int_escape($_POST["note_x1"]); + $noteY1 = int_escape($_POST["note_y1"]); + $noteHeight = int_escape($_POST["note_height"]); + $noteWidth = int_escape($_POST["note_width"]); + $noteText = html_escape($_POST["note_text"]); - $database->execute(" + $database->execute( + " INSERT INTO notes (enable, image_id, user_id, user_ip, date, x1, y1, height, width, note) VALUES (?, ?, ?, ?, now(), ?, ?, ?, ?, ?)", - array(1, $imageID, $user_id, $_SERVER['REMOTE_ADDR'], $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText)); + [1, $imageID, $user_id, $_SERVER['REMOTE_ADDR'], $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText] + ); - $noteID = $database->get_last_insert_id('notes_id_seq'); + $noteID = $database->get_last_insert_id('notes_id_seq'); - log_info("notes", "Note added {$noteID} by {$user->name}"); + log_info("notes", "Note added {$noteID} by {$user->name}"); - $database->execute("UPDATE images SET notes=(SELECT COUNT(*) FROM notes WHERE image_id=?) WHERE id=?", array($imageID, $imageID)); + $database->execute("UPDATE images SET notes=(SELECT COUNT(*) FROM notes WHERE image_id=?) WHERE id=?", [$imageID, $imageID]); - $this->add_history(1, $noteID, $imageID, $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText); - } + $this->add_history(1, $noteID, $imageID, $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText); + } - /* - * HERE WE ADD A REQUEST TO DATABASE - */ - private function add_note_request() { - global $database, $user; + /* + * HERE WE ADD A REQUEST TO DATABASE + */ + private function add_note_request() + { + global $database, $user; - $image_id = int_escape($_POST["image_id"]); - $user_id = $user->id; + $image_id = int_escape($_POST["image_id"]); + $user_id = $user->id; - $database->execute(" + $database->execute( + " INSERT INTO note_request (image_id, user_id, date) VALUES (?, ?, now())", - array($image_id, $user_id)); + [$image_id, $user_id] + ); - $resultID = $database->get_last_insert_id('note_request_id_seq'); + $resultID = $database->get_last_insert_id('note_request_id_seq'); - log_info("notes", "Note requested {$resultID} by {$user->name}"); - } + log_info("notes", "Note requested {$resultID} by {$user->name}"); + } - /* - * HERE WE EDIT THE NOTE - */ - private function update_note() { - global $database; + /* + * HERE WE EDIT THE NOTE + */ + private function update_note() + { + global $database; - $note = array( - "noteX1" => int_escape($_POST["note_x1"]), - "noteY1" => int_escape($_POST["note_y1"]), - "noteHeight" => int_escape($_POST["note_height"]), - "noteWidth" => int_escape($_POST["note_width"]), - "noteText" => sql_escape(html_escape($_POST["note_text"])), - "imageID" => int_escape($_POST["image_id"]), - "noteID" => int_escape($_POST["note_id"]) - ); + $note = [ + "noteX1" => int_escape($_POST["note_x1"]), + "noteY1" => int_escape($_POST["note_y1"]), + "noteHeight" => int_escape($_POST["note_height"]), + "noteWidth" => int_escape($_POST["note_width"]), + "noteText" => sql_escape(html_escape($_POST["note_text"])), + "imageID" => int_escape($_POST["image_id"]), + "noteID" => int_escape($_POST["note_id"]) + ]; - // validate parameters - if (array_search(NULL, $note)|| strlen($note['noteText']) == 0) { - return; - } + // validate parameters + if (array_search(null, $note)|| strlen($note['noteText']) == 0) { + return; + } - $database->execute("UPDATE notes ". - "SET x1 = ?, ". - "y1 = ?, ". - "height = ?, ". - "width = ?,". - "note = ? ". - "WHERE image_id = ? AND id = ?", array_values($note)); + $database->execute("UPDATE notes ". + "SET x1 = ?, ". + "y1 = ?, ". + "height = ?, ". + "width = ?,". + "note = ? ". + "WHERE image_id = ? AND id = ?", array_values($note)); - $this->add_history(1, $note['noteID'], $note['imageID'], $note['noteX1'], $note['noteY1'], $note['noteHeight'], $note['noteWidth'], $note['noteText']); - } + $this->add_history(1, $note['noteID'], $note['imageID'], $note['noteX1'], $note['noteY1'], $note['noteHeight'], $note['noteWidth'], $note['noteText']); + } - /* - * HERE WE DELETE THE NOTE - */ - private function delete_note() { - global $user, $database; + /* + * HERE WE DELETE THE NOTE + */ + private function delete_note() + { + global $user, $database; - $imageID = int_escape($_POST["image_id"]); - $noteID = int_escape($_POST["note_id"]); + $imageID = int_escape($_POST["image_id"]); + $noteID = int_escape($_POST["note_id"]); - // validate parameters - if(is_null($imageID) || !is_numeric($imageID) || is_null($noteID) || !is_numeric($noteID)) { - return; - } + // validate parameters + if (is_null($imageID) || !is_numeric($imageID) || is_null($noteID) || !is_numeric($noteID)) { + return; + } - $database->execute("UPDATE notes ". - "SET enable = ? ". - "WHERE image_id = ? AND id = ?", array(0, $imageID, $noteID)); + $database->execute("UPDATE notes ". + "SET enable = ? ". + "WHERE image_id = ? AND id = ?", [0, $imageID, $noteID]); - log_info("notes", "Note deleted {$noteID} by {$user->name}"); - } + log_info("notes", "Note deleted {$noteID} by {$user->name}"); + } - /* - * HERE WE DELETE ALL NOTES FROM IMAGE - */ - private function nuke_notes() { - global $database, $user; - $image_id = int_escape($_POST["image_id"]); - $database->execute("DELETE FROM notes WHERE image_id = ?", array($image_id)); - log_info("notes", "Notes deleted from {$image_id} by {$user->name}"); - } + /* + * HERE WE DELETE ALL NOTES FROM IMAGE + */ + private function nuke_notes() + { + global $database, $user; + $image_id = int_escape($_POST["image_id"]); + $database->execute("DELETE FROM notes WHERE image_id = ?", [$image_id]); + log_info("notes", "Notes deleted from {$image_id} by {$user->name}"); + } - /* - * HERE WE DELETE ALL REQUESTS FOR IMAGE - */ - private function nuke_requests() { - global $database, $user; - $image_id = int_escape($_POST["image_id"]); + /* + * HERE WE DELETE ALL REQUESTS FOR IMAGE + */ + private function nuke_requests() + { + global $database, $user; + $image_id = int_escape($_POST["image_id"]); - $database->execute("DELETE FROM note_request WHERE image_id = ?", array($image_id)); + $database->execute("DELETE FROM note_request WHERE image_id = ?", [$image_id]); - log_info("notes", "Requests deleted from {$image_id} by {$user->name}"); - } + log_info("notes", "Requests deleted from {$image_id} by {$user->name}"); + } - /** - * HERE WE ALL IMAGES THAT HAVE NOTES - * @param PageRequestEvent $event - */ - private function get_notes_list(PageRequestEvent $event) { - global $database, $config; + /** + * HERE WE ALL IMAGES THAT HAVE NOTES + */ + private function get_notes_list(PageRequestEvent $event) + { + global $database, $config; - $pageNumber = $event->get_arg(1); - if(is_null($pageNumber) || !is_numeric($pageNumber) || $pageNumber <= 0) { - $pageNumber = 0; - } else { - $pageNumber--; - } + $pageNumber = $event->get_arg(1); + if (is_null($pageNumber) || !is_numeric($pageNumber) || $pageNumber <= 0) { + $pageNumber = 0; + } else { + $pageNumber--; + } - $notesPerPage = $config->get_int('notesNotesPerPage'); + $notesPerPage = $config->get_int('notesNotesPerPage'); - //$result = $database->get_all("SELECT * FROM pool_images WHERE pool_id=?", array($poolID)); - $result = $database->execute("SELECT DISTINCT image_id". - "FROM notes ". - "WHERE enable = ? ". - "ORDER BY date DESC LIMIT ?, ?", - array(1, $pageNumber * $notesPerPage, $notesPerPage)); + //$result = $database->get_all("SELECT * FROM pool_images WHERE pool_id=?", array($poolID)); + $result = $database->execute( + "SELECT DISTINCT image_id". + "FROM notes ". + "WHERE enable = ? ". + "ORDER BY date DESC LIMIT ?, ?", + [1, $pageNumber * $notesPerPage, $notesPerPage] + ); - $totalPages = ceil($database->get_one("SELECT COUNT(DISTINCT image_id) FROM notes") / $notesPerPage); + $totalPages = ceil($database->get_one("SELECT COUNT(DISTINCT image_id) FROM notes") / $notesPerPage); - $images = array(); - while($row = $result->fetch()) { - $images[] = array(Image::by_id($row["image_id"])); - } + $images = []; + while ($row = $result->fetch()) { + $images[] = [Image::by_id($row["image_id"])]; + } - $this->theme->display_note_list($images, $pageNumber + 1, $totalPages); - } + $this->theme->display_note_list($images, $pageNumber + 1, $totalPages); + } - /** - * HERE WE GET ALL NOTE REQUESTS - * @param PageRequestEvent $event - */ - private function get_notes_requests(PageRequestEvent $event) { - global $config, $database; + /** + * HERE WE GET ALL NOTE REQUESTS + */ + private function get_notes_requests(PageRequestEvent $event) + { + global $config, $database; - $pageNumber = $event->get_arg(1); - if(is_null($pageNumber) || !is_numeric($pageNumber) || $pageNumber <= 0) { - $pageNumber = 0; - } else { - $pageNumber--; - } + $pageNumber = $event->get_arg(1); + if (is_null($pageNumber) || !is_numeric($pageNumber) || $pageNumber <= 0) { + $pageNumber = 0; + } else { + $pageNumber--; + } - $requestsPerPage = $config->get_int('notesRequestsPerPage'); + $requestsPerPage = $config->get_int('notesRequestsPerPage'); - //$result = $database->get_all("SELECT * FROM pool_images WHERE pool_id=?", array($poolID)); + //$result = $database->get_all("SELECT * FROM pool_images WHERE pool_id=?", array($poolID)); - $result = $database->execute(" + $result = $database->execute( + " SELECT DISTINCT image_id FROM note_request ORDER BY date DESC LIMIT ?, ?", - array($pageNumber * $requestsPerPage, $requestsPerPage)); + [$pageNumber * $requestsPerPage, $requestsPerPage] + ); - $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM note_request") / $requestsPerPage); + $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM note_request") / $requestsPerPage); - $images = array(); - while($row = $result->fetch()) { - $images[] = array(Image::by_id($row["image_id"])); - } + $images = []; + while ($row = $result->fetch()) { + $images[] = [Image::by_id($row["image_id"])]; + } - $this->theme->display_note_requests($images, $pageNumber + 1, $totalPages); - } + $this->theme->display_note_requests($images, $pageNumber + 1, $totalPages); + } - /* - * HERE WE ADD HISTORY TO TRACK THE CHANGES OF THE NOTES FOR THE IMAGES. - */ - private function add_history($noteEnable, $noteID, $imageID, $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText){ - global $user, $database; + /* + * HERE WE ADD HISTORY TO TRACK THE CHANGES OF THE NOTES FOR THE IMAGES. + */ + private function add_history($noteEnable, $noteID, $imageID, $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText) + { + global $user, $database; - $reviewID = $database->get_one("SELECT COUNT(*) FROM note_histories WHERE note_id = ?", array($noteID)); - $reviewID = $reviewID + 1; + $reviewID = $database->get_one("SELECT COUNT(*) FROM note_histories WHERE note_id = ?", [$noteID]); + $reviewID = $reviewID + 1; - $database->execute(" + $database->execute( + " INSERT INTO note_histories (note_enable, note_id, review_id, image_id, user_id, user_ip, date, x1, y1, height, width, note) VALUES (?, ?, ?, ?, ?, ?, now(), ?, ?, ?, ?, ?)", - array($noteEnable, $noteID, $reviewID, $imageID, $user->id, $_SERVER['REMOTE_ADDR'], $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText)); - } + [$noteEnable, $noteID, $reviewID, $imageID, $user->id, $_SERVER['REMOTE_ADDR'], $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText] + ); + } - /** - * HERE WE GET ALL HISTORIES. - * @param PageRequestEvent $event - */ - private function get_histories(PageRequestEvent $event){ - global $config, $database; + /** + * HERE WE GET ALL HISTORIES. + */ + private function get_histories(PageRequestEvent $event) + { + global $config, $database; - $pageNumber = $event->get_arg(1); - if (is_null($pageNumber) || !is_numeric($pageNumber) || $pageNumber <= 0) { - $pageNumber = 0; - } else { - $pageNumber--; - } + $pageNumber = $event->get_arg(1); + if (is_null($pageNumber) || !is_numeric($pageNumber) || $pageNumber <= 0) { + $pageNumber = 0; + } else { + $pageNumber--; + } - $historiesPerPage = $config->get_int('notesHistoriesPerPage'); + $historiesPerPage = $config->get_int('notesHistoriesPerPage'); - //ORDER BY IMAGE & DATE - $histories = $database->get_all("SELECT h.note_id, h.review_id, h.image_id, h.date, h.note, u.name AS user_name ". - "FROM note_histories AS h ". - "INNER JOIN users AS u ". - "ON u.id = h.user_id ". - "ORDER BY date DESC LIMIT ?, ?", - array($pageNumber * $historiesPerPage, $historiesPerPage)); + //ORDER BY IMAGE & DATE + $histories = $database->get_all( + "SELECT h.note_id, h.review_id, h.image_id, h.date, h.note, u.name AS user_name ". + "FROM note_histories AS h ". + "INNER JOIN users AS u ". + "ON u.id = h.user_id ". + "ORDER BY date DESC LIMIT ?, ?", + [$pageNumber * $historiesPerPage, $historiesPerPage] + ); - $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM note_histories") / $historiesPerPage); + $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM note_histories") / $historiesPerPage); - $this->theme->display_histories($histories, $pageNumber + 1, $totalPages); - } + $this->theme->display_histories($histories, $pageNumber + 1, $totalPages); + } - /** - * HERE WE THE HISTORY FOR A SPECIFIC NOTE. - * @param PageRequestEvent $event - */ - private function get_history(PageRequestEvent $event){ - global $config, $database; + /** + * HERE WE THE HISTORY FOR A SPECIFIC NOTE. + */ + private function get_history(PageRequestEvent $event) + { + global $config, $database; - $noteID = $event->get_arg(1); + $noteID = $event->get_arg(1); - $pageNumber = $event->get_arg(2); - if (is_null($pageNumber) || !is_numeric($pageNumber) || $pageNumber <= 0) { - $pageNumber = 0; - } else { - $pageNumber--; - } + $pageNumber = $event->get_arg(2); + if (is_null($pageNumber) || !is_numeric($pageNumber) || $pageNumber <= 0) { + $pageNumber = 0; + } else { + $pageNumber--; + } - $historiesPerPage = $config->get_int('notesHistoriesPerPage'); + $historiesPerPage = $config->get_int('notesHistoriesPerPage'); - $histories = $database->get_all("SELECT h.note_id, h.review_id, h.image_id, h.date, h.note, u.name AS user_name ". - "FROM note_histories AS h ". - "INNER JOIN users AS u ". - "ON u.id = h.user_id ". - "WHERE note_id = ? ". - "ORDER BY date DESC LIMIT ?, ?", - array($noteID, $pageNumber * $historiesPerPage, $historiesPerPage)); + $histories = $database->get_all( + "SELECT h.note_id, h.review_id, h.image_id, h.date, h.note, u.name AS user_name ". + "FROM note_histories AS h ". + "INNER JOIN users AS u ". + "ON u.id = h.user_id ". + "WHERE note_id = ? ". + "ORDER BY date DESC LIMIT ?, ?", + [$noteID, $pageNumber * $historiesPerPage, $historiesPerPage] + ); - $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM note_histories WHERE note_id = ?", array($noteID)) / $historiesPerPage); + $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM note_histories WHERE note_id = ?", [$noteID]) / $historiesPerPage); - $this->theme->display_history($histories, $pageNumber + 1, $totalPages); - } + $this->theme->display_history($histories, $pageNumber + 1, $totalPages); + } - /** - * HERE GO BACK IN HISTORY AND SET THE OLD NOTE. IF WAS REMOVED WE RE-ADD IT. - * @param int $noteID - * @param int $reviewID - */ - private function revert_history($noteID, $reviewID){ - global $database; + /** + * HERE GO BACK IN HISTORY AND SET THE OLD NOTE. IF WAS REMOVED WE RE-ADD IT. + */ + private function revert_history(int $noteID, int $reviewID) + { + global $database; - $history = $database->get_row("SELECT * FROM note_histories WHERE note_id = ? AND review_id = ?", array($noteID, $reviewID)); + $history = $database->get_row("SELECT * FROM note_histories WHERE note_id = ? AND review_id = ?", [$noteID, $reviewID]); - $noteEnable = $history['note_enable']; - $noteID = $history['note_id']; - $imageID = $history['image_id']; - $noteX1 = $history['x1']; - $noteY1 = $history['y1']; - $noteHeight = $history['height']; - $noteWidth = $history['width']; - $noteText = $history['note']; + $noteEnable = $history['note_enable']; + $noteID = $history['note_id']; + $imageID = $history['image_id']; + $noteX1 = $history['x1']; + $noteY1 = $history['y1']; + $noteHeight = $history['height']; + $noteWidth = $history['width']; + $noteText = $history['note']; - $database->execute("UPDATE notes ". - "SET enable = ?, x1 = ?, y1 = ?, height = ?, width = ?, note = ? ". - "WHERE image_id = ? AND id = ?", - array(1, $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText, $imageID, $noteID)); + $database->execute( + "UPDATE notes ". + "SET enable = ?, x1 = ?, y1 = ?, height = ?, width = ?, note = ? ". + "WHERE image_id = ? AND id = ?", + [1, $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText, $imageID, $noteID] + ); - $this->add_history($noteEnable, $noteID, $imageID, $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText); - } + $this->add_history($noteEnable, $noteID, $imageID, $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText); + } } diff --git a/ext/notes/theme.php b/ext/notes/theme.php index ec8d7f35..5d4d016b 100644 --- a/ext/notes/theme.php +++ b/ext/notes/theme.php @@ -1,74 +1,81 @@ Add a note -->
    '; - } - public function request_button($image_id) { - return make_form(make_link("note/add_request")) . ' + } + public function request_button($image_id) + { + return make_form(make_link("note/add_request")) . ' '; - } - public function nuke_notes_button($image_id) { - return make_form(make_link("note/nuke_notes")) . ' + } + public function nuke_notes_button($image_id) + { + return make_form(make_link("note/nuke_notes")) . ' '; - } - public function nuke_requests_button($image_id) { - return make_form(make_link("note/nuke_requests")) . ' + } + public function nuke_requests_button($image_id) + { + return make_form(make_link("note/nuke_requests")) . ' '; - } + } - public function search_notes_page(Page $page) { //IN DEVELOPMENT, NOT FULLY WORKING - $html = '
    + public function search_notes_page(Page $page) + { //IN DEVELOPMENT, NOT FULLY WORKING + $html = '
    '; - $page->set_title(html_escape("Search Note")); - $page->set_heading(html_escape("Search Note")); - $page->add_block(new Block("Search Note", $html, "main", 10)); - } + $page->set_title(html_escape("Search Note")); + $page->set_heading(html_escape("Search Note")); + $page->add_block(new Block("Search Note", $html, "main", 10)); + } - // check action POST on form - public function display_note_system(Page $page, $image_id, $recovered_notes, $adminOptions) { - $base_href = get_base_href(); + // check action POST on form + public function display_note_system(Page $page, $image_id, $recovered_notes, $adminOptions) + { + $base_href = get_base_href(); - $page->add_html_header(""); - $page->add_html_header(""); - $page->add_html_header(""); + $page->add_html_header(""); + $page->add_html_header(""); + $page->add_html_header(""); - $to_json = array(); - foreach($recovered_notes as $note) { - $parsedNote = $note["note"]; - $parsedNote = str_replace("\n", "\\n", $parsedNote); - $parsedNote = str_replace("\r", "\\r", $parsedNote); + $to_json = []; + foreach ($recovered_notes as $note) { + $parsedNote = $note["note"]; + $parsedNote = str_replace("\n", "\\n", $parsedNote); + $parsedNote = str_replace("\r", "\\r", $parsedNote); - $to_json[] = array( - 'x1' => $note["x1"], - 'y1' => $note["y1"], - 'height' => $note["height"], - 'width' => $note["width"], - 'note' => $parsedNote, - 'note_id' => $note["id"], - ); - } + $to_json[] = [ + 'x1' => $note["x1"], + 'y1' => $note["y1"], + 'height' => $note["height"], + 'width' => $note["width"], + 'note' => $parsedNote, + 'note_id' => $note["id"], + ]; + } - $html = ""; + $html = ""; - $html .= " + $html .= "
    ".make_form(make_link("note/add_note"))." @@ -112,8 +119,8 @@ class NotesTheme extends Themelet { "; - if($adminOptions) - $html .= " + if ($adminOptions) { + $html .= " ".make_form(make_link("note/delete_note"))." @@ -124,119 +131,143 @@ class NotesTheme extends Themelet { "; + } - $html .= "
    "; + $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..ca2b77ab --- /dev/null +++ b/ext/numeric_score/info.php @@ -0,0 +1,23 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: Allow users to score images + * Documentation: + */ + +class NumericScoreInfo extends ExtensionInfo +{ + public const KEY = "numeric_score"; + + public $key = self::KEY; + public $name = "Image Scores (Numeric)"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Allow users to score images"; + public $documentation ="Each registered user may vote an image +1 or -1, the image's score is the sum of all votes."; +} diff --git a/ext/numeric_score/main.php b/ext/numeric_score/main.php index 2b6f49eb..bff6701b 100644 --- a/ext/numeric_score/main.php +++ b/ext/numeric_score/main.php @@ -1,196 +1,205 @@ - * 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. - */ -class NumericScoreSetEvent extends Event { - public $image_id, $user, $score; +class NumericScoreSetEvent extends Event +{ + public $image_id; + public $user; + public $score; - /** - * @param int $image_id - * @param User $user - * @param int $score - */ - public function __construct($image_id, User $user, $score) { - $this->image_id = $image_id; - $this->user = $user; - $this->score = $score; - } + public function __construct(int $image_id, User $user, int $score) + { + $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 +{ + public function onInitExt(InitExtEvent $event) + { + global $config; + if ($config->get_int("ext_numeric_score_version", 0) < 1) { + $this->install(); + } + } - 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_id = url_escape($event->display_user->id); + $n_up = Image::count_images(["upvoted_by_id={$event->display_user->id}"]); + $link_up = make_link("post/list/upvoted_by_id=$u_id/1"); + $n_down = Image::count_images(["downvoted_by_id={$event->display_user->id}"]); + $link_down = make_link("post/list/downvoted_by_id=$u_id/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 + 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")); + [$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] + ); + $database->execute( + "UPDATE images SET numeric_score=0 WHERE 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("index_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", ["image_id"=>$event->image_id]); + $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]); - 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=? AND image_id IN (".$id_list.")", + [$user_id] + ); + $database->execute(" UPDATE images SET numeric_score=COALESCE( ( @@ -201,82 +210,108 @@ 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', $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) + { + $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(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)); - } - } + public function onTagTermParse(TagTermParseEvent $event) + { + $matches = []; - if(!empty($matches)) $event->metatag = true; - } + 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)); + } + } - private function install() { - global $database; - global $config; + if (!empty($matches)) { + $event->metatag = true; + } + } - 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", " + 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"); + } + } + + private function install() + { + global $database; + global $config; + + 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", " image_id INTEGER NOT NULL, user_id INTEGER NOT NULL, score INTEGER NOT NULL, @@ -284,38 +319,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)", []); + $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); + } + } - /** - * @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..3fa7a5d5 100644 --- a/ext/numeric_score/test.php +++ b/ext/numeric_score/test.php @@ -1,60 +1,61 @@ log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - $this->get_page("post/view/$image_id"); +class NumericScoreTest extends ShimmiePHPUnitTestCase +{ + public function testNumericScore() + { + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + $this->get_page("post/view/$image_id"); - $this->markTestIncomplete(); + $this->markTestIncomplete(); - $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 + $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 - # test search by score - $this->get_page("post/list/score=1/1"); - $this->assert_title("Image $image_id: pbx"); + # test search by score + $this->get_page("post/list/score=1/1"); + $this->assert_title("Image $image_id: pbx"); - $this->get_page("post/list/score>0/1"); - $this->assert_title("Image $image_id: pbx"); + $this->get_page("post/list/score>0/1"); + $this->assert_title("Image $image_id: pbx"); - $this->get_page("post/list/score>-5/1"); - $this->assert_title("Image $image_id: pbx"); + $this->get_page("post/list/score>-5/1"); + $this->assert_title("Image $image_id: pbx"); - $this->get_page("post/list/-score>5/1"); - $this->assert_title("Image $image_id: pbx"); + $this->get_page("post/list/-score>5/1"); + $this->assert_title("Image $image_id: pbx"); - $this->get_page("post/list/-score<-5/1"); - $this->assert_title("Image $image_id: pbx"); + $this->get_page("post/list/-score<-5/1"); + $this->assert_title("Image $image_id: pbx"); - # 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"); + # 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"); - # and downvote - $this->get_page("post/list/downvoted_by=test/1"); - $this->assert_text("No Images Found"); + # and downvote + $this->get_page("post/list/downvoted_by=test/1"); + $this->assert_text("No Images Found"); - # 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 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"); - $this->log_out(); + $this->log_out(); - $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(); + } } - diff --git a/ext/numeric_score/theme.php b/ext/numeric_score/theme.php index d1a9f38b..1d427322 100644 --- a/ext/numeric_score/theme.php +++ b/ext/numeric_score/theme.php @@ -1,12 +1,14 @@ id); - $i_score = int_escape($image->numeric_score); +class NumericScoreTheme extends Themelet +{ + public function get_voter(Image $image) + { + global $user, $page; + $i_image_id = int_escape($image->id); + $i_score = int_escape($image->numeric_score); - $html = " + $html = " Current Score: $i_score

    @@ -30,8 +32,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 +47,90 @@ 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". + "

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

    \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/info.php b/ext/oekaki/info.php new file mode 100644 index 00000000..2600bf7f --- /dev/null +++ b/ext/oekaki/info.php @@ -0,0 +1,19 @@ +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)); + if ($event->page_matches("oekaki")) { + if ($user->can(Permissions::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'); + $pathinfo = pathinfo($tmpname); + if (!array_key_exists('extension', $pathinfo)) { + throw new UploadException("File has no extension"); + } + log_info("oekaki", "Processing file [{$pathinfo['filename']}]"); + $metadata = []; + $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(PageMode::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); + $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]"); + log_info("oekaki", "Uploading file [$uploadname]"); - $success = TRUE; - if (isset($_FILES["chibifile"])) - $success = $success && move_uploaded_file($_FILES['chibifile']['tmp_name'], $uploadfile . ".chi"); + $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"; - } - } - } - } + // 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(); - } - } + // FIXME: "edit this image" button on existing images? + public function onPostListBuilding(PostListBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::CREATE_IMAGE)) { + $this->theme->display_block(); + } + } } - diff --git a/ext/oekaki/test.php b/ext/oekaki/test.php index 1061595c..c094bce3 100644 --- a/ext/oekaki/test.php +++ b/ext/oekaki/test.php @@ -1,7 +1,9 @@ log_in_as_user(); - $this->get_page("oekaki/create"); - } +class OekakiTest extends ShimmiePHPUnitTestCase +{ + public function testLog() + { + $this->log_in_as_user(); + $this->get_page("oekaki/create"); + } } diff --git a/ext/oekaki/theme.php b/ext/oekaki/theme.php index 8a0ee9b9..ae29a6ac 100644 --- a/ext/oekaki/theme.php +++ b/ext/oekaki/theme.php @@ -2,22 +2,24 @@ // FIXME: Move all the stuff that handles size input to main.php // FIXME: Move default canvas size to config file; changeable in board config // While we're here, add maximum and minimum image sizes in config -// Maybe allow the resolution limiter extension to have a say in this +// Maybe allow the resolution limiter extension to have a say in this -class OekakiTheme extends Themelet { - public function display_page() { - global $config, $page; +class OekakiTheme extends Themelet +{ + public function display_page() + { + global $config, $page; - $base_href = get_base_href(); + $base_href = get_base_href(); - $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']); - } + $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']); + } - $html = " + $html = " @@ -28,37 +30,40 @@ class OekakiTheme extends Themelet { "; -# -# - // 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)); - } + # + # + // 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 + 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']); - } + $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", - " + $page->add_block(new Block( + "Oekaki", + " ". - "x". - "". - " + "x". + "". + " - " - , "left", 21)); // upload is 20 - } + ", + "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..224c5023 --- /dev/null +++ b/ext/ouroboros_api/info.php @@ -0,0 +1,42 @@ + + * Description: Ouroboros-like API for Shimmie + * Version: 0.2 + * Documentation: + * + */ + + +class OuroborosAPIInfo extends ExtensionInfo +{ + public const KEY = "ouroboros_api"; + + public $key = self::KEY; + public $name = "Ouroboros API"; + public $authors = ["Diftraku"=>"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..7bf969ea 100644 --- a/ext/ouroboros_api/main.php +++ b/ext/ouroboros_api/main.php @@ -1,32 +1,5 @@ - * 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 - */ - class _SafeOuroborosImage { @@ -190,10 +163,6 @@ class _SafeOuroborosImage */ public $sample_width = null; - /** - * Constructor - * @param Image $img - */ public function __construct(Image $img) { global $config; @@ -213,20 +182,20 @@ class _SafeOuroborosImage // meta $this->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,8 @@ class OuroborosPost extends _SafeOuroborosImage /** * Initialize an OuroborosPost for creation * Mainly just acts as a wrapper and validation layer - * @param array $post - * @param string $md5 */ - 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,19 +377,18 @@ 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 @@ -438,7 +404,7 @@ 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(); + $tags = !empty($_REQUEST['tags']) ? filter_var($_REQUEST['tags'], FILTER_SANITIZE_STRING) : []; if (!empty($tags)) { $tags = Tag::explode($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,32 +450,28 @@ 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 + $post->file_url, + FILTER_VALIDATE_URL ) !== false ) { // Transload from source @@ -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,21 +568,11 @@ 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(); + $tag_data = []; switch ($order) { case 'name': $tag_data = $database->get_col( @@ -638,7 +585,7 @@ class OuroborosAPI extends Extension 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) + ['tags_min' => $config->get_int(TagListConfig::TAGS_MIN), 'start' => $start, 'max_items' => $limit] ); break; case 'count': @@ -649,7 +596,7 @@ 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) + ['tags_min' => $config->get_int(TagListConfig::TAGS_MIN), 'start' => $start, 'max_items' => $limit] ); break; case 'date': @@ -660,11 +607,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) + ['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 +627,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 +654,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 +681,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 +714,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 +735,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 +750,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 +763,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..795ee31f --- /dev/null +++ b/ext/pm/info.php @@ -0,0 +1,26 @@ + + * License: GPLv2 + * Description: Allow users to send messages to eachother + * Documentation: + * + */ + +class PrivMsgInfo extends ExtensionInfo +{ + public const KEY = "pm"; + + public $key = self::KEY; + public $name = "Private Messaging"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Allow users to send messages to eachother"; + public $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."; +} diff --git a/ext/pm/main.php b/ext/pm/main.php index 6de56ad7..a717c0c8 100644 --- a/ext/pm/main.php +++ b/ext/pm/main.php @@ -1,58 +1,60 @@ - * 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. - */ -class SendPMEvent extends Event { - public $pm; +class SendPMEvent extends Event +{ + public $pm; - public function __construct(PM $pm) { - $this->pm = $pm; - } + public function __construct(PM $pm) + { + $this->pm = $pm; + } } -class PM { - public $id, $from_id, $from_ip, $to_id, $sent_date, $subject, $message, $is_read; +class PM +{ + public $id; + public $from_id; + public $from_ip; + public $to_id; + public $sent_date; + public $subject; + public $message; + 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 = $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; + } + } } -class PrivMsg extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config, $database; +class PrivMsg extends Extension +{ + public function onInitExt(InitExtEvent $event) + { + global $config, $database; - // shortcut to latest - if($config->get_int("pm_version") < 1) { - $database->create_table("private_message", " + // 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, @@ -64,150 +66,168 @@ class PrivMsg extends Extension { 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->is_anonymous()) { + $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->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 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 $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", ["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_escape($pm["from_id"])); + if ($pm["to_id"] == $user->id) { + $database->execute("UPDATE private_message SET is_read='Y' WHERE id = :id", ["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", ["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]); + $database->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->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(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 $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] + ); + $database->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 $database; - $count = $database->cache->get("pm-count:{$user->id}"); - if(is_null($count) || $count === false) { - $count = $database->get_one(" + $count = $database->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"]); + $database->cache->set("pm-count:{$user->id}", $count, 600); + } + return $count; + } } - diff --git a/ext/pm/test.php b/ext/pm/test.php index df065139..ea19e722 100644 --- a/ext/pm/test.php +++ b/ext/pm/test.php @@ -1,59 +1,61 @@ log_in_as_admin(); - $this->get_page("user/test"); +class PrivMsgTest extends ShimmiePHPUnitTestCase +{ + public function testPM() + { + $this->log_in_as_admin(); + $this->get_page("user/test"); - $this->markTestIncomplete(); + $this->markTestIncomplete(); - $this->set_field('subject', "message demo to test"); - $this->set_field('message', "message contents"); - $this->click("Send"); - $this->log_out(); + $this->set_field('subject', "message demo to test"); + $this->set_field('message', "message contents"); + $this->click("Send"); + $this->log_out(); - $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"); + $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"); - $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"); + $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"); - $this->log_out(); - } + $this->log_out(); + } - public function testAdminAccess() { - $this->log_in_as_admin(); - $this->get_page("user/test"); + public function testAdminAccess() + { + $this->log_in_as_admin(); + $this->get_page("user/test"); - $this->markTestIncomplete(); + $this->markTestIncomplete(); - $this->set_field('subject', "message demo to test"); - $this->set_field('message', "message contents"); - $this->click("Send"); + $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"); + $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(); - } + # 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(); + } } - diff --git a/ext/pm/theme.php b/ext/pm/theme.php index 81242c9c..f69240d9 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..d36ca3b5 --- /dev/null +++ b/ext/pm_triggers/info.php @@ -0,0 +1,21 @@ + + * License: GPLv2 + * Description: Send PMs in response to certain events (eg image deletion) + */ + +class PMTriggerInfo extends ExtensionInfo +{ + public const KEY = "pm_triggers"; + + public $key = self::KEY; + public $name = "PM triggers"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Send PMs in response to certain events (eg image deletion)"; + public $beta = true; +} diff --git a/ext/pm_triggers/main.php b/ext/pm_triggers/main.php index 7cb80013..15bbf50c 100644 --- a/ext/pm_triggers/main.php +++ b/ext/pm_triggers/main.php @@ -1,29 +1,25 @@ - * License: GPLv2 - * Description: Send PMs in response to certain events (eg image deletion) - */ -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()})" - ); - } +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..5acf4629 --- /dev/null +++ b/ext/pools/info.php @@ -0,0 +1,22 @@ +, jgen , Daku + * License: GPLv2 + * Description: Allow users to create groups of images and order them. + * Documentation: + */ + +class PoolsInfo extends ExtensionInfo +{ + public const KEY = "pools"; + + public $key = self::KEY; + public $name = "Pools System"; + public $authors = ["Sein Kraft"=>"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..6604e4f2 100644 --- a/ext/pools/main.php +++ b/ext/pools/main.php @@ -1,47 +1,83 @@ , 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. - */ + +abstract class PoolsConfig +{ + const MAX_IMPORT_RESULTS = "poolsMaxImportResults"; + const IMAGES_PER_PAGE = "poolsImagesPerPage"; + const LISTS_PER_PAGE = "poolsListsPerPage"; + const UPDATED_PER_PAGE = "poolsUpdatedPerPage"; + const INFO_ON_VIEW_IMAGE = "poolsInfoOnViewImage"; + const ADDER_ON_VIEW_IMAGE = "poolsAdderOnViewImage"; + const SHOW_NAV_LINKS = "poolsShowNavLinks"; + const AUTO_INCREMENT_ORDER = "poolsAutoIncrementOrder"; +} /** * This class is just a wrapper around SCoreException. */ -class PoolCreationException extends SCoreException { - /** @var string */ - public $error; +class PoolCreationException extends SCoreException +{ + /** @var string */ + public $error; - /** - * @param string $error - */ - public function __construct($error) { - $this->error = $error; - } + public function __construct(string $error) + { + $this->error = $error; + } } -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) + { + $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 = "") + { + global $user; + + $this->title = $title; + $this->user = $pool_user ?? $user; + $this->public = $public; + $this->description = $description; + } +} + +class Pools extends Extension +{ + public function onInitExt(InitExtEvent $event) + { + global $config, $database; + + // 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); + + // Create the database tables + if ($config->get_int("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, @@ -51,14 +87,14 @@ class Pools extends Extension { 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, @@ -69,330 +105,411 @@ class Pools extends Extension { 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); + $config->set_int("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 ($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;"); - $config->set_int("ext_pools_version", 3); // skip 2 - } - } + $config->set_int("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, $database; - $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, int_escape($event->get_arg(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) + { + $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 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($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->id, true, $image_order); + } + } + + if (!empty($matches)) { + $event->metatag = true; + } + } + + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) + { + global $user, $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 +517,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 = 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 = $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 +756,183 @@ 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 = int_escape($event->get_arg(2)); + if (is_null($pageNumber) || !is_numeric($pageNumber)) { + $pageNumber = 0; + } elseif ($pageNumber <= 0) { + $pageNumber = 0; + } else { + $pageNumber--; + } - $poolID = int_escape($poolID); - $pool = $this->get_pool($poolID); + $poolID = int_escape($poolID); + $pool = $this->get_pool($poolID); - $imagesPerPage = $config->get_int("poolsImagesPerPage"); + $imagesPerPage = $config->get_int(PoolsConfig::IMAGES_PER_PAGE); - // 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) + $query = " + INNER JOIN images AS i ON i.id = p.image_id + WHERE p.pool_id = :pid + "; + + + // 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 "); + } + + $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); - } - - $images = array(); - foreach($result as $singleResult) { - $images[] = Image::by_id($singleResult["image_id"]); - } - - $this->theme->view_pool($pool, $images, $pageNumber + 1, $totalPages); - } + $totalPages = ceil($database->get_one( + " + SELECT COUNT(*) FROM pool_images p + $query", + ["pid" => $poolID] + ) / $imagesPerPage); - /** - * 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; - $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; - } + $images = []; + foreach ($result as $singleResult) { + $images[] = Image::by_id($singleResult["image_id"]); + } + + $this->theme->view_pool($pool, $images, $pageNumber + 1, $totalPages); + } - /** - * 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; + /** + * 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 = $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 = 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. + * + * #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", ["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 +942,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 = 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 = $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 = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid" => $poolID]); + $this->add_history($poolID, 1, $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 = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid" => $poolID]); + $this->add_history($poolID, 0, $imageID, $count); + } + } } - diff --git a/ext/pools/test.php b/ext/pools/test.php index 0a3cc265..5f4fadd9 100644 --- a/ext/pools/test.php +++ b/ext/pools/test.php @@ -1,42 +1,43 @@ get_page('pool/list'); - $this->assert_title("Pools"); +class PoolsTest extends ShimmiePHPUnitTestCase +{ + public function testPools() + { + $this->get_page('pool/list'); + $this->assert_title("Pools"); - $this->get_page('pool/new'); - $this->assert_title("Error"); + $this->get_page('pool/new'); + $this->assert_title("Error"); - $this->log_in_as_user(); - $this->get_page('pool/list'); + $this->log_in_as_user(); + $this->get_page('pool/list'); - $this->markTestIncomplete(); + $this->markTestIncomplete(); - $this->click("Create Pool"); - $this->assert_title("Create Pool"); - $this->click("Create"); - $this->assert_title("Error"); + $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->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_out(); - $this->log_in_as_admin(); + $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->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->log_out(); + } } - diff --git a/ext/pools/theme.php b/ext/pools/theme.php index 32c5f0f0..d9278847 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, $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 onUserBlockBuilding(UserBlockBuildingEvent $event) { - global $user; - if($user->can("change_setting")) { - $event->add_link("Board Config", make_link("setup")); - } - } + 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")); + } + } } - diff --git a/ext/setup/test.php b/ext/setup/test.php index 2989472d..9f55a090 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"); - } +class SetupTest extends ShimmiePHPUnitTestCase +{ + public function testNiceUrlsTest() + { + # XXX: this only checks that the text is "ok", to check + # for a bug where it was coming out as "\nok"; it doesn't + # check that niceurls actually work + $this->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..fc3f97cc 100644 --- a/ext/setup/theme.php +++ b/ext/setup/theme.php @@ -1,61 +1,63 @@ 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) { + $h_name = html_escape($name); + $h_value = html_escape($value); - $h_box = ""; - if(strpos($value, "\n") > 0) { - $h_box .= ""; - } - else { - $h_box .= ""; - } - $h_box .= ""; - $h_rows .= ""; - } + $h_box = ""; + if (strpos($value, "\n") > 0) { + $h_box .= ""; + } else { + $h_box .= ""; + } + $h_box .= ""; + $h_rows .= ""; + } - $table = " + $table = " ".make_form(make_link("setup/save"))."
    Title:
    Public?
    $h_name$h_box
    $h_name$h_box
    @@ -65,31 +67,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..af1ded44 --- /dev/null +++ b/ext/shimmie_api/info.php @@ -0,0 +1,33 @@ + + * Description: A JSON interface to shimmie data [WARNING] + * Documentation: + * + */ + + +class ShimmieApiInfo extends ExtensionInfo +{ + public const KEY = "shimmie_api"; + + public $key = self::KEY; + public $name = "Shimmie JSON API"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $description = "A JSON interface to shimmie data [WARNING]"; + public $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.
    "; +} diff --git a/ext/shimmie_api/main.php b/ext/shimmie_api/main.php index a672f85b..a3bcf703 100644 --- a/ext/shimmie_api/main.php +++ b/ext/shimmie_api/main.php @@ -1,173 +1,158 @@ - * 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.
    - */ +class _SafeImage +{ + public $id; + public $height; + public $width; + public $hash; + public $filesize; + public $ext; + public $posted; + public $source; + public $owner_id; + public $tags; -class _SafeImage { - public $id; - public $height; - public $width; - public $hash; - public $filesize; - public $ext; - public $posted; - public $source; - public $owner_id; - public $tags; - - 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(); - } + 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")) { + $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)); + } 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 ?", [$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] + ); - $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'])) { + $recent = $database->get_all( + "SELECT * FROM images WHERE owner_id=? ORDER BY id DESC LIMIT 0, 5", + [$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']); - /** - * @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..2f6e2f39 100644 --- a/ext/shimmie_api/test.php +++ b/ext/shimmie_api/test.php @@ -1,23 +1,25 @@ log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); +class ShimmieApiTest extends ShimmiePHPUnitTestCase +{ + public function testAPI() + { + $this->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"); + $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: test unspecified / bad values - // FIXME: test that json is encoded properly - } + // 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..2bbc25e7 --- /dev/null +++ b/ext/site_description/info.php @@ -0,0 +1,26 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Visibility: admin + * Description: A description for search engines + * Documentation: + * + */ +class SiteDescriptionInfo extends ExtensionInfo +{ + public const KEY = "site_description"; + + public $key = self::KEY; + public $name = "Site Description"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $visibility = self::VISIBLE_ADMIN; + public $description = "A description for search engines"; + public $documentation = +"This extension sets the \"description\" meta tag in the header of pages so that search engines can pick it up"; +} diff --git a/ext/site_description/main.php b/ext/site_description/main.php index 80563617..58e56780 100644 --- a/ext/site_description/main.php +++ b/ext/site_description/main.php @@ -1,33 +1,25 @@ - * 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(""); - } - } - 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); - } +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(""); + } + } + + 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..6cd01aa0 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() - ); - } +class SiteDescriptionTest extends ShimmiePHPUnitTestCase +{ + public function testSiteDescription() + { + global $config, $page; + $config->set_string("site_description", "A Shimmie testbed"); + $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->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->assertContains( + '', + $page->get_all_html_headers() + ); + } } - diff --git a/ext/sitemap/info.php b/ext/sitemap/info.php new file mode 100644 index 00000000..3cee3e3a --- /dev/null +++ b/ext/sitemap/info.php @@ -0,0 +1,23 @@ + + * Author: Drudex Software + * Link: http://drudexsoftware.com + * License: GPLv2 + * Description: Sitemap with caching & advanced priorities + * Documentation: + */ + +class XMLSitemapInfo extends ExtensionInfo +{ + public const KEY = "sitemap"; + + public $key = self::KEY; + public $name = "XML Sitemap"; + public $url = "http://drudexsoftware.com"; + public $authors = ["Sein Kraft"=>"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..d74d7e9f 100644 --- a/ext/sitemap/main.php +++ b/ext/sitemap/main.php @@ -1,195 +1,186 @@ - * 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 = []; + 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))); + $this->add_sitemap_queue($latestimages_urllist, "monthly", "0.8", 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(); + } - // 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 = []; + 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 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, []); + 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))); - /* --- 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..638c4c38 100644 --- a/ext/sitemap/test.php +++ b/ext/sitemap/test.php @@ -1,8 +1,10 @@ get_page('sitemap.xml'); - } +class XMLSitemapTest extends ShimmiePHPUnitTestCase +{ + public function testBasic() + { + # this will implicitly check that there are no + # PHP-level error messages + $this->get_page('sitemap.xml'); + } } diff --git a/ext/source_history/info.php b/ext/source_history/info.php new file mode 100644 index 00000000..cce9a369 --- /dev/null +++ b/ext/source_history/info.php @@ -0,0 +1,18 @@ +set_default_int("history_limit", -1); + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_int("history_limit", -1); - // shimmie is being installed so call install to create the table. - if($config->get_int("ext_source_history_version") < 3) { - $this->install(); - } - } + // shimmie is being installed so call install to create the table. + if ($config->get_int("ext_source_history_version") < 3) { + $this->install(); + } + } - 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($_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")); + } + } + + protected function install() + { + global $database, $config; + + if ($config->get_int("ext_source_history_version") < 1) { + $database->create_table("source_histories", " id SCORE_AIPK, image_id INTEGER NOT NULL, user_id INTEGER NOT NULL, @@ -97,202 +110,188 @@ class Source_History 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 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)", []); + $config->set_int("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 ($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); + } - /** - * This function is called when a revert request is received. - * @param int $revert_id - */ - private function process_revert_request($revert_id) { - global $page; + 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); + } + } - $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.']'); + $revert_id = int_escape($revert_id); - $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.'); - } + // check for the nothing case + if ($revert_id < 1) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link()); + return; + } - // 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)); - } + // lets get this revert id assuming it exists + $result = $this->get_source_history_from_revert($revert_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 (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."); + } - 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(); - } + // lets get the values out of the result + //$stored_result_id = $result['id']; + $stored_image_id = $result['image_id']; + $stored_source = $result['source']; - /** - * @param int $revert_id - * @return mixed|null - */ - public function get_source_history_from_revert(/*int*/ $revert_id) { - global $database; - $row = $database->get_row(" + log_debug("source_history", 'Reverting source of Image #'.$stored_image_id.' to ['.$stored_source.']'); + + $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 = ?", [$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; + $row = $database->get_all( + " SELECT source_histories.*, users.name FROM source_histories JOIN users ON source_histories.user_id = users.id WHERE image_id = ? ORDER BY source_histories.id DESC", - array($image_id)); - return ($row ? $row : array()); - } + [$image_id] + ); + return ($row ? $row : []); + } - /** - * @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; + $row = $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]); + return ($row ? $row : []); + } - /** - * 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 = ?'; + $select_args[] = $duser->id; + } + } - if(!is_null($ip)) { - $select_code[] = 'user_ip = ?'; - $select_args[] = $ip; - } + if (!is_null($ip)) { + $select_code[] = 'user_ip = ?'; + $select_args[] = $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 >= ?'; + $select_args[] = $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) @@ -300,110 +299,116 @@ class Source_History extends Extension { 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)) { + // 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 (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]); + 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++; - } + [$image->id, $old_source, $config->get_int('anon_id'), '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)); - } - } + [$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 = ?", [$image->id]); + $database->execute("DELETE FROM source_histories WHERE id = ?", [$min_id]); + } + } } - diff --git a/ext/source_history/theme.php b/ext/source_history/theme.php index c02ee222..667dec53 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/statsd/info.php b/ext/statsd/info.php new file mode 100644 index 00000000..0b1879f8 --- /dev/null +++ b/ext/statsd/info.php @@ -0,0 +1,25 @@ + +* License: GPLv2 +* Visibility: admin +* Description: Sends Shimmie stats to a StatsD server +* Documentation: +* +*/ + +class StatsDInterfaceInfo extends ExtensionInfo +{ + public const KEY = "statsd"; + + public $key = self::KEY; + public $name = "StatsD Interface"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $visibility = self::VISIBLE_ADMIN; + public $description = "Sends Shimmie stats to a StatsD server"; + public $documentation = "define('STATSD_HOST', 'my.server.com:8125'); in shimmie.conf.php to set the host"; +} diff --git a/ext/statsd/main.php b/ext/statsd/main.php index 79fe7030..dd0b0be3 100644 --- a/ext/statsd/main.php +++ b/ext/statsd/main.php @@ -1,100 +1,91 @@ -* 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 -*/ _d("STATSD_HOST", null); -function dstat($name, $val) { - StatsDInterface::$stats["shimmie.$name"] = $val; +function dstat($name, $val) +{ + StatsDInterface::$stats["shimmie.$name"] = $val; } -class StatsDInterface extends Extension { - public static $stats = array(); +class StatsDInterface extends Extension +{ + public static $stats = []; - /** @param string $type */ - private function _stats($type) { - global $_shm_event_count, $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"] = $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, $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"] = $database->cache->get_hits()."|c"; + StatsDInterface::$stats["shimmie.$type.cache-misses"] = $database->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, int $sampleRate=1) + { + if (!STATSD_HOST) { + return; + } // sampling - $sampledData = array(); + $sampledData = []; if ($sampleRate < 1) { foreach ($data as $stat => $value) { @@ -106,7 +97,9 @@ 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 { @@ -114,7 +107,9 @@ class StatsDInterface extends Extension { $host = $parts[0]; $port = $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..7f32847f --- /dev/null +++ b/ext/system/info.php @@ -0,0 +1,19 @@ + + * Description: Provides system screen + */ + +class SystemInfo extends ExtensionInfo +{ + public const KEY = "system"; + + public $key = self::KEY; + public $name = "System"; + public $authors = ["Matthew Barbour"=>"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..5c9a21d1 --- /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..f23f2a82 --- /dev/null +++ b/ext/tag_categories/config.php @@ -0,0 +1,8 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * Description: Let tags be split into 'categories', like Danbooru's tagging + */ +class TagCategoriesInfo extends ExtensionInfo +{ + public const KEY = "tag_categories"; + + public $key = self::KEY; + public $name = "Tag Categories"; + public $url = "http://code.shishnet.org/shimmie2/"; + public $authors = ["Daniel Oaks"=>"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..54e90322 100644 --- a/ext/tag_categories/main.php +++ b/ext/tag_categories/main.php @@ -1,158 +1,182 @@ - * Link: http://code.shishnet.org/shimmie2/ - * Description: Let tags be split into 'categories', like Danbooru's tagging - */ -class TagCategories extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config, $database; - - // 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("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 +{ + public function onInitExt(InitExtEvent $event) + { + global $config, $database; + + // 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); + + if ($config->get_int(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); + $config->set_int(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 (?, ?, ?, ?)', + ["artist", "Artist", "Artists", "#BB6666"] + ); + $database->execute( + 'INSERT INTO image_tag_categories VALUES (?, ?, ?, ?)', + ["series", "Series", "Series", "#AA00AA"] + ); + $database->execute( + 'INSERT INTO image_tag_categories VALUES (?, ?, ?, ?)', + ["character", "Character", "Characters", "#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) + { + $matches = []; - if(preg_match("/^(.+)tags([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])([0-9]+)$/i", $event->term, $matches)) { - global $database; - $type = $matches[1]; - $cmp = ltrim($matches[2], ":") ?: "="; - $count = $matches[3]; + 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( + $database->scoreql_to_sql('SELECT SCORE_STRNORM(category) FROM image_tag_categories') + ); + if (in_array($type, $types)) { + $event->add_querylet( + new Querylet($database->scoreql_to_sql("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 SCORE_STRNORM(t.tag) LIKE SCORE_STRNORM('$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 $tc_dict; - } + $tc_dict = $database->get_all('SELECT * FROM image_tag_categories;'); - public function getKeyedDict($key_with = 'category') { - $tc_dict = $this->getDict(); - $tc_keyed_dict = array(); + return $tc_dict; + } - foreach ($tc_dict as $row) { - $key = $row[$key_with]; - $tc_keyed_dict[$key] = $row; - } + public function getKeyedDict($key_with = 'category') + { + $tc_dict = $this->getDict(); + $tc_keyed_dict = []; - return $tc_keyed_dict; - } + foreach ($tc_dict as $row) { + $key = $row[$key_with]; + $tc_keyed_dict[$key] = $row; + } - public function page_update() { - global $user, $database; + return $tc_keyed_dict; + } - if(!$user->is_admin()) { - return false; - } + public function page_update() + { + global $user, $database; - 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 (!$user->can(Permissions::EDIT_TAG_CATEGORIES)) { + return false; + } - if($_POST['tc_status'] == 'edit') { - $is_success = $database->execute('UPDATE image_tag_categories + 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 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..e0229151 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..d19e1f45 --- /dev/null +++ b/ext/tag_edit/info.php @@ -0,0 +1,55 @@ + +
  • 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..6015ffe1 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 - *
      - *
    - */ /* * OwnerSetEvent: @@ -46,366 +6,355 @@ * $source * */ -class OwnerSetEvent extends Event { - /** @var \Image */ - public $image; - /** @var \User */ - public $owner; +class OwnerSetEvent extends Event +{ + /** @var Image */ + public $image; + /** @var User */ + public $owner; - /** - * @param Image $image - * @param User $owner - */ - public function __construct(Image $image, User $owner) { - $this->image = $image; - $this->owner = $owner; - } + public function __construct(Image $image, User $owner) + { + $this->image = $image; + $this->owner = $owner; + } } -/* - * SourceSetEvent: - * $image_id - * $source - * - */ -class SourceSetEvent extends Event { - /** @var \Image */ - public $image; - /** @var string */ - public $source; +class SourceSetEvent extends Event +{ + /** @var Image */ + public $image; + /** @var string */ + public $source; - /** - * @param Image $image - * @param string $source - */ - public function __construct(Image $image, $source) { - $this->image = $image; - $this->source = $source; - } + public function __construct(Image $image, string $source=null) + { + $this->image = $image; + $this->source = $source; + } } -/* - * TagSetEvent: - * $image_id - * $tags - * - */ -class TagSetEvent extends Event { - /** @var \Image */ - public $image; - public $tags; - public $metatags; +class TagSetEvent extends Event +{ + /** @var Image */ + public $image; + public $tags; + public $metatags; - /** - * @param Image $image - * @param string[] $tags - */ - public function __construct(Image $image, array $tags) { - $this->image = $image; + /** + * #param string[] $tags + */ + public function __construct(Image $image, array $tags) + { + $this->image = $image; - $this->tags = array(); - $this->metatags = array(); + $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; - } + 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); + $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); - } - } - } + //seperate tags from metatags + if (!$ttpe->is_metatag()) { + array_push($this->tags, $tag); + } else { + array_push($this->metatags, $tag); + } + } + } } -class LockSetEvent extends Event { - /** @var \Image */ - public $image; - /** @var bool */ - public $locked; +class LockSetEvent extends Event +{ + /** @var Image */ + public $image; + /** @var bool */ + public $locked; - /** - * @param Image $image - * @param bool $locked - */ - public function __construct(Image $image, $locked) { - assert('is_bool($locked)'); - - $this->image = $image; - $this->locked = $locked; - } + public function __construct(Image $image, bool $locked) + { + $this->image = $image; + $this->locked = $locked; + } } /* * 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 +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)'); + public function __construct(string $term, int $id, bool $parse) + { + $this->term = $term; + $this->id = $id; + $this->parse = $parse; + } - $this->term = $term; - $this->id = $id; - $this->parse = $parse; - } - - /** - * @return bool - */ - public function is_metatag() { - return $this->metatag; - } + public function is_metatag(): bool + { + 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")); - } - } - } - } +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(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")); + } + } + } + } - 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 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("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 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 ($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(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("edit_image_owner") && (!$event->image->is_locked() || $user->can("edit_image_lock"))) { - $event->image->set_owner($event->owner); - } - } + 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("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 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); + } + $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 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("edit_image_lock")) { - $event->image->set_locked($event->locked); - } - } + 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 onImageDeletion(ImageDeletionEvent $event) + { + $event->image->delete_tags_from_image(); + } - public function onAdminBuilding(AdminBuildingEvent $event) { - $this->theme->display_mass_editor(); - } + 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 onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + if ($event->parent=="tags") { + $event->add_nav_link("tags_help", new Link('ext_doc/tag_edit'), "Help"); + } + } - 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)); - } + /** + * When an alias is added, oldtag becomes inaccessible. + */ + public function onAddAlias(AddAliasEvent $event) + { + $this->mass_tag_edit($event->oldtag, $event->newtag); + } - if(!empty($matches)) $event->metatag = true; - } + 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); + } - /** - * @param Image $image - * @return bool - */ - private function can_tag(Image $image) { - global $user; - return ($user->can("edit_image_tag") || !$image->is_locked()); - } + public function onTagTermParse(TagTermParseEvent $event) + { + $matches = []; - /** - * @param Image $image - * @return bool - */ - private function can_source(Image $image) { - global $user; - return ($user->can("edit_image_source") || !$image->is_locked()); - } + 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)); + } - /** - * @param string $search - * @param string $replace - */ - private function mass_tag_edit($search, $replace) { - global $database; + if (!empty($matches)) { + $event->metatag = true; + } + } - $search_set = Tag::explode(strtolower($search), false); - $replace_set = Tag::explode(strtolower($replace), false); + private function can_tag(Image $image): bool + { + global $user; + return ($user->can(Permissions::EDIT_IMAGE_TAG) || !$image->is_locked()); + } - log_info("tag_edit", "Mass editing tags: '$search' -> '$replace'"); + private function can_source(Image $image): bool + { + global $user; + return ($user->can(Permissions::EDIT_IMAGE_SOURCE) || !$image->is_locked()); + } - 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; - } - } + private function mass_tag_edit(string $search, string $replace) + { + global $database; - $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"; - } + $search_set = Tag::explode(strtolower($search), false); + $replace_set = Tag::explode(strtolower($replace), false); - $images = Image::find_images(0, 100, $search_forward); - if(count($images) == 0) break; + log_info("tag_edit", "Mass editing tags: '$search' -> '$replace'"); - 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; - } - } + 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; + } + } - // add the replace'd tags - foreach($replace_set as $tag) { - $after[] = $tag; - } + $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"; + } - // replace'd tag may already exist in tag set, so remove dupes to avoid integrity constraint violations. - $after = array_unique($after); + $images = Image::find_images(0, 100, $search_forward); + if (count($images) == 0) { + break; + } - $image->set_tags($after); + 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; + } + } - $last_id = $image->id; - } - } - } + // add the replace'd tags + foreach ($replace_set as $tag) { + $after[] = $tag; + } - /** - * @param string $tags - * @param string $source - */ - private function mass_source_edit($tags, $source) { - assert('is_string($tags)'); - assert('is_string($source)'); + // replace'd tag may already exist in tag set, so remove dupes to avoid integrity constraint violations. + $after = array_unique($after); - $tags = Tag::explode($tags); + $image->set_tags($after); - $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"; + $last_id = $image->id; + } + } + } - $images = Image::find_images(0, 100, $search_forward); - if(count($images) == 0) break; + private function mass_source_edit(string $tags, string $source) + { + $tags = Tag::explode($tags); - foreach($images as $image) { - $image->set_source($source); - $last_id = $image->id; - } - } - } + $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..ba36ebf7 100644 --- a/ext/tag_edit/test.php +++ b/ext/tag_edit/test.php @@ -1,84 +1,88 @@ 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"); +class TagEditTest extends ShimmiePHPUnitTestCase +{ + public function testTagEdit() + { + $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"); - $this->markTestIncomplete(); + $this->markTestIncomplete(); - $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(); + $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(); - $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(); + } - 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"); - } + 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"); + } - 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 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"); - $this->markTestIncomplete(); + $this->markTestIncomplete(); - $this->set_field("tag_edit__source", "example.com"); - $this->click("Set"); - $this->click("example.com"); - $this->assert_title("Example Domain"); - $this->back(); + $this->set_field("tag_edit__source", "example.com"); + $this->click("Set"); + $this->click("example.com"); + $this->assert_title("Example Domain"); + $this->back(); - $this->set_field("tag_edit__source", "http://example.com"); - $this->click("Set"); - $this->click("example.com"); - $this->assert_title("Example Domain"); - $this->back(); + $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_out(); - $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(); + } - /* - * FIXME: Mass Tagger seems to be broken, and this test case always fails. - */ - public function testMassEdit() { - $this->markTestIncomplete(); + /* + * FIXME: Mass Tagger seems to be broken, and this test case always fails. + */ + public function testMassEdit() + { + $this->markTestIncomplete(); - $this->log_in_as_admin(); + $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"); + $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("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->get_page("post/view/$image_id"); + $this->assert_title("Image $image_id: pox"); - $this->delete_image($image_id); + $this->delete_image($image_id); - $this->log_out(); - } + $this->log_out(); + } } - diff --git a/ext/tag_edit/theme.php b/ext/tag_edit/theme.php index 498cfcd1..fa95d7fa 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..003a6ee0 --- /dev/null +++ b/ext/tag_editcloud/info.php @@ -0,0 +1,19 @@ +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; + } + $relevant_tags = implode(",", array_map([$database,"escape"], $relevant_tags)); + $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) @@ -99,86 +94,85 @@ class TagEditCloud extends Extension { 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] + ); + 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..1bdc54ed --- /dev/null +++ b/ext/tag_history/info.php @@ -0,0 +1,17 @@ +, modified by jgen + * Description: Keep a record of tag changes, and allows you to revert changes. + */ + +class TagHistoryInfo extends ExtensionInfo +{ + public const KEY = "tag_history"; + + public $key = self::KEY; + public $name = "Tag History"; + public $authors = ["Bzchan"=>"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..ea243f2a 100644 --- a/ext/tag_history/main.php +++ b/ext/tag_history/main.php @@ -1,93 +1,107 @@ , modified by jgen - * Description: Keep a record of tag changes, and allows you to revert changes. - */ -class Tag_History extends Extension { - // in before tags are actually set, so that "get current tags" works - public function get_priority() {return 40;} +class TagHistory extends Extension +{ + // in before tags are actually set, so that "get current tags" works + public function get_priority(): int + { + return 40; + } - public function onInitExt(InitExtEvent $event) { - global $config; - $config->set_default_int("history_limit", -1); + public function onInitExt(InitExtEvent $event) + { + global $config; + $config->set_default_int("history_limit", -1); - // shimmie is being installed so call install to create the table. - if($config->get_int("ext_tag_history_version") < 3) { - $this->install(); - } - } + // shimmie is being installed so call install to create the table. + if ($config->get_int("ext_tag_history_version") < 3) { + $this->install(); + } + } - 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($_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")); + } + } + + protected function install() + { + global $database, $config; + + if ($config->get_int("ext_tag_history_version") < 1) { + $database->create_table("tag_histories", " id SCORE_AIPK, image_id INTEGER NOT NULL, user_id INTEGER NOT NULL, @@ -97,201 +111,185 @@ class Tag_History 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 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)", []); + $config->set_int("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 ($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); + } - /** - * 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 ($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); + } + } - $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']; + $revert_id = int_escape($revert_id); - $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); - } + // check for the nothing case + if ($revert_id < 1) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link()); + return; + } - 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)); - } + // lets get this revert id assuming it exists + $result = $this->get_tag_history_from_revert($revert_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 (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."); + } - 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(); - } + // lets get the values out of the result + $stored_image_id = int_escape($result['image_id']); + $stored_tags = $result['tags']; - /** - * @param int $revert_id - * @return mixed|null - */ - public function get_tag_history_from_revert(/*int*/ $revert_id) { - global $database; - $row = $database->get_row(" + $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); + } + + 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 = ?", [$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; + $row = $database->get_all( + " SELECT tag_histories.*, users.name FROM tag_histories JOIN users ON tag_histories.user_id = users.id WHERE image_id = ? ORDER BY tag_histories.id DESC", - array($image_id)); - return ($row ? $row : array()); - } + [$image_id] + ); + return ($row ? $row : []); + } - /** - * @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; + $row = $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]); + return ($row ? $row : []); + } - 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 = ?'; + $select_args[] = $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 = ?'; + $select_args[] = $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 >= ?'; + $select_args[] = $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) @@ -299,111 +297,117 @@ class Tag_History extends Extension { 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)) { + // 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); - 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_escape($result['id']); + $stored_image_id = int_escape($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]", false, ["image_id" => $image->id]); + } else { + log_debug("tag_history", "adding tag history: [$old_tags] -> [$new_tags]", false, ["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 = ?", [$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++; - } + [$image->id, $old_tags, $config->get_int('anon_id'), '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)); - } - } + [$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 = ?", [$image->id]); + $database->execute("DELETE FROM tag_histories WHERE id = ?", [$min_id]); + } + } } - diff --git a/ext/tag_history/test.php b/ext/tag_history/test.php index 4914be06..74182e40 100644 --- a/ext/tag_history/test.php +++ b/ext/tag_history/test.php @@ -1,24 +1,25 @@ 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"); +class TagHistoryTest extends ShimmiePHPUnitTestCase +{ + public function testTagHistory() + { + $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->markTestIncomplete(); + $this->markTestIncomplete(); - // 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"); + // 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"); - $this->get_page("tag_history/all/1"); - $this->assert_title("Global Tag History"); - } + $this->get_page("tag_history/all/1"); + $this->assert_title("Global Tag History"); + } } - diff --git a/ext/tag_history/theme.php b/ext/tag_history/theme.php index 7e6bb78c..099b8215 100644 --- a/ext/tag_history/theme.php +++ b/ext/tag_history/theme.php @@ -4,44 +4,40 @@ * Author: Bzchan , modified by jgen */ -class Tag_HistoryTheme extends Themelet { - private $messages = array(); +class TagHistoryTheme extends Themelet +{ + private $messages = []; - /** - * @param Page $page - * @param int $image_id - * @param array $history - */ - public function display_history_page(Page $page, /*int*/ $image_id, /*array*/ $history) { - global $user; - $start_string = " + public function display_history_page(Page $page, int $image_id, array $history) + { + global $user; + $start_string = "
    ".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)); + } + + /* + * 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/tag_list/config.php b/ext/tag_list/config.php new file mode 100644 index 00000000..1a033c5b --- /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..a44adfe3 --- /dev/null +++ b/ext/tag_list/info.php @@ -0,0 +1,20 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * Description: Show the tags in various ways + */ + +class TagListInfo extends ExtensionInfo +{ + public const KEY = "tag_list"; + + public $key = self::KEY; + public $name = "Tag List"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $description = "Show the tags in various ways"; + public $core = true; +} diff --git a/ext/tag_list/main.php b/ext/tag_list/main.php index 8649a18a..88f92793 100644 --- a/ext/tag_list/main.php +++ b/ext/tag_list/main.php @@ -1,236 +1,288 @@ - * Link: http://code.shishnet.org/shimmie2/ - * Description: Show the tags in various ways - */ -class TagList extends Extension { - public function onInitExt(InitExtEvent $event) { - global $config; - $config->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 +{ + 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); + } - 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 onPageRequest(PageRequestEvent $event) + { + global $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("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); + } elseif ($event->page_matches("api/internal/tag_list/complete")) { + if (!isset($_GET["s"]) || $_GET["s"] == "" || $_GET["s"] == "_") { + return; + } - $res = null; - $database->cache->get($cache_key); - if(!$res) { - $res = $database->get_col($database->scoreql_to_sql(" + //$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; + $database->cache->get($cache_key); + if (!$res) { + $res = $database->get_col($database->scoreql_to_sql(" SELECT tag FROM tags WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:search) AND count > 0 $limitSQL "), $SQLarr); - $database->cache->set($cache_key, $res, 600); - } + $database->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"); + $event->add_nav_link("tags_categories", new Link('tags/categories'), "Categories"); + } + } - $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); + } + // }}} + // misc {{{ + private function tag_link(string $tag): string + { + $u_tag = url_escape($tag); + return make_link("post/list/$u_tag/1"); + } - /** - * @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 $config, $database; + $tags_config = $config->get_string(TagListConfig::OMIT_TAGS); - $tag_data = $database->get_col($database->scoreql_to_sql(" + $results = $database->cache->get("tag_list_omitted_tags:".$tags_config); + + if ($results==null) { + $results = []; + $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); + + $database->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($database->scoreql_to_sql(" SELECT DISTINCT SCORE_STRNORM(substr(tag, 1, 1)) FROM tags WHERE count >= :tags_min ORDER BY SCORE_STRNORM(substr(tag, 1, 1)) - "), array("tags_min"=>$tags_min)); + "), ["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; + } + // }}} + // maps {{{ - /** - * @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_cats = "Categories"; + $h_all = "Show All"; + return "$h_index
     
    $h_map
    $h_alphabetic
    $h_popularity
    $h_cats
     
    $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($database->scoreql_to_sql(" SELECT tag, FLOOR(LOG(2.7, LOG(2.7, count - :tags_min2 + 1)+1)*1.5*100)/100 AS scaled @@ -238,315 +290,371 @@ class TagList extends Extension { 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)); + "), ["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->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($database->scoreql_to_sql(" 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)); + "), ["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) { + 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->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->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 build_tag_list(): string + { + global $database; - //$tags_min = $this->get_tags_min(); - $tag_data = $database->get_all("SELECT tag,count FROM tags ORDER BY count DESC, tag ASC LIMIT 9"); + //$tags_min = $this->get_tags_min(); + $tag_data = $database->get_all("SELECT tag,count FROM tags ORDER BY count DESC, tag ASC LIMIT 9"); - $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
    "; + $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([$row['tag']], 100); + 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
    "; - return $html; - } -// }}} -// blocks {{{ - /** - * @param Page $page - * @param Image $image - */ - private function add_related_block(Page $page, Image $image) { - global $database, $config; + return $html; + } + // }}} + // blocks {{{ + private function add_related_block(Page $page, Image $image): void + { + global $database, $config; - $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 + $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]); + + $starting_tags = array_diff($starting_tags, $omitted_tags); + + if (count($starting_tags) === 0) { + // No valid starting tags, so can't look anything up + return; + } + + $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 $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 = $database->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(); + $database->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 $database, $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 $config, $database; + + + $wild_tags = $search; + $str_search = Tag::implode($search); + $related_tags = $database->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); - } - } + $related_tags = $database->get_all($query, $args); + $database->cache->set("related_tags:$str_search", $related_tags, 60 * 60); + } + } + if ($related_tags === false) { + return []; + } else { + return $related_tags; + } + } - if(!empty($related_tags)) { - $this->theme->display_refine_block($page, $related_tags, $wild_tags); - } - } -// }}} + + // }}} } - diff --git a/ext/tag_list/test.php b/ext/tag_list/test.php index 7fe82e72..8f56f76e 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..c62e354b 100644 --- a/ext/tag_list/theme.php +++ b/ext/tag_list/theme.php @@ -1,320 +1,313 @@ 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); + + $tag_category_css = ''; + $tag_category_style = ''; + $h_tag_split = explode(':', html_escape($tag), 2); + $category = ' '; - // 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']).';" '; - } + // 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']).';" '; + } - $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.''; + $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.''; - if($config->get_bool("tag_list_numbers")) { - $display_html .= " $count"; - } + if ($config->get_bool("tag_list_numbers")) { + $display_html .= " $count"; + } - return array($category, $display_html); - } + return [$category, $display_html]; + } - /** - * @param string $tag - * @param string[] $tags - * @return string - */ - protected function ars(/*string*/ $tag, /*array(string)*/ $tags) { - assert(is_array($tags)); + 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; + } - // 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 get_remove_link(array $tags, string $tag): string + { + if (!in_array($tag, $tags) && !in_array("-$tag", $tags)) { + return ""; + } else { + $tags = array_remove($tags, $tag); + $tags = array_remove($tags, "-$tag"); + return "R"; + } + } - /** - * @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_add_link(array $tags, string $tag): string + { + if (in_array($tag, $tags)) { + return ""; + } else { + $tags = array_remove($tags, "-$tag"); + $tags = array_add($tags, $tag); + return "A"; + } + } - /** - * @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_subtract_link(array $tags, string $tag): string + { + if (in_array("-$tag", $tags)) { + return ""; + } else { + $tags = array_remove($tags, $tag); + $tags = array_add($tags, "-$tag"); + return "S"; + } + } - /** - * @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"; - } - } - - /** - * @param string $tag - * @return string - */ - protected function tag_link($tag) { - $u_tag = url_escape($tag); - return make_link("post/list/$u_tag/1"); - } + protected function tag_link(string $tag): string + { + $u_tag = url_escape($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..b240b70b --- /dev/null +++ b/ext/tagger/info.php @@ -0,0 +1,19 @@ + + * Do not remove this notice. + */ + +class TaggerInfo extends ExtensionInfo +{ + public const KEY = "tagger"; + + public $key = self::KEY; + public $name = "Tagger"; + public $authors = ["Artanis (Erik Youngren)"=>"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..a0541e6a 100644 --- a/ext/tagger/main.php +++ b/ext/tagger/main.php @@ -1,150 +1,25 @@ - * Do not remove this notice. - */ -class Tagger extends Extension { - public function onDisplayingImage(DisplayingImageEvent $event) { - global $page, $user; +class Tagger extends Extension +{ + public function onDisplayingImage(DisplayingImageEvent $event) + { + global $page, $user; - if($user->can("edit_image_tag") && ($event->image->is_locked() || $user->can("edit_image_lock"))) { - $this->theme->build_tagger($page,$event); - } - } + 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); - } + 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..799877bd 100644 --- a/ext/tagger/style.css +++ b/ext/tagger/style.css @@ -31,7 +31,6 @@ } #tagger_body { max-height:175px; - overflow:auto; overflow-x:hidden; overflow-y:auto; } diff --git a/ext/tagger/theme.php b/ext/tagger/theme.php index 5b446382..fbdb0a1e 100644 --- a/ext/tagger/theme.php +++ b/ext/tagger/theme.php @@ -5,51 +5,59 @@ * 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, $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, + $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']) : ""; - $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"); + $url_form = make_link("tag_edit/set"); - // TODO: option for initial Tagger window placement. - $html = <<< EOD + // TODO: option for initial Tagger window placement. + $html = <<< EOD

    "; + } + $upload_list .= ""; + + $js2 = 'javascript:$(function() { $("#url'.$i.'").hide(); $("#url'.$i.'").val(""); $("#data'.$i.'").show(); });'; - $upload_list .= " + $upload_list .= "
    File
    "; - - if($tl_enabled) { - $js = 'javascript:$(function() { + + if ($tl_enabled) { + $js = 'javascript:$(function() { $("#data'.$i.'").hide(); $("#data'.$i.'").val(""); $("#url'.$i.'").show(); });'; - - $upload_list .= - " URL
    + + $upload_list .= + " URL "; - } else { - $upload_list .= " + } else { + $upload_list .= " "; - } - - $upload_list .= " + } + + $upload_list .= " "; - } + } - return $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 = ''; + 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 = ''; - $js='javascript:( + $js='javascript:( function() { if(typeof window=="undefined" || !window.location || window.location.href=="about:blank") { window.location = "'. $main_page .'"; @@ -219,19 +215,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"); - $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 +291,51 @@ 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) + { + $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'); + + 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 +344,5 @@ class UploadTheme extends Themelet {
    "; - } + } } - diff --git a/ext/user/events.php b/ext/user/events.php new file mode 100644 index 00000000..fae10b73 --- /dev/null +++ b/ext/user/events.php @@ -0,0 +1,84 @@ +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) + { + $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) + { + $this->username = $name; + $this->password = $pass; + $this->email = $email; + } +} + +class UserLoginEvent extends Event +{ + public $user; + public function __construct(User $user) + { + $this->user = $user; + } +} + +class UserDeletionEvent extends Event +{ + /** @var int */ + public $id; + + public function __construct(int $id) + { + $this->id = $id; + } +} diff --git a/ext/user/info.php b/ext/user/info.php new file mode 100644 index 00000000..f8b5943e --- /dev/null +++ b/ext/user/info.php @@ -0,0 +1,18 @@ +parts[$position])) $position++; - $this->parts[$position] = array("name" => $name, "link" => $link); - } +class UserCreationException extends SCoreException +{ } -class UserPageBuildingEvent extends Event { - /** @var \User */ - public $display_user; - /** @var array */ - public $stats = array(); - - /** - * @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; - } +class NullUserException extends SCoreException +{ } -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 UserDeletionEvent extends Event { - /** @var int */ - public $id; - - /** - * @param int $id - */ - public function __construct($id) { - $this->id = $id; - } -} - -class UserCreationException extends SCoreException {} - -class NullUserException extends SCoreException {} - -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 onPageRequest(PageRequestEvent $event) { - global $config, $database, $page, $user; - - $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; - - $q = "SELECT * FROM users WHERE 1=1"; - $a = array("offset"=>$offset, "limit"=>$limit); - - if(@$_GET['username']) { - $q .= " AND SCORE_STRNORM(name) LIKE SCORE_STRNORM(:name)"; - $a["name"] = '%' . $_GET['username'] . '%'; - } - - if($user->can('delete_user') && @$_GET['email']) { - $q .= " AND SCORE_STRNORM(name) LIKE SCORE_STRNORM(:email)"; - $a["email"] = '%' . $_GET['email'] . '%'; - } - - if(@$_GET['class']) { - $q .= " AND class LIKE :class"; - $a["class"] = $_GET['class']; - } - - $q .= " LIMIT :limit OFFSET :offset"; - - $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(); - } - - if(!$user->check_auth_token()) { - return; - } - - 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"])); - } - } - - 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..."); - } - } - } - - /** - * @param UserPageBuildingEvent $event - */ - public function onUserPageBuilding(UserPageBuildingEvent $event) { - global $user, $config; - - $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; - } - - $event->add_stats("Joined: $h_join_date", 10); - $event->add_stats("Class: $h_class", 90); - - $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 - ); - } - } - - /** - * @param UserPageBuildingEvent $event - */ - private function display_stats(UserPageBuildingEvent $event) { - global $user, $page, $config; - - 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)); - } - } - - /** - * @param SetupBuildingEvent $event - */ - public function onSetupBuilding(SetupBuildingEvent $event) { - global $config; - - $hosts = array( - "None" => "none", - "Gravatar" => "gravatar" - ); - - $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: "); - - 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: "); - } - - $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); - } - - /** - * @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); - } - - /** - * @param UserCreationEvent $event - */ - public function onUserCreation(UserCreationEvent $event) { - $this->check_user_creation($event); - $this->create_user($event); - } - - /** - * @param SearchTermParseEvent $event - */ - public function onSearchTermParse(SearchTermParseEvent $event) { - global $user; - - $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; - - - if(empty($name) || empty($pass)) { - $this->theme->display_error(400, "Error", "Username or password left blank"); - return; - } - - $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"); - - // 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"); - } - } - - 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"); - - // 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()); - } - } - - /** - * @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_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"); - } - - $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()); - } - } - } - - /** - * @param UserCreationEvent $event - * @throws UserCreationException - */ - private function check_user_creation(UserCreationEvent $event) { - $name = $event->username; - //$pass = $event->password; - //$email = $event->email; - - 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"); - } - } - - private function create_user(UserCreationEvent $event) { - global $database, $user; - - $email = (!empty($event->email)) ? $event->email : null; - - // 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'; - - $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})"); - } - - /** - * @param string $name - * @param string $pass - */ - private function set_login_cookie(/*string*/ $name, /*string*/ $pass) { - global $config, $page; - - $addr = get_session_ip($config); - $hash = User::by_name($name)->passhash; - - $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; - } - - 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; - } - } - - private function redirect_to_user(User $duser) { - global $page, $user; - - 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}")); - } - } - - private function change_name_wrapper(User $duser, $name) { - global $user; - - 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"); - } - } - - /** - * @param User $duser - * @param string $pass1 - * @param string $pass2 - */ - private function change_password_wrapper(User $duser, $pass1, $pass2) { - global $user; - - 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); - - if($duser->id == $user->id) { - $this->set_login_cookie($duser->name, $pass1); - } - - flash_message("Password changed"); - $this->redirect_to_user($duser); - } - } - } - - /** - * @param User $duser - * @param string $address - */ - private function change_email_wrapper(User $duser, /*string(email)*/ $address) { - global $user; - - if($this->user_can_edit_user($user, $duser)) { - $duser->set_email($address); - - flash_message("Email changed"); - $this->redirect_to_user($duser); - } - } - - /** - * @param User $duser - * @param string $class - * @throws NullUserException - */ - private function change_class_wrapper(User $duser, /*string(class)*/ $class) { - global $user; - - 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(" +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 onUserLogin(UserLoginEvent $event) + { + global $user; + $user = $event->user; + } + + public function onPageRequest(PageRequestEvent $event) + { + global $config, $database, $page, $user; + + $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); + } + } 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") { + $limit = 50; + + $page_num = int_escape($event->get_arg(1)); + if ($page_num <= 0) { + $page_num = 1; + } + $offset = ($page_num-1) * $limit; + + $q = "WHERE 1=1"; + $a = []; + + if (@$_GET['username']) { + $q .= " AND SCORE_STRNORM(name) LIKE SCORE_STRNORM(:name)"; + $a["name"] = '%' . $_GET['username'] . '%'; + } + + if ($user->can(Permissions::DELETE_USER) && @$_GET['email']) { + $q .= " AND SCORE_STRNORM(email) LIKE SCORE_STRNORM(:email)"; + $a["email"] = '%' . $_GET['email'] . '%'; + } + + if (@$_GET['class']) { + $q .= " AND class LIKE :class"; + $a["class"] = $_GET['class']; + } + $where = $database->scoreql_to_sql($q); + + $count = $database->get_one("SELECT count(*) FROM users $where", $a); + $a["offset"] = $offset; + $a["limit"] = $limit; + $rows = $database->get_all("SELECT * FROM users $where LIMIT :limit OFFSET :offset", $a); + $users = array_map("_new_user", $rows); + $this->theme->display_user_list($page, $users, $user, $page_num, $count/$limit); + } elseif ($event->get_arg(0) == "logout") { + $this->page_logout(); + } + + 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 ($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..." + ); + } + } + } + + public function onUserPageBuilding(UserPageBuildingEvent $event) + { + global $user, $config; + + $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; + } + + $event->add_stats("Joined: $h_join_date", 10); + $event->add_stats("Class: $h_class", 90); + + $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 + ); + } + } + + 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); + } + } + + + private function display_stats(UserPageBuildingEvent $event) + { + global $user, $page, $config; + + ksort($event->stats); + $this->theme->display_user_page($event->display_user, $event->stats); + + if (!$user->is_anonymous()) { + if ($user->id == $event->display_user->id || $user->can("edit_user_info")) { + $uobe = new UserOptionsBuildingEvent(); + send_event($uobe); + + $page->add_block(new Block("Options", $this->theme->build_options($event->display_user, $uobe), "main", 60)); + } + } + + + 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) + ); + } + } + + public function onSetupBuilding(SetupBuildingEvent $event) + { + global $config; + + $hosts = [ + "None" => "none", + "Gravatar" => "gravatar" + ]; + + $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: "); + + 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: " + ); + } + + $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); + } + + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + global $user; + if ($event->parent==="system") { + if ($user->can(Permissions::EDIT_USER_CLASS)) { + $event->add_nav_link("user_admin", new Link('user_admin/list'), "User List", NavLink::is_active(["user_admin"])); + } + } + } + + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + $event->add_link("My Profile", make_link("user")); + if ($user->can(Permissions::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 onUserCreation(UserCreationEvent $event) + { + $this->check_user_creation($event); + $this->create_user($event); + } + + public function onSearchTermParse(SearchTermParseEvent $event) + { + global $user; + + $matches = []; + 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")); + } 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'")); + } + } + + 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); + } + } + + + 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; + + + if (empty($name) || empty($pass)) { + $this->theme->display_error(400, "Error", "Username or password left blank"); + return; + } + + $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 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"); + } + } + + 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(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()); + } + } + + private function page_recover(string $username) + { + $user = User::by_name($username); + if (is_null($user)) { + $this->theme->display_error(404, "Error", "There's no user with that name"); + } elseif (is_null($user->email)) { + $this->theme->display_error(400, "Error", "That user has no registered email address"); + } else { + // send email + } + } + + private function page_create() + { + global $config, $page; + 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"); + } + + $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()); + } + } + } + + private function check_user_creation(UserCreationEvent $event) + { + $name = $event->username; + //$pass = $event->password; + //$email = $event->email; + + 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"); + } + } + + private function create_user(UserCreationEvent $event) + { + global $database, $user; + + $email = (!empty($event->email)) ? $event->email : null; + + // 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'; + + $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)); + + log_info("user", "Created User #$uid ({$event->username})"); + } + + private function set_login_cookie(string $name, string $pass) + { + global $config, $page; + + $addr = get_session_ip($config); + $hash = User::by_name($name)->passhash; + + $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 {{{ + 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; + } + + 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; + } + } + + private function redirect_to_user(User $duser) + { + global $page, $user; + + 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}")); + } + } + + private function change_name_wrapper(User $duser, $name) + { + global $user; + + if ($user->can(Permissions::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 change_password_wrapper(User $duser, string $pass1, string $pass2) + { + global $user; + + 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); + + if ($duser->id == $user->id) { + $this->set_login_cookie($duser->name, $pass1); + } + + flash_message("Password changed"); + $this->redirect_to_user($duser); + } + } + } + + private function change_email_wrapper(User $duser, string $address) + { + global $user; + + if ($this->user_can_edit_user($user, $duser)) { + $duser->set_email($address); + + flash_message("Email changed"); + $this->redirect_to_user($duser); + } + } + + private function change_class_wrapper(User $duser, string $class) + { + global $user; + + if ($user->class->name == "admin") { + $duser->set_class($class); + flash_message("Class changed"); + $this->redirect_to_user($duser); + } + } + // }}} + // ips {{{ + private function count_upload_ips(User $duser): array + { + global $database; + $rows = $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]); + return $rows; + } - /** - * @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; + $rows = $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]); + return $rows; + } - /** - * @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; + $rows = $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]); + return $rows; + } - 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($_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..9b3d00af 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"); +class UserPageTest extends ShimmiePHPUnitTestCase +{ + public function testUserPage() + { + $this->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..fbed0d82 100644 --- a/ext/user/theme.php +++ b/ext/user/theme.php @@ -1,96 +1,137 @@ 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()); + /** + * #param User[] $users + */ + public function display_user_list(Page $page, array $users, User $user, int $page_num, int $page_total) + { + $page->set_title("User List"); + $page->set_heading("User List"); + $page->add_block(new NavBlock()); - $html = "
    "; + $html = "
    "; - $html .= ""; - $html .= ""; - if($user->can('delete_user')) - $html .= ""; - $html .= ""; - $html .= ""; - $html .= ""; + $html .= ""; + $html .= ""; + if ($user->can(Permissions::DELETE_USER)) { + $html .= ""; + } + $html .= ""; + $html .= ""; + $html .= ""; - $h_username = html_escape(@$_GET['username']); - $h_email = html_escape(@$_GET['email']); - $h_class = html_escape(@$_GET['class']); + $h_username = html_escape(@$_GET['username']); + $h_email = html_escape(@$_GET['email']); + $h_class = html_escape(@$_GET['class']); - $html .= "" . make_form("user_admin/list", "GET"); - $html .= ""; - if($user->can('delete_user')) - $html .= ""; - $html .= ""; - $html .= ""; - $html .= ""; + $html .= "" . make_form("user_admin/list", "GET"); + $html .= ""; + if ($user->can(Permissions::DELETE_USER)) { + $html .= ""; + } + $html .= ""; + $html .= ""; + $html .= ""; - 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"); + 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"); - $html .= ""; - $html .= ""; - if($user->can('delete_user')) - $html .= ""; - $html .= ""; - $html .= ""; - $html .= ""; - } + $html .= ""; + $html .= ""; + if ($user->can(Permissions::DELETE_USER)) { + $html .= ""; + } + $html .= ""; + $html .= ""; + $html .= ""; + } - $html .= "
    NameEmailClassAction
    NameEmailClassAction
    $h_name$h_email$h_classShow Posts
    $h_name$h_email$h_classShow Posts
    "; + $html .= ""; - $page->add_block(new Block("Users", $html)); - } + $page->add_block(new Block("Users", $html)); + $this->display_paginator($page, "user_admin/list", $this->get_args(), $page_num, $page_total); + } - public function display_user_links(Page $page, User $user, $parts) { - # $page->add_block(new Block("User Links", join(", ", $parts), "main", 10)); - } + protected function ueie($var) + { + if (isset($_GET[$var])) { + return $var."=".url_escape($_GET[$var]); + } else { + return ""; + } + } + protected function get_args() + { + $args = ""; + // Check if each arg is actually empty and skip it if so + if (strlen($this->ueie("username"))) { + $args .= $this->ueie("username")."&"; + } + if (strlen($this->ueie("email"))) { + $args .= $this->ueie("email")."&"; + } + if (strlen($this->ueie("class"))) { + $args .= $this->ueie("class")."&"; + } + // If there are no args at all, set $args to null to prevent an unnecessary ? at the end of the paginator url + if (strlen($args) == 0) { + $args = null; + } + return $args; + } - 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)); - } + public function display_user_links(Page $page, User $user, $parts) + { + # $page->add_block(new Block("User Links", join(", ", $parts), "main", 10)); + } - public function display_signup_page(Page $page) { - global $config; - $tac = $config->get_string("login_tac", ""); + 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)); + } - if($config->get_bool("login_tac_bbcode")) { - $tfe = new TextFormattingEvent($tac); - send_event($tfe); - $tac = $tfe->formatted; - } + public function display_signup_page(Page $page) + { + global $config; + $tac = $config->get_string("login_tac", ""); - if(empty($tac)) {$html = "";} - else {$html = '

    '.$tac.'

    ';} + if ($config->get_bool("login_tac_bbcode")) { + $tfe = new TextFormattingEvent($tac); + send_event($tfe); + $tac = $tfe->formatted; + } - $h_reca = "".captcha_get_html().""; + if (empty($tac)) { + $html = ""; + } else { + $html = '

    '.$tac.'

    '; + } - $html .= ' + $h_reca = "".captcha_get_html().""; + + $html .= ' '.make_form(make_link("user_admin/create"))." @@ -107,23 +148,27 @@ class UserPageTheme extends Themelet { "; - $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("Create Account"); + $page->set_heading("Create Account"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Signup", $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_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_login_block(Page $page) { - global $config; - $html = ' + public function display_login_block(Page $page) + { + global $config; + $html = ' '.make_form(make_link("user_admin/login"))."
    @@ -142,69 +187,71 @@ class UserPageTheme extends Themelet {
    "; - if($config->get_bool("login_signup_enabled")) { - $html .= "Create Account"; - } - $page->add_block(new Block("Login", $html, "left", 90)); - } + if ($config->get_bool("login_signup_enabled")) { + $html .= "Create Account"; + } + $page->add_block(new Block("Login", $html, "left", 90)); + } - /** - * @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; - } - } + public function display_ip_list(Page $page, array $uploads, array $comments, array $events) + { + $html = ""; + $html .= ""; - $html .= "
    Uploaded from: "; + $n = 0; + foreach ($uploads as $ip => $count) { + $html .= '
    '.$ip.' ('.$count.')'; + if (++$n >= 20) { + $html .= "
    ..."; + break; + } + } - $html .= "
    Commented from:"; - $n = 0; - foreach($comments as $ip => $count) { - $html .= '
    '.$ip.' ('.$count.')'; - if(++$n >= 20) { - $html .= "
    ..."; - break; - } - } + $html .= "
    Commented from:"; + $n = 0; + foreach ($comments as $ip => $count) { + $html .= '
    '.$ip.' ('.$count.')'; + if (++$n >= 20) { + $html .= "
    ..."; + break; + } + } - $html .= "
    (Most recent at top)
    "; + $html .= "
    Logged Events:"; + $n = 0; + foreach ($events as $ip => $count) { + $html .= '
    '.$ip.' ('.$count.')'; + if (++$n >= 20) { + $html .= "
    ..."; + break; + } + } - $page->add_block(new Block("IPs", $html, "main", 70)); - } + $html .= "
    (Most recent at top)
    "; - public function display_user_page(User $duser, $stats) { - global $page, $user; - assert(is_array($stats)); - $stats[] = 'User ID: '.$duser->id; + $page->add_block(new Block("IPs", $html, "main", 70)); + } - $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_user_page(User $duser, $stats) + { + global $page, $user; + assert(is_array($stats)); + $stats[] = 'User ID: '.$duser->id; - 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)); - } - } - } + $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)); + } - 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 .= " + public function build_options(User $duser, UserOptionsBuildingEvent $event) + { + 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(Permissions::EDIT_USER_NAME)) { + $html .= "

    ".make_form(make_link("user_admin/change_name"))." @@ -213,10 +260,10 @@ class UserPageTheme extends Themelet {
    - "; - } +

    "; + } - $html .= " + $html .= "

    ".make_form(make_link("user_admin/change_pass"))." @@ -232,7 +279,7 @@ class UserPageTheme extends Themelet {
    - +

    ".make_form(make_link("user_admin/change_email"))." @@ -241,20 +288,20 @@ class UserPageTheme extends Themelet {
    - "; +

    "; - $i_user_id = int_escape($duser->id); + $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 .= " + if ($user->can(Permissions::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"))." @@ -263,11 +310,11 @@ class UserPageTheme extends Themelet {
    - "; - } +

    "; + } - if($user->can("delete_user")) { - $html .= " + if ($user->can(Permissions::DELETE_USER)) { + $html .= "

    ".make_form(make_link("user_admin/delete_user"))." @@ -284,11 +331,39 @@ class UserPageTheme extends Themelet {
    - "; - } - } - return $html; - } -// }}} -} +

    "; + } + foreach ($event->parts as $part) { + $html .= $part; + } + } + return $html; + } + // }}} + public function get_help_html() + { + global $user; + $output = '

    Search for images posted by particular individuals.

    +
    +
    poster=username
    +

    Returns images posted by "username".

    +
    +
    +
    poster_id=123
    +

    Returns images posted by user 123.

    +
    + '; + + + if ($user->can(Permissions::VIEW_IP)) { + $output .=" +
    +
    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..75538a41 --- /dev/null +++ b/ext/user_config/info.php @@ -0,0 +1,21 @@ + + * Description: Provides system-wide support for user-specific settings + * Visibility: admin + */ + +class UserConfigInfo extends ExtensionInfo +{ + public const KEY = "user_config"; + + public $key = self::KEY; + public $name = "User-specific settings"; + public $authors = ["Matthew Barbour"=>"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..91aa375e --- /dev/null +++ b/ext/user_config/main.php @@ -0,0 +1,68 @@ +user = $user; + $this->user_config = $user_config; + } +} + +class UserConfig extends Extension +{ + private const VERSION = "ext_user_config_version"; + + public function onInitExt(InitExtEvent $event) + { + global $config; + + if ($config->get_int(self::VERSION, 0)<1) { + $this->install(); + } + } + + 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)); + } + + private function install(): void + { + global $config, $database; + + if ($config->get_int(self::VERSION, 0) < 1) { + log_info("upgrade", "Adding user config table"); + + $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)"); + + $config->set_int(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..eacaa0cd --- /dev/null +++ b/ext/varnish/info.php @@ -0,0 +1,22 @@ + +* License: GPLv2 +* Visibility: admin +* Description: Sends PURGE requests when a /post/view is updated +*/ + +class VarnishPurgerInfo extends ExtensionInfo +{ + public const KEY = "varnish"; + + public $key = self::KEY; + public $name = "Varnish Purger"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $visibility = self::VISIBLE_ADMIN; + public $description = "Sends PURGE requests when a /post/view is updated"; +} diff --git a/ext/varnish/main.php b/ext/varnish/main.php index bb3882f1..60fd58a7 100644 --- a/ext/varnish/main.php +++ b/ext/varnish/main.php @@ -1,42 +1,42 @@ -* License: GPLv2 -* Visibility: admin -* Description: Sends PURGE requests when a /post/view is updated -*/ -class VarnishPurger extends Extension { - private function curl_purge($path) { - // waiting for curl timeout adds ~5 minutes to unit tests - if(defined("UNITTEST")) return; +class VarnishPurger extends Extension +{ + private function curl_purge($path) + { + // waiting for curl timeout adds ~5 minutes to unit tests + if (defined("UNITTEST")) { + return; + } - $url = make_http(make_link($path)); - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PURGE"); - curl_setopt($ch, CURLOPT_TIMEOUT, 5); - $result = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - //return $result; - } + $url = make_http(make_link($path)); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PURGE"); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + $result = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + //return $result; + } - public function onCommentPosting(CommentPostingEvent $event) { - $this->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..872e425e --- /dev/null +++ b/ext/view/events/displaying_image_event.php @@ -0,0 +1,33 @@ +image = $image; + } + + public function get_image(): Image + { + return $this->image; + } + + public function set_title(String $title) + { + $this->title = $title; + } +} 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..971fe97e --- /dev/null +++ b/ext/view/events/image_admin_block_building_event.php @@ -0,0 +1,25 @@ +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..61577490 --- /dev/null +++ b/ext/view/events/image_info_box_building_event.php @@ -0,0 +1,25 @@ +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..a870f328 --- /dev/null +++ b/ext/view/events/image_info_set_event.php @@ -0,0 +1,12 @@ +image = $image; + } +} diff --git a/ext/view/info.php b/ext/view/info.php new file mode 100644 index 00000000..a8e9cc81 --- /dev/null +++ b/ext/view/info.php @@ -0,0 +1,20 @@ +image = $image; - } - /** - * @return Image - */ - public function get_image() { - return $this->image; - } +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 = []; + $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)) { + $die = new DisplayingImageEvent($image); + send_event($die); + $page->set_title(html_escape($die->title)); + $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']); + + send_event(new ImageInfoSetEvent(Image::by_id($image_id))); + + $page->set_mode(PageMode::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_meta_headers($event->get_image()); + + $event->title = "Image {$event->get_image()->id}: ".$event->get_image()->get_tag_list(); + + $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..a74f1c7d --- /dev/null +++ b/ext/view/script.js @@ -0,0 +1,12 @@ +$(document).ready(function() { + 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; + }); + } +}); diff --git a/ext/view/test.php b/ext/view/test.php index d4ae305c..d3d118f0 100644 --- a/ext/view/test.php +++ b/ext/view/test.php @@ -1,66 +1,71 @@ 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"); + $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; - $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->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"); + $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->click("Prev"); - $this->assert_title("Image $image_id_2: test2"); + $this->click("Prev"); + $this->assert_title("Image $image_id_2: test2"); - $this->click("Next"); - $this->assert_title("Image $image_id_1: test"); + $this->click("Next"); + $this->assert_title("Image $image_id_1: test"); - $this->click("Next"); - $this->assert_title("Image not found"); - } + $this->click("Next"); + $this->assert_title("Image not found"); + } - 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/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; - $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'); - } + $this->get_page('post/view/-1'); + $this->assert_title('Image not found'); + } - public function testNextSearchResult() { - $this->markTestIncomplete(); + 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"); + $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"); - } + // 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"); + } } - diff --git a/ext/view/theme.php b/ext/view/theme.php index 17b96694..ba085159 100644 --- a/ext/view/theme.php +++ b/ext/view/theme.php @@ -1,54 +1,58 @@ 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_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)); - } - } + 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 build_pin(Image $image) + { + if (isset($_GET['search'])) { + $query = "search=".url_escape($_GET['search']); + } else { + $query = null; + } - $h_prev = "Prev"; - $h_index = "Index"; - $h_next = "Next"; + $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 +60,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..b700e60f --- /dev/null +++ b/ext/wiki/info.php @@ -0,0 +1,23 @@ + + * License: GPLv2 + * Description: A simple wiki, for those who don't want the hugeness of mediawiki + * Documentation: + * + */ + +class WikiInfo extends ExtensionInfo +{ + public const KEY = "wiki"; + + public $key = self::KEY; + public $name = "Simple Wiki"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "A simple wiki, for those who don't want the hugeness of mediawiki"; + public $documentation = "Standard formatting APIs are used (This will be BBCode by default)"; +} diff --git a/ext/wiki/main.php b/ext/wiki/main.php index 5d5d28da..189a7797 100644 --- a/ext/wiki/main.php +++ b/ext/wiki/main.php @@ -1,96 +1,84 @@ - * 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) - */ -class WikiUpdateEvent extends Event { - /** @var \User */ - public $user; - /** @var \WikiPage */ - public $wikipage; +class WikiUpdateEvent extends Event +{ + /** @var User */ + public $user; + /** @var WikiPage */ + public $wikipage; - /** - * @param User $user - * @param WikiPage $wikipage - */ - public function __construct(User $user, WikiPage $wikipage) { - $this->user = $user; - $this->wikipage = $wikipage; - } + public function __construct(User $user, WikiPage $wikipage) + { + $this->user = $user; + $this->wikipage = $wikipage; + } } -class WikiUpdateException extends SCoreException { +class WikiUpdateException extends SCoreException +{ } -class WikiPage { - /** @var int|string */ - public $id; +class WikiPage +{ + /** @var int|string */ + public $id; - /** @var int */ - public $owner_id; + /** @var int */ + public $owner_id; - /** @var string */ - public $owner_ip; + /** @var string */ + public $owner_ip; - /** @var string */ - public $date; + /** @var string */ + public $date; - /** @var string */ - public $title; + /** @var string */ + public $title; - /** @var int */ - public $revision; + /** @var int */ + public $revision; - /** @var bool */ - public $locked; + /** @var bool */ + public $locked; - /** @var string */ - public $body; + /** @var string */ + public $body; - /** - * @param mixed $row - */ - public function __construct($row=null) { - //assert(!empty($row)); + public function __construct(array $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']; - } - } + 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); - } + public function get_owner(): User + { + return User::by_id($this->owner_id); + } - /** - * @return bool - */ - public function is_locked() { - return $this->locked; - } + public function is_locked(): bool + { + return $this->locked; + } } -class Wiki extends Extension { - public function onInitExt(InitExtEvent $event) { - global $database, $config; +class Wiki extends Extension +{ + public function onInitExt(InitExtEvent $event) + { + global $database, $config; - if($config->get_int("ext_wiki_version", 0) < 1) { - $database->create_table("wiki_pages", " + if ($config->get_int("ext_wiki_version", 0) < 1) { + $database->create_table("wiki_pages", " id SCORE_AIPK, owner_id INTEGER NOT NULL, owner_ip SCORE_INET NOT NULL, @@ -102,425 +90,417 @@ 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 + $config->set_int("ext_wiki_version", 2); + } + if ($config->get_int("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); - } - } + $config->set_int("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 (is_null($event->get_arg(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)) { + global $database; + $database->Execute( + "DELETE FROM wiki_pages WHERE title=:title AND revision=:rev", + ["title"=>$_POST["title"], "rev"=>$_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)) { + global $database; + $database->Execute( + "DELETE FROM wiki_pages WHERE title=:title", + ["title"=>$_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 (?, ?, now(), ?, ?, ?, ?)", [$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 :-("); + } + } - /** - * 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; + /** + * 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; + } - // anon / user can't ever edit locked pages - if($page->is_locked()) return false; + // anon / user can't ever edit locked pages + if ($page->is_locked()) { + return false; + } - // anon / user can edit if allowed by config - if($user->can("edit_wiki_page")) return true; + // anon / user can edit if allowed by config + if ($user->can(Permissions::EDIT_WIKI_PAGE)) { + return true; + } - return false; - } + 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(" + private function get_page(string $title, int $revision=-1): WikiPage + { + global $database; + // first try and get the actual page + $row = $database->get_row( + $database->scoreql_to_sql(" SELECT * FROM wiki_pages WHERE SCORE_STRNORM(title) LIKE SCORE_STRNORM(:title) ORDER BY revision DESC"), - array("title"=>$title)); + ["title"=>$title] + ); - // fall back to wiki:default - if(empty($row)) { - $row = $database->get_row(" + // 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")); + 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 + // php-diff {{{ + /** + 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) + ? 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 Exception("stat needs to be =, + or -"); + } + } + // }}} } - diff --git a/ext/wiki/test.php b/ext/wiki/test.php index 8d6e9bb2..dfd6d71b 100644 --- a/ext/wiki/test.php +++ b/ext/wiki/test.php @@ -1,122 +1,130 @@ get_page("wiki"); - $this->assert_title("Index"); - $this->assert_text("This is a default page"); - } +class WikiTest extends ShimmiePHPUnitTestCase +{ + public function testIndex() + { + $this->get_page("wiki"); + $this->assert_title("Index"); + $this->assert_text("This is a default page"); + } - public function testAccess() { - $this->markTestIncomplete(); + public function testAccess() + { + $this->markTestIncomplete(); - 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); - } + 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); + } - 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->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 ($allowed || $user == "admin") { + $this->get_page("wiki/test", ['edit'=>'on']); + $this->assert_text("Editor"); + } else { + $this->get_page("wiki/test", ['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 testLock() { - $this->markTestIncomplete(); + public function testLock() + { + $this->markTestIncomplete(); - global $config; - $config->set_bool("wiki_edit_anon", true); - $config->set_bool("wiki_edit_user", false); + global $config; + $config->set_bool("wiki_edit_anon", true); + $config->set_bool("wiki_edit_user", false); - $this->log_in_as_admin(); + $this->log_in_as_admin(); - $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(); + $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(); - $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(); + $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(); - $this->get_page("wiki/test_locked"); - $this->assert_title("test_locked"); - $this->assert_text("test_locked content"); - $this->assert_no_text("Edit"); + $this->get_page("wiki/test_locked"); + $this->assert_title("test_locked"); + $this->assert_text("test_locked content"); + $this->assert_no_text("Edit"); - $this->log_in_as_admin(); - $this->get_page("wiki/test_locked"); - $this->click("Delete All"); - $this->log_out(); - } + $this->log_in_as_admin(); + $this->get_page("wiki/test_locked"); + $this->click("Delete All"); + $this->log_out(); + } - public function testDefault() { - $this->markTestIncomplete(); + public function testDefault() + { + $this->markTestIncomplete(); - $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"); + $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"); - $this->get_page("wiki/something"); - $this->assert_text("Empty page! Fill it!"); + $this->get_page("wiki/something"); + $this->assert_text("Empty page! Fill it!"); - $this->get_page("wiki/wiki:default"); - $this->click("Delete All"); - $this->log_out(); - } + $this->get_page("wiki/wiki:default"); + $this->click("Delete All"); + $this->log_out(); + } - public function testRevisions() { - $this->markTestIncomplete(); + 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(); - } + $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(); + } } - diff --git a/ext/wiki/theme.php b/ext/wiki/theme.php index 662159fb..27c24dda 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 = int_escape($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,28 +61,29 @@ 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"))." @@ -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..89b76079 --- /dev/null +++ b/ext/word_filter/info.php @@ -0,0 +1,21 @@ + + * Link: http://code.shishnet.org/shimmie2/ + * License: GPLv2 + * Description: Simple search and replace + */ + +class WordFilterInfo extends ExtensionInfo +{ + public const KEY = "word_filter"; + + public $key = self::KEY; + public $name = "Word Filter"; + public $url = self::SHIMMIE_URL; + public $authors = self::SHISH_AUTHOR; + public $license = self::LICENSE_GPLV2; + public $description = "Simple search and replace"; +} diff --git a/ext/word_filter/main.php b/ext/word_filter/main.php index 9a12b4fa..4e15edd0 100644 --- a/ext/word_filter/main.php +++ b/ext/word_filter/main.php @@ -1,63 +1,58 @@ - * Link: http://code.shishnet.org/shimmie2/ - * License: GPLv2 - * Description: Simple search and replace - */ -class WordFilter extends Extension { - // before emoticon filter - public function get_priority() {return 40;} +class WordFilter extends Extension +{ + // before emoticon filter + public function get_priority(): int + { + return 40; + } - public function onTextFormatting(TextFormattingEvent $event) { - $event->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..c75069b2 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"); - } +class WordFilterTest extends ShimmiePHPUnitTestCase +{ + public function setUp() + { + global $config; + parent::setUp(); + $config->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..51f3a7a7 100644 --- a/index.php +++ b/index.php @@ -43,29 +43,24 @@ * 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; +if (!file_exists("data/config/shimmie.conf.php")) { + require_once "core/_install.php"; + exit; } -if(!file_exists("vendor/")) { - //CHECK: Should we just point to install.php instead? Seems unsafe though. - print << Shimmie Error - - + +
    @@ -73,40 +68,54 @@ 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 "core/_bootstrap.php"; +//$_tracer->mark(@$_SERVER["REQUEST_URI"]); +$_tracer->begin($_SERVER["REQUEST_URI"] ?? "No Request"); + try { - require_once "core/_bootstrap.inc.php"; - ctx_log_start(@$_SERVER["REQUEST_URI"], true, true); + // 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(); + } - // 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 ($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(); - $database->commit(); - ctx_log_endok(); -} -catch(Exception $e) { - if($database) $database->rollback(); - _fatal_error($e); - ctx_log_ender(); + // 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); } +$_tracer->end(); +if (TRACE_FILE) { + if ((microtime(true) - $_shm_load_start) > TRACE_THRESHOLD) { + $_tracer->flush(TRACE_FILE); + } +} 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/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/bootstrap.php b/tests/bootstrap.php index 961c0c0b..dfb761c8 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -8,153 +8,170 @@ define("CLI_LOG_LEVEL", 50); $_SERVER['QUERY_STRING'] = '/'; chdir(dirname(dirname(__FILE__))); -require_once "core/_bootstrap.inc.php"; +require_once "core/_bootstrap.php"; -if(is_null(User::by_name("demo"))) { - $userPage = new UserPage(); - $userPage->onUserCreation(new UserCreationEvent("demo", "demo", "")); - $userPage->onUserCreation(new UserCreationEvent("test", "test", "")); +if (is_null(User::by_name("demo"))) { + $userPage = new UserPage(); + $userPage->onUserCreation(new UserCreationEvent("demo", "demo", "")); + $userPage->onUserCreation(new UserCreationEvent("test", "test", "")); } -abstract class ShimmiePHPUnitTestCase extends \PHPUnit_Framework_TestCase { - protected $backupGlobalsBlacklist = array('database', 'config'); - private $images = array(); +abstract class ShimmiePHPUnitTestCase extends \PHPUnit\Framework\TestCase +{ + private $images = []; - 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"); - } + public function setUp() + { + $class = str_replace("Test", "", get_class($this)); + if (!class_exists($class)) { + $this->markTestSkipped("$class not loaded"); + } elseif (!ExtensionInfo::get_for_extension_class($class)->is_supported()) { + $this->markTestSkipped("$class not supported with this database"); + } - // things to do after bootstrap and before request - // log in as anon - $this->log_out(); - } + // things to do after bootstrap and before request + // log in as anon + $this->log_out(); + } - public function tearDown() { - foreach($this->images as $image_id) { - $this->delete_image($image_id); - } - } + public function tearDown() + { + foreach ($this->images as $image_id) { + $this->delete_image($image_id); + } + } - 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; - } - } + protected function get_page($page_name, $args=null) + { + // use a fresh page + global $page; + if (!$args) { + $args = []; + } + $_GET = $args; + $_POST = []; + $page = class_exists("CustomPage") ? new CustomPage() : new Page(); + send_event(new PageRequestEvent($page_name)); + if ($page->mode == PageMode::REDIRECT) { + $page->code = 302; + } + } - // page things - protected function assert_title($title) { - global $page; - $this->assertContains($title, $page->title); - } + protected function post_page($page_name, $args=null) + { + // use a fresh page + global $page; + if (!$args) { + $args = []; + } + $_GET = []; + $_POST = $args; + $page = class_exists("CustomPage") ? new CustomPage() : new Page(); + send_event(new PageRequestEvent($page_name)); + if ($page->mode == PageMode::REDIRECT) { + $page->code = 302; + } + } - protected function assert_no_title($title) { - global $page; - $this->assertNotContains($title, $page->title); - } + // page things + protected function assert_title(string $title) + { + global $page; + $this->assertContains($title, $page->title); + } - /** - * @param integer $code - */ - protected function assert_response($code) { - global $page; - $this->assertEquals($code, $page->code); - } + protected function assert_no_title(string $title) + { + global $page; + $this->assertNotContains($title, $page->title); + } - 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; - } + protected function assert_response(int $code) + { + global $page; + $this->assertEquals($code, $page->code); + } - protected function assert_text($text, $section=null) { - $this->assertContains($text, $this->page_to_text($section)); - } + protected function page_to_text(string $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; + } - /** - * @param string $text - */ - protected function assert_no_text($text, $section=null) { - $this->assertNotContains($text, $this->page_to_text($section)); - } + protected function assert_text(string $text, string $section=null) + { + $this->assertContains($text, $this->page_to_text($section)); + } - /** - * @param string $content - */ - protected function assert_content($content) { - global $page; - $this->assertContains($content, $page->data); - } + protected function assert_no_text(string $text, string $section=null) + { + $this->assertNotContains($text, $this->page_to_text($section)); + } - /** - * @param string $content - */ - protected function assert_no_content($content) { - global $page; - $this->assertNotContains($content, $page->data); - } + protected function assert_content(string $content) + { + global $page; + $this->assertContains($content, $page->data); + } - // user things - protected function log_in_as_admin() { - global $user; - $user = User::by_name('demo'); - $this->assertNotNull($user); - } + protected function assert_no_content(string $content) + { + global $page; + $this->assertNotContains($content, $page->data); + } - protected function log_in_as_user() { - global $user; - $user = User::by_name('test'); - $this->assertNotNull($user); - } + // user things + protected function log_in_as_admin() + { + global $user; + $user = User::by_name('demo'); + $this->assertNotNull($user); + send_event(new UserLoginEvent($user)); + } - protected function log_out() { - global $user, $config; - $user = User::by_id($config->get_int("anon_id", 0)); - $this->assertNotNull($user); - } + protected function log_in_as_user() + { + global $user; + $user = User::by_name('test'); + $this->assertNotNull($user); + send_event(new UserLoginEvent($user)); + } - // 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 log_out() + { + global $user, $config; + $user = User::by_id($config->get_int("anon_id", 0)); + $this->assertNotNull($user); + send_event(new UserLoginEvent($user)); + } - /** - * @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); - } - } + // 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); + $this->images[] = $dae->image_id; + 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/docker-init.sh b/tests/docker-init.sh new file mode 100644 index 00000000..09ce87e3 --- /dev/null +++ b/tests/docker-init.sh @@ -0,0 +1,3 @@ +#!/bin/sh +echo " 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..dc35c942 100644 --- a/tests/router.php +++ b/tests/router.php @@ -1,23 +1,25 @@ 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 = 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); - $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 index 4b36216c..f5641dc6 100644 --- a/themes/danbooru/custompage.class.php +++ b/themes/danbooru/custompage.class.php @@ -1,11 +1,12 @@ left_enabled = false; - } + public function disable_left() + { + $this->left_enabled = false; + } } - diff --git a/themes/danbooru/index.theme.php b/themes/danbooru/index.theme.php index 4c966ebd..9d55abd6 100644 --- a/themes/danbooru/index.theme.php +++ b/themes/danbooru/index.theme.php @@ -1,52 +1,48 @@ 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 = 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->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(); + $h_search = "

    @@ -54,22 +50,17 @@ class CustomIndexTheme extends IndexTheme {
    "; - return $h_search; - } + 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 index 9ac3a7b4..68c315f1 100644 --- a/themes/danbooru/layout.class.php +++ b/themes/danbooru/layout.class.php @@ -2,7 +2,7 @@ /** * Name: Danbooru Theme * Author: Bzchan -* Link: http://trac.shishnet.org/shimmie2/ +* 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 @@ -26,12 +26,12 @@ Changes in this theme include - $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) + 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. @@ -42,154 +42,96 @@ Tips * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -class Layout { - public function display_page(Page $page) { - global $config, $user; +class Layout +{ + public function display_page(Page $page, array $nav_links, array $sub_links) + { + 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(); + $theme_name = $config->get_string(SetupConfig::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 = ""; + $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; - } - } + 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(); + $debug = get_debug_info(); - $contact = empty($contact_link) ? "" : "
    Contact"; + $contact = empty($contact_link) ? "" : "
    Contact"; - if(empty($this->subheading)) { - $subheading = ""; - } - else { - $subheading = "

    {$this->subheading}
    "; - } + 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 + $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 - // 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_links = ""; + foreach ($nav_links as $nav_link) { + $custom_links .= "
  • ".$this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)."
  • "; + } - $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; - } + $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: 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

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

    $site_name

    "; - if($page->left_enabled) { - $left = ""; - $withleft = "withleft"; - } - else { - $left = ""; - $withleft = "noleft"; - } + 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]"; - } + $flash = $page->get_cookie("flash_message"); + $flash_html = ""; + if ($flash) { + $flash_html = "".nl2br(html_escape($flash))." [X]"; + } - print << @@ -220,10 +162,10 @@ $header_html