merge
This commit is contained in:
commit
794e4ebb7d
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
vendor
|
||||||
|
.git
|
||||||
|
*.phar
|
||||||
|
data
|
||||||
|
images
|
||||||
|
thumbs
|
||||||
|
composer.lock
|
||||||
|
*.sqlite
|
||||||
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@ -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
|
||||||
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,10 +2,9 @@ backup
|
|||||||
data
|
data
|
||||||
images
|
images
|
||||||
thumbs
|
thumbs
|
||||||
!lib/images
|
|
||||||
*.phar
|
*.phar
|
||||||
*.sqlite
|
*.sqlite
|
||||||
/lib/vendor/
|
.php_cs.cache
|
||||||
|
|
||||||
#Composer
|
#Composer
|
||||||
composer.phar
|
composer.phar
|
||||||
|
|||||||
@ -17,8 +17,8 @@
|
|||||||
# rather than link to images/ha/hash and have an ugly filename,
|
# rather than link to images/ha/hash and have an ugly filename,
|
||||||
# we link to images/hash/tags.ext; mod_rewrite splits things so
|
# we link to images/hash/tags.ext; mod_rewrite splits things so
|
||||||
# that shimmie sees hash and the user sees tags.ext
|
# that shimmie sees hash and the user sees tags.ext
|
||||||
RewriteRule ^_images/([0-9a-f]{2})([0-9a-f]{30}).*$ images/$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}).*$ thumbs/$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
|
# any requests for files which don't physically exist should be handled by index.php
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
<IfModule mod_expires.c>
|
<IfModule mod_expires.c>
|
||||||
ExpiresActive On
|
ExpiresActive On
|
||||||
<FilesMatch "([0-9a-f]{32}|\.(gif|jpe?g|png|css|js))$">
|
<FilesMatch "([0-9a-f]{32}|\.(gif|jpe?g|png|webp|css|js))$">
|
||||||
<IfModule mod_headers.c>
|
<IfModule mod_headers.c>
|
||||||
Header set Cache-Control "public, max-age=2629743"
|
Header set Cache-Control "public, max-age=2629743"
|
||||||
</IfModule>
|
</IfModule>
|
||||||
@ -46,6 +46,7 @@
|
|||||||
AddType image/jpeg jpg jpeg
|
AddType image/jpeg jpg jpeg
|
||||||
AddType image/gif gif
|
AddType image/gif gif
|
||||||
AddType image/png png
|
AddType image/png png
|
||||||
|
AddType image/webp webp
|
||||||
|
|
||||||
#EXT: handle_ico
|
#EXT: handle_ico
|
||||||
AddType image/x-icon ico ani cur
|
AddType image/x-icon ico ani cur
|
||||||
|
|||||||
19
.php_cs.dist
Normal file
19
.php_cs.dist
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$finder = PhpCsFixer\Finder::create()
|
||||||
|
->exclude('ext/amazon_s3/lib')
|
||||||
|
->exclude('vendor')
|
||||||
|
->exclude('data')
|
||||||
|
->in(__DIR__)
|
||||||
|
;
|
||||||
|
|
||||||
|
return PhpCsFixer\Config::create()
|
||||||
|
->setRules([
|
||||||
|
'@PSR2' => true,
|
||||||
|
//'strict_param' => true,
|
||||||
|
'array_syntax' => ['syntax' => 'short'],
|
||||||
|
])
|
||||||
|
->setFinder($finder)
|
||||||
|
;
|
||||||
|
|
||||||
|
?>
|
||||||
@ -3,7 +3,7 @@ imports:
|
|||||||
- php
|
- php
|
||||||
|
|
||||||
filter:
|
filter:
|
||||||
excluded_paths: [lib/*,ext/*/lib/*,ext/tagger/script.js,ext/chatbox/*]
|
excluded_paths: [ext/*/lib/*,ext/tagger/script.js,ext/chatbox/*]
|
||||||
|
|
||||||
tools:
|
tools:
|
||||||
external_code_coverage: true
|
external_code_coverage: true
|
||||||
|
|||||||
18
.travis.yml
18
.travis.yml
@ -1,8 +1,10 @@
|
|||||||
language: php
|
language: php
|
||||||
php:
|
php:
|
||||||
- 5.6
|
- 7.3
|
||||||
- 7.0
|
|
||||||
- 7.1
|
services:
|
||||||
|
- mysql
|
||||||
|
- postgresql
|
||||||
|
|
||||||
sudo: false
|
sudo: false
|
||||||
|
|
||||||
@ -11,8 +13,6 @@ env:
|
|||||||
- DB=mysql
|
- DB=mysql
|
||||||
- DB=pgsql
|
- DB=pgsql
|
||||||
- DB=sqlite
|
- DB=sqlite
|
||||||
allow_failures:
|
|
||||||
- DB=sqlite
|
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
@ -35,11 +35,13 @@ install:
|
|||||||
if [[ "$DB" == "mysql" ]]; then
|
if [[ "$DB" == "mysql" ]]; then
|
||||||
mysql -e "SET GLOBAL general_log = 'ON';" -uroot ;
|
mysql -e "SET GLOBAL general_log = 'ON';" -uroot ;
|
||||||
mysql -e "CREATE DATABASE shimmie;" -uroot ;
|
mysql -e "CREATE DATABASE shimmie;" -uroot ;
|
||||||
echo '<?php define("DATABASE_DSN", "mysql:user=root;password=;host=localhost;dbname=shimmie");' > data/config/auto_install.conf.php ;
|
echo '<?php define("DATABASE_DSN", "mysql:user=root;password=;host=127.0.0.1;dbname=shimmie");' > data/config/auto_install.conf.php ;
|
||||||
|
fi
|
||||||
|
- if [[ "$DB" == "sqlite" ]]; then
|
||||||
|
echo '<?php define("DATABASE_DSN", "sqlite:data/shimmie.sqlite");' > data/config/auto_install.conf.php ;
|
||||||
fi
|
fi
|
||||||
- if [[ "$DB" == "sqlite" ]]; then echo '<?php define("DATABASE_DSN", "sqlite:shimmie.sqlite");' > data/config/auto_install.conf.php ; fi
|
|
||||||
- composer install
|
- composer install
|
||||||
- php install.php
|
- php index.php
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover
|
- vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover
|
||||||
|
|||||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@ -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 "<?php define(\"DATABASE_DSN\", \"sqlite:data/shimmie.sqlite\");" > 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"
|
||||||
@ -29,7 +29,7 @@ check out one of the versioned branches.
|
|||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
- MySQL/MariaDB 5.1+ (with experimental support for PostgreSQL 9+ and SQLite 3)
|
- 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
|
- GD or ImageMagick
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
@ -50,40 +50,35 @@ check out one of the versioned branches.
|
|||||||
4. Run `composer install` in the shimmie folder.
|
4. Run `composer install` in the shimmie folder.
|
||||||
5. Follow instructions noted in "Installation" starting from step 3.
|
5. Follow instructions noted in "Installation" starting from step 3.
|
||||||
|
|
||||||
## Upgrade from 2.3.X
|
# Docker
|
||||||
|
|
||||||
1. Backup your current files and database!
|
Useful for testing in a known-good environment, this command will build a
|
||||||
2. Unzip into a clean folder
|
simple debian image and run all the unit tests inside it:
|
||||||
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:
|
|
||||||
|
|
||||||
OLD Format:
|
```
|
||||||
```php
|
docker build -t shimmie .
|
||||||
$database_dsn = "<proto>://<username>:<password>@<host>/<database>";
|
|
||||||
```
|
```
|
||||||
|
|
||||||
NEW Format:
|
Once you have an image which has passed all tests, you can then run it to get
|
||||||
```php
|
a live system:
|
||||||
define("DATABASE_DSN", "<proto>:user=<username>;password=<password>;host=<host>;dbname=<database>");
|
|
||||||
|
```
|
||||||
|
docker run -p 0.0.0.0:8123:8000 shimmie
|
||||||
```
|
```
|
||||||
|
|
||||||
The rest should be automatic~
|
Then you can visit your server on port 8123 to see the site.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
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
|
### Upgrade from earlier versions
|
||||||
|
|
||||||
I very much recommend going via each major release in turn (eg, 2.0.6
|
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).
|
-> 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
|
While the basic database and file formats haven't changed *completely*, it's
|
||||||
enough to be a pain.
|
different enough to be a pain.
|
||||||
|
|
||||||
|
|
||||||
## Custom Configuration
|
## Custom Configuration
|
||||||
@ -91,7 +86,7 @@ enough to be a pain.
|
|||||||
Various aspects of Shimmie can be configured to suit your site specific needs
|
Various aspects of Shimmie can be configured to suit your site specific needs
|
||||||
via the file `data/config/shimmie.conf.php` (created after installation).
|
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.
|
be used.
|
||||||
|
|
||||||
|
|
||||||
@ -100,35 +95,36 @@ be used.
|
|||||||
User classes can be added to or altered by placing them in
|
User classes can be added to or altered by placing them in
|
||||||
`data/config/user-classes.conf.php`.
|
`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
|
```php
|
||||||
new UserClass("anonymous", "base", array(
|
new UserClass("anonymous", "base", [
|
||||||
"create_comment" => True,
|
Permissions::CREATE_COMMENT => True,
|
||||||
"edit_image_tag" => True,
|
Permissions::EDIT_IMAGE_TAG => True,
|
||||||
"edit_image_source" => True,
|
Permissions::EDIT_IMAGE_SOURCE => True,
|
||||||
"create_image_report" => True,
|
Permissions::CREATE_IMAGE_REPORT => True,
|
||||||
));
|
]);
|
||||||
```
|
```
|
||||||
|
|
||||||
For a moderator class, being a regular user who can delete images and comments:
|
For a moderator class, being a regular user who can delete images and comments:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
new UserClass("moderator", "user", array(
|
new UserClass("moderator", "user", [
|
||||||
"delete_image" => True,
|
Permissions::DELETE_IMAGE => True,
|
||||||
"delete_comment" => 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
|
# 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
|
(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:
|
themes, be careful with these, and avoid styling them, eg:
|
||||||
|
|
||||||
- shm-thumb = outermost element of a thumbnail
|
- shm-thumb = outermost element of a thumbnail
|
||||||
|
|||||||
@ -23,12 +23,15 @@
|
|||||||
],
|
],
|
||||||
|
|
||||||
"require" : {
|
"require" : {
|
||||||
"php" : ">=5.6",
|
"php" : ">=7.1",
|
||||||
|
"ext-pdo": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
|
||||||
"flexihash/flexihash" : "^2.0.0",
|
"flexihash/flexihash" : "^2.0.0",
|
||||||
"ifixit/php-akismet" : "1.*",
|
"ifixit/php-akismet" : "1.*",
|
||||||
"google/recaptcha" : "~1.1",
|
"google/recaptcha" : "~1.1",
|
||||||
"dapphp/securimage" : "3.6.*",
|
"dapphp/securimage" : "3.6.*",
|
||||||
|
"shish/eventtracer-php" : "dev-master",
|
||||||
"enshrined/svg-sanitize" : "0.8.2",
|
"enshrined/svg-sanitize" : "0.8.2",
|
||||||
|
|
||||||
"bower-asset/jquery" : "1.12.3",
|
"bower-asset/jquery" : "1.12.3",
|
||||||
@ -39,31 +42,21 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"require-dev" : {
|
"require-dev" : {
|
||||||
"phpunit/phpunit" : "5.*"
|
"phpunit/phpunit" : "7.*"
|
||||||
},
|
},
|
||||||
|
|
||||||
"vendor-copy": {
|
"suggest": {
|
||||||
"vendor/bower-asset/jquery/dist/jquery.min.js" : "lib/vendor/js/jquery-1.12.3.min.js",
|
"ext-memcache": "memcache caching",
|
||||||
"vendor/bower-asset/jquery/dist/jquery.min.map" : "lib/vendor/js/jquery-1.12.3.min.map",
|
"ext-memcached": "memcached caching",
|
||||||
"vendor/bower-asset/jquery-timeago/jquery.timeago.js" : "lib/vendor/js/jquery.timeago.js",
|
"ext-apc": "apc caching",
|
||||||
"vendor/bower-asset/tablesorter/jquery.tablesorter.min.js" : "lib/vendor/js/jquery.tablesorter.min.js",
|
"ext-redis": "redis caching",
|
||||||
"vendor/bower-asset/mediaelement/build/flashmediaelement.swf" : "lib/vendor/swf/flashmediaelement.swf",
|
"ext-dom": "some extensions",
|
||||||
"vendor/bower-asset/js-cookie/src/js.cookie.js" : "lib/vendor/js/js.cookie.js"
|
"ext-curl": "some extensions",
|
||||||
},
|
"ext-ctype": "some extensions",
|
||||||
|
"ext-json": "some extensions",
|
||||||
"scripts": {
|
"ext-zip": "self-updater extension",
|
||||||
"pre-install-cmd" : [
|
"ext-zlib": "anti-spam",
|
||||||
"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')));\""
|
"ext-xml": "some extensions",
|
||||||
],
|
"ext-gd": "GD-based thumbnailing"
|
||||||
"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']);\""
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
860
composer.lock
generated
860
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,51 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* Load all the files into memory, sanitise the environment, but don't
|
|
||||||
* actually do anything as far as the app is concerned
|
|
||||||
*/
|
|
||||||
|
|
||||||
global $config, $database, $user, $page;
|
|
||||||
|
|
||||||
require_once "core/sys_config.inc.php";
|
|
||||||
require_once "core/util.inc.php";
|
|
||||||
require_once "lib/context.php";
|
|
||||||
require_once "vendor/autoload.php";
|
|
||||||
require_once "core/imageboard.pack.php";
|
|
||||||
|
|
||||||
// set up and purify the environment
|
|
||||||
_version_check();
|
|
||||||
_sanitise_environment();
|
|
||||||
|
|
||||||
// load base files
|
|
||||||
ctx_log_start("Opening files");
|
|
||||||
$_shm_files = array_merge(
|
|
||||||
zglob("core/*.php"),
|
|
||||||
zglob("ext/{".ENABLED_EXTS."}/main.php")
|
|
||||||
);
|
|
||||||
foreach($_shm_files as $_shm_filename) {
|
|
||||||
if(basename($_shm_filename)[0] != "_") {
|
|
||||||
require_once $_shm_filename;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unset($_shm_files);
|
|
||||||
unset($_shm_filename);
|
|
||||||
ctx_log_endok();
|
|
||||||
|
|
||||||
// connect to the database
|
|
||||||
ctx_log_start("Connecting to DB");
|
|
||||||
$database = new Database();
|
|
||||||
$config = new DatabaseConfig($database);
|
|
||||||
ctx_log_endok();
|
|
||||||
|
|
||||||
// load the theme parts
|
|
||||||
ctx_log_start("Loading themelets");
|
|
||||||
foreach(_get_themelet_files(get_theme()) as $themelet) {
|
|
||||||
require_once $themelet;
|
|
||||||
}
|
|
||||||
unset($themelet);
|
|
||||||
$page = class_exists("CustomPage") ? new CustomPage() : new Page();
|
|
||||||
ctx_log_endok();
|
|
||||||
|
|
||||||
// hook up event handlers
|
|
||||||
_load_event_listeners();
|
|
||||||
send_event(new InitExtEvent());
|
|
||||||
77
core/_bootstrap.php
Normal file
77
core/_bootstrap.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Load all the files into memory, sanitise the environment, but don't
|
||||||
|
* actually do anything as far as the app is concerned
|
||||||
|
*/
|
||||||
|
|
||||||
|
global $config, $database, $user, $page, $_tracer;
|
||||||
|
|
||||||
|
require_once "core/sys_config.php";
|
||||||
|
require_once "core/polyfills.php";
|
||||||
|
require_once "core/util.php";
|
||||||
|
require_once "vendor/autoload.php";
|
||||||
|
|
||||||
|
// set up and purify the environment
|
||||||
|
_version_check();
|
||||||
|
_sanitise_environment();
|
||||||
|
|
||||||
|
// The trace system has a certain amount of memory consumption every time it is used,
|
||||||
|
// so to prevent running out of memory during complex operations code that uses it should
|
||||||
|
// check if tracer output is enabled before making use of it.
|
||||||
|
$tracer_enabled = constant('TRACE_FILE')!==null;
|
||||||
|
|
||||||
|
// load base files
|
||||||
|
$_tracer->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();
|
||||||
459
core/_install.php
Normal file
459
core/_install.php
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Shimmie Installer
|
||||||
|
*
|
||||||
|
* @package Shimmie
|
||||||
|
* @copyright Copyright (c) 2007-2015, Shish et al.
|
||||||
|
* @author Shish [webmaster at shishnet.org], jgen [jeffgenovy at gmail.com]
|
||||||
|
* @link http://code.shishnet.org/shimmie2/
|
||||||
|
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
|
||||||
|
*
|
||||||
|
* Initialise the database, check that folder
|
||||||
|
* permissions are set properly.
|
||||||
|
*
|
||||||
|
* This file should be independent of the database
|
||||||
|
* and other such things that aren't ready yet
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Rewrite the entire installer and make it more readable.
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
date_default_timezone_set('UTC');
|
||||||
|
define("DATABASE_TIMEOUT", 10000);
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Shimmie Installation</title>
|
||||||
|
<link rel="shortcut icon" href="ext/handle_static/static/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="lib/shimmie.css" type="text/css">
|
||||||
|
<script type="text/javascript" src="vendor/bower-asset/jquery/dist/jquery.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php if (false) { ?>
|
||||||
|
<div id="installer">
|
||||||
|
<h1>Install Error</h1>
|
||||||
|
<div class="container">
|
||||||
|
<p>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.</p>
|
||||||
|
<p>If you've installed a web server on your desktop PC, you probably
|
||||||
|
want to visit <a href="http://localhost/">the local web server</a>.<br/><br/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre style="display:none">
|
||||||
|
<?php } elseif (!file_exists("vendor/")) { ?>
|
||||||
|
<div id="installer">
|
||||||
|
<h1>Install Error</h1>
|
||||||
|
<h3>Warning: Composer vendor folder does not exist!</h3>
|
||||||
|
<div class="container">
|
||||||
|
<p>Shimmie is unable to find the composer vendor directory.<br>
|
||||||
|
Have you followed the composer setup instructions found in the <a href="https://github.com/shish/shimmie2#installation-development">README</a>?</>
|
||||||
|
|
||||||
|
<p>If you are not intending to do any development with Shimmie, it is highly recommend you use one of the pre-packaged releases found on <a href="https://github.com/shish/shimmie2/releases">Github</a> instead.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre style="display:none">
|
||||||
|
<?php }
|
||||||
|
|
||||||
|
// Pull in necessary files
|
||||||
|
require_once "vendor/autoload.php";
|
||||||
|
$_tracer = new EventTracer();
|
||||||
|
|
||||||
|
require_once "core/exceptions.php";
|
||||||
|
require_once "core/cacheengine.php";
|
||||||
|
require_once "core/dbengine.php";
|
||||||
|
require_once "core/database.php";
|
||||||
|
|
||||||
|
if (is_readable("data/config/shimmie.conf.php")) {
|
||||||
|
die("Shimmie is already installed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
do_install();
|
||||||
|
|
||||||
|
// utilities {{{
|
||||||
|
// TODO: Can some of these be pushed into "core/???.inc.php" ?
|
||||||
|
|
||||||
|
function check_gd_version(): int
|
||||||
|
{
|
||||||
|
$gdversion = 0;
|
||||||
|
|
||||||
|
if (function_exists('gd_info')) {
|
||||||
|
$gd_info = gd_info();
|
||||||
|
if (substr_count($gd_info['GD Version'], '2.')) {
|
||||||
|
$gdversion = 2;
|
||||||
|
} elseif (substr_count($gd_info['GD Version'], '1.')) {
|
||||||
|
$gdversion = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $gdversion;
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_im_version(): int
|
||||||
|
{
|
||||||
|
$convert_check = exec("convert");
|
||||||
|
|
||||||
|
return (empty($convert_check) ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function eok($name, $value)
|
||||||
|
{
|
||||||
|
echo "<br>$name ... ";
|
||||||
|
if ($value) {
|
||||||
|
echo "<span style='color: green'>ok</span>\n";
|
||||||
|
} else {
|
||||||
|
echo "<span style='color: green'>failed</span>\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) ? '<option value="'. DatabaseDriver::MYSQL .'">MySQL</option>' : "";
|
||||||
|
$db_p = in_array(DatabaseDriver::PGSQL, $drivers) ? '<option value="'. DatabaseDriver::PGSQL .'">PostgreSQL</option>' : "";
|
||||||
|
$db_s = in_array(DatabaseDriver::SQLITE, $drivers) ? '<option value="'. DatabaseDriver::SQLITE .'">SQLite</option>' : "";
|
||||||
|
|
||||||
|
$warn_msg = $warnings ? "<h3>Warnings</h3>".implode("\n<p>", $warnings) : "";
|
||||||
|
$err_msg = $errors ? "<h3>Errors</h3>".implode("\n<p>", $errors) : "";
|
||||||
|
|
||||||
|
print <<<EOD
|
||||||
|
<div id="installer">
|
||||||
|
<h1>Shimmie Installer</h1>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
$warn_msg
|
||||||
|
$err_msg
|
||||||
|
|
||||||
|
<h3>Database Install</h3>
|
||||||
|
<form action="index.php" method="POST">
|
||||||
|
<center>
|
||||||
|
<table class='form'>
|
||||||
|
<tr>
|
||||||
|
<th>Type:</th>
|
||||||
|
<td><select name="database_type" id="database_type" onchange="update_qs();">
|
||||||
|
$db_m
|
||||||
|
$db_p
|
||||||
|
$db_s
|
||||||
|
</select></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="dbconf mysql pgsql">
|
||||||
|
<th>Host:</th>
|
||||||
|
<td><input type="text" name="database_host" size="40" value="localhost"></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="dbconf mysql pgsql">
|
||||||
|
<th>Username:</th>
|
||||||
|
<td><input type="text" name="database_user" size="40"></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="dbconf mysql pgsql">
|
||||||
|
<th>Password:</th>
|
||||||
|
<td><input type="password" name="database_password" size="40"></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="dbconf mysql pgsql">
|
||||||
|
<th>DB Name:</th>
|
||||||
|
<td><input type="text" name="database_name" size="40" value="shimmie"></td>
|
||||||
|
</tr>
|
||||||
|
<tr><td colspan="2"><input type="submit" value="Go!"></td></tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
update_qs();
|
||||||
|
});
|
||||||
|
function update_qs() {
|
||||||
|
$(".dbconf").hide();
|
||||||
|
var seldb = $("#database_type").val() || "none";
|
||||||
|
$("."+seldb).show();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h3>Help</h3>
|
||||||
|
|
||||||
|
<p class="dbconf mysql pgsql">
|
||||||
|
Please make sure the database you have chosen exists and is empty.<br>
|
||||||
|
The username provided must have access to create tables within the database.
|
||||||
|
</p>
|
||||||
|
<p class="dbconf sqlite">
|
||||||
|
For SQLite the database name will be a filename on disk, relative to
|
||||||
|
where shimmie was installed.
|
||||||
|
</p>
|
||||||
|
<p class="dbconf none">
|
||||||
|
Drivers can generally be downloaded with your OS package manager;
|
||||||
|
for Debian / Ubuntu you want php-pgsql, php-mysql, or php-sqlite.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
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 <<<EOD
|
||||||
|
<div id="installer">
|
||||||
|
<h1>Shimmie Installer</h1>
|
||||||
|
<h3>Warning: The Database schema is not empty!</h3>
|
||||||
|
<div class="container">
|
||||||
|
<p>Please ensure that the database you are installing Shimmie with is empty before continuing.</p>
|
||||||
|
<p>Once you have emptied the database of any tables, please hit 'refresh' to continue.</p>
|
||||||
|
<br/><br/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
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 "
|
||||||
|
<div id='installer'>
|
||||||
|
<h1>Shimmie Installer</h1>
|
||||||
|
<h3>Directory Permissions Error:</h3>
|
||||||
|
<div class='container'>
|
||||||
|
<p>Shimmie needs to have a 'data' folder in its directory, writable by the PHP user.</p>
|
||||||
|
<p>If you see this error, if probably means the folder is owned by you, and it needs to be writable by the web server.</p>
|
||||||
|
<p>PHP reports that it is currently running as user: ".$_ENV["USER"]." (". $_SERVER["USER"] .")</p>
|
||||||
|
<p>Once you have created this folder and / or changed the ownership of the shimmie folder, hit 'refresh' to continue.</p>
|
||||||
|
<br/><br/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
";
|
||||||
|
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 <<<EOD
|
||||||
|
<div id="installer">
|
||||||
|
<h1>Shimmie Installer</h1>
|
||||||
|
<h3>Things are OK \o/</h3>
|
||||||
|
<div class="container">
|
||||||
|
<p>If you aren't redirected, <a href="index.php">click here to Continue</a>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
EOD;
|
||||||
|
} else {
|
||||||
|
$h_file_content = htmlentities($file_content);
|
||||||
|
print <<<EOD
|
||||||
|
<div id="installer">
|
||||||
|
<h1>Shimmie Installer</h1>
|
||||||
|
<h3>File Permissions Error:</h3>
|
||||||
|
<div class="container">
|
||||||
|
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 "?>"
|
||||||
|
|
||||||
|
<p><textarea cols="80" rows="2">$h_file_content</textarea>
|
||||||
|
|
||||||
|
<p>Once done, <a href="index.php">click here to Continue</a>.
|
||||||
|
<br/><br/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
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 <<<EOD
|
||||||
|
<div id="installer">
|
||||||
|
<h1>Shimmie Installer</h1>
|
||||||
|
<h3>Unknown Error:</h3>
|
||||||
|
<div class="container">
|
||||||
|
<p>{$errorMessage1}</p>
|
||||||
|
<p>{$errorMessage1Extra}</p>
|
||||||
|
<p>{$errorMessage2}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
EOD;
|
||||||
|
exit($exitCode);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,166 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class BaseThemelet
|
|
||||||
*
|
|
||||||
* A collection of common functions for theme parts
|
|
||||||
*/
|
|
||||||
class BaseThemelet {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic error message display
|
|
||||||
*
|
|
||||||
* @param int $code
|
|
||||||
* @param string $title
|
|
||||||
* @param string $message
|
|
||||||
*/
|
|
||||||
public function display_error(/*int*/ $code, /*string*/ $title, /*string*/ $message) {
|
|
||||||
global $page;
|
|
||||||
$page->set_code($code);
|
|
||||||
$page->set_title($title);
|
|
||||||
$page->set_heading($title);
|
|
||||||
$has_nav = false;
|
|
||||||
foreach($page->blocks as $block) {
|
|
||||||
if($block->header == "Navigation") {
|
|
||||||
$has_nav = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(!$has_nav) {
|
|
||||||
$page->add_block(new NavBlock());
|
|
||||||
}
|
|
||||||
$page->add_block(new Block("Error", $message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A specific, common error message
|
|
||||||
*/
|
|
||||||
public function display_permission_denied() {
|
|
||||||
$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 "<a href='$h_view_link' class='thumb shm-thumb shm-thumb-link {$custom_classes}' data-tags='$h_tags' data-post-id='$i_id'>".
|
|
||||||
"<img id='thumb_$i_id' title='$h_tip' alt='$h_tip' height='{$tsize[1]}' width='{$tsize[0]}' src='$h_thumb_link'>".
|
|
||||||
"</a>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 '<a href="'.$link.'">'.$name.'</a>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 .= "<b>";
|
|
||||||
$paginator .= $this->gen_page_link($base_url, $query, $page, $name);
|
|
||||||
if($page == $current_page) $paginator .= "</b>";
|
|
||||||
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
|
|
||||||
.'<br><< '.$pages_html.' >>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
139
core/basethemelet.php
Normal file
139
core/basethemelet.php
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class BaseThemelet
|
||||||
|
*
|
||||||
|
* A collection of common functions for theme parts
|
||||||
|
*/
|
||||||
|
class BaseThemelet
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic error message display
|
||||||
|
*/
|
||||||
|
public function display_error(int $code, string $title, string $message): void
|
||||||
|
{
|
||||||
|
global $page;
|
||||||
|
$page->set_code($code);
|
||||||
|
$page->set_title($title);
|
||||||
|
$page->set_heading($title);
|
||||||
|
$has_nav = false;
|
||||||
|
foreach ($page->blocks as $block) {
|
||||||
|
if ($block->header == "Navigation") {
|
||||||
|
$has_nav = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$has_nav) {
|
||||||
|
$page->add_block(new NavBlock());
|
||||||
|
}
|
||||||
|
$page->add_block(new Block("Error", $message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A specific, common error message
|
||||||
|
*/
|
||||||
|
public function display_permission_denied(): void
|
||||||
|
{
|
||||||
|
$this->display_error(403, "Permission Denied", "You do not have permission to access this page");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic thumbnail code; returns HTML rather than adding
|
||||||
|
* a block since thumbs tend to go inside blocks...
|
||||||
|
*/
|
||||||
|
public function build_thumb_html(Image $image): string
|
||||||
|
{
|
||||||
|
global $config;
|
||||||
|
|
||||||
|
$i_id = (int) $image->id;
|
||||||
|
$h_view_link = make_link('post/view/'.$i_id);
|
||||||
|
$h_thumb_link = $image->get_thumb_link();
|
||||||
|
$h_tip = html_escape($image->get_tooltip());
|
||||||
|
$h_tags = html_escape(strtolower($image->get_tag_list()));
|
||||||
|
|
||||||
|
$extArr = array_flip(['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 "<a href='$h_view_link' class='thumb shm-thumb shm-thumb-link {$custom_classes}' data-tags='$h_tags' data-post-id='$i_id'>".
|
||||||
|
"<img id='thumb_$i_id' title='$h_tip' alt='$h_tip' height='{$tsize[1]}' width='{$tsize[0]}' src='$h_thumb_link'>".
|
||||||
|
"</a>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function display_paginator(Page $page, string $base, ?string $query, int $page_number, int $total_pages, bool $show_random = false)
|
||||||
|
{
|
||||||
|
if ($total_pages == 0) {
|
||||||
|
$total_pages = 1;
|
||||||
|
}
|
||||||
|
$body = $this->build_paginator($page_number, $total_pages, $base, $query, $show_random);
|
||||||
|
$page->add_block(new Block(null, $body, "main", 90, "paginator"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function gen_page_link(string $base_url, ?string $query, string $page, string $name): string
|
||||||
|
{
|
||||||
|
$link = make_link($base_url.'/'.$page, $query);
|
||||||
|
return '<a href="'.$link.'">'.$name.'</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function gen_page_link_block(string $base_url, ?string $query, string $page, int $current_page, string $name): string
|
||||||
|
{
|
||||||
|
$paginator = "";
|
||||||
|
if ($page == $current_page) {
|
||||||
|
$paginator .= "<b>";
|
||||||
|
}
|
||||||
|
$paginator .= $this->gen_page_link($base_url, $query, $page, $name);
|
||||||
|
if ($page == $current_page) {
|
||||||
|
$paginator .= "</b>";
|
||||||
|
}
|
||||||
|
return $paginator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function build_paginator(int $current_page, int $total_pages, string $base_url, ?string $query, bool $show_random): string
|
||||||
|
{
|
||||||
|
$next = $current_page + 1;
|
||||||
|
$prev = $current_page - 1;
|
||||||
|
|
||||||
|
$at_start = ($current_page <= 1 || $total_pages <= 1);
|
||||||
|
$at_end = ($current_page >= $total_pages);
|
||||||
|
|
||||||
|
$first_html = $at_start ? "First" : $this->gen_page_link($base_url, $query, 1, "First");
|
||||||
|
$prev_html = $at_start ? "Prev" : $this->gen_page_link($base_url, $query, $prev, "Prev");
|
||||||
|
|
||||||
|
$random_html = "-";
|
||||||
|
if ($show_random) {
|
||||||
|
$rand = mt_rand(1, $total_pages);
|
||||||
|
$random_html = $this->gen_page_link($base_url, $query, $rand, "Random");
|
||||||
|
}
|
||||||
|
|
||||||
|
$next_html = $at_end ? "Next" : $this->gen_page_link($base_url, $query, $next, "Next");
|
||||||
|
$last_html = $at_end ? "Last" : $this->gen_page_link($base_url, $query, $total_pages, "Last");
|
||||||
|
|
||||||
|
$start = $current_page-5 > 1 ? $current_page-5 : 1;
|
||||||
|
$end = $start+10 < $total_pages ? $start+10 : $total_pages;
|
||||||
|
|
||||||
|
$pages = [];
|
||||||
|
foreach (range($start, $end) as $i) {
|
||||||
|
$pages[] = $this->gen_page_link_block($base_url, $query, $i, $current_page, $i);
|
||||||
|
}
|
||||||
|
$pages_html = implode(" | ", $pages);
|
||||||
|
|
||||||
|
return $first_html.' | '.$prev_html.' | '.$random_html.' | '.$next_html.' | '.$last_html
|
||||||
|
.'<br><< '.$pages_html.' >>';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,108 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class Block
|
|
||||||
*
|
|
||||||
* A basic chunk of a page.
|
|
||||||
*/
|
|
||||||
class Block {
|
|
||||||
/**
|
|
||||||
* The block's title.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public $header;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The content of the block.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public $body;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Where the block should be placed. The default theme supports
|
|
||||||
* "main" and "left", other themes can add their own areas.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public $section;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* How far down the section the block should appear, higher
|
|
||||||
* numbers appear lower. The scale is 0-100 by convention,
|
|
||||||
* though any number or string will work.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
public $position;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A unique ID for the block.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public $id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should this block count as content for the sake of
|
|
||||||
* the 404 handler
|
|
||||||
*
|
|
||||||
* @var boolean
|
|
||||||
*/
|
|
||||||
public $is_content = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a block.
|
|
||||||
*
|
|
||||||
* @param string $header
|
|
||||||
* @param string $body
|
|
||||||
* @param string $section
|
|
||||||
* @param int $position
|
|
||||||
* @param null|int $id A unique ID for the block (generated automatically if null).
|
|
||||||
*/
|
|
||||||
public function __construct($header, $body, /*string*/ $section="main", /*int*/ $position=50, $id=null) {
|
|
||||||
$this->header = $header;
|
|
||||||
$this->body = $body;
|
|
||||||
$this->section = $section;
|
|
||||||
$this->position = $position;
|
|
||||||
|
|
||||||
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 = "<section id='$i'>";
|
|
||||||
$h_toggler = $hidable ? " shm-toggler" : "";
|
|
||||||
if(!empty($h)) $html .= "<h3 data-toggle-sel='#$i' class='$h_toggler'>$h</h3>";
|
|
||||||
if(!empty($b)) $html .= "<div class='blockbody'>$b</div>";
|
|
||||||
$html .= "</section>\n";
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class NavBlock
|
|
||||||
*
|
|
||||||
* A generic navigation block with a link to the main page.
|
|
||||||
*
|
|
||||||
* Used because "new NavBlock()" is easier than "new Block('Navigation', ..."
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class NavBlock extends Block {
|
|
||||||
public function __construct() {
|
|
||||||
parent::__construct("Navigation", "<a href='".make_link()."'>Index</a>", "left", 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
105
core/block.php
Normal file
105
core/block.php
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Block
|
||||||
|
*
|
||||||
|
* A basic chunk of a page.
|
||||||
|
*/
|
||||||
|
class Block
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The block's title.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $header;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The content of the block.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $body;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where the block should be placed. The default theme supports
|
||||||
|
* "main" and "left", other themes can add their own areas.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $section;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far down the section the block should appear, higher
|
||||||
|
* numbers appear lower. The scale is 0-100 by convention,
|
||||||
|
* though any number or string will work.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $position;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A unique ID for the block.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should this block count as content for the sake of
|
||||||
|
* the 404 handler
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
public $is_content = true;
|
||||||
|
|
||||||
|
public function __construct(string $header=null, string $body=null, string $section="main", int $position=50, string $id=null)
|
||||||
|
{
|
||||||
|
$this->header = $header;
|
||||||
|
$this->body = $body;
|
||||||
|
$this->section = $section;
|
||||||
|
$this->position = $position;
|
||||||
|
|
||||||
|
if (is_null($id)) {
|
||||||
|
$id = (empty($header) ? md5($body) : $header) . $section;
|
||||||
|
}
|
||||||
|
$this->id = preg_replace('/[^\w]/', '', str_replace(' ', '_', $id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the HTML for this block.
|
||||||
|
*/
|
||||||
|
public function get_html(bool $hidable=false): string
|
||||||
|
{
|
||||||
|
$h = $this->header;
|
||||||
|
$b = $this->body;
|
||||||
|
$i = $this->id;
|
||||||
|
$html = "<section id='$i'>";
|
||||||
|
$h_toggler = $hidable ? " shm-toggler" : "";
|
||||||
|
if (!empty($h)) {
|
||||||
|
$html .= "<h3 data-toggle-sel='#$i' class='$h_toggler'>$h</h3>";
|
||||||
|
}
|
||||||
|
if (!empty($b)) {
|
||||||
|
$html .= "<div class='blockbody'>$b</div>";
|
||||||
|
}
|
||||||
|
$html .= "</section>\n";
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class NavBlock
|
||||||
|
*
|
||||||
|
* A generic navigation block with a link to the main page.
|
||||||
|
*
|
||||||
|
* Used because "new NavBlock()" is easier than "new Block('Navigation', ..."
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class NavBlock extends Block
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct("Navigation", "<a href='".make_link()."'>Index</a>", "left", 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
229
core/cacheengine.php
Normal file
229
core/cacheengine.php
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
<?php
|
||||||
|
interface CacheEngine
|
||||||
|
{
|
||||||
|
public function get(string $key);
|
||||||
|
public function set(string $key, $val, int $time=0);
|
||||||
|
public function delete(string $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoCache implements CacheEngine
|
||||||
|
{
|
||||||
|
public function get(string $key)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public function set(string $key, $val, int $time=0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public function delete(string $key)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemcacheCache implements CacheEngine
|
||||||
|
{
|
||||||
|
/** @var ?Memcache */
|
||||||
|
public $memcache=null;
|
||||||
|
|
||||||
|
public function __construct(string $args)
|
||||||
|
{
|
||||||
|
$hp = explode(":", $args);
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
core/captcha.php
Normal file
58
core/captcha.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||||
|
* CAPTCHA abstraction *
|
||||||
|
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
function captcha_get_html(): string
|
||||||
|
{
|
||||||
|
global $config, $user;
|
||||||
|
|
||||||
|
if (DEBUG && ip_in_range($_SERVER['REMOTE_ADDR'], "127.0.0.0/8")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
$captcha = "";
|
||||||
|
if ($user->is_anonymous() && $config->get_bool("comment_captcha")) {
|
||||||
|
$r_publickey = $config->get_string("api_recaptcha_pubkey");
|
||||||
|
if (!empty($r_publickey)) {
|
||||||
|
$captcha = "
|
||||||
|
<div class=\"g-recaptcha\" data-sitekey=\"{$r_publickey}\"></div>
|
||||||
|
<script type=\"text/javascript\" src=\"https://www.google.com/recaptcha/api.js\"></script>";
|
||||||
|
} else {
|
||||||
|
session_start();
|
||||||
|
$captcha = Securimage::getCaptchaHtml(['securimage_path' => './vendor/dapphp/securimage/']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $captcha;
|
||||||
|
}
|
||||||
|
|
||||||
|
function captcha_check(): bool
|
||||||
|
{
|
||||||
|
global $config, $user;
|
||||||
|
|
||||||
|
if (DEBUG && ip_in_range($_SERVER['REMOTE_ADDR'], "127.0.0.0/8")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->is_anonymous() && $config->get_bool("comment_captcha")) {
|
||||||
|
$r_privatekey = $config->get_string('api_recaptcha_privkey');
|
||||||
|
if (!empty($r_privatekey)) {
|
||||||
|
$recaptcha = new \ReCaptcha\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;
|
||||||
|
}
|
||||||
@ -1,429 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface Config
|
|
||||||
*
|
|
||||||
* An abstract interface for altering a name:value pair list.
|
|
||||||
*/
|
|
||||||
interface Config {
|
|
||||||
/**
|
|
||||||
* Save the list of name:value pairs to wherever they came from,
|
|
||||||
* so that the next time a page is loaded it will use the new
|
|
||||||
* configuration.
|
|
||||||
*
|
|
||||||
* @param null|string $name
|
|
||||||
* @return mixed|void
|
|
||||||
*/
|
|
||||||
public function save(/*string*/ $name=null);
|
|
||||||
|
|
||||||
//@{ /*--------------------------------- SET ------------------------------------------------------*/
|
|
||||||
/**
|
|
||||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
|
||||||
* @param string $name
|
|
||||||
* @param null|int $value
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function set_int(/*string*/ $name, $value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
|
||||||
* @param string $name
|
|
||||||
* @param null|string $value
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function set_string(/*string*/ $name, $value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
|
||||||
* @param string $name
|
|
||||||
* @param null|bool|string $value
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function set_bool(/*string*/ $name, $value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
|
||||||
* @param string $name
|
|
||||||
* @param array $value
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function set_array(/*string*/ $name, $value);
|
|
||||||
//@} /*--------------------------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
//@{ /*-------------------------------- SET DEFAULT -----------------------------------------------*/
|
|
||||||
/**
|
|
||||||
* Set a configuration option to a new value, if there is no value currently.
|
|
||||||
*
|
|
||||||
* Extensions should generally call these from their InitExtEvent handlers.
|
|
||||||
* This has the advantage that the values will show up in the "advanced" setup
|
|
||||||
* page where they can be modified, while calling get_* with a "default"
|
|
||||||
* parameter won't show up.
|
|
||||||
*
|
|
||||||
* @param string $name
|
|
||||||
* @param int $value
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function set_default_int(/*string*/ $name, $value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a configuration option to a new value, if there is no value currently.
|
|
||||||
*
|
|
||||||
* Extensions should generally call these from their InitExtEvent handlers.
|
|
||||||
* This has the advantage that the values will show up in the "advanced" setup
|
|
||||||
* page where they can be modified, while calling get_* with a "default"
|
|
||||||
* parameter won't show up.
|
|
||||||
*
|
|
||||||
* @param string $name
|
|
||||||
* @param string|null $value
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function set_default_string(/*string*/ $name, $value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a configuration option to a new value, if there is no value currently.
|
|
||||||
*
|
|
||||||
* Extensions should generally call these from their InitExtEvent handlers.
|
|
||||||
* This has the advantage that the values will show up in the "advanced" setup
|
|
||||||
* page where they can be modified, while calling get_* with a "default"
|
|
||||||
* parameter won't show up.
|
|
||||||
*
|
|
||||||
* @param string $name
|
|
||||||
* @param bool $value
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function set_default_bool(/*string*/ $name, /*bool*/ $value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a configuration option to a new value, if there is no value currently.
|
|
||||||
*
|
|
||||||
* Extensions should generally call these from their InitExtEvent handlers.
|
|
||||||
* This has the advantage that the values will show up in the "advanced" setup
|
|
||||||
* page where they can be modified, while calling get_* with a "default"
|
|
||||||
* parameter won't show up.
|
|
||||||
*
|
|
||||||
* @param string $name
|
|
||||||
* @param array $value
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function set_default_array(/*string*/ $name, $value);
|
|
||||||
//@} /*--------------------------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
//@{ /*--------------------------------- GET ------------------------------------------------------*/
|
|
||||||
/**
|
|
||||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
|
||||||
* @param string $name
|
|
||||||
* @param null|int $default
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function get_int(/*string*/ $name, $default=null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
|
||||||
* @param string $name
|
|
||||||
* @param null|string $default
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function get_string(/*string*/ $name, $default=null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
|
||||||
* @param string $name
|
|
||||||
* @param null|bool|string $default
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function get_bool(/*string*/ $name, $default=null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
|
||||||
* @param string $name
|
|
||||||
* @param array|null $default
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function get_array(/*string*/ $name, $default=array());
|
|
||||||
//@} /*--------------------------------------------------------------------------------------------*/
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class BaseConfig
|
|
||||||
*
|
|
||||||
* Common methods for manipulating the list, loading and saving is
|
|
||||||
* left to the concrete implementation
|
|
||||||
*/
|
|
||||||
abstract class BaseConfig implements Config {
|
|
||||||
public $values = array();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @param int|null $value
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function set_int(/*string*/ $name, $value) {
|
|
||||||
$this->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:
|
|
||||||
*
|
|
||||||
* <?php
|
|
||||||
* $config['foo'] = "bar";
|
|
||||||
* $config['baz'] = "qux";
|
|
||||||
* ?>
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
397
core/config.php
Normal file
397
core/config.php
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface Config
|
||||||
|
*
|
||||||
|
* An abstract interface for altering a name:value pair list.
|
||||||
|
*/
|
||||||
|
interface Config
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Save the list of name:value pairs to wherever they came from,
|
||||||
|
* so that the next time a page is loaded it will use the new
|
||||||
|
* configuration.
|
||||||
|
*/
|
||||||
|
public function save(string $name=null): void;
|
||||||
|
|
||||||
|
//@{ /*--------------------------------- SET ------------------------------------------------------*/
|
||||||
|
/**
|
||||||
|
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||||
|
*/
|
||||||
|
public function set_int(string $name, ?string $value): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||||
|
*/
|
||||||
|
public function set_float(string $name, ?string $value): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||||
|
*/
|
||||||
|
public function set_string(string $name, ?string $value): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||||
|
* @param null|bool|string $value
|
||||||
|
*/
|
||||||
|
public function set_bool(string $name, $value): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||||
|
*/
|
||||||
|
public function set_array(string $name, array $value): void;
|
||||||
|
//@} /*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
//@{ /*-------------------------------- SET DEFAULT -----------------------------------------------*/
|
||||||
|
/**
|
||||||
|
* Set a configuration option to a new value, if there is no value currently.
|
||||||
|
*
|
||||||
|
* Extensions should generally call these from their InitExtEvent handlers.
|
||||||
|
* This has the advantage that the values will show up in the "advanced" setup
|
||||||
|
* page where they can be modified, while calling get_* with a "default"
|
||||||
|
* parameter won't show up.
|
||||||
|
*/
|
||||||
|
public function set_default_int(string $name, int $value): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a configuration option to a new value, if there is no value currently.
|
||||||
|
*
|
||||||
|
* Extensions should generally call these from their InitExtEvent handlers.
|
||||||
|
* This has the advantage that the values will show up in the "advanced" setup
|
||||||
|
* page where they can be modified, while calling get_* with a "default"
|
||||||
|
* parameter won't show up.
|
||||||
|
*/
|
||||||
|
public function set_default_float(string $name, float $value): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a configuration option to a new value, if there is no value currently.
|
||||||
|
*
|
||||||
|
* Extensions should generally call these from their InitExtEvent handlers.
|
||||||
|
* This has the advantage that the values will show up in the "advanced" setup
|
||||||
|
* page where they can be modified, while calling get_* with a "default"
|
||||||
|
* parameter won't show up.
|
||||||
|
*/
|
||||||
|
public function set_default_string(string $name, string $value): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a configuration option to a new value, if there is no value currently.
|
||||||
|
*
|
||||||
|
* Extensions should generally call these from their InitExtEvent handlers.
|
||||||
|
* This has the advantage that the values will show up in the "advanced" setup
|
||||||
|
* page where they can be modified, while calling get_* with a "default"
|
||||||
|
* parameter won't show up.
|
||||||
|
*/
|
||||||
|
public function set_default_bool(string $name, bool $value): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a configuration option to a new value, if there is no value currently.
|
||||||
|
*
|
||||||
|
* Extensions should generally call these from their InitExtEvent handlers.
|
||||||
|
* This has the advantage that the values will show up in the "advanced" setup
|
||||||
|
* page where they can be modified, while calling get_* with a "default"
|
||||||
|
* parameter won't show up.
|
||||||
|
*/
|
||||||
|
public function set_default_array(string $name, array $value): void;
|
||||||
|
//@} /*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
//@{ /*--------------------------------- GET ------------------------------------------------------*/
|
||||||
|
/**
|
||||||
|
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||||
|
*/
|
||||||
|
public function get_int(string $name, ?int $default=null): ?int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||||
|
*/
|
||||||
|
public function get_float(string $name, ?float $default=null): ?float;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||||
|
*/
|
||||||
|
public function get_string(string $name, ?string $default=null): ?string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||||
|
*/
|
||||||
|
public function get_bool(string $name, ?bool $default=null): ?bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||||
|
*/
|
||||||
|
public function get_array(string $name, ?array $default=[]): ?array;
|
||||||
|
//@} /*--------------------------------------------------------------------------------------------*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class BaseConfig
|
||||||
|
*
|
||||||
|
* Common methods for manipulating the list, loading and saving is
|
||||||
|
* left to the concrete implementation
|
||||||
|
*/
|
||||||
|
abstract class BaseConfig implements Config
|
||||||
|
{
|
||||||
|
public $values = [];
|
||||||
|
|
||||||
|
public function set_int(string $name, ?string $value): void
|
||||||
|
{
|
||||||
|
$this->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:
|
||||||
|
*
|
||||||
|
* <?php
|
||||||
|
* $config['foo'] = "bar";
|
||||||
|
* $config['baz'] = "qux";
|
||||||
|
* ?>
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,963 +0,0 @@
|
|||||||
<?php
|
|
||||||
/** @privatesection */
|
|
||||||
// Querylet {{{
|
|
||||||
class Querylet {
|
|
||||||
/** @var string */
|
|
||||||
public $sql;
|
|
||||||
/** @var array */
|
|
||||||
public $variables;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $sql
|
|
||||||
* @param array $variables
|
|
||||||
*/
|
|
||||||
public function __construct($sql, $variables=array()) {
|
|
||||||
$this->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("<p><b>Database Transaction Error:</b> 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("<p><b>Database Transaction Error:</b> 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()."<p><b>Query:</b> ".$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() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
439
core/database.php
Normal file
439
core/database.php
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
<?php
|
||||||
|
abstract class DatabaseDriver
|
||||||
|
{
|
||||||
|
public const MYSQL = "mysql";
|
||||||
|
public const PGSQL = "pgsql";
|
||||||
|
public const SQLITE = "sqlite";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Cache|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->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("<p><b>Database Transaction Error:</b> Unable to call commit() as there is no transaction currently open.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new SCoreException("<p><b>Database Transaction Error:</b> 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("<p><b>Database Transaction Error:</b> Unable to call rollback() as there is no transaction currently open.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new SCoreException("<p><b>Database Transaction Error:</b> 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()."<p><b>Query:</b> ".$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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
216
core/dbengine.php
Normal file
216
core/dbengine.php
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
<?php
|
||||||
|
abstract class SCORE
|
||||||
|
{
|
||||||
|
const AIPK = "SCORE_AIPK";
|
||||||
|
const INET = "SCORE_INET";
|
||||||
|
const BOOL_Y = "SCORE_BOOL_Y";
|
||||||
|
const BOOL_N = "SCORE_BOOL_N";
|
||||||
|
const BOOL = "SCORE_BOOL";
|
||||||
|
const DATETIME = "SCORE_DATETIME";
|
||||||
|
const NOW = "SCORE_NOW";
|
||||||
|
const STRNORM = "SCORE_STRNORM";
|
||||||
|
const ILIKE = "SCORE_ILIKE";
|
||||||
|
}
|
||||||
|
|
||||||
|
class DBEngine
|
||||||
|
{
|
||||||
|
/** @var null|string */
|
||||||
|
public $name = null;
|
||||||
|
|
||||||
|
public $BOOL_Y = null;
|
||||||
|
public $BOOL_N = null;
|
||||||
|
|
||||||
|
public function init(PDO $db)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scoreql_to_sql(string $scoreql): string
|
||||||
|
{
|
||||||
|
return $scoreql;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create_table_sql(string $name, string $data): string
|
||||||
|
{
|
||||||
|
return 'CREATE TABLE '.$name.' ('.$data.')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MySQL extends DBEngine
|
||||||
|
{
|
||||||
|
/** @var string */
|
||||||
|
public $name = DatabaseDriver::MYSQL;
|
||||||
|
|
||||||
|
public $BOOL_Y = 'Y';
|
||||||
|
public $BOOL_N = 'N';
|
||||||
|
|
||||||
|
public function init(PDO $db)
|
||||||
|
{
|
||||||
|
$db->exec("SET NAMES utf8;");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scoreql_to_sql(string $data): string
|
||||||
|
{
|
||||||
|
$data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY auto_increment", $data);
|
||||||
|
$data = str_replace(SCORE::INET, "VARCHAR(45)", $data);
|
||||||
|
$data = str_replace(SCORE::BOOL_Y, "'$this->BOOL_Y'", $data);
|
||||||
|
$data = str_replace(SCORE::BOOL_N, "'$this->BOOL_N'", $data);
|
||||||
|
$data = str_replace(SCORE::BOOL, "ENUM('Y', 'N')", $data);
|
||||||
|
$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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,138 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class Email
|
|
||||||
*
|
|
||||||
* A generic email.
|
|
||||||
*/
|
|
||||||
class Email {
|
|
||||||
/** @var string */
|
|
||||||
public $to;
|
|
||||||
/** @var string */
|
|
||||||
public $subject;
|
|
||||||
/** @var string */
|
|
||||||
public $header;
|
|
||||||
/** @var null|string */
|
|
||||||
public $style;
|
|
||||||
/** @var null|string */
|
|
||||||
public $header_img;
|
|
||||||
/** @var null|string */
|
|
||||||
public $sitename;
|
|
||||||
/** @var null|string */
|
|
||||||
public $sitedomain;
|
|
||||||
/** @var null|string */
|
|
||||||
public $siteemail;
|
|
||||||
/** @var string */
|
|
||||||
public $date;
|
|
||||||
/** @var string */
|
|
||||||
public $body;
|
|
||||||
/** @var null|string */
|
|
||||||
public $footer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $to
|
|
||||||
* @param string $subject
|
|
||||||
* @param string $header
|
|
||||||
* @param string $body
|
|
||||||
*/
|
|
||||||
public function __construct($to, $subject, $header, $body) {
|
|
||||||
global $config;
|
|
||||||
$this->to = $to;
|
|
||||||
|
|
||||||
$sub_prefix = $config->get_string("mail_sub");
|
|
||||||
|
|
||||||
if(!isset($sub_prefix)){
|
|
||||||
$this->subject = $subject;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
$this->subject = $sub_prefix." ".$subject;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->style = $config->get_string("mail_style");
|
|
||||||
|
|
||||||
$this->header = html_escape($header);
|
|
||||||
$this->header_img = $config->get_string("mail_img");
|
|
||||||
$this->sitename = $config->get_string("site_title");
|
|
||||||
$this->sitedomain = make_http(make_link(""));
|
|
||||||
$this->siteemail = $config->get_string("site_email");
|
|
||||||
$this->date = date("F j, Y");
|
|
||||||
$this->body = $body;
|
|
||||||
$this->footer = $config->get_string("mail_fot");
|
|
||||||
}
|
|
||||||
|
|
||||||
public function send() {
|
|
||||||
$headers = "From: ".$this->sitename." <".$this->siteemail.">\r\n";
|
|
||||||
$headers .= "Reply-To: ".$this->siteemail."\r\n";
|
|
||||||
$headers .= "X-Mailer: PHP/" . phpversion(). "\r\n";
|
|
||||||
$headers .= "errors-to: ".$this->siteemail."\r\n";
|
|
||||||
$headers .= "Date: " . date(DATE_RFC2822);
|
|
||||||
$headers .= 'MIME-Version: 1.0' . "\r\n";
|
|
||||||
$headers .= 'Content-type: text/html; charset=iso-8859-1' . "\r\n";
|
|
||||||
$message = '
|
|
||||||
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="'.$this->style.'" type="text/css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0" bgcolor="#EEEEEE" >
|
|
||||||
<table width="100%" cellpadding="10" cellspacing="0" class="backgroundTable" bgcolor="#EEEEEE" >
|
|
||||||
<tr>
|
|
||||||
<td valign="top" align="center">
|
|
||||||
|
|
||||||
|
|
||||||
<table width="550" cellpadding="0" cellspacing="0">
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td style="background-color:#FFFFFF;border-top:0px solid #333333;border-bottom:10px solid #FFFFFF;"><center><a href="'.$this->sitedomain.'"><IMG SRC="'.$this->header_img.'" alt="'.$this->sitename.'" name="Header" BORDER="0" align="center" title="'.$this->sitename.'"></a>
|
|
||||||
</center></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table width="550" cellpadding="20" cellspacing="0" bgcolor="#FFFFFF">
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#FFFFFF" valign="top" style="font-size:12px;color:#000000;line-height:150%;font-family:trebuchet ms;">
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<span style="font-size:20px; font-weight:bold; color:#3399FF; font-family:arial; line-height:110%;">'.$this->header.'</span><br>
|
|
||||||
<span style="font-size:11px;font-weight:normal;color:#666666;font-style:italic;font-family:arial;">'.$this->date.'</span><br>
|
|
||||||
</p>
|
|
||||||
<p>'.$this->body.'</p>
|
|
||||||
<p>'.$this->footer.'</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td style="background-color:#FFFFCC;border-top:10px solid #FFFFFF;" valign="top">
|
|
||||||
<span style="font-size:10px;color:#996600;line-height:100%;font-family:verdana;">
|
|
||||||
This email was sent to you since you are a member of <a href="'.$this->sitedomain.'">'.$this->sitename.'</a>. To change your email preferences, visit your <a href="'.make_http(make_link("preferences")).'">Account preferences</a>.<br />
|
|
||||||
|
|
||||||
<br />
|
|
||||||
Contact us:<br />
|
|
||||||
<a href="'.$this->siteemail.'">'.$this->siteemail.'</a><br /><br />
|
|
||||||
Copyright (C) <a href="'.$this->sitedomain.'">'.$this->sitename.'</a><br />
|
|
||||||
</span></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
';
|
|
||||||
$sent = mail($this->to, $this->subject, $message, $headers);
|
|
||||||
if($sent){
|
|
||||||
log_info("mail", "Sent message '$this->subject' to '$this->to'");
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
log_info("mail", "Error sending message '$this->subject' to '$this->to'");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $sent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
132
core/email.php
Normal file
132
core/email.php
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Email
|
||||||
|
*
|
||||||
|
* A generic email.
|
||||||
|
*/
|
||||||
|
class Email
|
||||||
|
{
|
||||||
|
/** @var string */
|
||||||
|
public $to;
|
||||||
|
/** @var string */
|
||||||
|
public $subject;
|
||||||
|
/** @var string */
|
||||||
|
public $header;
|
||||||
|
/** @var null|string */
|
||||||
|
public $style;
|
||||||
|
/** @var null|string */
|
||||||
|
public $header_img;
|
||||||
|
/** @var null|string */
|
||||||
|
public $sitename;
|
||||||
|
/** @var null|string */
|
||||||
|
public $sitedomain;
|
||||||
|
/** @var null|string */
|
||||||
|
public $siteemail;
|
||||||
|
/** @var string */
|
||||||
|
public $date;
|
||||||
|
/** @var string */
|
||||||
|
public $body;
|
||||||
|
/** @var null|string */
|
||||||
|
public $footer;
|
||||||
|
|
||||||
|
public function __construct(string $to, string $subject, string $header, string $body)
|
||||||
|
{
|
||||||
|
global $config;
|
||||||
|
$this->to = $to;
|
||||||
|
|
||||||
|
$sub_prefix = $config->get_string("mail_sub");
|
||||||
|
|
||||||
|
if (!isset($sub_prefix)) {
|
||||||
|
$this->subject = $subject;
|
||||||
|
} else {
|
||||||
|
$this->subject = $sub_prefix." ".$subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->style = $config->get_string("mail_style");
|
||||||
|
|
||||||
|
$this->header = html_escape($header);
|
||||||
|
$this->header_img = $config->get_string("mail_img");
|
||||||
|
$this->sitename = $config->get_string("site_title");
|
||||||
|
$this->sitedomain = make_http(make_link(""));
|
||||||
|
$this->siteemail = $config->get_string("site_email");
|
||||||
|
$this->date = date("F j, Y");
|
||||||
|
$this->body = $body;
|
||||||
|
$this->footer = $config->get_string("mail_fot");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(): 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 = '
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="'.$this->style.'" type="text/css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0" bgcolor="#EEEEEE" >
|
||||||
|
<table width="100%" cellpadding="10" cellspacing="0" class="backgroundTable" bgcolor="#EEEEEE" >
|
||||||
|
<tr>
|
||||||
|
<td valign="top" align="center">
|
||||||
|
|
||||||
|
|
||||||
|
<table width="550" cellpadding="0" cellspacing="0">
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="background-color:#FFFFFF;border-top:0 solid #333333;border-bottom:10px solid #FFFFFF;"><center><a href="'.$this->sitedomain.'"><IMG SRC="'.$this->header_img.'" alt="'.$this->sitename.'" name="Header" BORDER="0" align="center" title="'.$this->sitename.'"></a>
|
||||||
|
</center></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table width="550" cellpadding="20" cellspacing="0" bgcolor="#FFFFFF">
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#FFFFFF" valign="top" style="font-size:12px;color:#000000;line-height:150%;font-family:trebuchet ms;">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<span style="font-size:20px; font-weight:bold; color:#3399FF; font-family:arial; line-height:110%;">'.$this->header.'</span><br>
|
||||||
|
<span style="font-size:11px;font-weight:normal;color:#666666;font-style:italic;font-family:arial;">'.$this->date.'</span><br>
|
||||||
|
</p>
|
||||||
|
<p>'.$this->body.'</p>
|
||||||
|
<p>'.$this->footer.'</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="background-color:#FFFFCC;border-top:10px solid #FFFFFF;" valign="top">
|
||||||
|
<span style="font-size:10px;color:#996600;line-height:100%;font-family:verdana;">
|
||||||
|
This email was sent to you since you are a member of <a href="'.$this->sitedomain.'">'.$this->sitename.'</a>. To change your email preferences, visit your <a href="'.make_http(make_link("preferences")).'">Account preferences</a>.<br />
|
||||||
|
|
||||||
|
<br />
|
||||||
|
Contact us:<br />
|
||||||
|
<a href="'.$this->siteemail.'">'.$this->siteemail.'</a><br /><br />
|
||||||
|
Copyright (C) <a href="'.$this->sitedomain.'">'.$this->sitename.'</a><br />
|
||||||
|
</span></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
';
|
||||||
|
$sent = mail($this->to, $this->subject, $message, $headers);
|
||||||
|
if ($sent) {
|
||||||
|
log_info("mail", "Sent message '$this->subject' to '$this->to'");
|
||||||
|
} else {
|
||||||
|
log_info("mail", "Error sending message '$this->subject' to '$this->to'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,325 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Generic parent class for all events.
|
|
||||||
*
|
|
||||||
* An event is anything that can be passed around via send_event($blah)
|
|
||||||
*/
|
|
||||||
abstract class Event {
|
|
||||||
public function __construct() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A wake-up call for extensions. Upon recieving an InitExtEvent an extension
|
|
||||||
* should check that it's database tables are there and install them if not,
|
|
||||||
* and set any defaults with Config::set_default_int() and such.
|
|
||||||
*
|
|
||||||
* This event is sent before $user is set to anything
|
|
||||||
*/
|
|
||||||
class InitExtEvent extends Event {}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A signal that a page has been requested.
|
|
||||||
*
|
|
||||||
* User requests /view/42 -> an event is generated with $args = array("view",
|
|
||||||
* "42"); when an event handler asks $event->page_matches("view"), it returns
|
|
||||||
* true and ignores the matched part, such that $event->count_args() = 1 and
|
|
||||||
* $event->get_arg(0) = "42"
|
|
||||||
*/
|
|
||||||
class PageRequestEvent extends Event {
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
public $args;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
public $arg_count;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
public $part_count;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
318
core/event.php
Normal file
318
core/event.php
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Generic parent class for all events.
|
||||||
|
*
|
||||||
|
* An event is anything that can be passed around via send_event($blah)
|
||||||
|
*/
|
||||||
|
abstract class Event
|
||||||
|
{
|
||||||
|
public $stop_processing = false;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wake-up call for extensions. Upon recieving an InitExtEvent an extension
|
||||||
|
* should check that it's database tables are there and install them if not,
|
||||||
|
* and set any defaults with Config::set_default_int() and such.
|
||||||
|
*
|
||||||
|
* This event is sent before $user is set to anything
|
||||||
|
*/
|
||||||
|
class InitExtEvent extends Event
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A signal that a page has been requested.
|
||||||
|
*
|
||||||
|
* User requests /view/42 -> an event is generated with $args = array("view",
|
||||||
|
* "42"); when an event handler asks $event->page_matches("view"), it returns
|
||||||
|
* true and ignores the matched part, such that $event->count_args() = 1 and
|
||||||
|
* $event->get_arg(0) = "42"
|
||||||
|
*/
|
||||||
|
class PageRequestEvent extends Event
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $args;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $arg_count;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $part_count;
|
||||||
|
|
||||||
|
public function __construct(string $path)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class SCoreException
|
|
||||||
*
|
|
||||||
* A base exception to be caught by the upper levels.
|
|
||||||
*/
|
|
||||||
class SCoreException extends Exception {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class PermissionDeniedException
|
|
||||||
*
|
|
||||||
* A fairly common, generic exception.
|
|
||||||
*/
|
|
||||||
class PermissionDeniedException extends SCoreException {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class ImageDoesNotExist
|
|
||||||
*
|
|
||||||
* This exception is used when an Image cannot be found by ID.
|
|
||||||
*
|
|
||||||
* Example: Image::by_id(-1) returns null
|
|
||||||
*/
|
|
||||||
class ImageDoesNotExist extends SCoreException {}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* For validate_input()
|
|
||||||
*/
|
|
||||||
class InvalidInput extends SCoreException {}
|
|
||||||
56
core/exceptions.php
Normal file
56
core/exceptions.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class SCoreException
|
||||||
|
*
|
||||||
|
* A base exception to be caught by the upper levels.
|
||||||
|
*/
|
||||||
|
class SCoreException extends Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class PermissionDeniedException
|
||||||
|
*
|
||||||
|
* A fairly common, generic exception.
|
||||||
|
*/
|
||||||
|
class PermissionDeniedException extends SCoreException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ImageDoesNotExist
|
||||||
|
*
|
||||||
|
* This exception is used when an Image cannot be found by ID.
|
||||||
|
*
|
||||||
|
* Example: Image::by_id(-1) returns null
|
||||||
|
*/
|
||||||
|
class ImageDoesNotExist extends SCoreException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For validate_input()
|
||||||
|
*/
|
||||||
|
class InvalidInput extends SCoreException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is used by the image resizing code when there is not enough memory to perform a resize.
|
||||||
|
*/
|
||||||
|
class InsufficientMemoryException extends SCoreException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* This is used by the image resizing code when there is an error while resizing
|
||||||
|
*/
|
||||||
|
class ImageResizeException extends SCoreException
|
||||||
|
{
|
||||||
|
public $error;
|
||||||
|
|
||||||
|
public function __construct(string $error)
|
||||||
|
{
|
||||||
|
$this->error = $error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,297 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* \page eande Events and Extensions
|
|
||||||
*
|
|
||||||
* An event is a little blob of data saying "something happened", possibly
|
|
||||||
* "something happened, here's the specific data". Events are sent with the
|
|
||||||
* send_event() function. Since events can store data, they can be used to
|
|
||||||
* return data to the extension which sent them, for example:
|
|
||||||
*
|
|
||||||
* \code
|
|
||||||
* $tfe = new TextFormattingEvent($original_text);
|
|
||||||
* send_event($tfe);
|
|
||||||
* $formatted_text = $tfe->formatted;
|
|
||||||
* \endcode
|
|
||||||
*
|
|
||||||
* An extension is something which is capable of reacting to events.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* \page hello The Hello World Extension
|
|
||||||
*
|
|
||||||
* \code
|
|
||||||
* // ext/hello/main.php
|
|
||||||
* public class HelloEvent extends Event {
|
|
||||||
* public function __construct($username) {
|
|
||||||
* $this->username = $username;
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* public class Hello extends Extension {
|
|
||||||
* public function onPageRequest(PageRequestEvent $event) { // Every time a page request is sent
|
|
||||||
* global $user; // Look at the global "currently logged in user" object
|
|
||||||
* send_event(new HelloEvent($user->name)); // Broadcast a signal saying hello to that user
|
|
||||||
* }
|
|
||||||
* public function onHello(HelloEvent $event) { // When the "Hello" signal is recieved
|
|
||||||
* $this->theme->display_hello($event->username); // Display a message on the web page
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // ext/hello/theme.php
|
|
||||||
* public class HelloTheme extends Themelet {
|
|
||||||
* public function display_hello($username) {
|
|
||||||
* global $page;
|
|
||||||
* $h_user = html_escape($username); // Escape the data before adding it to the page
|
|
||||||
* $block = new Block("Hello!", "Hello there $h_user"); // HTML-safe variables start with "h_"
|
|
||||||
* $page->add_block($block); // Add the block to the page
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // ext/hello/test.php
|
|
||||||
* public class HelloTest extends 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
450
core/extension.php
Normal file
450
core/extension.php
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* \page eande Events and Extensions
|
||||||
|
*
|
||||||
|
* An event is a little blob of data saying "something happened", possibly
|
||||||
|
* "something happened, here's the specific data". Events are sent with the
|
||||||
|
* send_event() function. Since events can store data, they can be used to
|
||||||
|
* return data to the extension which sent them, for example:
|
||||||
|
*
|
||||||
|
* \code
|
||||||
|
* $tfe = new TextFormattingEvent($original_text);
|
||||||
|
* send_event($tfe);
|
||||||
|
* $formatted_text = $tfe->formatted;
|
||||||
|
* \endcode
|
||||||
|
*
|
||||||
|
* An extension is something which is capable of reacting to events.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* \page hello The Hello World Extension
|
||||||
|
*
|
||||||
|
* \code
|
||||||
|
* // ext/hello/main.php
|
||||||
|
* public class HelloEvent extends Event {
|
||||||
|
* public function __construct($username) {
|
||||||
|
* $this->username = $username;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* public class Hello extends Extension {
|
||||||
|
* public function onPageRequest(PageRequestEvent $event) { // Every time a page request is sent
|
||||||
|
* global $user; // Look at the global "currently logged in user" object
|
||||||
|
* send_event(new HelloEvent($user->name)); // Broadcast a signal saying hello to that user
|
||||||
|
* }
|
||||||
|
* public function onHello(HelloEvent $event) { // When the "Hello" signal is recieved
|
||||||
|
* $this->theme->display_hello($event->username); // Display a message on the web page
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // ext/hello/theme.php
|
||||||
|
* public class HelloTheme extends Themelet {
|
||||||
|
* public function display_hello($username) {
|
||||||
|
* global $page;
|
||||||
|
* $h_user = html_escape($username); // Escape the data before adding it to the page
|
||||||
|
* $block = new Block("Hello!", "Hello there $h_user"); // HTML-safe variables start with "h_"
|
||||||
|
* $page->add_block($block); // Add the block to the page
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // ext/hello/test.php
|
||||||
|
* public class HelloTest extends 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;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
151
core/imageboard/event.php
Normal file
151
core/imageboard/event.php
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An image is being added to the database.
|
||||||
|
*/
|
||||||
|
class ImageAdditionEvent extends Event
|
||||||
|
{
|
||||||
|
/** @var User */
|
||||||
|
public $user;
|
||||||
|
|
||||||
|
/** @var Image */
|
||||||
|
public $image;
|
||||||
|
|
||||||
|
public $merged = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a new image into the database with its associated
|
||||||
|
* information. Also calls TagSetEvent to set the tags for
|
||||||
|
* this new image.
|
||||||
|
*/
|
||||||
|
public function __construct(Image $image)
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
1044
core/imageboard/image.php
Normal file
1044
core/imageboard/image.php
Normal file
File diff suppressed because it is too large
Load Diff
219
core/imageboard/misc.php
Normal file
219
core/imageboard/misc.php
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
<?php
|
||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||||
|
* 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): void
|
||||||
|
{
|
||||||
|
$target = warehouse_path(Image::IMAGE_DIR, $event->hash);
|
||||||
|
if (!@copy($event->tmpname, $target)) {
|
||||||
|
$errors = error_get_last();
|
||||||
|
throw new UploadException(
|
||||||
|
"Failed to copy file from uploads ({$event->tmpname}) to archive ($target): ".
|
||||||
|
"{$errors['type']} / {$errors['message']}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
58
core/imageboard/search.php
Normal file
58
core/imageboard/search.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
class Querylet
|
||||||
|
{
|
||||||
|
/** @var string */
|
||||||
|
public $sql;
|
||||||
|
/** @var array */
|
||||||
|
public $variables;
|
||||||
|
|
||||||
|
public function __construct(string $sql, array $variables=[])
|
||||||
|
{
|
||||||
|
$this->sql = $sql;
|
||||||
|
$this->variables = $variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function append(Querylet $querylet): void
|
||||||
|
{
|
||||||
|
$this->sql .= $querylet->sql;
|
||||||
|
$this->variables = array_merge($this->variables, $querylet->variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function append_sql(string $sql): void
|
||||||
|
{
|
||||||
|
$this->sql .= $sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add_variable($var): void
|
||||||
|
{
|
||||||
|
$this->variables[] = $var;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TagCondition
|
||||||
|
{
|
||||||
|
/** @var string */
|
||||||
|
public $tag;
|
||||||
|
/** @var bool */
|
||||||
|
public $positive;
|
||||||
|
|
||||||
|
public function __construct(string $tag, bool $positive)
|
||||||
|
{
|
||||||
|
$this->tag = $tag;
|
||||||
|
$this->positive = $positive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImgCondition
|
||||||
|
{
|
||||||
|
/** @var Querylet */
|
||||||
|
public $qlet;
|
||||||
|
/** @var bool */
|
||||||
|
public $positive;
|
||||||
|
|
||||||
|
public function __construct(Querylet $qlet, bool $positive)
|
||||||
|
{
|
||||||
|
$this->qlet = $qlet;
|
||||||
|
$this->positive = $positive;
|
||||||
|
}
|
||||||
|
}
|
||||||
116
core/imageboard/tag.php
Normal file
116
core/imageboard/tag.php
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Class Tag
|
||||||
|
*
|
||||||
|
* A class for organising the tag related functions.
|
||||||
|
*
|
||||||
|
* All the methods are static, one should never actually use a tag object.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class Tag
|
||||||
|
{
|
||||||
|
public static function implode(array $tags): string
|
||||||
|
{
|
||||||
|
sort($tags);
|
||||||
|
$tags = implode(' ', $tags);
|
||||||
|
|
||||||
|
return $tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn a human-supplied string into a valid tag array.
|
||||||
|
*
|
||||||
|
* #return string[]
|
||||||
|
*/
|
||||||
|
public static function explode(string $tags, bool $tagme=true): array
|
||||||
|
{
|
||||||
|
global $database;
|
||||||
|
|
||||||
|
$tags = explode(' ', trim($tags));
|
||||||
|
|
||||||
|
/* sanitise by removing invisible / dodgy characters */
|
||||||
|
$tag_array = [];
|
||||||
|
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 = ["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;
|
||||||
|
}
|
||||||
|
}
|
||||||
71
core/logging.php
Normal file
71
core/logging.php
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
function log_msg(string $section, int $priority, string $message, ?string $flash=null, $args=[])
|
||||||
|
{
|
||||||
|
send_event(new LogEvent($section, $priority, $message, $args));
|
||||||
|
$threshold = defined("CLI_LOG_LEVEL") ? CLI_LOG_LEVEL : 0;
|
||||||
|
|
||||||
|
if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && ($priority >= $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;
|
||||||
|
}
|
||||||
@ -1,424 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* \page themes Themes
|
|
||||||
*
|
|
||||||
* Each extension has a theme with a specific name -- eg. the extension Setup
|
|
||||||
* which is stored in ext/setup/main.php will have a theme called SetupTheme
|
|
||||||
* stored in ext/setup/theme.php. If you want to customise it, create a class
|
|
||||||
* in the file themes/mytheme/setup.theme.php called CustomSetupTheme which
|
|
||||||
* extends SetupTheme and overrides some of its methods.
|
|
||||||
*
|
|
||||||
* Generally an extension should only deal with processing data; whenever it
|
|
||||||
* wants to display something, it should pass the data to be displayed to the
|
|
||||||
* theme object, and the theme will add the data into the global $page
|
|
||||||
* structure.
|
|
||||||
*
|
|
||||||
* A page should make sure that all the data it outputs is free from dangerous
|
|
||||||
* data by using html_escape(), url_escape(), or int_escape() as appropriate.
|
|
||||||
*
|
|
||||||
* Because some HTML can be placed anywhere according to the theme, coming up
|
|
||||||
* with the correct way to link to a page can be hard -- thus we have the
|
|
||||||
* make_link() function, which will take a path like "post/list" and turn it
|
|
||||||
* into a full and correct link, eg /myboard/post/list, /foo/index.php?q=post/list,
|
|
||||||
* etc depending on how things are set up. This should always be used to link
|
|
||||||
* to pages rather than hardcoding a path.
|
|
||||||
*
|
|
||||||
* Various other common functions are available as part of the Themelet class.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class Page
|
|
||||||
*
|
|
||||||
* A data structure for holding all the bits of data that make up a page.
|
|
||||||
*
|
|
||||||
* The various extensions all add whatever they want to this structure,
|
|
||||||
* then Layout turns it into HTML.
|
|
||||||
*/
|
|
||||||
class Page {
|
|
||||||
/** @name Overall */
|
|
||||||
//@{
|
|
||||||
/** @var string */
|
|
||||||
public $mode = "page";
|
|
||||||
/** @var string */
|
|
||||||
public $type = "text/html; charset=utf-8";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set what this page should do; "page", "data", or "redirect".
|
|
||||||
* @param string $mode
|
|
||||||
*/
|
|
||||||
public function set_mode($mode) {
|
|
||||||
$this->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 <a href="'.$this->redirect.'">'.$this->redirect.'</a>';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
print "Invalid page mode";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function grabs all the CSS and JavaScript files sprinkled throughout Shimmie's folders,
|
|
||||||
* concatenates them together into two large files (one for CSS and one for JS) and then stores
|
|
||||||
* them in the /cache/ directory for serving to the user.
|
|
||||||
*
|
|
||||||
* Why do this? Two reasons:
|
|
||||||
* 1. Reduces the number of files the user's browser needs to download.
|
|
||||||
* 2. Allows these cached files to be compressed/minified by the admin.
|
|
||||||
*
|
|
||||||
* TODO: This should really be configurable somehow...
|
|
||||||
*/
|
|
||||||
public function add_auto_html_headers() {
|
|
||||||
global $config;
|
|
||||||
|
|
||||||
$data_href = get_base_href();
|
|
||||||
$theme_name = $config->get_string('theme', 'default');
|
|
||||||
|
|
||||||
$this->add_html_header("<script type='text/javascript'>base_href = '$data_href';</script>", 40);
|
|
||||||
|
|
||||||
# 404/static handler will map these to themes/foo/bar.ico or lib/static/bar.ico
|
|
||||||
$this->add_html_header("<link rel='icon' type='image/x-icon' href='$data_href/favicon.ico'>", 41);
|
|
||||||
$this->add_html_header("<link rel='apple-touch-icon' href='$data_href/apple-touch-icon.png'>", 42);
|
|
||||||
|
|
||||||
//We use $config_latest to make sure cache is reset if config is ever updated.
|
|
||||||
$config_latest = 0;
|
|
||||||
foreach(zglob("data/config/*") as $conf) {
|
|
||||||
$config_latest = max($config_latest, filemtime($conf));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Generate CSS cache files ***/
|
|
||||||
$css_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("<link rel='stylesheet' href='$data_href/$css_lib_cache_file' type='text/css'>", 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("<link rel='stylesheet' href='$data_href/$css_cache_file' type='text/css'>", 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("<script src='$data_href/$js_lib_cache_file' type='text/javascript'></script>", 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("<script src='$data_href/$js_cache_file' type='text/javascript'></script>", 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MockPage extends Page {
|
|
||||||
}
|
|
||||||
614
core/page.php
Normal file
614
core/page.php
Normal file
@ -0,0 +1,614 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* \page themes Themes
|
||||||
|
*
|
||||||
|
* Each extension has a theme with a specific name -- eg. the extension Setup
|
||||||
|
* which is stored in ext/setup/main.php will have a theme called SetupTheme
|
||||||
|
* stored in ext/setup/theme.php. If you want to customise it, create a class
|
||||||
|
* in the file themes/mytheme/setup.theme.php called CustomSetupTheme which
|
||||||
|
* extends SetupTheme and overrides some of its methods.
|
||||||
|
*
|
||||||
|
* Generally an extension should only deal with processing data; whenever it
|
||||||
|
* wants to display something, it should pass the data to be displayed to the
|
||||||
|
* theme object, and the theme will add the data into the global $page
|
||||||
|
* structure.
|
||||||
|
*
|
||||||
|
* A page should make sure that all the data it outputs is free from dangerous
|
||||||
|
* data by using html_escape(), url_escape(), or int_escape() as appropriate.
|
||||||
|
*
|
||||||
|
* Because some HTML can be placed anywhere according to the theme, coming up
|
||||||
|
* with the correct way to link to a page can be hard -- thus we have the
|
||||||
|
* make_link() function, which will take a path like "post/list" and turn it
|
||||||
|
* into a full and correct link, eg /myboard/post/list, /foo/index.php?q=post/list,
|
||||||
|
* etc depending on how things are set up. This should always be used to link
|
||||||
|
* to pages rather than hardcoding a path.
|
||||||
|
*
|
||||||
|
* Various other common functions are available as part of the Themelet class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abstract class PageMode
|
||||||
|
{
|
||||||
|
const REDIRECT = 'redirect';
|
||||||
|
const DATA = 'data';
|
||||||
|
const PAGE = 'page';
|
||||||
|
const FILE = 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Page
|
||||||
|
*
|
||||||
|
* A data structure for holding all the bits of data that make up a page.
|
||||||
|
*
|
||||||
|
* The various extensions all add whatever they want to this structure,
|
||||||
|
* then Layout turns it into HTML.
|
||||||
|
*/
|
||||||
|
class Page
|
||||||
|
{
|
||||||
|
/** @name Overall */
|
||||||
|
//@{
|
||||||
|
/** @var string */
|
||||||
|
public $mode = PageMode::PAGE;
|
||||||
|
/** @var string */
|
||||||
|
public $type = "text/html; charset=utf-8";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set what this page should do; "page", "data", or "redirect".
|
||||||
|
*/
|
||||||
|
public function set_mode(string $mode): void
|
||||||
|
{
|
||||||
|
$this->mode = $mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the page's MIME type.
|
||||||
|
*/
|
||||||
|
public function set_type(string $type): void
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//@}
|
||||||
|
// ==============================================
|
||||||
|
/** @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 <a href="' . $this->redirect . '">' . $this->redirect . '</a>';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
print "Invalid page mode";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function grabs all the CSS and JavaScript files sprinkled throughout Shimmie's folders,
|
||||||
|
* concatenates them together into two large files (one for CSS and one for JS) and then stores
|
||||||
|
* them in the /cache/ directory for serving to the user.
|
||||||
|
*
|
||||||
|
* Why do this? Two reasons:
|
||||||
|
* 1. Reduces the number of files the user's browser needs to download.
|
||||||
|
* 2. Allows these cached files to be compressed/minified by the admin.
|
||||||
|
*
|
||||||
|
* TODO: This should really be configurable somehow...
|
||||||
|
*/
|
||||||
|
public function add_auto_html_headers(): void
|
||||||
|
{
|
||||||
|
global $config;
|
||||||
|
|
||||||
|
$data_href = get_base_href();
|
||||||
|
$theme_name = $config->get_string(SetupConfig::THEME, 'default');
|
||||||
|
|
||||||
|
$this->add_html_header("<script type='text/javascript'>base_href = '$data_href';</script>", 40);
|
||||||
|
|
||||||
|
# static handler will map these to themes/foo/static/bar.ico or ext/handle_static/static/bar.ico
|
||||||
|
$this->add_html_header("<link rel='icon' type='image/x-icon' href='$data_href/favicon.ico'>", 41);
|
||||||
|
$this->add_html_header("<link rel='apple-touch-icon' href='$data_href/apple-touch-icon.png'>", 42);
|
||||||
|
|
||||||
|
//We use $config_latest to make sure cache is reset if config is ever updated.
|
||||||
|
$config_latest = 0;
|
||||||
|
foreach (zglob("data/config/*") as $conf) {
|
||||||
|
$config_latest = max($config_latest, filemtime($conf));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Generate CSS cache files ***/
|
||||||
|
$css_latest = $config_latest;
|
||||||
|
$css_files = array_merge(
|
||||||
|
zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/style.css"),
|
||||||
|
zglob("themes/$theme_name/style.css")
|
||||||
|
);
|
||||||
|
foreach ($css_files as $css) {
|
||||||
|
$css_latest = max($css_latest, filemtime($css));
|
||||||
|
}
|
||||||
|
$css_md5 = md5(serialize($css_files));
|
||||||
|
$css_cache_file = data_path("cache/style/{$theme_name}.{$css_latest}.{$css_md5}.css");
|
||||||
|
if (!file_exists($css_cache_file)) {
|
||||||
|
$css_data = "";
|
||||||
|
foreach ($css_files as $file) {
|
||||||
|
$file_data = file_get_contents($file);
|
||||||
|
$pattern = '/url[\s]*\([\s]*["\']?([^"\'\)]+)["\']?[\s]*\)/';
|
||||||
|
$replace = 'url("../../../' . dirname($file) . '/$1")';
|
||||||
|
$file_data = preg_replace($pattern, $replace, $file_data);
|
||||||
|
$css_data .= $file_data . "\n";
|
||||||
|
}
|
||||||
|
file_put_contents($css_cache_file, $css_data);
|
||||||
|
}
|
||||||
|
$this->add_html_header("<link rel='stylesheet' href='$data_href/$css_cache_file' type='text/css'>", 43);
|
||||||
|
|
||||||
|
/*** Generate JS cache files ***/
|
||||||
|
$js_latest = $config_latest;
|
||||||
|
$js_files = array_merge(
|
||||||
|
[
|
||||||
|
"vendor/bower-asset/jquery/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("<script src='$data_href/$js_cache_file' type='text/javascript'></script>", 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;
|
||||||
|
}
|
||||||
83
core/permissions.php
Normal file
83
core/permissions.php
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class Permissions
|
||||||
|
{
|
||||||
|
public const CHANGE_SETTING = "change_setting"; # modify web-level settings, eg the config table
|
||||||
|
public const OVERRIDE_CONFIG = "override_config"; # modify sys-level settings, eg shimmie.conf.php
|
||||||
|
public const BIG_SEARCH = "big_search"; # search for more than 3 tags at once (speed mode only)
|
||||||
|
|
||||||
|
public const MANAGE_EXTENSION_LIST = "manage_extension_list";
|
||||||
|
public const MANAGE_ALIAS_LIST = "manage_alias_list";
|
||||||
|
public const MASS_TAG_EDIT = "mass_tag_edit";
|
||||||
|
|
||||||
|
public const VIEW_IP = "view_ip"; # view IP addresses associated with things
|
||||||
|
public const BAN_IP = "ban_ip";
|
||||||
|
|
||||||
|
public const EDIT_USER_NAME = "edit_user_name";
|
||||||
|
public const EDIT_USER_PASSWORD = "edit_user_password";
|
||||||
|
public const EDIT_USER_INFO = "edit_user_info"; # email address, etc
|
||||||
|
public const EDIT_USER_CLASS = "edit_user_class";
|
||||||
|
public const DELETE_USER = "delete_user";
|
||||||
|
|
||||||
|
public const CREATE_COMMENT = "create_comment";
|
||||||
|
public const DELETE_COMMENT = "delete_comment";
|
||||||
|
public const BYPASS_COMMENT_CHECKS = "bypass_comment_checks"; # spam etc
|
||||||
|
|
||||||
|
public const REPLACE_IMAGE = "replace_image";
|
||||||
|
public const CREATE_IMAGE = "create_image";
|
||||||
|
public const EDIT_IMAGE_TAG = "edit_image_tag";
|
||||||
|
public const EDIT_IMAGE_SOURCE = "edit_image_source";
|
||||||
|
public const EDIT_IMAGE_OWNER = "edit_image_owner";
|
||||||
|
public const EDIT_IMAGE_LOCK = "edit_image_lock";
|
||||||
|
public const EDIT_IMAGE_TITLE = "edit_image_title";
|
||||||
|
public const BULK_EDIT_IMAGE_TAG = "bulk_edit_image_tag";
|
||||||
|
public const BULK_EDIT_IMAGE_SOURCE = "bulk_edit_image_source";
|
||||||
|
public const DELETE_IMAGE = "delete_image";
|
||||||
|
|
||||||
|
public const BAN_IMAGE = "ban_image";
|
||||||
|
|
||||||
|
public const VIEW_EVENTLOG = "view_eventlog";
|
||||||
|
public const IGNORE_DOWNTIME = "ignore_downtime";
|
||||||
|
|
||||||
|
public const CREATE_IMAGE_REPORT = "create_image_report";
|
||||||
|
public const VIEW_IMAGE_REPORT = "view_image_report"; # deal with reported images
|
||||||
|
|
||||||
|
public const WIKI_ADMIN = "wiki_admin";
|
||||||
|
public const EDIT_WIKI_PAGE = "edit_wiki_page";
|
||||||
|
public const DELETE_WIKI_PAGE = "delete_wiki_page";
|
||||||
|
|
||||||
|
public const MANAGE_BLOCKS = "manage_blocks";
|
||||||
|
|
||||||
|
public const MANAGE_ADMINTOOLS = "manage_admintools";
|
||||||
|
|
||||||
|
public const VIEW_OTHER_PMS = "view_other_pms";
|
||||||
|
public const EDIT_FEATURE = "edit_feature";
|
||||||
|
public const BULK_EDIT_VOTE = "bulk_edit_vote";
|
||||||
|
public const EDIT_OTHER_VOTE = "edit_other_vote";
|
||||||
|
public const VIEW_SYSINTO = "view_sysinfo";
|
||||||
|
|
||||||
|
public const HELLBANNED = "hellbanned";
|
||||||
|
public const VIEW_HELLBANNED = "view_hellbanned";
|
||||||
|
|
||||||
|
public const PROTECTED = "protected"; # only admins can modify protected users (stops a moderator changing an admin's password)
|
||||||
|
|
||||||
|
public const EDIT_IMAGE_RATING = "edit_image_rating";
|
||||||
|
public const BULK_EDIT_IMAGE_RATING = "bulk_edit_image_rating";
|
||||||
|
|
||||||
|
public const VIEW_TRASH = "view_trash";
|
||||||
|
|
||||||
|
public const PERFORM_BULK_ACTIONS = "perform_bulk_actions";
|
||||||
|
|
||||||
|
public const BULK_ADD = "bulk_add";
|
||||||
|
public const EDIT_FILES = "edit_files";
|
||||||
|
public const EDIT_TAG_CATEGORIES = "edit_tag_categories";
|
||||||
|
public const RESCAN_MEDIA = "rescan_media";
|
||||||
|
public const SEE_IMAGE_VIEW_COUNTS = "see_image_view_counts";
|
||||||
|
|
||||||
|
public const ARTISTS_ADMIN = "artists_admin";
|
||||||
|
public const BLOTTER_ADMIN = "blotter_admin";
|
||||||
|
public const FORUM_ADMIN = "forum_admin";
|
||||||
|
public const NOTES_ADMIN = "notes_admin";
|
||||||
|
public const POOLS_ADMIN = "pools_admin";
|
||||||
|
public const TIPS_ADMIN = "tips_admin";
|
||||||
|
}
|
||||||
834
core/polyfills.php
Normal file
834
core/polyfills.php
Normal file
@ -0,0 +1,834 @@
|
|||||||
|
<?php
|
||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||||
|
* Things which should be in the core API *
|
||||||
|
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an item from an array
|
||||||
|
*/
|
||||||
|
function array_remove(array $array, $to_remove): array
|
||||||
|
{
|
||||||
|
$array = array_unique($array);
|
||||||
|
$a2 = [];
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
function array_add(array $array, $element): array
|
||||||
|
{
|
||||||
|
// 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
|
||||||
|
*/
|
||||||
|
function array_iunique(array $array): array
|
||||||
|
{
|
||||||
|
$ok = [];
|
||||||
|
foreach ($array as $element) {
|
||||||
|
$found = false;
|
||||||
|
foreach ($ok as $existing) {
|
||||||
|
if (strtolower($element) == strtolower($existing)) {
|
||||||
|
$found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$found) {
|
||||||
|
$ok[] = $element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Figure out if an IP is in a specified range
|
||||||
|
*
|
||||||
|
* from http://uk.php.net/network
|
||||||
|
*/
|
||||||
|
function ip_in_range(string $IP, string $CIDR): bool
|
||||||
|
{
|
||||||
|
list($net, $mask) = explode("/", $CIDR);
|
||||||
|
|
||||||
|
$ip_net = ip2long($net);
|
||||||
|
$ip_mask = ~((1 << (32 - $mask)) - 1);
|
||||||
|
|
||||||
|
$ip_ip = ip2long($IP);
|
||||||
|
|
||||||
|
$ip_ip_net = $ip_ip & $ip_mask;
|
||||||
|
|
||||||
|
return ($ip_ip_net == $ip_net);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an entire file heirachy
|
||||||
|
*
|
||||||
|
* from a patch by Christian Walde; only intended for use in the
|
||||||
|
* "extension manager" extension, but it seems to fit better here
|
||||||
|
*/
|
||||||
|
function deltree(string $f): void
|
||||||
|
{
|
||||||
|
//Because Windows (I know, bad excuse)
|
||||||
|
if (PHP_OS === 'WINNT') {
|
||||||
|
$real = realpath($f);
|
||||||
|
$path = realpath('./').'\\'.str_replace('/', '\\', $f);
|
||||||
|
if ($path != $real) {
|
||||||
|
rmdir($path);
|
||||||
|
} else {
|
||||||
|
foreach (glob($f.'/*') as $sf) {
|
||||||
|
if (is_dir($sf) && !is_link($sf)) {
|
||||||
|
deltree($sf);
|
||||||
|
} else {
|
||||||
|
unlink($sf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rmdir($f);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (is_link($f)) {
|
||||||
|
unlink($f);
|
||||||
|
} elseif (is_dir($f)) {
|
||||||
|
foreach (glob($f.'/*') as $sf) {
|
||||||
|
if (is_dir($sf) && !is_link($sf)) {
|
||||||
|
deltree($sf);
|
||||||
|
} else {
|
||||||
|
unlink($sf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rmdir($f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy an entire file hierarchy
|
||||||
|
*
|
||||||
|
* from a comment on http://uk.php.net/copy
|
||||||
|
*/
|
||||||
|
function full_copy(string $source, string $target): void
|
||||||
|
{
|
||||||
|
if (is_dir($source)) {
|
||||||
|
@mkdir($target);
|
||||||
|
|
||||||
|
$d = dir($source);
|
||||||
|
|
||||||
|
while (false !== ($entry = $d->read())) {
|
||||||
|
if ($entry == '.' || $entry == '..') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$Entry = $source . '/' . $entry;
|
||||||
|
if (is_dir($Entry)) {
|
||||||
|
full_copy($Entry, $target . '/' . $entry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
copy($Entry, $target . '/' . $entry);
|
||||||
|
}
|
||||||
|
$d->close();
|
||||||
|
} else {
|
||||||
|
copy($source, $target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of all the regular files in a directory and subdirectories
|
||||||
|
*/
|
||||||
|
function list_files(string $base, string $_sub_dir=""): array
|
||||||
|
{
|
||||||
|
assert(is_dir($base));
|
||||||
|
|
||||||
|
$file_list = [];
|
||||||
|
|
||||||
|
$files = [];
|
||||||
|
$dir = opendir("$base/$_sub_dir");
|
||||||
|
while ($f = readdir($dir)) {
|
||||||
|
$files[] = $f;
|
||||||
|
}
|
||||||
|
closedir($dir);
|
||||||
|
sort($files);
|
||||||
|
|
||||||
|
foreach ($files as $filename) {
|
||||||
|
$full_path = "$base/$_sub_dir/$filename";
|
||||||
|
|
||||||
|
if (is_link($full_path)) {
|
||||||
|
// 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 .= "</$name>\n";
|
||||||
|
} else {
|
||||||
|
$xml .= "/>\n";
|
||||||
|
}
|
||||||
|
return $xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Original PHP code by Chirp Internet: www.chirp.com.au
|
||||||
|
* Please acknowledge use of this code by including this header.
|
||||||
|
*/
|
||||||
|
function truncate(string $string, int $limit, string $break=" ", string $pad="..."): string
|
||||||
|
{
|
||||||
|
// return with no change if string is shorter than $limit
|
||||||
|
if (strlen($string) <= $limit) {
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// is $break present between $limit and the end of the string?
|
||||||
|
if (false !== ($breakpoint = strpos($string, $break, $limit))) {
|
||||||
|
if ($breakpoint < strlen($string) - 1) {
|
||||||
|
$string = substr($string, 0, $breakpoint) . $pad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn a human readable filesize into an integer, eg 1KB -> 1024
|
||||||
|
*/
|
||||||
|
function parse_shorthand_int(string $limit): int
|
||||||
|
{
|
||||||
|
if (preg_match('/^([\d\.]+)([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 ? "<time datetime='$cpu'>$hum</time>" : $hum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given string is a valid date-time. ( Format: yyyy-mm-dd hh:mm:ss )
|
||||||
|
*/
|
||||||
|
function isValidDateTime(string $dateTime): bool
|
||||||
|
{
|
||||||
|
if (preg_match("/^(\d{4})-(\d{2})-(\d{2}) ([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/", $dateTime, $matches)) {
|
||||||
|
if (checkdate($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 (;$i<count($tokens);$i++) {
|
||||||
|
if ($tokens[$i][0] === T_CLASS) {
|
||||||
|
for ($j=$i+1;$j<count($tokens);$j++) {
|
||||||
|
if ($tokens[$j] === '{') {
|
||||||
|
$class = $tokens[$i+2][1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $class;
|
||||||
|
}
|
||||||
136
core/send_event.php
Normal file
136
core/send_event.php
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||||
|
* Event API *
|
||||||
|
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
global $_shm_event_listeners;
|
||||||
|
$_shm_event_listeners = [];
|
||||||
|
|
||||||
|
function _load_event_listeners(): void
|
||||||
|
{
|
||||||
|
global $_shm_event_listeners;
|
||||||
|
|
||||||
|
$cache_path = data_path("cache/shm_event_listeners.php");
|
||||||
|
if (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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _clear_cached_event_listeners(): void
|
||||||
|
{
|
||||||
|
if (file_exists(data_path("cache/shm_event_listeners.php"))) {
|
||||||
|
unlink(data_path("cache/shm_event_listeners.php"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _set_event_listeners(): void
|
||||||
|
{
|
||||||
|
global $_shm_event_listeners;
|
||||||
|
$_shm_event_listeners = [];
|
||||||
|
|
||||||
|
foreach (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->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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,16 +19,18 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @private */
|
function _d(string $name, $value): void
|
||||||
function _d($name, $value) {if(!defined($name)) define($name, $value);}
|
{
|
||||||
|
if (!defined($name)) {
|
||||||
|
define($name, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
_d("DATABASE_DSN", null); // string PDO database connection details
|
_d("DATABASE_DSN", null); // string PDO database connection details
|
||||||
_d("DATABASE_KA", true); // string Keep database connection alive
|
_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("CACHE_DSN", null); // string cache connection details
|
||||||
_d("DEBUG", false); // boolean print various debugging 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("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("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("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes
|
||||||
_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance
|
_d("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("NICE_URLS", false); // boolean force niceurl mode
|
||||||
_d("SEARCH_ACCEL", false); // boolean use search accelerator
|
_d("SEARCH_ACCEL", false); // boolean use search accelerator
|
||||||
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
|
_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("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("EXTRA_EXTS", ""); // string optional extra extensions
|
||||||
_d("BASE_URL", null); // string force a specific base URL (default is auto-detect)
|
_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
|
* Calculated settings - you should never need to change these
|
||||||
* directly, only the things they're built from
|
* directly, only the things they're built from
|
||||||
*/
|
*/
|
||||||
_d("SCORE_VERSION", 'develop/'.VERSION); // string SCore version
|
_d("SCORE_VERSION", 'develop/'.VERSION); // string SCore version
|
||||||
_d("ENABLED_EXTS", CORE_EXTS.",".EXTRA_EXTS);
|
|
||||||
|
|
||||||
|
|
||||||
119
core/tests/polyfills.test.php
Normal file
119
core/tests/polyfills.test.php
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
require_once "core/polyfills.php";
|
||||||
|
|
||||||
|
class PolyfillsTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
public function test_html_escape()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
html_escape("Foo & <waffles>"),
|
||||||
|
"Foo & <waffles>"
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
html_unescape("Foo & <waffles>"),
|
||||||
|
"Foo & <waffles>"
|
||||||
|
);
|
||||||
|
|
||||||
|
$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/\\/\/")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
core/tests/util.test.php
Normal file
65
core/tests/util.test.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
require_once "core/util.php";
|
||||||
|
|
||||||
|
class UtilTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
public function test_warehouse_path()
|
||||||
|
{
|
||||||
|
$hash = "7ac19c10d6859415";
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
join_path(DATA_DIR, "base", $hash),
|
||||||
|
warehouse_path("base", $hash, false, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
join_path(DATA_DIR, "base", "7a", $hash),
|
||||||
|
warehouse_path("base", $hash, false, 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
join_path(DATA_DIR, "base", "7a", "c1", $hash),
|
||||||
|
warehouse_path("base", $hash, false, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
join_path(DATA_DIR, "base", "7a", "c1", "9c", $hash),
|
||||||
|
warehouse_path("base", $hash, false, 3)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", $hash),
|
||||||
|
warehouse_path("base", $hash, false, 4)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", $hash),
|
||||||
|
warehouse_path("base", $hash, false, 5)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", $hash),
|
||||||
|
warehouse_path("base", $hash, false, 6)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", $hash),
|
||||||
|
warehouse_path("base", $hash, false, 7)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
|
||||||
|
warehouse_path("base", $hash, false, 8)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
|
||||||
|
warehouse_path("base", $hash, false, 9)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
|
||||||
|
warehouse_path("base", $hash, false, 10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
119
core/urls.php
Normal file
119
core/urls.php
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||||
|
* HTML Generation *
|
||||||
|
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
class Link
|
||||||
|
{
|
||||||
|
public $page;
|
||||||
|
public $query;
|
||||||
|
|
||||||
|
public function __construct(?string $page=null, ?string $query=null)
|
||||||
|
{
|
||||||
|
$this->page = $page;
|
||||||
|
$this->query = $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function make_link(): string
|
||||||
|
{
|
||||||
|
return make_link($this->page, $this->query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Figure out the correct way to link to a page, taking into account
|
||||||
|
* things like the nice URLs setting.
|
||||||
|
*
|
||||||
|
* eg make_link("post/list") becomes "/v2/index.php?q=post/list"
|
||||||
|
*/
|
||||||
|
function make_link(?string $page=null, ?string $query=null): string
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -1,300 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* @param mixed $row
|
|
||||||
* @return User
|
|
||||||
*/
|
|
||||||
function _new_user($row) {
|
|
||||||
return new User($row);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class User
|
|
||||||
*
|
|
||||||
* An object representing a row in the "users" table.
|
|
||||||
*
|
|
||||||
* The currently logged in user will always be accessible via the global variable $user.
|
|
||||||
*/
|
|
||||||
class User {
|
|
||||||
/** @var int */
|
|
||||||
public $id;
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
public $name;
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
public $email;
|
|
||||||
|
|
||||||
public $join_date;
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
public $passhash;
|
|
||||||
|
|
||||||
/** @var UserClass */
|
|
||||||
public $class;
|
|
||||||
|
|
||||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
* Initialisation *
|
|
||||||
* *
|
|
||||||
* User objects shouldn't be created directly, they should be *
|
|
||||||
* fetched from the database like so: *
|
|
||||||
* *
|
|
||||||
* $user = User::by_name("bob"); *
|
|
||||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One will very rarely construct a user directly, more common
|
|
||||||
* would be to use User::by_id, User::by_session, etc.
|
|
||||||
*
|
|
||||||
* @param mixed $row
|
|
||||||
* @throws SCoreException
|
|
||||||
*/
|
|
||||||
public function __construct($row) {
|
|
||||||
global $_shm_user_classes;
|
|
||||||
|
|
||||||
$this->id = int_escape($row['id']);
|
|
||||||
$this->name = $row['name'];
|
|
||||||
$this->email = $row['email'];
|
|
||||||
$this->join_date = $row['joindate'];
|
|
||||||
$this->passhash = $row['pass'];
|
|
||||||
|
|
||||||
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 "<img class=\"avatar gravatar\" src=\"http://www.gravatar.com/avatar/$hash.jpg?s=$s&d=$d&r=$r&cacheBreak=$cb\">";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an auth token to be used in POST forms
|
|
||||||
*
|
|
||||||
* password = secret, avoid storing directly
|
|
||||||
* passhash = bcrypt(password), so someone who gets to the database can't get passwords
|
|
||||||
* sesskey = md5(passhash . IP), so if it gets sniffed it can't be used from another IP,
|
|
||||||
* and it can't be used to get the passhash to generate new sesskeys
|
|
||||||
* authtok = md5(sesskey, salt), presented to the user in web forms, to make sure that
|
|
||||||
* the form was generated within the session. Salted and re-hashed so that
|
|
||||||
* reading a web page from the user's cache doesn't give access to the session key
|
|
||||||
*
|
|
||||||
* @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 '<input type="hidden" name="auth_token" value="'.$at.'">';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function check_auth_token() {
|
|
||||||
return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MockUser extends User {
|
|
||||||
public function __construct($name) {
|
|
||||||
$row = array(
|
|
||||||
"name" => $name,
|
|
||||||
"id" => 1,
|
|
||||||
"email" => "",
|
|
||||||
"joindate" => "",
|
|
||||||
"pass" => "",
|
|
||||||
"class" => "admin",
|
|
||||||
);
|
|
||||||
parent::__construct($row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
236
core/user.php
Normal file
236
core/user.php
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
function _new_user(array $row): User
|
||||||
|
{
|
||||||
|
return new User($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class User
|
||||||
|
*
|
||||||
|
* An object representing a row in the "users" table.
|
||||||
|
*
|
||||||
|
* The currently logged in user will always be accessible via the global variable $user.
|
||||||
|
*/
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
/** @var int */
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
public $name;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
public $email;
|
||||||
|
|
||||||
|
public $join_date;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
public $passhash;
|
||||||
|
|
||||||
|
/** @var UserClass */
|
||||||
|
public $class;
|
||||||
|
|
||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
* Initialisation *
|
||||||
|
* *
|
||||||
|
* User objects shouldn't be created directly, they should be *
|
||||||
|
* fetched from the database like so: *
|
||||||
|
* *
|
||||||
|
* $user = User::by_name("bob"); *
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One will very rarely construct a user directly, more common
|
||||||
|
* would be to use User::by_id, User::by_session, etc.
|
||||||
|
*
|
||||||
|
* @throws SCoreException
|
||||||
|
*/
|
||||||
|
public function __construct(array $row)
|
||||||
|
{
|
||||||
|
global $_shm_user_classes;
|
||||||
|
|
||||||
|
$this->id = int_escape($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 "<img class=\"avatar gravatar\" src=\"https://www.gravatar.com/avatar/$hash.jpg?s=$s&d=$d&r=$r&cacheBreak=$cb\">";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an auth token to be used in POST forms
|
||||||
|
*
|
||||||
|
* password = secret, avoid storing directly
|
||||||
|
* passhash = bcrypt(password), so someone who gets to the database can't get passwords
|
||||||
|
* sesskey = md5(passhash . IP), so if it gets sniffed it can't be used from another IP,
|
||||||
|
* and it can't be used to get the passhash to generate new sesskeys
|
||||||
|
* authtok = md5(sesskey, salt), presented to the user in web forms, to make sure that
|
||||||
|
* the form was generated within the session. Salted and re-hashed so that
|
||||||
|
* reading a web page from the user's cache doesn't give access to the session key
|
||||||
|
*/
|
||||||
|
public function get_auth_token(): string
|
||||||
|
{
|
||||||
|
global $config;
|
||||||
|
$salt = DATABASE_DSN;
|
||||||
|
$addr = get_session_ip($config);
|
||||||
|
return md5(md5($this->passhash . $addr) . "salty-csrf-" . $salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_auth_html(): string
|
||||||
|
{
|
||||||
|
$at = $this->get_auth_token();
|
||||||
|
return '<input type="hidden" name="auth_token" value="'.$at.'">';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function check_auth_token(): bool
|
||||||
|
{
|
||||||
|
return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,200 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @global UserClass[] $_shm_user_classes
|
|
||||||
*/
|
|
||||||
global $_shm_user_classes;
|
|
||||||
$_shm_user_classes = array();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class UserClass
|
|
||||||
*/
|
|
||||||
class UserClass {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var null|string
|
|
||||||
*/
|
|
||||||
public $name = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \UserClass|null
|
|
||||||
*/
|
|
||||||
public $parent = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
public $abilities = array();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @param null|string $parent
|
|
||||||
* @param array $abilities
|
|
||||||
*/
|
|
||||||
public function __construct($name, $parent=null, $abilities=array()) {
|
|
||||||
global $_shm_user_classes;
|
|
||||||
|
|
||||||
$this->name = $name;
|
|
||||||
$this->abilities = $abilities;
|
|
||||||
|
|
||||||
if(!is_null($parent)) {
|
|
||||||
$this->parent = $_shm_user_classes[$parent];
|
|
||||||
}
|
|
||||||
|
|
||||||
$_shm_user_classes[$name] = $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if this class of user can perform an action or has ability.
|
|
||||||
*
|
|
||||||
* @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";
|
|
||||||
|
|
||||||
235
core/userclass.php
Normal file
235
core/userclass.php
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @global UserClass[] $_shm_user_classes
|
||||||
|
*/
|
||||||
|
global $_shm_user_classes;
|
||||||
|
$_shm_user_classes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class UserClass
|
||||||
|
*/
|
||||||
|
class UserClass
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ?string
|
||||||
|
*/
|
||||||
|
public $name = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ?UserClass
|
||||||
|
*/
|
||||||
|
public $parent = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $abilities = [];
|
||||||
|
|
||||||
|
public function __construct(string $name, string $parent=null, array $abilities=[])
|
||||||
|
{
|
||||||
|
global $_shm_user_classes;
|
||||||
|
|
||||||
|
$this->name = $name;
|
||||||
|
$this->abilities = $abilities;
|
||||||
|
|
||||||
|
if (!is_null($parent)) {
|
||||||
|
$this->parent = $_shm_user_classes[$parent];
|
||||||
|
}
|
||||||
|
|
||||||
|
$_shm_user_classes[$name] = $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if this class of user can perform an action or has ability.
|
||||||
|
*
|
||||||
|
* @throws SCoreException
|
||||||
|
*/
|
||||||
|
public function can(string $ability): bool
|
||||||
|
{
|
||||||
|
if (array_key_exists($ability, $this->abilities)) {
|
||||||
|
$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";
|
||||||
1826
core/util.inc.php
1826
core/util.inc.php
File diff suppressed because it is too large
Load Diff
618
core/util.php
Normal file
618
core/util.php
Normal file
@ -0,0 +1,618 @@
|
|||||||
|
<?php
|
||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||||
|
* Misc *
|
||||||
|
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
const DATA_DIR = "data";
|
||||||
|
|
||||||
|
|
||||||
|
function mtimefile(string $file): string
|
||||||
|
{
|
||||||
|
$data_href = get_base_href();
|
||||||
|
$mtime = filemtime($file);
|
||||||
|
return "$data_href/$file?$mtime";
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_theme(): string
|
||||||
|
{
|
||||||
|
global $config;
|
||||||
|
$theme = $config->get_string(SetupConfig::THEME, "default");
|
||||||
|
if (!file_exists("themes/$theme")) {
|
||||||
|
$theme = "default";
|
||||||
|
}
|
||||||
|
return $theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
function contact_link(): ?string
|
||||||
|
{
|
||||||
|
global $config;
|
||||||
|
$text = $config->get_string('contact_link');
|
||||||
|
if (is_null($text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
startsWith($text, "http:") ||
|
||||||
|
startsWith($text, "https:") ||
|
||||||
|
startsWith($text, "mailto:")
|
||||||
|
) {
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strpos($text, "@")) {
|
||||||
|
return "mailto:$text";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strpos($text, "/")) {
|
||||||
|
return "http://$text";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if HTTPS is enabled for the server.
|
||||||
|
*/
|
||||||
|
function is_https_enabled(): bool
|
||||||
|
{
|
||||||
|
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two Block objects, used to sort them before being displayed
|
||||||
|
*/
|
||||||
|
function blockcmp(Block $a, Block $b): int
|
||||||
|
{
|
||||||
|
if ($a->position == $b->position) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return ($a->position > $b->position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = "<br>Took $time seconds (db:$dbtime) and {$i_mem}MB of RAM";
|
||||||
|
$debug .= "; Used $i_files files and {$database->query_count} queries";
|
||||||
|
$debug .= "; Sent $_shm_event_count events";
|
||||||
|
$debug .= "; $hits cache hits and $miss misses";
|
||||||
|
$debug .= "; Shimmie version ". VERSION . $commit; // .", 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'] = "<cli command>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 ? "<p><b>Hash:</b> $hash" : "";
|
||||||
|
//'.$h_hash.'
|
||||||
|
|
||||||
|
header("HTTP/1.0 500 Internal Error");
|
||||||
|
echo '
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Internal error - SCore-'.$version.'</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Internal Error</h1>
|
||||||
|
<p><b>Message:</b> '.$message.'
|
||||||
|
<p><b>Version:</b> '.$version.' (on '.phpversion().')
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) ? ", <a href='".make_link("ip_ban/list", "ip=$ip&reason=$u_reason&end=$u_end#add")."'>Ban</a>" : "";
|
||||||
|
$ip = $user->can(Permissions::VIEW_IP) ? $ip.$ban : "";
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a form tag with relevant auth token and stuff
|
||||||
|
*/
|
||||||
|
function make_form(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit=""): string
|
||||||
|
{
|
||||||
|
global $user;
|
||||||
|
if ($method == "GET") {
|
||||||
|
$link = html_escape($target);
|
||||||
|
$target = make_link($target);
|
||||||
|
$extra_inputs = "<input type='hidden' name='q' value='$link'>";
|
||||||
|
} else {
|
||||||
|
$extra_inputs = $user->get_auth_html();
|
||||||
|
}
|
||||||
|
|
||||||
|
$extra = empty($form_id) ? '' : 'id="'. $form_id .'"';
|
||||||
|
if ($multipart) {
|
||||||
|
$extra .= " enctype='multipart/form-data'";
|
||||||
|
}
|
||||||
|
if ($onsubmit) {
|
||||||
|
$extra .= ' onsubmit="'.$onsubmit.'"';
|
||||||
|
}
|
||||||
|
return '<form action="'.$target.'" method="'.$method.'" '.$extra.'>'.$extra_inputs;
|
||||||
|
}
|
||||||
33
ext/admin/info.php
Normal file
33
ext/admin/info.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name: Admin Controls
|
||||||
|
* Author: Shish <webmaster@shishnet.org>
|
||||||
|
* Link: http://code.shishnet.org/shimmie2/
|
||||||
|
* License: GPLv2
|
||||||
|
* Description: Various things to make admins' lives easier
|
||||||
|
* Documentation:
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
class AdminPageInfo extends ExtensionInfo
|
||||||
|
{
|
||||||
|
public const KEY = "admin";
|
||||||
|
|
||||||
|
public $key = self::KEY;
|
||||||
|
public $name = "Admin Controls";
|
||||||
|
public $url = self::SHIMMIE_URL;
|
||||||
|
public $authors = self::SHISH_AUTHOR;
|
||||||
|
public $license = self::LICENSE_GPLV2;
|
||||||
|
public $description = "Various things to make admins' lives easier";
|
||||||
|
public $documentation =
|
||||||
|
"Various moderate-level tools for admins; for advanced, obscure, and possibly dangerous tools see the shimmie2-utils script set
|
||||||
|
<p>Lowercase all tags:
|
||||||
|
<br>Set all tags to lowercase for consistency
|
||||||
|
<p>Recount tag use:
|
||||||
|
<br>If the counts of images per tag get messed up somehow, this will reset them, and remove any unused tags
|
||||||
|
<p>Database dump:
|
||||||
|
<br>Download the contents of the database in plain text format, useful for backups.
|
||||||
|
<p>Image dump:
|
||||||
|
<br>Download all the images as a .zip file (Requires ZipArchive)";
|
||||||
|
}
|
||||||
@ -1,78 +1,56 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* Name: Admin Controls
|
|
||||||
* Author: Shish <webmaster@shishnet.org>
|
|
||||||
* Link: http://code.shishnet.org/shimmie2/
|
|
||||||
* License: GPLv2
|
|
||||||
* Description: Various things to make admins' lives easier
|
|
||||||
* Documentation:
|
|
||||||
* Various moderate-level tools for admins; for advanced, obscure, and
|
|
||||||
* possibly dangerous tools see the shimmie2-utils script set
|
|
||||||
* <p>Lowercase all tags:
|
|
||||||
* <br>Set all tags to lowercase for consistency
|
|
||||||
* <p>Recount tag use:
|
|
||||||
* <br>If the counts of images per tag get messed up somehow, this will
|
|
||||||
* reset them, and remove any unused tags
|
|
||||||
* <p>Database dump:
|
|
||||||
* <br>Download the contents of the database in plain text format, useful
|
|
||||||
* for backups.
|
|
||||||
* <p>Image dump:
|
|
||||||
* <br>Download all the images as a .zip file (Requires ZipArchive)
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sent when the admin page is ready to be added to
|
* Sent when the admin page is ready to be added to
|
||||||
*/
|
*/
|
||||||
class AdminBuildingEvent extends Event {
|
class AdminBuildingEvent extends Event
|
||||||
/** @var \Page */
|
{
|
||||||
|
/** @var Page */
|
||||||
public $page;
|
public $page;
|
||||||
|
|
||||||
/**
|
public function __construct(Page $page)
|
||||||
* @param Page $page
|
{
|
||||||
*/
|
|
||||||
public function __construct(Page $page) {
|
|
||||||
$this->page = $page;
|
$this->page = $page;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AdminActionEvent extends Event {
|
class AdminActionEvent extends Event
|
||||||
|
{
|
||||||
/** @var string */
|
/** @var string */
|
||||||
public $action;
|
public $action;
|
||||||
/** @var bool */
|
/** @var bool */
|
||||||
public $redirect = true;
|
public $redirect = true;
|
||||||
|
|
||||||
/**
|
public function __construct(string $action)
|
||||||
* @param string $action
|
{
|
||||||
*/
|
|
||||||
public function __construct(/*string*/ $action) {
|
|
||||||
$this->action = $action;
|
$this->action = $action;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AdminPage extends Extension {
|
class AdminPage extends Extension
|
||||||
public function onPageRequest(PageRequestEvent $event) {
|
{
|
||||||
|
public function onPageRequest(PageRequestEvent $event)
|
||||||
|
{
|
||||||
global $page, $user;
|
global $page, $user;
|
||||||
|
|
||||||
if($event->page_matches("admin")) {
|
if ($event->page_matches("admin")) {
|
||||||
if(!$user->can("manage_admintools")) {
|
if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) {
|
||||||
$this->theme->display_permission_denied();
|
$this->theme->display_permission_denied();
|
||||||
}
|
} else {
|
||||||
else {
|
if ($event->count_args() == 0) {
|
||||||
if($event->count_args() == 0) {
|
|
||||||
send_event(new AdminBuildingEvent($page));
|
send_event(new AdminBuildingEvent($page));
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$action = $event->get_arg(0);
|
$action = $event->get_arg(0);
|
||||||
$aae = new AdminActionEvent($action);
|
$aae = new AdminActionEvent($action);
|
||||||
|
|
||||||
if($user->check_auth_token()) {
|
if ($user->check_auth_token()) {
|
||||||
log_info("admin", "Util: $action");
|
log_info("admin", "Util: $action");
|
||||||
set_time_limit(0);
|
set_time_limit(0);
|
||||||
send_event($aae);
|
send_event($aae);
|
||||||
}
|
}
|
||||||
|
|
||||||
if($aae->redirect) {
|
if ($aae->redirect) {
|
||||||
$page->set_mode("redirect");
|
$page->set_mode(PageMode::REDIRECT);
|
||||||
$page->set_redirect(make_link("admin"));
|
$page->set_redirect(make_link("admin"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,83 +58,113 @@ class AdminPage extends Extension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onCommand(CommandEvent $event) {
|
public function onCommand(CommandEvent $event)
|
||||||
if($event->cmd == "help") {
|
{
|
||||||
print " get-page [query string]\n";
|
if ($event->cmd == "help") {
|
||||||
print " eg 'get-page post/list'\n\n";
|
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") {
|
if ($event->cmd == "get-page") {
|
||||||
global $page;
|
global $page;
|
||||||
send_event(new PageRequestEvent($event->args[0]));
|
send_event(new PageRequestEvent($event->args[0]));
|
||||||
$page->display();
|
$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) {
|
public function onAdminBuilding(AdminBuildingEvent $event)
|
||||||
|
{
|
||||||
$this->theme->display_page();
|
$this->theme->display_page();
|
||||||
$this->theme->display_form();
|
$this->theme->display_form();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event) {
|
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||||
|
{
|
||||||
global $user;
|
global $user;
|
||||||
if($user->can("manage_admintools")) {
|
if ($event->parent==="system") {
|
||||||
|
if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
|
||||||
|
$event->add_nav_link("admin", new Link('admin'), "Board Admin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
|
||||||
|
{
|
||||||
|
global $user;
|
||||||
|
if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
|
||||||
$event->add_link("Board Admin", make_link("admin"));
|
$event->add_link("Board Admin", make_link("admin"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onAdminAction(AdminActionEvent $event) {
|
public function onAdminAction(AdminActionEvent $event)
|
||||||
|
{
|
||||||
$action = $event->action;
|
$action = $event->action;
|
||||||
if(method_exists($this, $action)) {
|
if (method_exists($this, $action)) {
|
||||||
$event->redirect = $this->$action();
|
$event->redirect = $this->$action();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onPostListBuilding(PostListBuildingEvent $event) {
|
// public function onPostListBuilding(PostListBuildingEvent $event)
|
||||||
global $user;
|
// {
|
||||||
if($user->can("manage_admintools") && !empty($event->search_terms)) {
|
// global $user;
|
||||||
$event->add_control($this->theme->dbq_html(implode(" ", $event->search_terms)));
|
// if ($user->can("manage_admintools") && !empty($event->search_terms)) {
|
||||||
}
|
// $event->add_control($this->theme->dbq_html(Tag::implode($event->search_terms)));
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
private function delete_by_query() {
|
private function delete_by_query()
|
||||||
|
{
|
||||||
global $page;
|
global $page;
|
||||||
$query = $_POST['query'];
|
$query = $_POST['query'];
|
||||||
$reason = @$_POST['reason'];
|
$reason = @$_POST['reason'];
|
||||||
|
|
||||||
assert(strlen($query) > 1);
|
assert(strlen($query) > 1);
|
||||||
|
|
||||||
log_warning("admin", "Mass deleting: $query");
|
$images = Image::find_images(0, 1000000, Tag::explode($query));
|
||||||
$count = 0;
|
$count = count($images);
|
||||||
foreach(Image::find_images(0, 1000000, Tag::explode($query)) as $image) {
|
log_warning("admin", "Mass-deleting $count images from $query", "Mass deleted $count images");
|
||||||
if($reason && class_exists("ImageBan")) {
|
foreach ($images as $image) {
|
||||||
|
if ($reason && class_exists("ImageBan")) {
|
||||||
send_event(new AddImageHashBanEvent($image->hash, $reason));
|
send_event(new AddImageHashBanEvent($image->hash, $reason));
|
||||||
}
|
}
|
||||||
send_event(new ImageDeletionEvent($image));
|
send_event(new ImageDeletionEvent($image, true));
|
||||||
$count++;
|
|
||||||
}
|
}
|
||||||
log_debug("admin", "Deleted $count images", true);
|
|
||||||
|
|
||||||
$page->set_mode("redirect");
|
$page->set_mode(PageMode::REDIRECT);
|
||||||
$page->set_redirect(make_link("post/list"));
|
$page->set_redirect(make_link("post/list"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function set_tag_case() {
|
private function set_tag_case()
|
||||||
|
{
|
||||||
global $database;
|
global $database;
|
||||||
$database->execute($database->scoreql_to_sql(
|
$database->execute($database->scoreql_to_sql(
|
||||||
"UPDATE tags SET tag=:tag1 WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag2)"
|
"UPDATE tags SET tag=:tag1 WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag2)"
|
||||||
), array("tag1" => $_POST['tag'], "tag2" => $_POST['tag']));
|
), ["tag1" => $_POST['tag'], "tag2" => $_POST['tag']]);
|
||||||
log_info("admin", "Fixed the case of ".html_escape($_POST['tag']), true);
|
log_info("admin", "Fixed the case of ".html_escape($_POST['tag']), "Fixed case");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function lowercase_all_tags() {
|
private function lowercase_all_tags()
|
||||||
|
{
|
||||||
global $database;
|
global $database;
|
||||||
$database->execute("UPDATE tags SET tag=lower(tag)");
|
$database->execute("UPDATE tags SET tag=lower(tag)");
|
||||||
log_warning("admin", "Set all tags to lowercase", true);
|
log_warning("admin", "Set all tags to lowercase", "Set all tags to lowercase");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function recount_tag_use() {
|
private function recount_tag_use()
|
||||||
|
{
|
||||||
global $database;
|
global $database;
|
||||||
$database->Execute("
|
$database->Execute("
|
||||||
UPDATE tags
|
UPDATE tags
|
||||||
@ -166,15 +174,16 @@ class AdminPage extends Extension {
|
|||||||
)
|
)
|
||||||
");
|
");
|
||||||
$database->Execute("DELETE FROM tags WHERE count=0");
|
$database->Execute("DELETE FROM tags WHERE count=0");
|
||||||
log_warning("admin", "Re-counted tags", true);
|
log_warning("admin", "Re-counted tags", "Re-counted tags");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private function database_dump() {
|
private function database_dump()
|
||||||
|
{
|
||||||
global $page;
|
global $page;
|
||||||
|
|
||||||
$matches = array();
|
$matches = [];
|
||||||
preg_match("#^(?P<proto>\w+)\:(?:user=(?P<user>\w+)(?:;|$)|password=(?P<password>\w*)(?:;|$)|host=(?P<host>[\w\.\-]+)(?:;|$)|dbname=(?P<dbname>[\w_]+)(?:;|$))+#", DATABASE_DSN, $matches);
|
preg_match("#^(?P<proto>\w+)\:(?:user=(?P<user>\w+)(?:;|$)|password=(?P<password>\w*)(?:;|$)|host=(?P<host>[\w\.\-]+)(?:;|$)|dbname=(?P<dbname>[\w_]+)(?:;|$))+#", DATABASE_DSN, $matches);
|
||||||
$software = $matches['proto'];
|
$software = $matches['proto'];
|
||||||
$username = $matches['user'];
|
$username = $matches['user'];
|
||||||
@ -182,15 +191,15 @@ class AdminPage extends Extension {
|
|||||||
$hostname = $matches['host'];
|
$hostname = $matches['host'];
|
||||||
$database = $matches['dbname'];
|
$database = $matches['dbname'];
|
||||||
|
|
||||||
switch($software) {
|
switch ($software) {
|
||||||
case 'mysql':
|
case DatabaseDriver::MYSQL:
|
||||||
$cmd = "mysqldump -h$hostname -u$username -p$password $database";
|
$cmd = "mysqldump -h$hostname -u$username -p$password $database";
|
||||||
break;
|
break;
|
||||||
case 'pgsql':
|
case DatabaseDriver::PGSQL:
|
||||||
putenv("PGPASSWORD=$password");
|
putenv("PGPASSWORD=$password");
|
||||||
$cmd = "pg_dump -h $hostname -U $username $database";
|
$cmd = "pg_dump -h $hostname -U $username $database";
|
||||||
break;
|
break;
|
||||||
case 'sqlite':
|
case DatabaseDriver::SQLITE:
|
||||||
$cmd = "sqlite3 $database .dump";
|
$cmd = "sqlite3 $database .dump";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -199,8 +208,8 @@ class AdminPage extends Extension {
|
|||||||
|
|
||||||
//FIXME: .SQL dump is empty if cmd doesn't exist
|
//FIXME: .SQL dump is empty if cmd doesn't exist
|
||||||
|
|
||||||
if($cmd) {
|
if ($cmd) {
|
||||||
$page->set_mode("data");
|
$page->set_mode(PageMode::DATA);
|
||||||
$page->set_type("application/x-unknown");
|
$page->set_type("application/x-unknown");
|
||||||
$page->set_filename('shimmie-'.date('Ymd').'.sql');
|
$page->set_filename('shimmie-'.date('Ymd').'.sql');
|
||||||
$page->set_data(shell_exec($cmd));
|
$page->set_data(shell_exec($cmd));
|
||||||
@ -209,48 +218,50 @@ class AdminPage extends Extension {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function download_all_images() {
|
private function download_all_images()
|
||||||
|
{
|
||||||
global $database, $page;
|
global $database, $page;
|
||||||
|
|
||||||
$images = $database->get_all("SELECT hash, ext FROM images");
|
$images = $database->get_all("SELECT hash, ext FROM images");
|
||||||
$filename = data_path('imgdump-'.date('Ymd').'.zip');
|
$filename = data_path('imgdump-'.date('Ymd').'.zip');
|
||||||
|
|
||||||
$zip = new ZipArchive;
|
$zip = new ZipArchive;
|
||||||
if($zip->open($filename, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE) === TRUE){
|
if ($zip->open($filename, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE) === true) {
|
||||||
foreach($images as $img){
|
foreach ($images as $img) {
|
||||||
$img_loc = warehouse_path("images", $img["hash"], FALSE);
|
$img_loc = warehouse_path(Image::IMAGE_DIR, $img["hash"], false);
|
||||||
$zip->addFile($img_loc, $img["hash"].".".$img["ext"]);
|
$zip->addFile($img_loc, $img["hash"].".".$img["ext"]);
|
||||||
}
|
}
|
||||||
$zip->close();
|
$zip->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
$page->set_mode("redirect");
|
$page->set_mode(PageMode::REDIRECT);
|
||||||
$page->set_redirect(make_link($filename)); //TODO: Delete file after downloaded?
|
$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;
|
global $database;
|
||||||
|
|
||||||
//TODO: Make work with PostgreSQL + SQLite
|
//TODO: Make work with PostgreSQL + SQLite
|
||||||
//TODO: Update score_log (Having an optional ID column for score_log would be nice..)
|
//TODO: Update score_log (Having an optional ID column for score_log would be nice..)
|
||||||
preg_match("#^(?P<proto>\w+)\:(?:user=(?P<user>\w+)(?:;|$)|password=(?P<password>\w*)(?:;|$)|host=(?P<host>[\w\.\-]+)(?:;|$)|dbname=(?P<dbname>[\w_]+)(?:;|$))+#", DATABASE_DSN, $matches);
|
preg_match("#^(?P<proto>\w+)\:(?:user=(?P<user>\w+)(?:;|$)|password=(?P<password>\w*)(?:;|$)|host=(?P<host>[\w\.\-]+)(?:;|$)|dbname=(?P<dbname>[\w_]+)(?:;|$))+#", DATABASE_DSN, $matches);
|
||||||
|
|
||||||
if($matches['proto'] == "mysql"){
|
if ($matches['proto'] == DatabaseDriver::MYSQL) {
|
||||||
$tables = $database->get_col("SELECT TABLE_NAME
|
$tables = $database->get_col("SELECT TABLE_NAME
|
||||||
FROM information_schema.KEY_COLUMN_USAGE
|
FROM information_schema.KEY_COLUMN_USAGE
|
||||||
WHERE TABLE_SCHEMA = :db
|
WHERE TABLE_SCHEMA = :db
|
||||||
AND REFERENCED_COLUMN_NAME = 'id'
|
AND REFERENCED_COLUMN_NAME = 'id'
|
||||||
AND REFERENCED_TABLE_NAME = 'images'", array("db" => $matches['dbname']));
|
AND REFERENCED_TABLE_NAME = 'images'", ["db" => $matches['dbname']]);
|
||||||
|
|
||||||
$i = 1;
|
$i = 1;
|
||||||
$ids = $database->get_col("SELECT id FROM images ORDER BY images.id ASC");
|
$ids = $database->get_col("SELECT id FROM images ORDER BY images.id ASC");
|
||||||
foreach($ids as $id){
|
foreach ($ids as $id) {
|
||||||
$sql = "SET FOREIGN_KEY_CHECKS=0;
|
$sql = "SET FOREIGN_KEY_CHECKS=0;
|
||||||
UPDATE images SET id={$i} WHERE image_id={$id};";
|
UPDATE images SET id={$i} WHERE image_id={$id};";
|
||||||
|
|
||||||
foreach($tables as $table){
|
foreach ($tables as $table) {
|
||||||
$sql .= "UPDATE {$table} SET image_id={$i} WHERE image_id={$id};";
|
$sql .= "UPDATE {$table} SET image_id={$i} WHERE image_id={$id};";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,12 +271,11 @@ class AdminPage extends Extension {
|
|||||||
$i++;
|
$i++;
|
||||||
}
|
}
|
||||||
$database->execute("ALTER TABLE images AUTO_INCREMENT=".(count($ids) + 1));
|
$database->execute("ALTER TABLE images AUTO_INCREMENT=".(count($ids) + 1));
|
||||||
}elseif($matches['proto'] == "pgsql"){
|
} elseif ($matches['proto'] == DatabaseDriver::PGSQL) {
|
||||||
//TODO: Make this work with PostgreSQL
|
//TODO: Make this work with PostgreSQL
|
||||||
}elseif($matches['proto'] == "sqlite"){
|
} elseif ($matches['proto'] == DatabaseDriver::SQLITE) {
|
||||||
//TODO: Make this work with SQLite
|
//TODO: Make this work with SQLite
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
class AdminPageTest extends ShimmiePHPUnitTestCase {
|
class AdminPageTest extends ShimmiePHPUnitTestCase
|
||||||
public function testAuth() {
|
{
|
||||||
|
public function testAuth()
|
||||||
|
{
|
||||||
$this->get_page('admin');
|
$this->get_page('admin');
|
||||||
$this->assert_response(403);
|
$this->assert_response(403);
|
||||||
$this->assert_title("Permission Denied");
|
$this->assert_title("Permission Denied");
|
||||||
@ -16,7 +18,8 @@ class AdminPageTest extends ShimmiePHPUnitTestCase {
|
|||||||
$this->assert_title("Admin Tools");
|
$this->assert_title("Admin Tools");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testLowercase() {
|
public function testLowercase()
|
||||||
|
{
|
||||||
$ts = time(); // we need a tag that hasn't been used before
|
$ts = time(); // we need a tag that hasn't been used before
|
||||||
|
|
||||||
$this->log_in_as_admin();
|
$this->log_in_as_admin();
|
||||||
@ -37,7 +40,8 @@ class AdminPageTest extends ShimmiePHPUnitTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# FIXME: make sure the admin tools actually work
|
# FIXME: make sure the admin tools actually work
|
||||||
public function testRecount() {
|
public function testRecount()
|
||||||
|
{
|
||||||
$this->log_in_as_admin();
|
$this->log_in_as_admin();
|
||||||
$this->get_page('admin');
|
$this->get_page('admin');
|
||||||
$this->assert_title("Admin Tools");
|
$this->assert_title("Admin Tools");
|
||||||
@ -46,7 +50,8 @@ class AdminPageTest extends ShimmiePHPUnitTestCase {
|
|||||||
send_event(new AdminActionEvent('recount_tag_use'));
|
send_event(new AdminActionEvent('recount_tag_use'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDump() {
|
public function testDump()
|
||||||
|
{
|
||||||
$this->log_in_as_admin();
|
$this->log_in_as_admin();
|
||||||
$this->get_page('admin');
|
$this->get_page('admin');
|
||||||
$this->assert_title("Admin Tools");
|
$this->assert_title("Admin Tools");
|
||||||
@ -57,7 +62,8 @@ class AdminPageTest extends ShimmiePHPUnitTestCase {
|
|||||||
//$this->assert_response(200);
|
//$this->assert_response(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDBQ() {
|
public function testDBQ()
|
||||||
|
{
|
||||||
$this->log_in_as_user();
|
$this->log_in_as_user();
|
||||||
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test");
|
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test");
|
||||||
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "test2");
|
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "test2");
|
||||||
@ -81,4 +87,3 @@ class AdminPageTest extends ShimmiePHPUnitTestCase {
|
|||||||
$this->delete_image($image_id_3);
|
$this->delete_image($image_id_3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class AdminPageTheme extends Themelet {
|
class AdminPageTheme extends Themelet
|
||||||
|
{
|
||||||
/*
|
/*
|
||||||
* Show the basics of a page, for other extensions to add to
|
* Show the basics of a page, for other extensions to add to
|
||||||
*/
|
*/
|
||||||
public function display_page() {
|
public function display_page()
|
||||||
|
{
|
||||||
global $page;
|
global $page;
|
||||||
|
|
||||||
$page->set_title("Admin Tools");
|
$page->set_title("Admin Tools");
|
||||||
@ -12,20 +14,14 @@ class AdminPageTheme extends Themelet {
|
|||||||
$page->add_block(new NavBlock());
|
$page->add_block(new NavBlock());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected function button(string $name, string $action, bool $protected=false): string
|
||||||
* @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" : "";
|
$c_protected = $protected ? " protected" : "";
|
||||||
$html = make_form(make_link("admin/$action"), "POST", false, "admin$c_protected");
|
$html = make_form(make_link("admin/$action"), "POST", false, "admin$c_protected");
|
||||||
if($protected) {
|
if ($protected) {
|
||||||
$html .= "<input type='submit' id='$action' value='$name' disabled='disabled'>";
|
$html .= "<input type='submit' id='$action' value='$name' disabled='disabled'>";
|
||||||
$html .= "<input type='checkbox' onclick='$(\"#$action\").attr(\"disabled\", !$(this).is(\":checked\"))'>";
|
$html .= "<input type='checkbox' onclick='$(\"#$action\").attr(\"disabled\", !$(this).is(\":checked\"))'>";
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$html .= "<input type='submit' id='$action' value='$name'>";
|
$html .= "<input type='submit' id='$action' value='$name'>";
|
||||||
}
|
}
|
||||||
$html .= "</form>\n";
|
$html .= "</form>\n";
|
||||||
@ -38,17 +34,20 @@ class AdminPageTheme extends Themelet {
|
|||||||
* 'recount tag use'
|
* 'recount tag use'
|
||||||
* etc
|
* etc
|
||||||
*/
|
*/
|
||||||
public function display_form() {
|
public function display_form()
|
||||||
|
{
|
||||||
global $page, $database;
|
global $page, $database;
|
||||||
|
|
||||||
$html = "";
|
$html = "";
|
||||||
$html .= $this->button("All tags to lowercase", "lowercase_all_tags", true);
|
$html .= $this->button("All tags to lowercase", "lowercase_all_tags", true);
|
||||||
$html .= $this->button("Recount tag use", "recount_tag_use", false);
|
$html .= $this->button("Recount tag use", "recount_tag_use", false);
|
||||||
if(class_exists('ZipArchive'))
|
if (class_exists('ZipArchive')) {
|
||||||
$html .= $this->button("Download all images", "download_all_images", false);
|
$html .= $this->button("Download all images", "download_all_images", false);
|
||||||
|
}
|
||||||
$html .= $this->button("Download database contents", "database_dump", false);
|
$html .= $this->button("Download database contents", "database_dump", false);
|
||||||
if($database->get_driver_name() == "mysql")
|
if ($database->get_driver_name() == DatabaseDriver::MYSQL) {
|
||||||
$html .= $this->button("Reset image IDs", "reset_image_ids", true);
|
$html .= $this->button("Reset image IDs", "reset_image_ids", true);
|
||||||
|
}
|
||||||
$page->add_block(new Block("Misc Admin Tools", $html));
|
$page->add_block(new Block("Misc Admin Tools", $html));
|
||||||
|
|
||||||
$html = make_form(make_link("admin/set_tag_case"), "POST");
|
$html = make_form(make_link("admin/set_tag_case"), "POST");
|
||||||
@ -58,13 +57,15 @@ class AdminPageTheme extends Themelet {
|
|||||||
$page->add_block(new Block("Set Tag Case", $html));
|
$page->add_block(new Block("Set Tag Case", $html));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function dbq_html($terms) {
|
public function dbq_html($terms)
|
||||||
$h_terms = html_escape($terms);
|
{
|
||||||
$h_reason = "";
|
if (Extension::is_enabled(TrashInfo::KEY)) {
|
||||||
if(class_exists("ImageBan")) {
|
$warning = "This delete method will bypass the trash<br/>";
|
||||||
|
}
|
||||||
|
if (class_exists("ImageBan")) {
|
||||||
$h_reason = "<input type='text' name='reason' placeholder='Ban reason (leave blank to not ban)'>";
|
$h_reason = "<input type='text' name='reason' placeholder='Ban reason (leave blank to not ban)'>";
|
||||||
}
|
}
|
||||||
$html = make_form(make_link("admin/delete_by_query"), "POST") . "
|
$html = $warning.make_form(make_link("admin/delete_by_query"), "POST") . "
|
||||||
<input type='button' class='shm-unlocker' data-unlock-sel='#dbqsubmit' value='Unlock'>
|
<input type='button' class='shm-unlocker' data-unlock-sel='#dbqsubmit' value='Unlock'>
|
||||||
<input type='hidden' name='query' value='$h_terms'>
|
<input type='hidden' name='query' value='$h_terms'>
|
||||||
$h_reason
|
$h_reason
|
||||||
@ -74,4 +75,3 @@ class AdminPageTheme extends Themelet {
|
|||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
24
ext/alias_editor/info.php
Normal file
24
ext/alias_editor/info.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name: Alias Editor
|
||||||
|
* Author: Shish <webmaster@shishnet.org>
|
||||||
|
* 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 <a href="$site/alias/list">/alias/list</a>; only site admins can edit it, other people can view and download it';
|
||||||
|
public $core = true;
|
||||||
|
}
|
||||||
@ -1,163 +1,147 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* Name: Alias Editor
|
|
||||||
* Author: Shish <webmaster@shishnet.org>
|
|
||||||
* Link: http://code.shishnet.org/shimmie2/
|
|
||||||
* License: GPLv2
|
|
||||||
* Description: Edit the alias list
|
|
||||||
* Documentation:
|
|
||||||
* The list is visible at <a href="$site/alias/list">/alias/list</a>; only
|
|
||||||
* site admins can edit it, other people can view and download it
|
|
||||||
*/
|
|
||||||
|
|
||||||
class AddAliasEvent extends Event {
|
class AddAliasEvent extends Event
|
||||||
|
{
|
||||||
/** @var string */
|
/** @var string */
|
||||||
public $oldtag;
|
public $oldtag;
|
||||||
/** @var string */
|
/** @var string */
|
||||||
public $newtag;
|
public $newtag;
|
||||||
|
|
||||||
/**
|
public function __construct(string $oldtag, string $newtag)
|
||||||
* @param string $oldtag
|
{
|
||||||
* @param string $newtag
|
|
||||||
*/
|
|
||||||
public function __construct($oldtag, $newtag) {
|
|
||||||
$this->oldtag = trim($oldtag);
|
$this->oldtag = trim($oldtag);
|
||||||
$this->newtag = trim($newtag);
|
$this->newtag = trim($newtag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AddAliasException extends SCoreException {}
|
class AddAliasException extends SCoreException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
class AliasEditor extends Extension {
|
class AliasEditor extends Extension
|
||||||
public function onPageRequest(PageRequestEvent $event) {
|
{
|
||||||
|
public function onPageRequest(PageRequestEvent $event)
|
||||||
|
{
|
||||||
global $config, $database, $page, $user;
|
global $config, $database, $page, $user;
|
||||||
|
|
||||||
if($event->page_matches("alias")) {
|
if ($event->page_matches("alias")) {
|
||||||
if($event->get_arg(0) == "add") {
|
if ($event->get_arg(0) == "add") {
|
||||||
if($user->can("manage_alias_list")) {
|
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
|
||||||
if(isset($_POST['oldtag']) && isset($_POST['newtag'])) {
|
if (isset($_POST['oldtag']) && isset($_POST['newtag'])) {
|
||||||
try {
|
try {
|
||||||
$aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']);
|
$aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']);
|
||||||
send_event($aae);
|
send_event($aae);
|
||||||
$page->set_mode("redirect");
|
$page->set_mode(PageMode::REDIRECT);
|
||||||
$page->set_redirect(make_link("alias/list"));
|
$page->set_redirect(make_link("alias/list"));
|
||||||
}
|
} catch (AddAliasException $ex) {
|
||||||
catch(AddAliasException $ex) {
|
|
||||||
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
|
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} elseif ($event->get_arg(0) == "remove") {
|
||||||
else if($event->get_arg(0) == "remove") {
|
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
|
||||||
if($user->can("manage_alias_list")) {
|
if (isset($_POST['oldtag'])) {
|
||||||
if(isset($_POST['oldtag'])) {
|
$database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", ["oldtag" => $_POST['oldtag']]);
|
||||||
$database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", array("oldtag" => $_POST['oldtag']));
|
log_info("alias_editor", "Deleted alias for ".$_POST['oldtag'], "Deleted alias");
|
||||||
log_info("alias_editor", "Deleted alias for ".$_POST['oldtag'], true);
|
|
||||||
|
|
||||||
$page->set_mode("redirect");
|
$page->set_mode(PageMode::REDIRECT);
|
||||||
$page->set_redirect(make_link("alias/list"));
|
$page->set_redirect(make_link("alias/list"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} elseif ($event->get_arg(0) == "list") {
|
||||||
else if($event->get_arg(0) == "list") {
|
|
||||||
$page_number = $event->get_arg(1);
|
$page_number = $event->get_arg(1);
|
||||||
if(is_null($page_number) || !is_numeric($page_number)) {
|
if (is_null($page_number) || !is_numeric($page_number)) {
|
||||||
$page_number = 0;
|
$page_number = 0;
|
||||||
}
|
} elseif ($page_number <= 0) {
|
||||||
else if ($page_number <= 0) {
|
|
||||||
$page_number = 0;
|
$page_number = 0;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$page_number--;
|
$page_number--;
|
||||||
}
|
}
|
||||||
|
|
||||||
$alias_per_page = $config->get_int('alias_items_per_page', 30);
|
$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";
|
$query = "SELECT oldtag, newtag FROM aliases ORDER BY newtag ASC LIMIT :limit OFFSET :offset";
|
||||||
$alias = $database->get_pairs($query,
|
$alias = $database->get_pairs(
|
||||||
array("limit"=>$alias_per_page, "offset"=>$page_number * $alias_per_page)
|
$query,
|
||||||
|
["limit"=>$alias_per_page, "offset"=>$page_number * $alias_per_page]
|
||||||
);
|
);
|
||||||
|
|
||||||
$total_pages = ceil($database->get_one("SELECT COUNT(*) FROM aliases") / $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);
|
$this->theme->display_aliases($alias, $page_number + 1, $total_pages);
|
||||||
}
|
} elseif ($event->get_arg(0) == "export") {
|
||||||
else if($event->get_arg(0) == "export") {
|
$page->set_mode(PageMode::DATA);
|
||||||
$page->set_mode("data");
|
|
||||||
$page->set_type("text/csv");
|
$page->set_type("text/csv");
|
||||||
$page->set_filename("aliases.csv");
|
$page->set_filename("aliases.csv");
|
||||||
$page->set_data($this->get_alias_csv($database));
|
$page->set_data($this->get_alias_csv($database));
|
||||||
}
|
} elseif ($event->get_arg(0) == "import") {
|
||||||
else if($event->get_arg(0) == "import") {
|
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
|
||||||
if($user->can("manage_alias_list")) {
|
if (count($_FILES) > 0) {
|
||||||
if(count($_FILES) > 0) {
|
|
||||||
$tmp = $_FILES['alias_file']['tmp_name'];
|
$tmp = $_FILES['alias_file']['tmp_name'];
|
||||||
$contents = file_get_contents($tmp);
|
$contents = file_get_contents($tmp);
|
||||||
$this->add_alias_csv($database, $contents);
|
$this->add_alias_csv($database, $contents);
|
||||||
log_info("alias_editor", "Imported aliases from file", true); # FIXME: how many?
|
log_info("alias_editor", "Imported aliases from file", "Imported aliases"); # FIXME: how many?
|
||||||
$page->set_mode("redirect");
|
$page->set_mode(PageMode::REDIRECT);
|
||||||
$page->set_redirect(make_link("alias/list"));
|
$page->set_redirect(make_link("alias/list"));
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$this->theme->display_error(400, "No File Specified", "You have to upload a file");
|
$this->theme->display_error(400, "No File Specified", "You have to upload a file");
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$this->theme->display_error(401, "Admins Only", "Only admins can edit the alias list");
|
$this->theme->display_error(401, "Admins Only", "Only admins can edit the alias list");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onAddAlias(AddAliasEvent $event) {
|
public function onAddAlias(AddAliasEvent $event)
|
||||||
|
{
|
||||||
global $database;
|
global $database;
|
||||||
$pair = array("oldtag" => $event->oldtag, "newtag" => $event->newtag);
|
$pair = ["oldtag" => $event->oldtag, "newtag" => $event->newtag];
|
||||||
if($database->get_row("SELECT * FROM aliases WHERE oldtag=:oldtag AND lower(newtag)=lower(:newtag)", $pair)) {
|
if ($database->get_row("SELECT * FROM aliases WHERE oldtag=:oldtag AND lower(newtag)=lower(:newtag)", $pair)) {
|
||||||
throw new AddAliasException("That alias already exists");
|
throw new AddAliasException("That alias already exists");
|
||||||
}
|
} elseif ($database->get_row("SELECT * FROM aliases WHERE oldtag=:newtag", ["newtag" => $event->newtag])) {
|
||||||
else if($database->get_row("SELECT * FROM aliases WHERE oldtag=:newtag", array("newtag" => $event->newtag))) {
|
|
||||||
throw new AddAliasException("{$event->newtag} is itself an alias");
|
throw new AddAliasException("{$event->newtag} is itself an alias");
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$database->execute("INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)", $pair);
|
$database->execute("INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)", $pair);
|
||||||
log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}", true);
|
log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}", "Added alias");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event) {
|
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;
|
global $user;
|
||||||
if($user->can("manage_alias_list")) {
|
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
|
||||||
$event->add_link("Alias Editor", make_link("alias/list"));
|
$event->add_link("Alias Editor", make_link("alias/list"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function get_alias_csv(Database $database): string
|
||||||
* @param Database $database
|
{
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function get_alias_csv(Database $database) {
|
|
||||||
$csv = "";
|
$csv = "";
|
||||||
$aliases = $database->get_pairs("SELECT oldtag, newtag FROM aliases ORDER BY newtag");
|
$aliases = $database->get_pairs("SELECT oldtag, newtag FROM aliases ORDER BY newtag");
|
||||||
foreach($aliases as $old => $new) {
|
foreach ($aliases as $old => $new) {
|
||||||
$csv .= "\"$old\",\"$new\"\n";
|
$csv .= "\"$old\",\"$new\"\n";
|
||||||
}
|
}
|
||||||
return $csv;
|
return $csv;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function add_alias_csv(Database $database, string $csv)
|
||||||
* @param Database $database
|
{
|
||||||
* @param string $csv
|
|
||||||
*/
|
|
||||||
private function add_alias_csv(Database $database, /*string*/ $csv) {
|
|
||||||
$csv = str_replace("\r", "\n", $csv);
|
$csv = str_replace("\r", "\n", $csv);
|
||||||
foreach(explode("\n", $csv) as $line) {
|
foreach (explode("\n", $csv) as $line) {
|
||||||
$parts = str_getcsv($line);
|
$parts = str_getcsv($line);
|
||||||
if(count($parts) == 2) {
|
if (count($parts) == 2) {
|
||||||
try {
|
try {
|
||||||
$aae = new AddAliasEvent($parts[0], $parts[1]);
|
$aae = new AddAliasEvent($parts[0], $parts[1]);
|
||||||
send_event($aae);
|
send_event($aae);
|
||||||
}
|
} catch (AddAliasException $ex) {
|
||||||
catch(AddAliasException $ex) {
|
|
||||||
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
|
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,9 +154,9 @@ class AliasEditor extends Extension {
|
|||||||
* Add alias *after* mass tag editing, else the MTE will
|
* Add alias *after* mass tag editing, else the MTE will
|
||||||
* search for the images and be redirected to the alias,
|
* search for the images and be redirected to the alias,
|
||||||
* missing out the images tagged with the old tag.
|
* missing out the images tagged with the old tag.
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
*/
|
||||||
public function get_priority() {return 60;}
|
public function get_priority(): int
|
||||||
|
{
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
class AliasEditorTest extends ShimmiePHPUnitTestCase {
|
class AliasEditorTest extends ShimmiePHPUnitTestCase
|
||||||
public function testAliasList() {
|
{
|
||||||
|
public function testAliasList()
|
||||||
|
{
|
||||||
$this->get_page('alias/list');
|
$this->get_page('alias/list');
|
||||||
$this->assert_response(200);
|
$this->assert_response(200);
|
||||||
$this->assert_title("Alias List");
|
$this->assert_title("Alias List");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAliasListReadOnly() {
|
public function testAliasListReadOnly()
|
||||||
|
{
|
||||||
// Check that normal users can't add aliases.
|
// Check that normal users can't add aliases.
|
||||||
$this->log_in_as_user();
|
$this->log_in_as_user();
|
||||||
$this->get_page('alias/list');
|
$this->get_page('alias/list');
|
||||||
@ -14,7 +17,8 @@ class AliasEditorTest extends ShimmiePHPUnitTestCase {
|
|||||||
$this->assert_no_text("Add");
|
$this->assert_no_text("Add");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAliasEditor() {
|
public function testAliasEditor()
|
||||||
|
{
|
||||||
/*
|
/*
|
||||||
**********************************************************************
|
**********************************************************************
|
||||||
* FIXME: TODO:
|
* FIXME: TODO:
|
||||||
@ -101,4 +105,3 @@ class AliasEditorTest extends ShimmiePHPUnitTestCase {
|
|||||||
$this->assert_no_text("Add");
|
$this->assert_no_text("Add");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class AliasEditorTheme extends Themelet {
|
class AliasEditorTheme extends Themelet
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* Show a page of aliases.
|
* Show a page of aliases.
|
||||||
*
|
*
|
||||||
* Note: $can_manage = whether things like "add new alias" should be shown
|
* Note: $can_manage = whether things like "add new alias" should be shown
|
||||||
*
|
|
||||||
* @param array $aliases An array of ($old_tag => $new_tag)
|
|
||||||
* @param int $pageNumber
|
|
||||||
* @param int $totalPages
|
|
||||||
*/
|
*/
|
||||||
public function display_aliases($aliases, $pageNumber, $totalPages) {
|
public function display_aliases(array $aliases, int $pageNumber, int $totalPages): void
|
||||||
|
{
|
||||||
global $page, $user;
|
global $page, $user;
|
||||||
|
|
||||||
$can_manage = $user->can("manage_alias_list");
|
$can_manage = $user->can(Permissions::MANAGE_ALIAS_LIST);
|
||||||
if($can_manage) {
|
if ($can_manage) {
|
||||||
$h_action = "<th width='10%'>Action</th>";
|
$h_action = "<th width='10%'>Action</th>";
|
||||||
$h_add = "
|
$h_add = "
|
||||||
<tr>
|
<tr>
|
||||||
@ -25,19 +23,18 @@ class AliasEditorTheme extends Themelet {
|
|||||||
</form>
|
</form>
|
||||||
</tr>
|
</tr>
|
||||||
";
|
";
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$h_action = "";
|
$h_action = "";
|
||||||
$h_add = "";
|
$h_add = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
$h_aliases = "";
|
$h_aliases = "";
|
||||||
foreach($aliases as $old => $new) {
|
foreach ($aliases as $old => $new) {
|
||||||
$h_old = html_escape($old);
|
$h_old = html_escape($old);
|
||||||
$h_new = "<a href='".make_link("post/list/".url_escape($new)."/1")."'>".html_escape($new)."</a>";
|
$h_new = "<a href='".make_link("post/list/".url_escape($new)."/1")."'>".html_escape($new)."</a>";
|
||||||
|
|
||||||
$h_aliases .= "<tr><td>$h_old</td><td>$h_new</td>";
|
$h_aliases .= "<tr><td>$h_old</td><td>$h_new</td>";
|
||||||
if($can_manage) {
|
if ($can_manage) {
|
||||||
$h_aliases .= "
|
$h_aliases .= "
|
||||||
<td>
|
<td>
|
||||||
".make_form(make_link("alias/remove"))."
|
".make_form(make_link("alias/remove"))."
|
||||||
@ -69,11 +66,10 @@ class AliasEditorTheme extends Themelet {
|
|||||||
$page->set_heading("Alias List");
|
$page->set_heading("Alias List");
|
||||||
$page->add_block(new NavBlock());
|
$page->add_block(new NavBlock());
|
||||||
$page->add_block(new Block("Aliases", $html));
|
$page->add_block(new Block("Aliases", $html));
|
||||||
if($can_manage) {
|
if ($can_manage) {
|
||||||
$page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51));
|
$page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->display_paginator($page, "alias/list", null, $pageNumber, $totalPages);
|
$this->display_paginator($page, "alias/list", null, $pageNumber, $totalPages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* Name: Amazon S3 Mirror
|
|
||||||
* Author: Shish <webmaster@shishnet.org>
|
|
||||||
* 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", "<br>Secret key: ");
|
|
||||||
$sb->add_text_option("amazon_s3_bucket", "<br>Bucket: ");
|
|
||||||
$event->panel->add_block($sb);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onImageAddition(ImageAdditionEvent $event) {
|
|
||||||
global $config;
|
|
||||||
$access = $config->get_string("amazon_s3_access");
|
|
||||||
$secret = $config->get_string("amazon_s3_secret");
|
|
||||||
$bucket = $config->get_string("amazon_s3_bucket");
|
|
||||||
if(!empty($bucket)) {
|
|
||||||
log_debug("amazon_s3", "Mirroring Image #".$event->image->id." to S3 #$bucket");
|
|
||||||
$s3 = new S3($access, $secret);
|
|
||||||
$s3->putBucket($bucket, S3::ACL_PUBLIC_READ);
|
|
||||||
$s3->putObjectFile(
|
|
||||||
warehouse_path("thumbs", $event->image->hash),
|
|
||||||
$bucket,
|
|
||||||
'thumbs/'.$event->image->hash,
|
|
||||||
S3::ACL_PUBLIC_READ,
|
|
||||||
array(),
|
|
||||||
array(
|
|
||||||
"Content-Type" => "image/jpeg",
|
|
||||||
"Content-Disposition" => "inline; filename=image-" . $event->image->id . ".jpg",
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$s3->putObjectFile(
|
|
||||||
warehouse_path("images", $event->image->hash),
|
|
||||||
$bucket,
|
|
||||||
'images/'.$event->image->hash,
|
|
||||||
S3::ACL_PUBLIC_READ,
|
|
||||||
array(),
|
|
||||||
array(
|
|
||||||
"Content-Type" => $event->image->get_mime_type(),
|
|
||||||
"Content-Disposition" => "inline; filename=image-" . $event->image->id . "." . $event->image->ext,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onImageDeletion(ImageDeletionEvent $event) {
|
|
||||||
global $config;
|
|
||||||
$access = $config->get_string("amazon_s3_access");
|
|
||||||
$secret = $config->get_string("amazon_s3_secret");
|
|
||||||
$bucket = $config->get_string("amazon_s3_bucket");
|
|
||||||
if(!empty($bucket)) {
|
|
||||||
log_debug("amazon_s3", "Deleting Image #".$event->image->id." from S3");
|
|
||||||
$s3 = new S3($access, $secret);
|
|
||||||
$s3->deleteObject($bucket, "images/" . $event->image->hash);
|
|
||||||
$s3->deleteObject($bucket, "thumbs/" . $event->image->hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
23
ext/arrowkey_navigation/info.php
Normal file
23
ext/arrowkey_navigation/info.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Name: Arrow Key Navigation
|
||||||
|
* Author: Drudex Software <support@drudexsoftware.com>
|
||||||
|
* 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.";
|
||||||
|
}
|
||||||
@ -1,20 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* Name: Arrow Key Navigation
|
class ArrowkeyNavigation extends Extension
|
||||||
* Author: Drudex Software <support@drudexsoftware.com>
|
{
|
||||||
* 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.
|
* Adds functionality for post/view on images.
|
||||||
*
|
|
||||||
* @param DisplayingImageEvent $event
|
|
||||||
*/
|
*/
|
||||||
public function onDisplayingImage(DisplayingImageEvent $event) {
|
public function onDisplayingImage(DisplayingImageEvent $event)
|
||||||
|
{
|
||||||
$prev_url = make_http(make_link("post/prev/".$event->image->id));
|
$prev_url = make_http(make_link("post/prev/".$event->image->id));
|
||||||
$next_url = make_http(make_link("post/next/".$event->image->id));
|
$next_url = make_http(make_link("post/next/".$event->image->id));
|
||||||
$this->add_arrowkeys_code($prev_url, $next_url);
|
$this->add_arrowkeys_code($prev_url, $next_url);
|
||||||
@ -22,11 +14,10 @@ class ArrowkeyNavigation extends Extension {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds functionality for post/list.
|
* Adds functionality for post/list.
|
||||||
*
|
|
||||||
* @param PageRequestEvent $event
|
|
||||||
*/
|
*/
|
||||||
public function onPageRequest(PageRequestEvent $event) {
|
public function onPageRequest(PageRequestEvent $event)
|
||||||
if($event->page_matches("post/list")) {
|
{
|
||||||
|
if ($event->page_matches("post/list")) {
|
||||||
$pageinfo = $this->get_list_pageinfo($event);
|
$pageinfo = $this->get_list_pageinfo($event);
|
||||||
$prev_url = make_http(make_link("post/list/".$pageinfo["prev"]));
|
$prev_url = make_http(make_link("post/list/".$pageinfo["prev"]));
|
||||||
$next_url = make_http(make_link("post/list/".$pageinfo["next"]));
|
$next_url = make_http(make_link("post/list/".$pageinfo["next"]));
|
||||||
@ -36,11 +27,9 @@ class ArrowkeyNavigation extends Extension {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the javascript to the page with the given urls.
|
* 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) {
|
private function add_arrowkeys_code(string $prev_url, string $next_url)
|
||||||
|
{
|
||||||
global $page;
|
global $page;
|
||||||
|
|
||||||
$page->add_html_header("<script type=\"text/javascript\">
|
$page->add_html_header("<script type=\"text/javascript\">
|
||||||
@ -57,45 +46,49 @@ class ArrowkeyNavigation extends Extension {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns info about the current page number.
|
* Returns info about the current page number.
|
||||||
*
|
|
||||||
* @param PageRequestEvent $event
|
|
||||||
* @return array
|
|
||||||
*/
|
*/
|
||||||
private function get_list_pageinfo(PageRequestEvent $event) {
|
private function get_list_pageinfo(PageRequestEvent $event): array
|
||||||
|
{
|
||||||
global $config, $database;
|
global $config, $database;
|
||||||
|
|
||||||
// get the amount of images per page
|
// get the amount of images per page
|
||||||
$images_per_page = $config->get_int('index_images');
|
$images_per_page = $config->get_int('index_images');
|
||||||
|
|
||||||
// if there are no tags, use default
|
// if there are no tags, use default
|
||||||
if (is_null($event->get_arg(1))){
|
if (is_null($event->get_arg(1))) {
|
||||||
$prefix = "";
|
$prefix = "";
|
||||||
$page_number = int_escape($event->get_arg(0));
|
$page_number = int_escape($event->get_arg(0));
|
||||||
$total_pages = ceil($database->get_one(
|
$total_pages = ceil($database->get_one(
|
||||||
"SELECT COUNT(*) FROM images") / $images_per_page);
|
"SELECT COUNT(*) FROM images"
|
||||||
}
|
) / $images_per_page);
|
||||||
else { // if there are tags, use pages with tags
|
} else { // if there are tags, use pages with tags
|
||||||
$prefix = url_escape($event->get_arg(0)) . "/";
|
$prefix = url_escape($event->get_arg(0)) . "/";
|
||||||
$page_number = int_escape($event->get_arg(1));
|
$page_number = int_escape($event->get_arg(1));
|
||||||
$total_pages = ceil($database->get_one(
|
$total_pages = ceil($database->get_one(
|
||||||
"SELECT count FROM tags WHERE tag=:tag",
|
"SELECT count FROM tags WHERE tag=:tag",
|
||||||
array("tag"=>$event->get_arg(0))) / $images_per_page);
|
["tag"=>$event->get_arg(0)]
|
||||||
|
) / $images_per_page);
|
||||||
}
|
}
|
||||||
|
|
||||||
// creates previous & next values
|
// creates previous & next values
|
||||||
// When previous first page, go to last page
|
// When previous first page, go to last page
|
||||||
if ($page_number <= 1) $prev = $total_pages;
|
if ($page_number <= 1) {
|
||||||
else $prev = $page_number-1;
|
$prev = $total_pages;
|
||||||
if ($page_number >= $total_pages) $next = 1;
|
} else {
|
||||||
else $next = $page_number+1;
|
$prev = $page_number-1;
|
||||||
|
}
|
||||||
|
if ($page_number >= $total_pages) {
|
||||||
|
$next = 1;
|
||||||
|
} else {
|
||||||
|
$next = $page_number+1;
|
||||||
|
}
|
||||||
|
|
||||||
// Create return array
|
// Create return array
|
||||||
$pageinfo = array(
|
$pageinfo = [
|
||||||
"prev" => $prefix.$prev,
|
"prev" => $prefix.$prev,
|
||||||
"next" => $prefix.$next,
|
"next" => $prefix.$next,
|
||||||
);
|
];
|
||||||
|
|
||||||
return $pageinfo;
|
return $pageinfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
ext/artists/info.php
Normal file
23
ext/artists/info.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name: [Beta] Artists System
|
||||||
|
* Author: Sein Kraft <mail@seinkraft.info>
|
||||||
|
* Alpha <alpha@furries.com.ar>
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
class ArtistTest extends ShimmiePHPUnitTestCase {
|
class ArtistTest extends ShimmiePHPUnitTestCase
|
||||||
public function testSearch() {
|
{
|
||||||
|
public function testSearch()
|
||||||
|
{
|
||||||
# FIXME: check that the results are there
|
# FIXME: check that the results are there
|
||||||
$this->get_page("post/list/author=bob/1");
|
$this->get_page("post/list/author=bob/1");
|
||||||
#$this->assert_response(200);
|
#$this->assert_response(200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
class ArtistsTheme extends Themelet {
|
class ArtistsTheme extends Themelet
|
||||||
|
{
|
||||||
/**
|
public function get_author_editor_html(string $author): string
|
||||||
* @param string $author
|
{
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function get_author_editor_html(/*string*/ $author) {
|
|
||||||
$h_author = html_escape($author);
|
$h_author = html_escape($author);
|
||||||
return "
|
return "
|
||||||
<tr>
|
<tr>
|
||||||
@ -18,66 +15,65 @@ class ArtistsTheme extends Themelet {
|
|||||||
";
|
";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function sidebar_options(string $mode, ?int $artistID=null, $is_admin=false): void
|
||||||
* @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;
|
global $page, $user;
|
||||||
|
|
||||||
$html = "";
|
$html = "";
|
||||||
|
|
||||||
if($mode == "neutral"){
|
if ($mode == "neutral") {
|
||||||
$html = "<form method='post' action='".make_link("artist/new_artist")."'>
|
$html = "<form method='post' action='".make_link("artist/new_artist")."'>
|
||||||
".$user->get_auth_html()."
|
".$user->get_auth_html()."
|
||||||
<input type='submit' name='edit' id='edit' value='New Artist'/>
|
<input type='submit' name='edit' id='edit' value='New Artist'/>
|
||||||
</form>";
|
</form>";
|
||||||
}
|
}
|
||||||
|
|
||||||
if($mode == "editor"){
|
if ($mode == "editor") {
|
||||||
$html = "<form method='post' action='".make_link("artist/new_artist")."'>
|
$html = "<form method='post' action='".make_link("artist/new_artist")."'>
|
||||||
".$user->get_auth_html()."
|
".$user->get_auth_html()."
|
||||||
<input type='submit' name='edit' id='edit' value='New Artist'/>
|
<input type='submit' name='edit' value='New Artist'/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form method='post' action='".make_link("artist/edit_artist")."'>
|
<form method='post' action='".make_link("artist/edit_artist")."'>
|
||||||
".$user->get_auth_html()."
|
".$user->get_auth_html()."
|
||||||
<input type='submit' name='edit' id='edit' value='Edit Artist'/>
|
<input type='submit' name='edit' value='Edit Artist'/>
|
||||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||||
</form>";
|
</form>";
|
||||||
|
|
||||||
if($is_admin){
|
if ($is_admin) {
|
||||||
$html .= "<form method='post' action='".make_link("artist/nuke_artist")."'>
|
$html .= "<form method='post' action='".make_link("artist/nuke_artist")."'>
|
||||||
".$user->get_auth_html()."
|
".$user->get_auth_html()."
|
||||||
<input type='submit' name='edit' id='edit' value='Delete Artist'/>
|
<input type='submit' name='edit' value='Delete Artist'/>
|
||||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||||
</form>";
|
</form>";
|
||||||
}
|
}
|
||||||
|
|
||||||
$html .= "<form method='post' action='".make_link("artist/add_alias")."'>
|
$html .= "<form method='post' action='".make_link("artist/add_alias")."'>
|
||||||
".$user->get_auth_html()."
|
".$user->get_auth_html()."
|
||||||
<input type='submit' name='edit' id='edit' value='Add Alias'/>
|
<input type='submit' name='edit' value='Add Alias'/>
|
||||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form method='post' action='".make_link("artist/add_member")."'>
|
<form method='post' action='".make_link("artist/add_member")."'>
|
||||||
".$user->get_auth_html()."
|
".$user->get_auth_html()."
|
||||||
<input type='submit' name='edit' id='edit' value='Add Member'/>
|
<input type='submit' name='edit' value='Add Member'/>
|
||||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form method='post' action='".make_link("artist/add_url")."'>
|
<form method='post' action='".make_link("artist/add_url")."'>
|
||||||
".$user->get_auth_html()."
|
".$user->get_auth_html()."
|
||||||
<input type='submit' name='edit' id='edit' value='Add Url'/>
|
<input type='submit' name='edit' value='Add Url'/>
|
||||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||||
</form>";
|
</form>";
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
public function show_artist_editor($artist, $aliases, $members, $urls)
|
||||||
|
{
|
||||||
global $user;
|
global $user;
|
||||||
|
|
||||||
$artistName = $artist['name'];
|
$artistName = $artist['name'];
|
||||||
@ -136,7 +132,8 @@ class ArtistsTheme extends Themelet {
|
|||||||
$page->add_block(new Block("Edit artist", $html, "main", 10));
|
$page->add_block(new Block("Edit artist", $html, "main", 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function new_artist_composer() {
|
public function new_artist_composer()
|
||||||
|
{
|
||||||
global $page, $user;
|
global $page, $user;
|
||||||
|
|
||||||
$html = "<form action=".make_link("artist/create")." method='POST'>
|
$html = "<form action=".make_link("artist/create")." method='POST'>
|
||||||
@ -156,7 +153,8 @@ class ArtistsTheme extends Themelet {
|
|||||||
$page->add_block(new Block("Artists", $html, "main", 10));
|
$page->add_block(new Block("Artists", $html, "main", 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function list_artists($artists, $pageNumber, $totalPages) {
|
public function list_artists($artists, $pageNumber, $totalPages)
|
||||||
|
{
|
||||||
global $user, $page;
|
global $user, $page;
|
||||||
|
|
||||||
$html = "<table id='poolsList' class='zebra'>".
|
$html = "<table id='poolsList' class='zebra'>".
|
||||||
@ -166,31 +164,34 @@ class ArtistsTheme extends Themelet {
|
|||||||
"<th>Last updater</th>".
|
"<th>Last updater</th>".
|
||||||
"<th>Posts</th>";
|
"<th>Posts</th>";
|
||||||
|
|
||||||
if(!$user->is_anonymous()) $html .= "<th colspan='2'>Action</th>"; // space for edit link
|
if (!$user->is_anonymous()) {
|
||||||
|
$html .= "<th colspan='2'>Action</th>";
|
||||||
|
} // space for edit link
|
||||||
|
|
||||||
$html .= "</tr></thead>";
|
$html .= "</tr></thead>";
|
||||||
|
|
||||||
$deletionLinkActionArray = array(
|
$deletionLinkActionArray = [
|
||||||
'artist' => 'artist/nuke/',
|
'artist' => 'artist/nuke/',
|
||||||
'alias' => 'artist/alias/delete/',
|
'alias' => 'artist/alias/delete/',
|
||||||
'member' => 'artist/member/delete/',
|
'member' => 'artist/member/delete/',
|
||||||
);
|
];
|
||||||
|
|
||||||
$editionLinkActionArray = array(
|
$editionLinkActionArray = [
|
||||||
'artist' => 'artist/edit/',
|
'artist' => 'artist/edit/',
|
||||||
'alias' => 'artist/alias/edit/',
|
'alias' => 'artist/alias/edit/',
|
||||||
'member' => 'artist/member/edit/',
|
'member' => 'artist/member/edit/',
|
||||||
);
|
];
|
||||||
|
|
||||||
$typeTextArray = array(
|
$typeTextArray = [
|
||||||
'artist' => 'Artist',
|
'artist' => 'Artist',
|
||||||
'alias' => 'Alias',
|
'alias' => 'Alias',
|
||||||
'member' => 'Member',
|
'member' => 'Member',
|
||||||
);
|
];
|
||||||
|
|
||||||
foreach ($artists as $artist) {
|
foreach ($artists as $artist) {
|
||||||
if ($artist['type'] != 'artist')
|
if ($artist['type'] != 'artist') {
|
||||||
$artist['name'] = str_replace("_", " ", $artist['name']);
|
$artist['name'] = str_replace("_", " ", $artist['name']);
|
||||||
|
}
|
||||||
|
|
||||||
$elementLink = "<a href='".make_link("artist/view/".$artist['artist_id'])."'>".str_replace("_", " ", $artist['name'])."</a>";
|
$elementLink = "<a href='".make_link("artist/view/".$artist['artist_id'])."'>".str_replace("_", " ", $artist['name'])."</a>";
|
||||||
//$artist_link = "<a href='".make_link("artist/view/".$artist['artist_id'])."'>".str_replace("_", " ", $artist['artist_name'])."</a>";
|
//$artist_link = "<a href='".make_link("artist/view/".$artist['artist_id'])."'>".str_replace("_", " ", $artist['artist_name'])."</a>";
|
||||||
@ -212,8 +213,12 @@ class ArtistsTheme extends Themelet {
|
|||||||
"<td>".$user_link."</td>".
|
"<td>".$user_link."</td>".
|
||||||
"<td>".$artist['posts']."</td>";
|
"<td>".$artist['posts']."</td>";
|
||||||
|
|
||||||
if(!$user->is_anonymous()) $html .= "<td>".$edit_link."</td>";
|
if (!$user->is_anonymous()) {
|
||||||
if($user->is_admin()) $html .= "<td>".$del_link."</td>";
|
$html .= "<td>".$edit_link."</td>";
|
||||||
|
}
|
||||||
|
if ($user->can(Permissions::ARTISTS_ADMIN)) {
|
||||||
|
$html .= "<td>".$del_link."</td>";
|
||||||
|
}
|
||||||
|
|
||||||
$html .= "</tr>";
|
$html .= "</tr>";
|
||||||
}
|
}
|
||||||
@ -227,7 +232,8 @@ class ArtistsTheme extends Themelet {
|
|||||||
$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) {
|
public function show_new_alias_composer($artistID)
|
||||||
|
{
|
||||||
global $user;
|
global $user;
|
||||||
|
|
||||||
$html = '
|
$html = '
|
||||||
@ -245,7 +251,8 @@ class ArtistsTheme extends Themelet {
|
|||||||
$page->add_block(new Block("Artist Aliases", $html, "main", 20));
|
$page->add_block(new Block("Artist Aliases", $html, "main", 20));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show_new_member_composer($artistID) {
|
public function show_new_member_composer($artistID)
|
||||||
|
{
|
||||||
global $user;
|
global $user;
|
||||||
|
|
||||||
$html = '
|
$html = '
|
||||||
@ -263,7 +270,8 @@ class ArtistsTheme extends Themelet {
|
|||||||
$page->add_block(new Block("Artist members", $html, "main", 30));
|
$page->add_block(new Block("Artist members", $html, "main", 30));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show_new_url_composer($artistID) {
|
public function show_new_url_composer($artistID)
|
||||||
|
{
|
||||||
global $user;
|
global $user;
|
||||||
|
|
||||||
$html = '
|
$html = '
|
||||||
@ -281,14 +289,15 @@ class ArtistsTheme extends Themelet {
|
|||||||
$page->add_block(new Block("Artist URLs", $html, "main", 40));
|
$page->add_block(new Block("Artist URLs", $html, "main", 40));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show_alias_editor($alias) {
|
public function show_alias_editor($alias)
|
||||||
|
{
|
||||||
global $user;
|
global $user;
|
||||||
|
|
||||||
$html = '
|
$html = '
|
||||||
<form method="POST" action="'.make_link("artist/alias/edited/".$alias['id']).'">
|
<form method="POST" action="'.make_link("artist/alias/edited/".$alias['id']).'">
|
||||||
'.$user->get_auth_html().'
|
'.$user->get_auth_html().'
|
||||||
<label for="alias">Alias:</label>
|
<label for="alias">Alias:</label>
|
||||||
<input type="text" name="alias" value="'.$alias['alias'].'" />
|
<input type="text" name="alias" id="alias" value="'.$alias['alias'].'" />
|
||||||
<input type="hidden" name="aliasID" value="'.$alias['id'].'" />
|
<input type="hidden" name="aliasID" value="'.$alias['id'].'" />
|
||||||
<input type="submit" value="Submit" />
|
<input type="submit" value="Submit" />
|
||||||
</form>
|
</form>
|
||||||
@ -298,14 +307,15 @@ class ArtistsTheme extends Themelet {
|
|||||||
$page->add_block(new Block("Edit Alias", $html, "main", 10));
|
$page->add_block(new Block("Edit Alias", $html, "main", 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show_url_editor($url) {
|
public function show_url_editor($url)
|
||||||
|
{
|
||||||
global $user;
|
global $user;
|
||||||
|
|
||||||
$html = '
|
$html = '
|
||||||
<form method="POST" action="'.make_link("artist/url/edited/".$url['id']).'">
|
<form method="POST" action="'.make_link("artist/url/edited/".$url['id']).'">
|
||||||
'.$user->get_auth_html().'
|
'.$user->get_auth_html().'
|
||||||
<label for="url">URL:</label>
|
<label for="url">URL:</label>
|
||||||
<input type="text" name="url" value="'.$url['url'].'" />
|
<input type="text" name="url" id="url" value="'.$url['url'].'" />
|
||||||
<input type="hidden" name="urlID" value="'.$url['id'].'" />
|
<input type="hidden" name="urlID" value="'.$url['id'].'" />
|
||||||
<input type="submit" value="Submit" />
|
<input type="submit" value="Submit" />
|
||||||
</form>
|
</form>
|
||||||
@ -315,14 +325,15 @@ class ArtistsTheme extends Themelet {
|
|||||||
$page->add_block(new Block("Edit URL", $html, "main", 10));
|
$page->add_block(new Block("Edit URL", $html, "main", 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show_member_editor($member) {
|
public function show_member_editor($member)
|
||||||
|
{
|
||||||
global $user;
|
global $user;
|
||||||
|
|
||||||
$html = '
|
$html = '
|
||||||
<form method="POST" action="'.make_link("artist/member/edited/".$member['id']).'">
|
<form method="POST" action="'.make_link("artist/member/edited/".$member['id']).'">
|
||||||
'.$user->get_auth_html().'
|
'.$user->get_auth_html().'
|
||||||
<label for="member">Member name:</label>
|
<label for="name">Member name:</label>
|
||||||
<input type="text" name="name" value="'.$member['name'].'" />
|
<input type="text" name="name" id="name" value="'.$member['name'].'" />
|
||||||
<input type="hidden" name="memberID" value="'.$member['id'].'" />
|
<input type="hidden" name="memberID" value="'.$member['id'].'" />
|
||||||
<input type="submit" value="Submit" />
|
<input type="submit" value="Submit" />
|
||||||
</form>
|
</form>
|
||||||
@ -332,7 +343,8 @@ class ArtistsTheme extends Themelet {
|
|||||||
$page->add_block(new Block("Edit Member", $html, "main", 10));
|
$page->add_block(new Block("Edit Member", $html, "main", 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show_artist($artist, $aliases, $members, $urls, $images, $userIsLogged, $userIsAdmin) {
|
public function show_artist($artist, $aliases, $members, $urls, $images, $userIsLogged, $userIsAdmin)
|
||||||
|
{
|
||||||
global $page;
|
global $page;
|
||||||
|
|
||||||
$artist_link = "<a href='".make_link("post/list/".$artist['name']."/1")."'>".str_replace("_", " ", $artist['name'])."</a>";
|
$artist_link = "<a href='".make_link("post/list/".$artist['name']."/1")."'>".str_replace("_", " ", $artist['name'])."</a>";
|
||||||
@ -343,8 +355,12 @@ class ArtistsTheme extends Themelet {
|
|||||||
<th></th>
|
<th></th>
|
||||||
<th></th>";
|
<th></th>";
|
||||||
|
|
||||||
if ($userIsLogged) $html .= "<th></th>";
|
if ($userIsLogged) {
|
||||||
if ($userIsAdmin) $html .= "<th></th>";
|
$html .= "<th></th>";
|
||||||
|
}
|
||||||
|
if ($userIsAdmin) {
|
||||||
|
$html .= "<th></th>";
|
||||||
|
}
|
||||||
|
|
||||||
$html .= " <tr>
|
$html .= " <tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -352,8 +368,12 @@ class ArtistsTheme extends Themelet {
|
|||||||
<tr>
|
<tr>
|
||||||
<td class='left'>Name:</td>
|
<td class='left'>Name:</td>
|
||||||
<td class='left'>".$artist_link."</td>";
|
<td class='left'>".$artist_link."</td>";
|
||||||
if ($userIsLogged) $html .= "<td></td>";
|
if ($userIsLogged) {
|
||||||
if ($userIsAdmin) $html .= "<td></td>";
|
$html .= "<td></td>";
|
||||||
|
}
|
||||||
|
if ($userIsAdmin) {
|
||||||
|
$html .= "<td></td>";
|
||||||
|
}
|
||||||
$html .= "</tr>";
|
$html .= "</tr>";
|
||||||
|
|
||||||
$html .= $this->render_aliases($aliases, $userIsLogged, $userIsAdmin);
|
$html .= $this->render_aliases($aliases, $userIsLogged, $userIsAdmin);
|
||||||
@ -363,8 +383,12 @@ class ArtistsTheme extends Themelet {
|
|||||||
$html .= "<tr>
|
$html .= "<tr>
|
||||||
<td class='left'>Notes:</td>
|
<td class='left'>Notes:</td>
|
||||||
<td class='left'>".$artist["notes"]."</td>";
|
<td class='left'>".$artist["notes"]."</td>";
|
||||||
if ($userIsLogged) $html .= "<td></td>";
|
if ($userIsLogged) {
|
||||||
if ($userIsAdmin) $html .= "<td></td>";
|
$html .= "<td></td>";
|
||||||
|
}
|
||||||
|
if ($userIsAdmin) {
|
||||||
|
$html .= "<td></td>";
|
||||||
|
}
|
||||||
//TODO how will notes be edited? On edit artist? (should there be an editartist?) or on a editnotes?
|
//TODO how will notes be edited? On edit artist? (should there be an editartist?) or on a editnotes?
|
||||||
//same question for deletion
|
//same question for deletion
|
||||||
$html .= "</tr>
|
$html .= "</tr>
|
||||||
@ -376,7 +400,7 @@ class ArtistsTheme extends Themelet {
|
|||||||
|
|
||||||
//we show the images for the artist
|
//we show the images for the artist
|
||||||
$artist_images = "";
|
$artist_images = "";
|
||||||
foreach($images as $image) {
|
foreach ($images as $image) {
|
||||||
$thumb_html = $this->build_thumb_html($image);
|
$thumb_html = $this->build_thumb_html($image);
|
||||||
|
|
||||||
$artist_images .= '<span class="thumb">'.
|
$artist_images .= '<span class="thumb">'.
|
||||||
@ -387,15 +411,10 @@ class ArtistsTheme extends Themelet {
|
|||||||
$page->add_block(new Block("Artist Images", $artist_images, "main", 20));
|
$page->add_block(new Block("Artist Images", $artist_images, "main", 20));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function render_aliases(array $aliases, bool $userIsLogged, bool $userIsAdmin): string
|
||||||
* @param $aliases
|
{
|
||||||
* @param $userIsLogged
|
|
||||||
* @param $userIsAdmin
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function render_aliases($aliases, $userIsLogged, $userIsAdmin) {
|
|
||||||
$html = "";
|
$html = "";
|
||||||
if(count($aliases) > 0) {
|
if (count($aliases) > 0) {
|
||||||
$aliasViewLink = str_replace("_", " ", $aliases[0]['alias_name']); // no link anymore
|
$aliasViewLink = str_replace("_", " ", $aliases[0]['alias_name']); // no link anymore
|
||||||
$aliasEditLink = "<a href='" . make_link("artist/alias/edit/" . $aliases[0]['alias_id']) . "'>Edit</a>";
|
$aliasEditLink = "<a href='" . make_link("artist/alias/edit/" . $aliases[0]['alias_id']) . "'>Edit</a>";
|
||||||
$aliasDeleteLink = "<a href='" . make_link("artist/alias/delete/" . $aliases[0]['alias_id']) . "'>Delete</a>";
|
$aliasDeleteLink = "<a href='" . make_link("artist/alias/delete/" . $aliases[0]['alias_id']) . "'>Delete</a>";
|
||||||
@ -404,11 +423,13 @@ class ArtistsTheme extends Themelet {
|
|||||||
<td class='left'>Aliases:</td>
|
<td class='left'>Aliases:</td>
|
||||||
<td class='left'>" . $aliasViewLink . "</td>";
|
<td class='left'>" . $aliasViewLink . "</td>";
|
||||||
|
|
||||||
if ($userIsLogged)
|
if ($userIsLogged) {
|
||||||
$html .= "<td class='left'>" . $aliasEditLink . "</td>";
|
$html .= "<td class='left'>" . $aliasEditLink . "</td>";
|
||||||
|
}
|
||||||
|
|
||||||
if ($userIsAdmin)
|
if ($userIsAdmin) {
|
||||||
$html .= "<td class='left'>" . $aliasDeleteLink . "</td>";
|
$html .= "<td class='left'>" . $aliasDeleteLink . "</td>";
|
||||||
|
}
|
||||||
|
|
||||||
$html .= "</tr>";
|
$html .= "</tr>";
|
||||||
|
|
||||||
@ -421,10 +442,12 @@ class ArtistsTheme extends Themelet {
|
|||||||
$html .= "<tr>
|
$html .= "<tr>
|
||||||
<td class='left'> </td>
|
<td class='left'> </td>
|
||||||
<td class='left'>" . $aliasViewLink . "</td>";
|
<td class='left'>" . $aliasViewLink . "</td>";
|
||||||
if ($userIsLogged)
|
if ($userIsLogged) {
|
||||||
$html .= "<td class='left'>" . $aliasEditLink . "</td>";
|
$html .= "<td class='left'>" . $aliasEditLink . "</td>";
|
||||||
if ($userIsAdmin)
|
}
|
||||||
|
if ($userIsAdmin) {
|
||||||
$html .= "<td class='left'>" . $aliasDeleteLink . "</td>";
|
$html .= "<td class='left'>" . $aliasDeleteLink . "</td>";
|
||||||
|
}
|
||||||
|
|
||||||
$html .= "</tr>";
|
$html .= "</tr>";
|
||||||
}
|
}
|
||||||
@ -433,15 +456,10 @@ class ArtistsTheme extends Themelet {
|
|||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function render_members(array $members, bool $userIsLogged, bool $userIsAdmin): string
|
||||||
* @param $members
|
{
|
||||||
* @param $userIsLogged
|
|
||||||
* @param $userIsAdmin
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function render_members($members, $userIsLogged, $userIsAdmin) {
|
|
||||||
$html = "";
|
$html = "";
|
||||||
if(count($members) > 0) {
|
if (count($members) > 0) {
|
||||||
$memberViewLink = str_replace("_", " ", $members[0]['name']); // no link anymore
|
$memberViewLink = str_replace("_", " ", $members[0]['name']); // no link anymore
|
||||||
$memberEditLink = "<a href='" . make_link("artist/member/edit/" . $members[0]['id']) . "'>Edit</a>";
|
$memberEditLink = "<a href='" . make_link("artist/member/edit/" . $members[0]['id']) . "'>Edit</a>";
|
||||||
$memberDeleteLink = "<a href='" . make_link("artist/member/delete/" . $members[0]['id']) . "'>Delete</a>";
|
$memberDeleteLink = "<a href='" . make_link("artist/member/delete/" . $members[0]['id']) . "'>Delete</a>";
|
||||||
@ -449,10 +467,12 @@ class ArtistsTheme extends Themelet {
|
|||||||
$html .= "<tr>
|
$html .= "<tr>
|
||||||
<td class='left'>Members:</td>
|
<td class='left'>Members:</td>
|
||||||
<td class='left'>" . $memberViewLink . "</td>";
|
<td class='left'>" . $memberViewLink . "</td>";
|
||||||
if ($userIsLogged)
|
if ($userIsLogged) {
|
||||||
$html .= "<td class='left'>" . $memberEditLink . "</td>";
|
$html .= "<td class='left'>" . $memberEditLink . "</td>";
|
||||||
if ($userIsAdmin)
|
}
|
||||||
|
if ($userIsAdmin) {
|
||||||
$html .= "<td class='left'>" . $memberDeleteLink . "</td>";
|
$html .= "<td class='left'>" . $memberDeleteLink . "</td>";
|
||||||
|
}
|
||||||
|
|
||||||
$html .= "</tr>";
|
$html .= "</tr>";
|
||||||
|
|
||||||
@ -465,10 +485,12 @@ class ArtistsTheme extends Themelet {
|
|||||||
$html .= "<tr>
|
$html .= "<tr>
|
||||||
<td class='left'> </td>
|
<td class='left'> </td>
|
||||||
<td class='left'>" . $memberViewLink . "</td>";
|
<td class='left'>" . $memberViewLink . "</td>";
|
||||||
if ($userIsLogged)
|
if ($userIsLogged) {
|
||||||
$html .= "<td class='left'>" . $memberEditLink . "</td>";
|
$html .= "<td class='left'>" . $memberEditLink . "</td>";
|
||||||
if ($userIsAdmin)
|
}
|
||||||
|
if ($userIsAdmin) {
|
||||||
$html .= "<td class='left'>" . $memberDeleteLink . "</td>";
|
$html .= "<td class='left'>" . $memberDeleteLink . "</td>";
|
||||||
|
}
|
||||||
|
|
||||||
$html .= "</tr>";
|
$html .= "</tr>";
|
||||||
}
|
}
|
||||||
@ -477,15 +499,10 @@ class ArtistsTheme extends Themelet {
|
|||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function render_urls(array $urls, bool $userIsLogged, bool $userIsAdmin): string
|
||||||
* @param $urls
|
{
|
||||||
* @param $userIsLogged
|
|
||||||
* @param $userIsAdmin
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function render_urls($urls, $userIsLogged, $userIsAdmin) {
|
|
||||||
$html = "";
|
$html = "";
|
||||||
if(count($urls) > 0) {
|
if (count($urls) > 0) {
|
||||||
$urlViewLink = "<a href='" . str_replace("_", " ", $urls[0]['url']) . "' target='_blank'>" . str_replace("_", " ", $urls[0]['url']) . "</a>";
|
$urlViewLink = "<a href='" . str_replace("_", " ", $urls[0]['url']) . "' target='_blank'>" . str_replace("_", " ", $urls[0]['url']) . "</a>";
|
||||||
$urlEditLink = "<a href='" . make_link("artist/url/edit/" . $urls[0]['id']) . "'>Edit</a>";
|
$urlEditLink = "<a href='" . make_link("artist/url/edit/" . $urls[0]['id']) . "'>Edit</a>";
|
||||||
$urlDeleteLink = "<a href='" . make_link("artist/url/delete/" . $urls[0]['id']) . "'>Delete</a>";
|
$urlDeleteLink = "<a href='" . make_link("artist/url/delete/" . $urls[0]['id']) . "'>Delete</a>";
|
||||||
@ -494,11 +511,13 @@ class ArtistsTheme extends Themelet {
|
|||||||
<td class='left'>URLs:</td>
|
<td class='left'>URLs:</td>
|
||||||
<td class='left'>" . $urlViewLink . "</td>";
|
<td class='left'>" . $urlViewLink . "</td>";
|
||||||
|
|
||||||
if ($userIsLogged)
|
if ($userIsLogged) {
|
||||||
$html .= "<td class='left'>" . $urlEditLink . "</td>";
|
$html .= "<td class='left'>" . $urlEditLink . "</td>";
|
||||||
|
}
|
||||||
|
|
||||||
if ($userIsAdmin)
|
if ($userIsAdmin) {
|
||||||
$html .= "<td class='left'>" . $urlDeleteLink . "</td>";
|
$html .= "<td class='left'>" . $urlDeleteLink . "</td>";
|
||||||
|
}
|
||||||
|
|
||||||
$html .= "</tr>";
|
$html .= "</tr>";
|
||||||
|
|
||||||
@ -511,11 +530,13 @@ class ArtistsTheme extends Themelet {
|
|||||||
$html .= "<tr>
|
$html .= "<tr>
|
||||||
<td class='left'> </td>
|
<td class='left'> </td>
|
||||||
<td class='left'>" . $urlViewLink . "</td>";
|
<td class='left'>" . $urlViewLink . "</td>";
|
||||||
if ($userIsLogged)
|
if ($userIsLogged) {
|
||||||
$html .= "<td class='left'>" . $urlEditLink . "</td>";
|
$html .= "<td class='left'>" . $urlEditLink . "</td>";
|
||||||
|
}
|
||||||
|
|
||||||
if ($userIsAdmin)
|
if ($userIsAdmin) {
|
||||||
$html .= "<td class='left'>" . $urlDeleteLink . "</td>";
|
$html .= "<td class='left'>" . $urlDeleteLink . "</td>";
|
||||||
|
}
|
||||||
|
|
||||||
$html .= "</tr>";
|
$html .= "</tr>";
|
||||||
}
|
}
|
||||||
@ -525,5 +546,13 @@ class ArtistsTheme extends Themelet {
|
|||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function get_help_html()
|
||||||
|
{
|
||||||
|
return '<p>Search for images with a particular artist.</p>
|
||||||
|
<div class="command_example">
|
||||||
|
<pre>artist=leonardo</pre>
|
||||||
|
<p>Returns images with the artist "leonardo".</p>
|
||||||
|
</div>
|
||||||
|
';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
ext/autocomplete/info.php
Normal file
17
ext/autocomplete/info.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Name: Autocomplete
|
||||||
|
* Author: Daku <admin@codeanimu.net>
|
||||||
|
* 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.";
|
||||||
|
}
|
||||||
@ -1,44 +1,63 @@
|
|||||||
<?php
|
<?php
|
||||||
/*
|
|
||||||
* Name: Autocomplete
|
|
||||||
* Author: Daku <admin@codeanimu.net>
|
|
||||||
* Description: Adds autocomplete to search & tagging.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class AutoComplete extends Extension {
|
class AutoComplete extends Extension
|
||||||
public function get_priority() {return 30;} // before Home
|
{
|
||||||
|
public function get_priority(): int
|
||||||
|
{
|
||||||
|
return 30;
|
||||||
|
} // before Home
|
||||||
|
|
||||||
public function onPageRequest(PageRequestEvent $event) {
|
public function onPageRequest(PageRequestEvent $event)
|
||||||
|
{
|
||||||
global $page, $database;
|
global $page, $database;
|
||||||
|
|
||||||
if($event->page_matches("api/internal/autocomplete")) {
|
if ($event->page_matches("api/internal/autocomplete")) {
|
||||||
if(!isset($_GET["s"])) return;
|
if (!isset($_GET["s"])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$page->set_mode(PageMode::DATA);
|
||||||
|
$page->set_type("application/json");
|
||||||
|
|
||||||
|
$s = strtolower($_GET["s"]);
|
||||||
|
if (
|
||||||
|
$s == '' ||
|
||||||
|
$s[0] == '_' ||
|
||||||
|
$s[0] == '%' ||
|
||||||
|
strlen($s) > 32
|
||||||
|
) {
|
||||||
|
$page->set_data("{}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//$limit = 0;
|
//$limit = 0;
|
||||||
$cache_key = "autocomplete-" . strtolower($_GET["s"]);
|
$cache_key = "autocomplete-$s";
|
||||||
$limitSQL = "";
|
$limitSQL = "";
|
||||||
$SQLarr = array("search"=>$_GET["s"]."%");
|
$s = str_replace('_', '\_', $s);
|
||||||
if(isset($_GET["limit"]) && $_GET["limit"] !== 0){
|
$s = str_replace('%', '\%', $s);
|
||||||
|
$SQLarr = ["search"=>"$s%"]; #, "cat_search"=>"%:$s%"];
|
||||||
|
if (isset($_GET["limit"]) && $_GET["limit"] !== 0) {
|
||||||
$limitSQL = "LIMIT :limit";
|
$limitSQL = "LIMIT :limit";
|
||||||
$SQLarr['limit'] = $_GET["limit"];
|
$SQLarr['limit'] = $_GET["limit"];
|
||||||
$cache_key .= "-" . $_GET["limit"];
|
$cache_key .= "-" . $_GET["limit"];
|
||||||
}
|
}
|
||||||
|
|
||||||
$res = $database->cache->get($cache_key);
|
$res = $database->cache->get($cache_key);
|
||||||
if(!$res) {
|
if (!$res) {
|
||||||
$res = $database->get_pairs($database->scoreql_to_sql("
|
$res = $database->get_pairs(
|
||||||
|
$database->scoreql_to_sql("
|
||||||
SELECT tag, count
|
SELECT tag, count
|
||||||
FROM tags
|
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
|
AND count > 0
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
$limitSQL"), $SQLarr
|
$limitSQL"),
|
||||||
|
$SQLarr
|
||||||
);
|
);
|
||||||
$database->cache->set($cache_key, $res, 600);
|
$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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
$(function(){
|
$(function(){
|
||||||
var metatags = ['order:id', 'order:width', 'order:height', 'order:filesize', 'order:filename'];
|
var metatags = ['order:id', 'order:width', 'order:height', 'order:filesize', 'order:filename'];
|
||||||
|
|
||||||
$('[name=search]').tagit({
|
$('[name="search"]').tagit({
|
||||||
singleFieldDelimiter: ' ',
|
singleFieldDelimiter: ' ',
|
||||||
beforeTagAdded: function(event, ui) {
|
beforeTagAdded: function(event, ui) {
|
||||||
if(metatags.indexOf(ui.tagLabel) !== -1) {
|
if(metatags.indexOf(ui.tagLabel) !== -1) {
|
||||||
@ -51,7 +51,7 @@ $(function(){
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
error : function (request, status, error) {
|
error : function (request, status, error) {
|
||||||
alert(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -66,7 +66,7 @@ $(function(){
|
|||||||
if(keyCode == 32) {
|
if(keyCode == 32) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
$('[name=search]').tagit('createTag', $(this).val());
|
$('.autocomplete_tags').tagit('createTag', $(this).val());
|
||||||
$(this).autocomplete('close');
|
$(this).autocomplete('close');
|
||||||
} else if (keyCode == 9) {
|
} else if (keyCode == 9) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class AutoCompleteTheme extends Themelet {
|
class AutoCompleteTheme extends Themelet
|
||||||
public function build_autocomplete(Page $page) {
|
{
|
||||||
|
public function build_autocomplete(Page $page)
|
||||||
|
{
|
||||||
$base_href = get_base_href();
|
$base_href = get_base_href();
|
||||||
// TODO: AJAX test and fallback.
|
// TODO: AJAX test and fallback.
|
||||||
|
|
||||||
|
|||||||
36
ext/ban_words/info.php
Normal file
36
ext/ban_words/info.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Name: Comment Word Ban
|
||||||
|
* Author: Shish <webmaster@shishnet.org>
|
||||||
|
* 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.
|
||||||
|
<p>Regex bans are also supported, allowing more complicated
|
||||||
|
bans like <code>/http:.*\.cn\//</code> to block links to
|
||||||
|
chinese websites, or <code>/.*?http.*?http.*?http.*?http.*?/</code>
|
||||||
|
to block comments with four (or more) links in.
|
||||||
|
<p>Note that for non-regex matches, only whole words are
|
||||||
|
matched, eg banning \"sex\" would block the comment \"get free
|
||||||
|
sex call this number\", but allow \"This is a photo of Bob
|
||||||
|
from Essex\"";
|
||||||
|
}
|
||||||
@ -1,27 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
/*
|
|
||||||
* Name: Comment Word Ban
|
|
||||||
* Author: Shish <webmaster@shishnet.org>
|
|
||||||
* 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.
|
|
||||||
* <p>Regex bans are also supported, allowing more complicated
|
|
||||||
* bans like <code>/http:.*\.cn\//</code> to block links to
|
|
||||||
* chinese websites, or <code>/.*?http.*?http.*?http.*?http.*?/</code>
|
|
||||||
* to block comments with four (or more) links in.
|
|
||||||
* <p>Note that for non-regex matches, only whole words are
|
|
||||||
* matched, eg banning "sex" would block the comment "get free
|
|
||||||
* sex call this number", but allow "This is a photo of Bob
|
|
||||||
* from Essex"
|
|
||||||
*/
|
|
||||||
|
|
||||||
class BanWords extends Extension {
|
class BanWords extends Extension
|
||||||
public function onInitExt(InitExtEvent $event) {
|
{
|
||||||
|
public function onInitExt(InitExtEvent $event)
|
||||||
|
{
|
||||||
global $config;
|
global $config;
|
||||||
$config->set_default_string('banned_words', "
|
$config->set_default_string('banned_words', "
|
||||||
a href=
|
a href=
|
||||||
@ -53,34 +35,38 @@ xanax
|
|||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onCommentPosting(CommentPostingEvent $event) {
|
public function onCommentPosting(CommentPostingEvent $event)
|
||||||
|
{
|
||||||
global $user;
|
global $user;
|
||||||
if(!$user->can("bypass_comment_checks")) {
|
if (!$user->can(Permissions::BYPASS_COMMENT_CHECKS)) {
|
||||||
$this->test_text($event->comment, new CommentPostingException("Comment contains banned terms"));
|
$this->test_text($event->comment, new CommentPostingException("Comment contains banned terms"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onSourceSet(SourceSetEvent $event) {
|
public function onSourceSet(SourceSetEvent $event)
|
||||||
|
{
|
||||||
$this->test_text($event->source, new SCoreException("Source contains banned terms"));
|
$this->test_text($event->source, new SCoreException("Source contains banned terms"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onTagSet(TagSetEvent $event) {
|
public function onTagSet(TagSetEvent $event)
|
||||||
|
{
|
||||||
$this->test_text(Tag::implode($event->tags), new SCoreException("Tags contain banned terms"));
|
$this->test_text(Tag::implode($event->tags), new SCoreException("Tags contain banned terms"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onSetupBuilding(SetupBuildingEvent $event) {
|
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||||
|
{
|
||||||
$sb = new SetupBlock("Banned Phrases");
|
$sb = new SetupBlock("Banned Phrases");
|
||||||
$sb->add_label("One per line, lines that start with slashes are treated as regex<br/>");
|
$sb->add_label("One per line, lines that start with slashes are treated as regex<br/>");
|
||||||
$sb->add_longtext_option("banned_words");
|
$sb->add_longtext_option("banned_words");
|
||||||
$failed = array();
|
$failed = [];
|
||||||
foreach($this->get_words() as $word) {
|
foreach ($this->get_words() as $word) {
|
||||||
if($word[0] == '/') {
|
if ($word[0] == '/') {
|
||||||
if(preg_match($word, "") === false) {
|
if (preg_match($word, "") === false) {
|
||||||
$failed[] = $word;
|
$failed[] = $word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if($failed) {
|
if ($failed) {
|
||||||
$sb->add_label("Failed regexes: ".join(", ", $failed));
|
$sb->add_label("Failed regexes: ".join(", ", $failed));
|
||||||
}
|
}
|
||||||
$event->panel->add_block($sb);
|
$event->panel->add_block($sb);
|
||||||
@ -88,40 +74,35 @@ xanax
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Throws if the comment contains banned words.
|
* 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) {
|
private function test_text(string $comment, Exception $ex): void
|
||||||
|
{
|
||||||
$comment = strtolower($comment);
|
$comment = strtolower($comment);
|
||||||
|
|
||||||
foreach($this->get_words() as $word) {
|
foreach ($this->get_words() as $word) {
|
||||||
if($word[0] == '/') {
|
if ($word[0] == '/') {
|
||||||
// lines that start with slash are regex
|
// lines that start with slash are regex
|
||||||
if(preg_match($word, $comment) === 1) {
|
if (preg_match($word, $comment) === 1) {
|
||||||
throw $ex;
|
throw $ex;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// other words are literal
|
// other words are literal
|
||||||
if(strpos($comment, $word) !== false) {
|
if (strpos($comment, $word) !== false) {
|
||||||
throw $ex;
|
throw $ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function get_words(): array
|
||||||
* @return string[]
|
{
|
||||||
*/
|
|
||||||
private function get_words() {
|
|
||||||
global $config;
|
global $config;
|
||||||
$words = array();
|
$words = [];
|
||||||
|
|
||||||
$banned = $config->get_string("banned_words");
|
$banned = $config->get_string("banned_words");
|
||||||
foreach(explode("\n", $banned) as $word) {
|
foreach (explode("\n", $banned) as $word) {
|
||||||
$word = trim(strtolower($word));
|
$word = trim(strtolower($word));
|
||||||
if(strlen($word) == 0) {
|
if (strlen($word) == 0) {
|
||||||
// line is blank
|
// line is blank
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -131,6 +112,8 @@ xanax
|
|||||||
return $words;
|
return $words;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_priority() {return 30;}
|
public function get_priority(): int
|
||||||
|
{
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
class BanWordsTest extends ShimmiePHPUnitTestCase {
|
class BanWordsTest extends ShimmiePHPUnitTestCase
|
||||||
public function check_blocked($image_id, $words) {
|
{
|
||||||
|
public function check_blocked($image_id, $words)
|
||||||
|
{
|
||||||
global $user;
|
global $user;
|
||||||
try {
|
try {
|
||||||
send_event(new CommentPostingEvent($image_id, $user, $words));
|
send_event(new CommentPostingEvent($image_id, $user, $words));
|
||||||
$this->fail("Exception not thrown");
|
$this->fail("Exception not thrown");
|
||||||
}
|
} catch (CommentPostingException $e) {
|
||||||
catch(CommentPostingException $e) {
|
|
||||||
$this->assertEquals($e->getMessage(), "Comment contains banned terms");
|
$this->assertEquals($e->getMessage(), "Comment contains banned terms");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testWordBan() {
|
public function testWordBan()
|
||||||
|
{
|
||||||
global $config;
|
global $config;
|
||||||
$config->set_string("banned_words", "viagra\nporn\n\n/http:.*\.cn\//");
|
$config->set_string("banned_words", "viagra\nporn\n\n/http:.*\.cn\//");
|
||||||
|
|
||||||
@ -30,4 +32,3 @@ class BanWordsTest extends ShimmiePHPUnitTestCase {
|
|||||||
$this->assert_no_text('http://something.cn/');
|
$this->assert_no_text('http://something.cn/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
40
ext/bbcode/info.php
Normal file
40
ext/bbcode/info.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name: BBCode
|
||||||
|
* Author: Shish <webmaster@shishnet.org>
|
||||||
|
* 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:
|
||||||
|
<ul>
|
||||||
|
<li>[img]url[/img]
|
||||||
|
<li>[url]<a href=\"{self::SHIMMIE_URL}\">http://code.shishnet.org/</a>[/url]
|
||||||
|
<li>[email]<a href=\"mailto:{self::SHISH_EMAIL}\">webmaster@shishnet.org</a>[/email]
|
||||||
|
<li>[b]<b>bold</b>[/b]
|
||||||
|
<li>[i]<i>italic</i>[/i]
|
||||||
|
<li>[u]<u>underline</u>[/u]
|
||||||
|
<li>[s]<s>strikethrough</s>[/s]
|
||||||
|
<li>[sup]<sup>superscript</sup>[/sup]
|
||||||
|
<li>[sub]<sub>subscript</sub>[/sub]
|
||||||
|
<li>[[wiki article]]
|
||||||
|
<li>[[wiki article|with some text]]
|
||||||
|
<li>[quote]text[/quote]
|
||||||
|
<li>[quote=Username]text[/quote]
|
||||||
|
<li>>>123 (link to image #123)
|
||||||
|
</ul>";
|
||||||
|
}
|
||||||
@ -1,40 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* Name: BBCode
|
|
||||||
* Author: Shish <webmaster@shishnet.org>
|
|
||||||
* Link: http://code.shishnet.org/shimmie2/
|
|
||||||
* License: GPLv2
|
|
||||||
* Description: Turns BBCode into HTML
|
|
||||||
* Documentation:
|
|
||||||
* Supported tags:
|
|
||||||
* <ul>
|
|
||||||
* <li>[img]url[/img]
|
|
||||||
* <li>[url]<a href="http://code.shishnet.org/shimmie2/">http://code.shishnet.org/</a>[/url]
|
|
||||||
* <li>[email]<a href="mailto:webmaster@shishnet.org">webmaster@shishnet.org</a>[/email]
|
|
||||||
* <li>[b]<b>bold</b>[/b]
|
|
||||||
* <li>[i]<i>italic</i>[/i]
|
|
||||||
* <li>[u]<u>underline</u>[/u]
|
|
||||||
* <li>[s]<s>strikethrough</s>[/s]
|
|
||||||
* <li>[sup]<sup>superscript</sup>[/sup]
|
|
||||||
* <li>[sub]<sub>subscript</sub>[/sub]
|
|
||||||
* <li>[[wiki article]]
|
|
||||||
* <li>[[wiki article|with some text]]
|
|
||||||
* <li>[quote]text[/quote]
|
|
||||||
* <li>[quote=Username]text[/quote]
|
|
||||||
* <li>>>123 (link to image #123)
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
|
|
||||||
class BBCode extends FormatterExtension {
|
|
||||||
/**
|
class BBCode extends FormatterExtension
|
||||||
* @param string $text
|
{
|
||||||
* @return string
|
public function format(string $text): string
|
||||||
*/
|
{
|
||||||
public function format(/*string*/ $text) {
|
|
||||||
$text = $this->extract_code($text);
|
$text = $this->extract_code($text);
|
||||||
foreach(array(
|
foreach ([
|
||||||
"b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4",
|
"b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4",
|
||||||
) as $el) {
|
] as $el) {
|
||||||
$text = preg_replace("!\[$el\](.*?)\[/$el\]!s", "<$el>$1</$el>", $text);
|
$text = preg_replace("!\[$el\](.*?)\[/$el\]!s", "<$el>$1</$el>", $text);
|
||||||
}
|
}
|
||||||
$text = preg_replace('!^>>([^\d].+)!', '<blockquote><small>$1</small></blockquote>', $text);
|
$text = preg_replace('!^>>([^\d].+)!', '<blockquote><small>$1</small></blockquote>', $text);
|
||||||
@ -52,12 +26,15 @@ class BBCode extends FormatterExtension {
|
|||||||
$text = str_replace("\n", "\n<br>", $text);
|
$text = str_replace("\n", "\n<br>", $text);
|
||||||
$text = preg_replace("/\[quote\](.*?)\[\/quote\]/s", "<blockquote><small>\\1</small></blockquote>", $text);
|
$text = preg_replace("/\[quote\](.*?)\[\/quote\]/s", "<blockquote><small>\\1</small></blockquote>", $text);
|
||||||
$text = preg_replace("/\[quote=(.*?)\](.*?)\[\/quote\]/s", "<blockquote><em>\\1 said:</em><br><small>\\2</small></blockquote>", $text);
|
$text = preg_replace("/\[quote=(.*?)\](.*?)\[\/quote\]/s", "<blockquote><em>\\1 said:</em><br><small>\\2</small></blockquote>", $text);
|
||||||
while(preg_match("/\[list\](.*?)\[\/list\]/s", $text))
|
while (preg_match("/\[list\](.*?)\[\/list\]/s", $text)) {
|
||||||
$text = preg_replace("/\[list\](.*?)\[\/list\]/s", "<ul>\\1</ul>", $text);
|
$text = preg_replace("/\[list\](.*?)\[\/list\]/s", "<ul>\\1</ul>", $text);
|
||||||
while(preg_match("/\[ul\](.*?)\[\/ul\]/s", $text))
|
}
|
||||||
|
while (preg_match("/\[ul\](.*?)\[\/ul\]/s", $text)) {
|
||||||
$text = preg_replace("/\[ul\](.*?)\[\/ul\]/s", "<ul>\\1</ul>", $text);
|
$text = preg_replace("/\[ul\](.*?)\[\/ul\]/s", "<ul>\\1</ul>", $text);
|
||||||
while(preg_match("/\[ol\](.*?)\[\/ol\]/s", $text))
|
}
|
||||||
|
while (preg_match("/\[ol\](.*?)\[\/ol\]/s", $text)) {
|
||||||
$text = preg_replace("/\[ol\](.*?)\[\/ol\]/s", "<ol>\\1</ol>", $text);
|
$text = preg_replace("/\[ol\](.*?)\[\/ol\]/s", "<ol>\\1</ol>", $text);
|
||||||
|
}
|
||||||
$text = preg_replace("/\[li\](.*?)\[\/li\]/s", "<li>\\1</li>", $text);
|
$text = preg_replace("/\[li\](.*?)\[\/li\]/s", "<li>\\1</li>", $text);
|
||||||
$text = preg_replace("#\[\*\]#s", "<li>", $text);
|
$text = preg_replace("#\[\*\]#s", "<li>", $text);
|
||||||
$text = preg_replace("#<br><(li|ul|ol|/ul|/ol)>#s", "<\\1>", $text);
|
$text = preg_replace("#<br><(li|ul|ol|/ul|/ol)>#s", "<\\1>", $text);
|
||||||
@ -67,15 +44,12 @@ class BBCode extends FormatterExtension {
|
|||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function strip(string $text): string
|
||||||
* @param string $text
|
{
|
||||||
* @return string
|
foreach ([
|
||||||
*/
|
|
||||||
public function strip(/*string*/ $text) {
|
|
||||||
foreach(array(
|
|
||||||
"b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4",
|
"b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4",
|
||||||
"code", "url", "email", "li",
|
"code", "url", "email", "li",
|
||||||
) as $el) {
|
] as $el) {
|
||||||
$text = preg_replace("!\[$el\](.*?)\[/$el\]!s", '$1', $text);
|
$text = preg_replace("!\[$el\](.*?)\[/$el\]!s", '$1', $text);
|
||||||
}
|
}
|
||||||
$text = preg_replace("!\[anchor=(.*?)\](.*?)\[/anchor\]!s", '$2', $text);
|
$text = preg_replace("!\[anchor=(.*?)\](.*?)\[/anchor\]!s", '$2', $text);
|
||||||
@ -91,32 +65,33 @@ class BBCode extends FormatterExtension {
|
|||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function filter_spoiler(string $text): string
|
||||||
* @param string $text
|
{
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function filter_spoiler(/*string*/ $text) {
|
|
||||||
return str_replace(
|
return str_replace(
|
||||||
array("[spoiler]","[/spoiler]"),
|
["[spoiler]","[/spoiler]"],
|
||||||
array("<span style=\"background-color:#000; color:#000;\">","</span>"),
|
["<span style=\"background-color:#000; color:#000;\">","</span>"],
|
||||||
$text);
|
$text
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function strip_spoiler(string $text): string
|
||||||
* @param string $text
|
{
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function strip_spoiler(/*string*/ $text) {
|
|
||||||
$l1 = strlen("[spoiler]");
|
$l1 = strlen("[spoiler]");
|
||||||
$l2 = strlen("[/spoiler]");
|
$l2 = strlen("[/spoiler]");
|
||||||
while(true) {
|
while (true) {
|
||||||
$start = strpos($text, "[spoiler]");
|
$start = strpos($text, "[spoiler]");
|
||||||
if($start === false) break;
|
if ($start === false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$end = strpos($text, "[/spoiler]");
|
$end = strpos($text, "[/spoiler]");
|
||||||
if($end === false) break;
|
if ($end === false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if($end < $start) break;
|
if ($end < $start) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$beginning = substr($text, 0, $start);
|
$beginning = substr($text, 0, $start);
|
||||||
$middle = str_rot13(substr($text, $start+$l1, ($end-$start-$l1)));
|
$middle = str_rot13(substr($text, $start+$l1, ($end-$start-$l1)));
|
||||||
@ -127,11 +102,8 @@ class BBCode extends FormatterExtension {
|
|||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function extract_code(string $text): string
|
||||||
* @param string $text
|
{
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function extract_code(/*string*/ $text) {
|
|
||||||
# at the end of this function, the only code! blocks should be
|
# at the end of this function, the only code! blocks should be
|
||||||
# the ones we've added -- others may contain malicious content,
|
# the ones we've added -- others may contain malicious content,
|
||||||
# which would only appear after decoding
|
# which would only appear after decoding
|
||||||
@ -140,14 +112,20 @@ class BBCode extends FormatterExtension {
|
|||||||
|
|
||||||
$l1 = strlen("[code]");
|
$l1 = strlen("[code]");
|
||||||
$l2 = strlen("[/code]");
|
$l2 = strlen("[/code]");
|
||||||
while(true) {
|
while (true) {
|
||||||
$start = strpos($text, "[code]");
|
$start = strpos($text, "[code]");
|
||||||
if($start === false) break;
|
if ($start === false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$end = strpos($text, "[/code]", $start);
|
$end = strpos($text, "[/code]", $start);
|
||||||
if($end === false) break;
|
if ($end === false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if($end < $start) break;
|
if ($end < $start) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$beginning = substr($text, 0, $start);
|
$beginning = substr($text, 0, $start);
|
||||||
$middle = base64_encode(substr($text, $start+$l1, ($end-$start-$l1)));
|
$middle = base64_encode(substr($text, $start+$l1, ($end-$start-$l1)));
|
||||||
@ -158,19 +136,20 @@ class BBCode extends FormatterExtension {
|
|||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function insert_code(string $text): string
|
||||||
* @param string $text
|
{
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function insert_code(/*string*/ $text) {
|
|
||||||
$l1 = strlen("[code!]");
|
$l1 = strlen("[code!]");
|
||||||
$l2 = strlen("[/code!]");
|
$l2 = strlen("[/code!]");
|
||||||
while(true) {
|
while (true) {
|
||||||
$start = strpos($text, "[code!]");
|
$start = strpos($text, "[code!]");
|
||||||
if($start === false) break;
|
if ($start === false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$end = strpos($text, "[/code!]");
|
$end = strpos($text, "[/code!]");
|
||||||
if($end === false) break;
|
if ($end === false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$beginning = substr($text, 0, $start);
|
$beginning = substr($text, 0, $start);
|
||||||
$middle = base64_decode(substr($text, $start+$l1, ($end-$start-$l1)));
|
$middle = base64_decode(substr($text, $start+$l1, ($end-$start-$l1)));
|
||||||
@ -181,4 +160,3 @@ class BBCode extends FormatterExtension {
|
|||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
ext/bbcode/script.js
Normal file
18
ext/bbcode/script.js
Normal file
@ -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());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,85 +1,110 @@
|
|||||||
<?php
|
<?php
|
||||||
class BBCodeTest extends ShimmiePHPUnitTestCase {
|
class BBCodeTest extends ShimmiePHPUnitTestCase
|
||||||
public function testBasics() {
|
{
|
||||||
|
public function testBasics()
|
||||||
|
{
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->filter("[b]bold[/b][i]italic[/i]"),
|
$this->filter("[b]bold[/b][i]italic[/i]"),
|
||||||
"<b>bold</b><i>italic</i>");
|
"<b>bold</b><i>italic</i>"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testStacking() {
|
public function testStacking()
|
||||||
|
{
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->filter("[b]B[/b][i]I[/i][b]B[/b]"),
|
$this->filter("[b]B[/b][i]I[/i][b]B[/b]"),
|
||||||
"<b>B</b><i>I</i><b>B</b>");
|
"<b>B</b><i>I</i><b>B</b>"
|
||||||
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->filter("[b]bold[i]bolditalic[/i]bold[/b]"),
|
$this->filter("[b]bold[i]bolditalic[/i]bold[/b]"),
|
||||||
"<b>bold<i>bolditalic</i>bold</b>");
|
"<b>bold<i>bolditalic</i>bold</b>"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFailure() {
|
public function testFailure()
|
||||||
|
{
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->filter("[b]bold[i]italic"),
|
$this->filter("[b]bold[i]italic"),
|
||||||
"[b]bold[i]italic");
|
"[b]bold[i]italic"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCode() {
|
public function testCode()
|
||||||
|
{
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->filter("[code][b]bold[/b][/code]"),
|
$this->filter("[code][b]bold[/b][/code]"),
|
||||||
"<pre>[b]bold[/b]</pre>");
|
"<pre>[b]bold[/b]</pre>"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNestedList() {
|
public function testNestedList()
|
||||||
|
{
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->filter("[list][*]a[list][*]a[*]b[/list][*]b[/list]"),
|
$this->filter("[list][*]a[list][*]a[*]b[/list][*]b[/list]"),
|
||||||
"<ul><li>a<ul><li>a<li>b</ul><li>b</ul>");
|
"<ul><li>a<ul><li>a<li>b</ul><li>b</ul>"
|
||||||
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->filter("[ul][*]a[ol][*]a[*]b[/ol][*]b[/ul]"),
|
$this->filter("[ul][*]a[ol][*]a[*]b[/ol][*]b[/ul]"),
|
||||||
"<ul><li>a<ol><li>a<li>b</ol><li>b</ul>");
|
"<ul><li>a<ol><li>a<li>b</ol><li>b</ul>"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSpoiler() {
|
public function testSpoiler()
|
||||||
|
{
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->filter("[spoiler]ShishNet[/spoiler]"),
|
$this->filter("[spoiler]ShishNet[/spoiler]"),
|
||||||
"<span style=\"background-color:#000; color:#000;\">ShishNet</span>");
|
"<span style=\"background-color:#000; color:#000;\">ShishNet</span>"
|
||||||
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->strip("[spoiler]ShishNet[/spoiler]"),
|
$this->strip("[spoiler]ShishNet[/spoiler]"),
|
||||||
"FuvfuArg");
|
"FuvfuArg"
|
||||||
|
);
|
||||||
#$this->assertEquals(
|
#$this->assertEquals(
|
||||||
# $this->filter("[spoiler]ShishNet"),
|
# $this->filter("[spoiler]ShishNet"),
|
||||||
# "[spoiler]ShishNet");
|
# "[spoiler]ShishNet");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testURL() {
|
public function testURL()
|
||||||
|
{
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->filter("[url]http://shishnet.org[/url]"),
|
$this->filter("[url]http://shishnet.org[/url]"),
|
||||||
"<a href=\"http://shishnet.org\">http://shishnet.org</a>");
|
"<a href=\"http://shishnet.org\">http://shishnet.org</a>"
|
||||||
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->filter("[url=http://shishnet.org]ShishNet[/url]"),
|
$this->filter("[url=http://shishnet.org]ShishNet[/url]"),
|
||||||
"<a href=\"http://shishnet.org\">ShishNet</a>");
|
"<a href=\"http://shishnet.org\">ShishNet</a>"
|
||||||
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->filter("[url=javascript:alert(\"owned\")]click to fail[/url]"),
|
$this->filter("[url=javascript:alert(\"owned\")]click to fail[/url]"),
|
||||||
"[url=javascript:alert(\"owned\")]click to fail[/url]");
|
"[url=javascript:alert(\"owned\")]click to fail[/url]"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testEmailURL() {
|
public function testEmailURL()
|
||||||
|
{
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->filter("[email]spam@shishnet.org[/email]"),
|
$this->filter("[email]spam@shishnet.org[/email]"),
|
||||||
"<a href=\"mailto:spam@shishnet.org\">spam@shishnet.org</a>");
|
"<a href=\"mailto:spam@shishnet.org\">spam@shishnet.org</a>"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAnchor() {
|
public function testAnchor()
|
||||||
|
{
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$this->filter("[anchor=rules]Rules[/anchor]"),
|
$this->filter("[anchor=rules]Rules[/anchor]"),
|
||||||
'<span class="anchor">Rules <a class="alink" href="#bb-rules" name="bb-rules" title="link to this anchor"> ¶ </a></span>');
|
'<span class="anchor">Rules <a class="alink" href="#bb-rules" name="bb-rules" title="link to this anchor"> ¶ </a></span>'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function filter($in) {
|
private function filter($in)
|
||||||
|
{
|
||||||
$bb = new BBCode();
|
$bb = new BBCode();
|
||||||
return $bb->format($in);
|
return $bb->format($in);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function strip($in) {
|
private function strip($in)
|
||||||
|
{
|
||||||
$bb = new BBCode();
|
$bb = new BBCode();
|
||||||
return $bb->strip($in);
|
return $bb->strip($in);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
ext/blocks/info.php
Normal file
21
ext/blocks/info.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Name: Generic Blocks
|
||||||
|
* Author: Shish <webmaster@shishnet.org>
|
||||||
|
* 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)";
|
||||||
|
}
|
||||||
@ -1,16 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
/*
|
|
||||||
* Name: Generic Blocks
|
|
||||||
* Author: Shish <webmaster@shishnet.org>
|
|
||||||
* Link: http://code.shishnet.org/shimmie2/
|
|
||||||
* License: GPLv2
|
|
||||||
* Description: Add HTML to some space (News, Ads, etc)
|
|
||||||
*/
|
|
||||||
|
|
||||||
class Blocks extends Extension {
|
class Blocks extends Extension
|
||||||
public function onInitExt(InitExtEvent $event) {
|
{
|
||||||
|
public function onInitExt(InitExtEvent $event)
|
||||||
|
{
|
||||||
global $config, $database;
|
global $config, $database;
|
||||||
if($config->get_int("ext_blocks_version") < 1) {
|
if ($config->get_int("ext_blocks_version") < 1) {
|
||||||
$database->create_table("blocks", "
|
$database->create_table("blocks", "
|
||||||
id SCORE_AIPK,
|
id SCORE_AIPK,
|
||||||
pages VARCHAR(128) NOT NULL,
|
pages VARCHAR(128) NOT NULL,
|
||||||
@ -19,73 +14,82 @@ class Blocks extends Extension {
|
|||||||
priority INTEGER NOT NULL,
|
priority INTEGER NOT NULL,
|
||||||
content TEXT NOT NULL
|
content TEXT NOT NULL
|
||||||
");
|
");
|
||||||
$database->execute("CREATE INDEX blocks_pages_idx ON blocks(pages)", array());
|
$database->execute("CREATE INDEX blocks_pages_idx ON blocks(pages)", []);
|
||||||
$config->set_int("ext_blocks_version", 1);
|
$config->set_int("ext_blocks_version", 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event) {
|
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||||
|
{
|
||||||
global $user;
|
global $user;
|
||||||
if($user->can("manage_blocks")) {
|
if ($event->parent==="system") {
|
||||||
|
if ($user->can(Permissions::MANAGE_BLOCKS)) {
|
||||||
|
$event->add_nav_link("blocks", new Link('blocks/list'), "Blocks Editor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
|
||||||
|
{
|
||||||
|
global $user;
|
||||||
|
if ($user->can(Permissions::MANAGE_BLOCKS)) {
|
||||||
$event->add_link("Blocks Editor", make_link("blocks/list"));
|
$event->add_link("Blocks Editor", make_link("blocks/list"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onPageRequest(PageRequestEvent $event) {
|
public function onPageRequest(PageRequestEvent $event)
|
||||||
|
{
|
||||||
global $database, $page, $user;
|
global $database, $page, $user;
|
||||||
|
|
||||||
$blocks = $database->cache->get("blocks");
|
$blocks = $database->cache->get("blocks");
|
||||||
if($blocks === false) {
|
if ($blocks === false) {
|
||||||
$blocks = $database->get_all("SELECT * FROM blocks");
|
$blocks = $database->get_all("SELECT * FROM blocks");
|
||||||
$database->cache->set("blocks", $blocks, 600);
|
$database->cache->set("blocks", $blocks, 600);
|
||||||
}
|
}
|
||||||
foreach($blocks as $block) {
|
foreach ($blocks as $block) {
|
||||||
$path = implode("/", $event->args);
|
$path = implode("/", $event->args);
|
||||||
if(strlen($path) < 4000 && fnmatch($block['pages'], $path)) {
|
if (strlen($path) < 4000 && fnmatch($block['pages'], $path)) {
|
||||||
$b = new Block($block['title'], $block['content'], $block['area'], $block['priority']);
|
$b = new Block($block['title'], $block['content'], $block['area'], $block['priority']);
|
||||||
$b->is_content = false;
|
$b->is_content = false;
|
||||||
$page->add_block($b);
|
$page->add_block($b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if($event->page_matches("blocks") && $user->can("manage_blocks")) {
|
if ($event->page_matches("blocks") && $user->can(Permissions::MANAGE_BLOCKS)) {
|
||||||
if($event->get_arg(0) == "add") {
|
if ($event->get_arg(0) == "add") {
|
||||||
if($user->check_auth_token()) {
|
if ($user->check_auth_token()) {
|
||||||
$database->execute("
|
$database->execute("
|
||||||
INSERT INTO blocks (pages, title, area, priority, content)
|
INSERT INTO blocks (pages, title, area, priority, content)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
", array($_POST['pages'], $_POST['title'], $_POST['area'], (int)$_POST['priority'], $_POST['content']));
|
", [$_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'].")");
|
log_info("blocks", "Added Block #".($database->get_last_insert_id('blocks_id_seq'))." (".$_POST['title'].")");
|
||||||
$database->cache->delete("blocks");
|
$database->cache->delete("blocks");
|
||||||
$page->set_mode("redirect");
|
$page->set_mode(PageMode::REDIRECT);
|
||||||
$page->set_redirect(make_link("blocks/list"));
|
$page->set_redirect(make_link("blocks/list"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if($event->get_arg(0) == "update") {
|
if ($event->get_arg(0) == "update") {
|
||||||
if($user->check_auth_token()) {
|
if ($user->check_auth_token()) {
|
||||||
if(!empty($_POST['delete'])) {
|
if (!empty($_POST['delete'])) {
|
||||||
$database->execute("
|
$database->execute("
|
||||||
DELETE FROM blocks
|
DELETE FROM blocks
|
||||||
WHERE id=?
|
WHERE id=?
|
||||||
", array($_POST['id']));
|
", [$_POST['id']]);
|
||||||
log_info("blocks", "Deleted Block #".$_POST['id']);
|
log_info("blocks", "Deleted Block #".$_POST['id']);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$database->execute("
|
$database->execute("
|
||||||
UPDATE blocks SET pages=?, title=?, area=?, priority=?, content=?
|
UPDATE blocks SET pages=?, title=?, area=?, priority=?, content=?
|
||||||
WHERE id=?
|
WHERE id=?
|
||||||
", array($_POST['pages'], $_POST['title'], $_POST['area'], (int)$_POST['priority'], $_POST['content'], $_POST['id']));
|
", [$_POST['pages'], $_POST['title'], $_POST['area'], (int)$_POST['priority'], $_POST['content'], $_POST['id']]);
|
||||||
log_info("blocks", "Updated Block #".$_POST['id']." (".$_POST['title'].")");
|
log_info("blocks", "Updated Block #".$_POST['id']." (".$_POST['title'].")");
|
||||||
}
|
}
|
||||||
$database->cache->delete("blocks");
|
$database->cache->delete("blocks");
|
||||||
$page->set_mode("redirect");
|
$page->set_mode(PageMode::REDIRECT);
|
||||||
$page->set_redirect(make_link("blocks/list"));
|
$page->set_redirect(make_link("blocks/list"));
|
||||||
}
|
}
|
||||||
}
|
} elseif ($event->get_arg(0) == "list") {
|
||||||
else if($event->get_arg(0) == "list") {
|
|
||||||
$this->theme->display_blocks($database->get_all("SELECT * FROM blocks ORDER BY area, priority"));
|
$this->theme->display_blocks($database->get_all("SELECT * FROM blocks ORDER BY area, priority"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
class BlocksTest extends ShimmiePHPUnitTestCase {
|
class BlocksTest extends ShimmiePHPUnitTestCase
|
||||||
public function testBlocks() {
|
{
|
||||||
|
public function testBlocks()
|
||||||
|
{
|
||||||
$this->log_in_as_admin();
|
$this->log_in_as_admin();
|
||||||
$this->get_page("blocks/list");
|
$this->get_page("blocks/list");
|
||||||
$this->assert_response(200);
|
$this->assert_response(200);
|
||||||
$this->assert_title("Blocks");
|
$this->assert_title("Blocks");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
class BlocksTheme extends Themelet {
|
class BlocksTheme extends Themelet
|
||||||
public function display_blocks($blocks) {
|
{
|
||||||
|
public function display_blocks($blocks)
|
||||||
|
{
|
||||||
global $page;
|
global $page;
|
||||||
|
|
||||||
$html = "<table class='form' style='width: 100%;'>";
|
$html = "<table class='form' style='width: 100%;'>";
|
||||||
foreach($blocks as $block) {
|
foreach ($blocks as $block) {
|
||||||
$html .= make_form(make_link("blocks/update"));
|
$html .= make_form(make_link("blocks/update"));
|
||||||
$html .= "<input type='hidden' name='id' value='".html_escape($block['id'])."'>";
|
$html .= "<input type='hidden' name='id' value='".html_escape($block['id'])."'>";
|
||||||
$html .= "<tr>";
|
$html .= "<tr>";
|
||||||
@ -43,4 +45,3 @@ class BlocksTheme extends Themelet {
|
|||||||
$page->add_block(new Block("Block Editor", $html));
|
$page->add_block(new Block("Block Editor", $html));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
ext/blotter/info.php
Normal file
22
ext/blotter/info.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Name: Blotter
|
||||||
|
* Author: Zach Hall <zach@sosguy.net> [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";
|
||||||
|
}
|
||||||
@ -1,15 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
/*
|
|
||||||
* Name: Blotter
|
class Blotter extends Extension
|
||||||
* Author: Zach Hall <zach@sosguy.net> [http://seemslegit.com/]
|
{
|
||||||
* License: GPLv2
|
public function onInitExt(InitExtEvent $event)
|
||||||
* 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...
|
* I love re-using this installer don't I...
|
||||||
*/
|
*/
|
||||||
@ -20,7 +14,7 @@ class Blotter extends Extension {
|
|||||||
*
|
*
|
||||||
* REMINDER: If I change the database tables, I must change up version by 1.
|
* REMINDER: If I change the database tables, I must change up version by 1.
|
||||||
*/
|
*/
|
||||||
if($version < 1) {
|
if ($version < 1) {
|
||||||
/**
|
/**
|
||||||
* Installer
|
* Installer
|
||||||
*/
|
*/
|
||||||
@ -32,8 +26,10 @@ class Blotter extends Extension {
|
|||||||
important SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N
|
important SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N
|
||||||
");
|
");
|
||||||
// Insert sample data:
|
// Insert sample data:
|
||||||
$database->execute("INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), ?, ?)",
|
$database->execute(
|
||||||
array("Installed the blotter extension!", "Y"));
|
"INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), ?, ?)",
|
||||||
|
["Installed the blotter extension!", "Y"]
|
||||||
|
);
|
||||||
log_info("blotter", "Installed tables for blotter extension.");
|
log_info("blotter", "Installed tables for blotter extension.");
|
||||||
$config->set_int("blotter_version", 1);
|
$config->set_int("blotter_version", 1);
|
||||||
}
|
}
|
||||||
@ -43,30 +39,44 @@ class Blotter extends Extension {
|
|||||||
$config->set_default_string("blotter_position", "subheading");
|
$config->set_default_string("blotter_position", "subheading");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onSetupBuilding(SetupBuildingEvent $event) {
|
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||||
|
{
|
||||||
$sb = new SetupBlock("Blotter");
|
$sb = new SetupBlock("Blotter");
|
||||||
$sb->add_int_option("blotter_recent", "<br />Number of recent entries to display: ");
|
$sb->add_int_option("blotter_recent", "<br />Number of recent entries to display: ");
|
||||||
$sb->add_text_option("blotter_color", "<br />Color of important updates: (ABCDEF format) ");
|
$sb->add_text_option("blotter_color", "<br />Color of important updates: (ABCDEF format) ");
|
||||||
$sb->add_choice_option("blotter_position", array("Top of page" => "subheading", "In navigation bar" => "left"), "<br>Position: ");
|
$sb->add_choice_option("blotter_position", ["Top of page" => "subheading", "In navigation bar" => "left"], "<br>Position: ");
|
||||||
$event->panel->add_block($sb);
|
$event->panel->add_block($sb);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event) {
|
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||||
|
{
|
||||||
global $user;
|
global $user;
|
||||||
if($user->is_admin()) {
|
if ($event->parent==="system") {
|
||||||
|
if ($user->can(Permissions::BLOTTER_ADMIN)) {
|
||||||
|
$event->add_nav_link("blotter", new Link('blotter/editor'), "Blotter Editor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
|
||||||
|
{
|
||||||
|
global $user;
|
||||||
|
if ($user->can(Permissions::BLOTTER_ADMIN)) {
|
||||||
$event->add_link("Blotter Editor", make_link("blotter/editor"));
|
$event->add_link("Blotter Editor", make_link("blotter/editor"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onPageRequest(PageRequestEvent $event) {
|
public function onPageRequest(PageRequestEvent $event)
|
||||||
|
{
|
||||||
global $page, $database, $user;
|
global $page, $database, $user;
|
||||||
if($event->page_matches("blotter")) {
|
if ($event->page_matches("blotter")) {
|
||||||
switch($event->get_arg(0)) {
|
switch ($event->get_arg(0)) {
|
||||||
case "editor":
|
case "editor":
|
||||||
/**
|
/**
|
||||||
* Displays the blotter editor.
|
* Displays the blotter editor.
|
||||||
*/
|
*/
|
||||||
if(!$user->is_admin()) {
|
if (!$user->can(Permissions::BLOTTER_ADMIN)) {
|
||||||
$this->theme->display_permission_denied();
|
$this->theme->display_permission_denied();
|
||||||
} else {
|
} else {
|
||||||
$entries = $database->get_all("SELECT * FROM blotter ORDER BY id DESC");
|
$entries = $database->get_all("SELECT * FROM blotter ORDER BY id DESC");
|
||||||
@ -77,17 +87,25 @@ class Blotter extends Extension {
|
|||||||
/**
|
/**
|
||||||
* Adds an entry
|
* Adds an entry
|
||||||
*/
|
*/
|
||||||
if(!$user->is_admin() || !$user->check_auth_token()) {
|
if (!$user->can(Permissions::BLOTTER_ADMIN) || !$user->check_auth_token()) {
|
||||||
$this->theme->display_permission_denied();
|
$this->theme->display_permission_denied();
|
||||||
} else {
|
} else {
|
||||||
$entry_text = $_POST['entry_text'];
|
$entry_text = $_POST['entry_text'];
|
||||||
if($entry_text == "") { die("No entry message!"); }
|
if ($entry_text == "") {
|
||||||
if(isset($_POST['important'])) { $important = 'Y'; } else { $important = 'N'; }
|
die("No entry message!");
|
||||||
|
}
|
||||||
|
if (isset($_POST['important'])) {
|
||||||
|
$important = 'Y';
|
||||||
|
} else {
|
||||||
|
$important = 'N';
|
||||||
|
}
|
||||||
// Now insert into db:
|
// Now insert into db:
|
||||||
$database->execute("INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), ?, ?)",
|
$database->execute(
|
||||||
array($entry_text, $important));
|
"INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), ?, ?)",
|
||||||
|
[$entry_text, $important]
|
||||||
|
);
|
||||||
log_info("blotter", "Added Message: $entry_text");
|
log_info("blotter", "Added Message: $entry_text");
|
||||||
$page->set_mode("redirect");
|
$page->set_mode(PageMode::REDIRECT);
|
||||||
$page->set_redirect(make_link("blotter/editor"));
|
$page->set_redirect(make_link("blotter/editor"));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -95,14 +113,16 @@ class Blotter extends Extension {
|
|||||||
/**
|
/**
|
||||||
* Removes an entry
|
* Removes an entry
|
||||||
*/
|
*/
|
||||||
if(!$user->is_admin() || !$user->check_auth_token()) {
|
if (!$user->can(Permissions::BLOTTER_ADMIN) || !$user->check_auth_token()) {
|
||||||
$this->theme->display_permission_denied();
|
$this->theme->display_permission_denied();
|
||||||
} else {
|
} else {
|
||||||
$id = int_escape($_POST['id']);
|
$id = int_escape($_POST['id']);
|
||||||
if(!isset($id)) { die("No ID!"); }
|
if (!isset($id)) {
|
||||||
$database->Execute("DELETE FROM blotter WHERE id=:id", array("id"=>$id));
|
die("No ID!");
|
||||||
|
}
|
||||||
|
$database->Execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]);
|
||||||
log_info("blotter", "Removed Entry #$id");
|
log_info("blotter", "Removed Entry #$id");
|
||||||
$page->set_mode("redirect");
|
$page->set_mode(PageMode::REDIRECT);
|
||||||
$page->set_redirect(make_link("blotter/editor"));
|
$page->set_redirect(make_link("blotter/editor"));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -121,7 +141,8 @@ class Blotter extends Extension {
|
|||||||
$this->display_blotter();
|
$this->display_blotter();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function display_blotter() {
|
private function display_blotter()
|
||||||
|
{
|
||||||
global $database, $config;
|
global $database, $config;
|
||||||
$limit = $config->get_int("blotter_recent", 5);
|
$limit = $config->get_int("blotter_recent", 5);
|
||||||
$sql = 'SELECT * FROM blotter ORDER BY id DESC LIMIT '.intval($limit);
|
$sql = 'SELECT * FROM blotter ORDER BY id DESC LIMIT '.intval($limit);
|
||||||
@ -129,4 +150,3 @@ class Blotter extends Extension {
|
|||||||
$this->theme->display_blotter($entries);
|
$this->theme->display_blotter($entries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
class BlotterTest extends ShimmiePHPUnitTestCase {
|
class BlotterTest extends ShimmiePHPUnitTestCase
|
||||||
public function testLogin() {
|
{
|
||||||
|
public function testLogin()
|
||||||
|
{
|
||||||
$this->log_in_as_admin();
|
$this->log_in_as_admin();
|
||||||
//$this->assert_text("Blotter Editor");
|
//$this->assert_text("Blotter Editor");
|
||||||
//$this->click("Blotter Editor");
|
//$this->click("Blotter Editor");
|
||||||
//$this->log_out();
|
//$this->log_out();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDenial() {
|
public function testDenial()
|
||||||
|
{
|
||||||
$this->get_page("blotter/editor");
|
$this->get_page("blotter/editor");
|
||||||
$this->assert_response(403);
|
$this->assert_response(403);
|
||||||
$this->get_page("blotter/add");
|
$this->get_page("blotter/add");
|
||||||
@ -16,7 +19,8 @@ class BlotterTest extends ShimmiePHPUnitTestCase {
|
|||||||
$this->assert_response(403);
|
$this->assert_response(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAddViewRemove() {
|
public function testAddViewRemove()
|
||||||
|
{
|
||||||
$this->log_in_as_admin();
|
$this->log_in_as_admin();
|
||||||
|
|
||||||
$this->get_page("blotter/editor");
|
$this->get_page("blotter/editor");
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
class BlotterTheme extends Themelet {
|
class BlotterTheme extends Themelet
|
||||||
public function display_editor($entries) {
|
{
|
||||||
|
public function display_editor($entries)
|
||||||
|
{
|
||||||
global $page;
|
global $page;
|
||||||
$html = $this->get_html_for_blotter_editor($entries);
|
$html = $this->get_html_for_blotter_editor($entries);
|
||||||
$page->set_title("Blotter Editor");
|
$page->set_title("Blotter Editor");
|
||||||
@ -9,7 +11,8 @@ class BlotterTheme extends Themelet {
|
|||||||
$page->add_block(new Block("Navigation", "<a href='".make_link()."'>Index</a>", "left", 0));
|
$page->add_block(new Block("Navigation", "<a href='".make_link()."'>Index</a>", "left", 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function display_blotter_page($entries) {
|
public function display_blotter_page($entries)
|
||||||
|
{
|
||||||
global $page;
|
global $page;
|
||||||
$html = $this->get_html_for_blotter_page($entries);
|
$html = $this->get_html_for_blotter_page($entries);
|
||||||
$page->set_title("Blotter");
|
$page->set_title("Blotter");
|
||||||
@ -17,14 +20,16 @@ class BlotterTheme extends Themelet {
|
|||||||
$page->add_block(new Block("Blotter Entries", $html, "main", 10));
|
$page->add_block(new Block("Blotter Entries", $html, "main", 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function display_blotter($entries) {
|
public function display_blotter($entries)
|
||||||
|
{
|
||||||
global $page, $config;
|
global $page, $config;
|
||||||
$html = $this->get_html_for_blotter($entries);
|
$html = $this->get_html_for_blotter($entries);
|
||||||
$position = $config->get_string("blotter_position", "subheading");
|
$position = $config->get_string("blotter_position", "subheading");
|
||||||
$page->add_block(new Block(null, $html, $position, 20));
|
$page->add_block(new Block(null, $html, $position, 20));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function get_html_for_blotter_editor($entries) {
|
private function get_html_for_blotter_editor($entries)
|
||||||
|
{
|
||||||
global $user;
|
global $user;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,7 +64,11 @@ class BlotterTheme extends Themelet {
|
|||||||
$id = $entries[$i]['id'];
|
$id = $entries[$i]['id'];
|
||||||
$entry_date = $entries[$i]['entry_date'];
|
$entry_date = $entries[$i]['entry_date'];
|
||||||
$entry_text = $entries[$i]['entry_text'];
|
$entry_text = $entries[$i]['entry_text'];
|
||||||
if($entries[$i]['important'] == 'Y') { $important = 'Y'; } else { $important = 'N'; }
|
if ($entries[$i]['important'] == 'Y') {
|
||||||
|
$important = 'Y';
|
||||||
|
} else {
|
||||||
|
$important = 'N';
|
||||||
|
}
|
||||||
|
|
||||||
// Add the new table row(s)
|
// Add the new table row(s)
|
||||||
$table_rows .=
|
$table_rows .=
|
||||||
@ -90,7 +99,8 @@ class BlotterTheme extends Themelet {
|
|||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function get_html_for_blotter_page($entries) {
|
private function get_html_for_blotter_page($entries)
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* This one displays a list of all blotter entries.
|
* This one displays a list of all blotter entries.
|
||||||
*/
|
*/
|
||||||
@ -110,7 +120,7 @@ class BlotterTheme extends Themelet {
|
|||||||
$messy_date = $entries[$i]['entry_date'];
|
$messy_date = $entries[$i]['entry_date'];
|
||||||
$clean_date = date("y/m/d", strtotime($messy_date));
|
$clean_date = date("y/m/d", strtotime($messy_date));
|
||||||
$entry_text = $entries[$i]['entry_text'];
|
$entry_text = $entries[$i]['entry_text'];
|
||||||
if($entries[$i]['important'] == 'Y') {
|
if ($entries[$i]['important'] == 'Y') {
|
||||||
$i_open = "<font color='#{$i_color}'>";
|
$i_open = "<font color='#{$i_color}'>";
|
||||||
$i_close="</font>";
|
$i_close="</font>";
|
||||||
}
|
}
|
||||||
@ -120,7 +130,8 @@ class BlotterTheme extends Themelet {
|
|||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function get_html_for_blotter($entries) {
|
private function get_html_for_blotter($entries)
|
||||||
|
{
|
||||||
global $config;
|
global $config;
|
||||||
$i_color = $config->get_string("blotter_color", "#FF0000");
|
$i_color = $config->get_string("blotter_color", "#FF0000");
|
||||||
$position = $config->get_string("blotter_position", "subheading");
|
$position = $config->get_string("blotter_position", "subheading");
|
||||||
@ -137,7 +148,7 @@ class BlotterTheme extends Themelet {
|
|||||||
$messy_date = $entries[$i]['entry_date'];
|
$messy_date = $entries[$i]['entry_date'];
|
||||||
$clean_date = date("m/d/y", strtotime($messy_date));
|
$clean_date = date("m/d/y", strtotime($messy_date));
|
||||||
$entry_text = $entries[$i]['entry_text'];
|
$entry_text = $entries[$i]['entry_text'];
|
||||||
if($entries[$i]['important'] == 'Y') {
|
if ($entries[$i]['important'] == 'Y') {
|
||||||
$i_open = "<font color='#{$i_color}'>";
|
$i_open = "<font color='#{$i_color}'>";
|
||||||
$i_close="</font>";
|
$i_close="</font>";
|
||||||
}
|
}
|
||||||
@ -147,16 +158,15 @@ class BlotterTheme extends Themelet {
|
|||||||
$pos_break = "";
|
$pos_break = "";
|
||||||
$pos_align = "text-align: right; position: absolute; right: 0px;";
|
$pos_align = "text-align: right; position: absolute; right: 0px;";
|
||||||
|
|
||||||
if($position === "left") {
|
if ($position === "left") {
|
||||||
$pos_break = "<br />";
|
$pos_break = "<br />";
|
||||||
$pos_align = "";
|
$pos_align = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if(count($entries) === 0) {
|
if (count($entries) === 0) {
|
||||||
$out_text = "No blotter entries yet.";
|
$out_text = "No blotter entries yet.";
|
||||||
$in_text = "Empty.";
|
$in_text = "Empty.";
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$clean_date = date("m/d/y", strtotime($entries[0]['entry_date']));
|
$clean_date = date("m/d/y", strtotime($entries[0]['entry_date']));
|
||||||
$out_text = "Blotter updated: {$clean_date}";
|
$out_text = "Blotter updated: {$clean_date}";
|
||||||
$in_text = "<ul>$entries_list</ul>";
|
$in_text = "<ul>$entries_list</ul>";
|
||||||
|
|||||||
30
ext/browser_search/info.php
Normal file
30
ext/browser_search/info.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Name: Browser Search
|
||||||
|
* Author: ATravelingGeek <atg@atravelinggeek.com>
|
||||||
|
* Some code (and lots of help) by Artanis (Erik Youngren <artanis.00@gmail.com>) 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 <artanis.00@gmail.com>) from the 'tagger' extension - Used with permission";
|
||||||
|
}
|
||||||
@ -1,40 +1,30 @@
|
|||||||
<?php
|
<?php
|
||||||
/*
|
|
||||||
* Name: Browser Search
|
|
||||||
* Author: ATravelingGeek <atg@atravelinggeek.com>
|
|
||||||
* Some code (and lots of help) by Artanis (Erik Youngren <artanis.00@gmail.com>) 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 {
|
class BrowserSearch extends Extension
|
||||||
public function onInitExt(InitExtEvent $event) {
|
{
|
||||||
|
public function onInitExt(InitExtEvent $event)
|
||||||
|
{
|
||||||
global $config;
|
global $config;
|
||||||
$config->set_default_string("search_suggestions_results_order", 'a');
|
$config->set_default_string("search_suggestions_results_order", 'a');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onPageRequest(PageRequestEvent $event) {
|
public function onPageRequest(PageRequestEvent $event)
|
||||||
|
{
|
||||||
global $config, $database, $page;
|
global $config, $database, $page;
|
||||||
|
|
||||||
// Add in header code to let the browser know that the search plugin exists
|
// Add in header code to let the browser know that the search plugin exists
|
||||||
// We need to build the data for the header
|
// We need to build the data for the header
|
||||||
$search_title = $config->get_string('title');
|
$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');
|
$search_file_url = make_link('browser_search/please_dont_use_this_tag_as_it_would_break_stuff__search.xml');
|
||||||
$page->add_html_header("<link rel='search' type='application/opensearchdescription+xml' title='$search_title' href='$search_file_url'>");
|
$page->add_html_header("<link rel='search' type='application/opensearchdescription+xml' title='$search_title' href='$search_file_url'>");
|
||||||
|
|
||||||
// The search.xml file that is generated on the fly
|
// 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")) {
|
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
|
// First, we need to build all the variables we'll need
|
||||||
$search_title = $config->get_string('title');
|
$search_title = $config->get_string(SetupConfig::TITLE);
|
||||||
$search_form_url = make_link('post/list/{searchTerms}');
|
$search_form_url = make_link('post/list/{searchTerms}');
|
||||||
$suggenton_url = make_link('browser_search/')."{searchTerms}";
|
$suggenton_url = make_link('browser_search/')."{searchTerms}";
|
||||||
$icon_b64 = base64_encode(file_get_contents("lib/static/favicon.ico"));
|
$icon_b64 = base64_encode(file_get_contents("ext/handle_static/static/favicon.ico"));
|
||||||
|
|
||||||
// Now for the XML
|
// Now for the XML
|
||||||
$xml = "
|
$xml = "
|
||||||
@ -51,12 +41,10 @@ class BrowserSearch extends Extension {
|
|||||||
";
|
";
|
||||||
|
|
||||||
// And now to send it to the browser
|
// And now to send it to the browser
|
||||||
$page->set_mode("data");
|
$page->set_mode(PageMode::DATA);
|
||||||
$page->set_type("text/xml");
|
$page->set_type("text/xml");
|
||||||
$page->set_data($xml);
|
$page->set_data($xml);
|
||||||
}
|
} elseif (
|
||||||
|
|
||||||
else if(
|
|
||||||
$event->page_matches("browser_search") &&
|
$event->page_matches("browser_search") &&
|
||||||
!$config->get_bool("disable_search_suggestions")
|
!$config->get_bool("disable_search_suggestions")
|
||||||
) {
|
) {
|
||||||
@ -64,10 +52,10 @@ class BrowserSearch extends Extension {
|
|||||||
$tag_search = $event->get_arg(0);
|
$tag_search = $event->get_arg(0);
|
||||||
|
|
||||||
// Now to get DB results
|
// Now to get DB results
|
||||||
if($config->get_string("search_suggestions_results_order") == "a") {
|
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."%"));
|
$tags = $database->execute("SELECT tag FROM tags WHERE tag LIKE ? AND count > 0 ORDER BY tag ASC LIMIT 30", [$tag_search."%"]);
|
||||||
} else {
|
} else {
|
||||||
$tags = $database->execute("SELECT tag FROM tags WHERE tag LIKE ? AND count > 0 ORDER BY count DESC LIMIT 30",array($tag_search."%"));
|
$tags = $database->execute("SELECT tag FROM tags WHERE tag LIKE ? AND count > 0 ORDER BY count DESC LIMIT 30", [$tag_search."%"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -75,22 +63,23 @@ class BrowserSearch extends Extension {
|
|||||||
// ["shimmie",["shimmies","shimmy","shimmie","21 shimmies","hip shimmies","skea shimmies"],[],[]]
|
// ["shimmie",["shimmies","shimmy","shimmie","21 shimmies","hip shimmies","skea shimmies"],[],[]]
|
||||||
$json_tag_list = "";
|
$json_tag_list = "";
|
||||||
|
|
||||||
$tags_array = array();
|
$tags_array = [];
|
||||||
foreach($tags as $tag) {
|
foreach ($tags as $tag) {
|
||||||
array_push($tags_array,$tag['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
|
// And now for the final output
|
||||||
$json_string = "[\"$tag_search\",[\"$json_tag_list\"],[],[]]";
|
$json_string = "[\"$tag_search\",[\"$json_tag_list\"],[],[]]";
|
||||||
$page->set_mode("data");
|
$page->set_mode(PageMode::DATA);
|
||||||
$page->set_data($json_string);
|
$page->set_data($json_string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onSetupBuilding(SetupBuildingEvent $event) {
|
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||||
$sort_by = array();
|
{
|
||||||
|
$sort_by = [];
|
||||||
$sort_by['Alphabetical'] = 'a';
|
$sort_by['Alphabetical'] = 'a';
|
||||||
$sort_by['Tag Count'] = 't';
|
$sort_by['Tag Count'] = 't';
|
||||||
|
|
||||||
@ -101,4 +90,3 @@ class BrowserSearch extends Extension {
|
|||||||
$event->panel->add_block($sb);
|
$event->panel->add_block($sb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
class BrowserSearchTest extends ShimmiePHPUnitTestCase {
|
class BrowserSearchTest extends ShimmiePHPUnitTestCase
|
||||||
public function testBasic() {
|
{
|
||||||
|
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/please_dont_use_this_tag_as_it_would_break_stuff__search.xml");
|
||||||
$this->get_page("browser_search/test");
|
$this->get_page("browser_search/test");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
ext/bulk_actions/info.php
Normal file
23
ext/bulk_actions/info.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Name: Bulk Actions
|
||||||
|
* Author: Matthew Barbour
|
||||||
|
* License: WTFPL
|
||||||
|
* Description: Provides query and selection-based bulk action support
|
||||||
|
* Documentation: Provides bulk action section in list view. Allows performing actions against a set of images based on query or manual selection.
|
||||||
|
* Based on Mass Tagger by Christian Walde <walde.christian@googlemail.com>, contributions by Shish and Agasa.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
class BulkActionsInfo extends ExtensionInfo
|
||||||
|
{
|
||||||
|
public const KEY = "bulk_actions";
|
||||||
|
|
||||||
|
public $key = self::KEY;
|
||||||
|
public $name = "Bulk Actions";
|
||||||
|
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||||
|
public $license = self::LICENSE_WTFPL;
|
||||||
|
public $description = "Provides query and selection-based bulk action support";
|
||||||
|
public $documentation = "Provides bulk action section in list view. Allows performing actions against a set of images based on query or manual selection. Based on Mass Tagger by Christian Walde <walde.christian@googlemail.com>, contributions by Shish and Agasa.";
|
||||||
|
}
|
||||||
271
ext/bulk_actions/main.php
Normal file
271
ext/bulk_actions/main.php
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class BulkActionBlockBuildingEvent extends Event
|
||||||
|
{
|
||||||
|
/** @var array */
|
||||||
|
public $actions = [];
|
||||||
|
|
||||||
|
public $search_terms = [];
|
||||||
|
|
||||||
|
public function add_action(String $action, string $button_text, string $access_key = null, String $confirmation_message = "", String $block = "", int $position = 40)
|
||||||
|
{
|
||||||
|
if ($block == null) {
|
||||||
|
$block = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($access_key)) {
|
||||||
|
assert(strlen($access_key)==1);
|
||||||
|
foreach ($this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
197
ext/bulk_actions/script.js
Normal file
197
ext/bulk_actions/script.js
Normal file
@ -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<id) {
|
||||||
|
select_range(id, last_clicked_item);
|
||||||
|
} else {
|
||||||
|
select_range(last_clicked_item, id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
last_clicked_item = id;
|
||||||
|
toggle_selection(id);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$block.find("A").click(c);
|
||||||
|
$block.click(c); // sometimes the thumbs *is* the A
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
// Clear the selection, in case it was autocompleted by the browser.
|
||||||
|
$('#bulk_selected_ids').val("");
|
||||||
|
});
|
||||||
10
ext/bulk_actions/style.css
Normal file
10
ext/bulk_actions/style.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.selected {
|
||||||
|
outline: 3px solid blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk_action {
|
||||||
|
margin-top: 8pt;
|
||||||
|
}
|
||||||
|
.bulk_selector_controls table td {
|
||||||
|
width: 33%;
|
||||||
|
}
|
||||||
69
ext/bulk_actions/theme.php
Normal file
69
ext/bulk_actions/theme.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class BulkActionsTheme extends Themelet
|
||||||
|
{
|
||||||
|
public function display_selector(Page $page, array $actions, string $query)
|
||||||
|
{
|
||||||
|
global $user;
|
||||||
|
|
||||||
|
$body = "<input type='hidden' name='bulk_selected_ids' id='bulk_selected_ids' />
|
||||||
|
<input id='bulk_selector_activate' type='button' onclick='activate_bulk_selector();' value='Activate (M)anual Select' accesskey='m'/>
|
||||||
|
<div id='bulk_selector_controls' style='display: none;'>
|
||||||
|
<input id='bulk_selector_deactivate' type='button' onclick='deactivate_bulk_selector();' value='Deactivate (M)anual Select' accesskey='m'/>
|
||||||
|
Click on images to mark them.
|
||||||
|
<br />
|
||||||
|
<table><tr><td>
|
||||||
|
<input id='bulk_selector_select_all' type='button'
|
||||||
|
onclick='select_all();' value='All'/>
|
||||||
|
</td><td>
|
||||||
|
<input id='bulk_selector_select_invert' type='button'
|
||||||
|
onclick='select_invert();' value='Invert'/>
|
||||||
|
</td><td>
|
||||||
|
<input id='bulk)selector_select_none' type='button'
|
||||||
|
onclick='select_none();' value='Clear'/>
|
||||||
|
</td></tr></table>
|
||||||
|
";
|
||||||
|
|
||||||
|
$hasQuery = ($query != null && $query != "");
|
||||||
|
|
||||||
|
if ($hasQuery) {
|
||||||
|
$body .= "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($actions as $action) {
|
||||||
|
$body .= "<div class='bulk_action'>" . make_form(make_link("bulk_action"), "POST", false, "", "return validate_selections(this,'" . html_escape($action["confirmation_message"]) . "');") .
|
||||||
|
"<input type='hidden' name='bulk_query' value='" . html_escape($query) . "'>" .
|
||||||
|
"<input type='hidden' name='bulk_selected_ids' />" .
|
||||||
|
"<input type='hidden' name='bulk_action' value='" . $action["action"] . "' />" .
|
||||||
|
$action["block"] .
|
||||||
|
"<input type='submit' name='submit_button' accesskey='{$action["access_key"]}' value='" . $action["button_text"] . "'/>" .
|
||||||
|
"</form></div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$hasQuery) {
|
||||||
|
$body .= "</div>";
|
||||||
|
}
|
||||||
|
$block = new Block("Bulk Actions", $body, "left", 30);
|
||||||
|
$page->add_block($block);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render_ban_reason_input()
|
||||||
|
{
|
||||||
|
if (class_exists("ImageBan")) {
|
||||||
|
return "<input type='text' name='bulk_ban_reason' placeholder='Ban reason (leave blank to not ban)' />";
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render_tag_input()
|
||||||
|
{
|
||||||
|
return "<label><input type='checkbox' style='width:13px;' name='bulk_tags_replace' value='true'/>Replace tags</label>" .
|
||||||
|
"<input type='text' name='bulk_tags' required='required' placeholder='Enter tags here' />";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render_source_input()
|
||||||
|
{
|
||||||
|
return "<input type='text' name='bulk_source' required='required' placeholder='Enter source here' />";
|
||||||
|
}
|
||||||
|
}
|
||||||
31
ext/bulk_add/info.php
Normal file
31
ext/bulk_add/info.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Name: Bulk Add
|
||||||
|
* Author: Shish <webmaster@shishnet.org>
|
||||||
|
* 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 <code>/home/bob/uploads/holiday/2008/</code> and point
|
||||||
|
shimmie at <code>/home/bob/uploads</code>, then images will be
|
||||||
|
tagged \"holiday 2008\")
|
||||||
|
<p><b>Note:</b> requires the \"admin\" extension to be enabled
|
||||||
|
";
|
||||||
|
}
|
||||||
@ -1,56 +1,43 @@
|
|||||||
<?php
|
<?php
|
||||||
/*
|
|
||||||
* Name: Bulk Add
|
|
||||||
* Author: Shish <webmaster@shishnet.org>
|
|
||||||
* 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 <code>/home/bob/uploads/holiday/2008/</code> and point
|
|
||||||
* shimmie at <code>/home/bob/uploads</code>, then images will be
|
|
||||||
* tagged "holiday 2008")
|
|
||||||
* <p><b>Note:</b> requires the "admin" extension to be enabled
|
|
||||||
*/
|
|
||||||
|
|
||||||
class BulkAddEvent extends Event {
|
class BulkAddEvent extends Event
|
||||||
public $dir, $results;
|
{
|
||||||
|
public $dir;
|
||||||
|
public $results;
|
||||||
|
|
||||||
public function __construct($dir) {
|
public function __construct(string $dir)
|
||||||
|
{
|
||||||
$this->dir = $dir;
|
$this->dir = $dir;
|
||||||
$this->results = array();
|
$this->results = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BulkAdd extends Extension {
|
class BulkAdd extends Extension
|
||||||
public function onPageRequest(PageRequestEvent $event) {
|
{
|
||||||
|
public function onPageRequest(PageRequestEvent $event)
|
||||||
|
{
|
||||||
global $page, $user;
|
global $page, $user;
|
||||||
if($event->page_matches("bulk_add")) {
|
if ($event->page_matches("bulk_add")) {
|
||||||
if($user->is_admin() && $user->check_auth_token() && isset($_POST['dir'])) {
|
if ($user->can(Permissions::BULK_ADD) && $user->check_auth_token() && isset($_POST['dir'])) {
|
||||||
set_time_limit(0);
|
set_time_limit(0);
|
||||||
$bae = new BulkAddEvent($_POST['dir']);
|
$bae = new BulkAddEvent($_POST['dir']);
|
||||||
send_event($bae);
|
send_event($bae);
|
||||||
if(is_array($bae->results)) {
|
foreach ($bae->results as $result) {
|
||||||
foreach($bae->results as $result) {
|
|
||||||
$this->theme->add_status("Adding files", $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);
|
$this->theme->display_upload_results($page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onCommand(CommandEvent $event) {
|
public function onCommand(CommandEvent $event)
|
||||||
if($event->cmd == "help") {
|
{
|
||||||
|
if ($event->cmd == "help") {
|
||||||
print "\tbulk-add [directory]\n";
|
print "\tbulk-add [directory]\n";
|
||||||
print "\t\tImport this directory\n\n";
|
print "\t\tImport this directory\n\n";
|
||||||
}
|
}
|
||||||
if($event->cmd == "bulk-add") {
|
if ($event->cmd == "bulk-add") {
|
||||||
if(count($event->args) == 1) {
|
if (count($event->args) == 1) {
|
||||||
$bae = new BulkAddEvent($event->args[0]);
|
$bae = new BulkAddEvent($event->args[0]);
|
||||||
send_event($bae);
|
send_event($bae);
|
||||||
print(implode("\n", $bae->results));
|
print(implode("\n", $bae->results));
|
||||||
@ -58,15 +45,16 @@ class BulkAdd extends Extension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onAdminBuilding(AdminBuildingEvent $event) {
|
public function onAdminBuilding(AdminBuildingEvent $event)
|
||||||
|
{
|
||||||
$this->theme->display_admin_block();
|
$this->theme->display_admin_block();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onBulkAdd(BulkAddEvent $event) {
|
public function onBulkAdd(BulkAddEvent $event)
|
||||||
if(is_dir($event->dir) && is_readable($event->dir)) {
|
{
|
||||||
|
if (is_dir($event->dir) && is_readable($event->dir)) {
|
||||||
$event->results = add_dir($event->dir);
|
$event->results = add_dir($event->dir);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$h_dir = html_escape($event->dir);
|
$h_dir = html_escape($event->dir);
|
||||||
$event->results[] = "Error, $h_dir is not a readable directory";
|
$event->results[] = "Error, $h_dir is not a readable directory";
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user