This commit is contained in:
Shish 2019-10-01 10:44:52 +01:00
commit 794e4ebb7d
525 changed files with 38110 additions and 36923 deletions

8
.dockerignore Normal file
View File

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

21
.editorconfig Normal file
View 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
View File

@ -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

View File

@ -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
View 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)
;
?>

View File

@ -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

View File

@ -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
View 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"

View File

@ -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

View File

@ -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",
@ -36,34 +39,24 @@
"bower-asset/tablesorter" : "dev-master", "bower-asset/tablesorter" : "dev-master",
"bower-asset/mediaelement" : "2.21.1", "bower-asset/mediaelement" : "2.21.1",
"bower-asset/js-cookie" : "2.1.1" "bower-asset/js-cookie" : "2.1.1"
}, },
"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

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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
View 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&nbsp;Name:</th>
<td><input type="text" name="database_name" size="40" value="shimmie"></td>
</tr>
<tr><td colspan="2"><input type="submit" value="Go!"></td></tr>
</table>
</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 "&lt;?php" or after the "?&gt;"
<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>

View File

@ -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>&lt;&lt; '.$pages_html.' &gt;&gt;';
}
}

139
core/basethemelet.php Normal file
View 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>&lt;&lt; '.$pages_html.' &gt;&gt;';
}
}

View File

@ -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
View 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
View 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
View 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;
}

View File

@ -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
View 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);
}
}

View File

@ -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
View 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
View 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";
}
}

View File

@ -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
View 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;
}
}

View File

@ -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
View 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();
}
}

View File

@ -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
View 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;
}
}

View File

@ -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
View 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
View 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

File diff suppressed because it is too large Load Diff

219
core/imageboard/misc.php Normal file
View 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);
}

View 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
View 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
View 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;
}

View File

@ -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
View 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
View 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
View 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('&#039;', '&apos;', 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
View 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();
}
}

View File

@ -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);

View 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 &amp; &lt;waffles&gt;"
);
$this->assertEquals(
html_unescape("Foo &amp; &lt;waffles&gt;"),
"Foo & <waffles>"
);
$x = "Foo &amp; &lt;waffles&gt;";
$this->assertEquals(html_escape(html_unescape($x)), $x);
}
public function test_int_escape()
{
$this->assertEquals(int_escape(""), 0);
$this->assertEquals(int_escape("1"), 1);
$this->assertEquals(int_escape("-1"), -1);
$this->assertEquals(int_escape("-1.5"), -1);
}
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
View 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
View 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;
}

View File

@ -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
View 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());
}
}

View File

@ -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
View 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";

File diff suppressed because it is too large Load Diff

618
core/util.php Normal file
View 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
View 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)";
}

View File

@ -1,271 +1,281 @@
<?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 */ {
public $page; /** @var Page */
public $page;
/** public function __construct(Page $page)
* @param Page $page {
*/ $this->page = $page;
public function __construct(Page $page) { }
$this->page = $page;
}
} }
class AdminActionEvent extends Event { class AdminActionEvent extends Event
/** @var string */ {
public $action; /** @var string */
/** @var bool */ public $action;
public $redirect = true; /** @var bool */
public $redirect = true;
/** public function __construct(string $action)
* @param string $action {
*/ $this->action = $action;
public function __construct(/*string*/ $action) { }
$this->action = $action;
}
} }
class AdminPage extends Extension { class AdminPage extends Extension
public function onPageRequest(PageRequestEvent $event) { {
global $page, $user; public function onPageRequest(PageRequestEvent $event)
{
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 {
} $action = $event->get_arg(0);
else { $aae = new AdminActionEvent($action);
$action = $event->get_arg(0);
$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"));
} }
} }
} }
} }
} }
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";
if($event->cmd == "get-page") { print "\tregen-thumb [hash]\n";
global $page; print "\t\tregenerate a thumbnail\n\n";
send_event(new PageRequestEvent($event->args[0])); }
$page->display(); if ($event->cmd == "get-page") {
} global $page;
} send_event(new PageRequestEvent($event->args[0]));
$page->display();
}
if ($event->cmd == "regen-thumb") {
$image = Image::by_hash($event->args[0]);
if ($image) {
print("Regenerating thumb for image {$image->id} ({$image->hash})\n");
send_event(new ThumbnailGenerationEvent($image->hash, $image->ext, true));
} else {
print("Can't find image with hash {$event->args[0]}\n");
}
}
}
public function onAdminBuilding(AdminBuildingEvent $event) { public function onAdminBuilding(AdminBuildingEvent $event)
$this->theme->display_page(); {
$this->theme->display_form(); $this->theme->display_page();
} $this->theme->display_form();
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event) { public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
global $user; {
if($user->can("manage_admintools")) { global $user;
$event->add_link("Board Admin", make_link("admin")); if ($event->parent==="system") {
} if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
} $event->add_nav_link("admin", new Link('admin'), "Board Admin");
}
}
}
public function onAdminAction(AdminActionEvent $event) { public function onUserBlockBuilding(UserBlockBuildingEvent $event)
$action = $event->action; {
if(method_exists($this, $action)) { global $user;
$event->redirect = $this->$action(); if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
} $event->add_link("Board Admin", make_link("admin"));
} }
}
public function onPostListBuilding(PostListBuildingEvent $event) { public function onAdminAction(AdminActionEvent $event)
global $user; {
if($user->can("manage_admintools") && !empty($event->search_terms)) { $action = $event->action;
$event->add_control($this->theme->dbq_html(implode(" ", $event->search_terms))); if (method_exists($this, $action)) {
} $event->redirect = $this->$action();
} }
}
private function delete_by_query() { // public function onPostListBuilding(PostListBuildingEvent $event)
global $page; // {
$query = $_POST['query']; // global $user;
$reason = @$_POST['reason']; // if ($user->can("manage_admintools") && !empty($event->search_terms)) {
assert(strlen($query) > 1); // $event->add_control($this->theme->dbq_html(Tag::implode($event->search_terms)));
// }
// }
log_warning("admin", "Mass deleting: $query"); private function delete_by_query()
$count = 0; {
foreach(Image::find_images(0, 1000000, Tag::explode($query)) as $image) { global $page;
if($reason && class_exists("ImageBan")) { $query = $_POST['query'];
send_event(new AddImageHashBanEvent($image->hash, $reason)); $reason = @$_POST['reason'];
}
send_event(new ImageDeletionEvent($image));
$count++;
}
log_debug("admin", "Deleted $count images", true);
$page->set_mode("redirect"); assert(strlen($query) > 1);
$page->set_redirect(make_link("post/list"));
return false;
}
private function set_tag_case() { $images = Image::find_images(0, 1000000, Tag::explode($query));
global $database; $count = count($images);
$database->execute($database->scoreql_to_sql( log_warning("admin", "Mass-deleting $count images from $query", "Mass deleted $count images");
"UPDATE tags SET tag=:tag1 WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag2)" foreach ($images as $image) {
), array("tag1" => $_POST['tag'], "tag2" => $_POST['tag'])); if ($reason && class_exists("ImageBan")) {
log_info("admin", "Fixed the case of ".html_escape($_POST['tag']), true); send_event(new AddImageHashBanEvent($image->hash, $reason));
return true; }
} send_event(new ImageDeletionEvent($image, true));
}
private function lowercase_all_tags() { $page->set_mode(PageMode::REDIRECT);
global $database; $page->set_redirect(make_link("post/list"));
$database->execute("UPDATE tags SET tag=lower(tag)"); return false;
log_warning("admin", "Set all tags to lowercase", true); }
return true;
}
private function recount_tag_use() { private function set_tag_case()
global $database; {
$database->Execute(" global $database;
$database->execute($database->scoreql_to_sql(
"UPDATE tags SET tag=:tag1 WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag2)"
), ["tag1" => $_POST['tag'], "tag2" => $_POST['tag']]);
log_info("admin", "Fixed the case of ".html_escape($_POST['tag']), "Fixed case");
return true;
}
private function lowercase_all_tags()
{
global $database;
$database->execute("UPDATE tags SET tag=lower(tag)");
log_warning("admin", "Set all tags to lowercase", "Set all tags to lowercase");
return true;
}
private function recount_tag_use()
{
global $database;
$database->Execute("
UPDATE tags UPDATE tags
SET count = COALESCE( SET count = COALESCE(
(SELECT COUNT(image_id) FROM image_tags WHERE tag_id=tags.id GROUP BY tag_id), (SELECT COUNT(image_id) FROM image_tags WHERE tag_id=tags.id GROUP BY tag_id),
0 0
) )
"); ");
$database->Execute("DELETE FROM tags WHERE count=0"); $database->Execute("DELETE FROM tags WHERE count=0");
log_warning("admin", "Re-counted tags", 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'];
$password = $matches['password']; $password = $matches['password'];
$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:
$cmd = false; $cmd = false;
} }
//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));
} }
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};";
} }
$sql .= " SET FOREIGN_KEY_CHECKS=1;"; $sql .= " SET FOREIGN_KEY_CHECKS=1;";
$database->execute($sql); $database->execute($sql);
$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;
} }
} }

View File

@ -1,84 +1,89 @@
<?php <?php
class AdminPageTest extends ShimmiePHPUnitTestCase { class AdminPageTest extends ShimmiePHPUnitTestCase
public function testAuth() { {
$this->get_page('admin'); public function testAuth()
$this->assert_response(403); {
$this->assert_title("Permission Denied"); $this->get_page('admin');
$this->assert_response(403);
$this->assert_title("Permission Denied");
$this->log_in_as_user(); $this->log_in_as_user();
$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");
$this->log_in_as_admin(); $this->log_in_as_admin();
$this->get_page('admin'); $this->get_page('admin');
$this->assert_response(200); $this->assert_response(200);
$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();
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "TeStCase$ts"); $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "TeStCase$ts");
$this->get_page("post/view/$image_id_1"); $this->get_page("post/view/$image_id_1");
$this->assert_title("Image $image_id_1: TeStCase$ts"); $this->assert_title("Image $image_id_1: TeStCase$ts");
$this->get_page('admin'); $this->get_page('admin');
$this->assert_title("Admin Tools"); $this->assert_title("Admin Tools");
//$this->click("All tags to lowercase"); //$this->click("All tags to lowercase");
send_event(new AdminActionEvent('lowercase_all_tags')); send_event(new AdminActionEvent('lowercase_all_tags'));
$this->get_page("post/view/$image_id_1"); $this->get_page("post/view/$image_id_1");
$this->assert_title("Image $image_id_1: testcase$ts"); $this->assert_title("Image $image_id_1: testcase$ts");
$this->delete_image($image_id_1); $this->delete_image($image_id_1);
} }
# 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->get_page('admin'); $this->log_in_as_admin();
$this->assert_title("Admin Tools"); $this->get_page('admin');
$this->assert_title("Admin Tools");
//$this->click("Recount tag use"); //$this->click("Recount tag use");
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->get_page('admin'); $this->log_in_as_admin();
$this->assert_title("Admin Tools"); $this->get_page('admin');
$this->assert_title("Admin Tools");
// this calls mysqldump which jams up travis prompting for a password // this calls mysqldump which jams up travis prompting for a password
//$this->click("Download database contents"); //$this->click("Download database contents");
//send_event(new AdminActionEvent('database_dump')); //send_event(new AdminActionEvent('database_dump'));
//$this->assert_response(200); //$this->assert_response(200);
} }
public function testDBQ() { public function testDBQ()
$this->log_in_as_user(); {
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test"); $this->log_in_as_user();
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "test2"); $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test");
$image_id_3 = $this->post_image("tests/favicon.png", "test"); $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "test2");
$image_id_3 = $this->post_image("tests/favicon.png", "test");
$this->get_page("post/list/test/1"); $this->get_page("post/list/test/1");
//$this->click("Delete All These Images"); //$this->click("Delete All These Images");
$_POST['query'] = 'test'; $_POST['query'] = 'test';
//$_POST['reason'] = 'reason'; // non-null-reason = add a hash ban //$_POST['reason'] = 'reason'; // non-null-reason = add a hash ban
send_event(new AdminActionEvent('delete_by_query')); send_event(new AdminActionEvent('delete_by_query'));
$this->get_page("post/view/$image_id_1"); $this->get_page("post/view/$image_id_1");
$this->assert_response(404); $this->assert_response(404);
$this->get_page("post/view/$image_id_2"); $this->get_page("post/view/$image_id_2");
$this->assert_response(200); $this->assert_response(200);
$this->get_page("post/view/$image_id_3"); $this->get_page("post/view/$image_id_3");
$this->assert_response(404); $this->assert_response(404);
$this->delete_image($image_id_1); $this->delete_image($image_id_1);
$this->delete_image($image_id_2); $this->delete_image($image_id_2);
$this->delete_image($image_id_3); $this->delete_image($image_id_3);
} }
} }

View File

@ -1,77 +1,77 @@
<?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() { */
global $page; public function display_page()
{
global $page;
$page->set_title("Admin Tools"); $page->set_title("Admin Tools");
$page->set_heading("Admin Tools"); $page->set_heading("Admin Tools");
$page->add_block(new NavBlock()); $page->add_block(new NavBlock());
} }
/** protected function button(string $name, string $action, bool $protected=false): string
* @param string $name {
* @param string $action $c_protected = $protected ? " protected" : "";
* @param bool $protected $html = make_form(make_link("admin/$action"), "POST", false, "admin$c_protected");
* @return string if ($protected) {
*/ $html .= "<input type='submit' id='$action' value='$name' disabled='disabled'>";
protected function button(/*string*/ $name, /*string*/ $action, /*boolean*/ $protected=false) { $html .= "<input type='checkbox' onclick='$(\"#$action\").attr(\"disabled\", !$(this).is(\":checked\"))'>";
$c_protected = $protected ? " protected" : ""; } else {
$html = make_form(make_link("admin/$action"), "POST", false, "admin$c_protected"); $html .= "<input type='submit' id='$action' value='$name'>";
if($protected) { }
$html .= "<input type='submit' id='$action' value='$name' disabled='disabled'>"; $html .= "</form>\n";
$html .= "<input type='checkbox' onclick='$(\"#$action\").attr(\"disabled\", !$(this).is(\":checked\"))'>"; return $html;
} }
else {
$html .= "<input type='submit' id='$action' value='$name'>";
}
$html .= "</form>\n";
return $html;
}
/* /*
* Show a form which links to admin_utils with POST[action] set to one of: * Show a form which links to admin_utils with POST[action] set to one of:
* 'lowercase all tags' * 'lowercase all tags'
* 'recount tag use' * 'recount tag use'
* etc * 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");
$html .= "<input type='text' name='tag' placeholder='Enter tag with correct case' class='autocomplete_tags' autocomplete='off'>"; $html .= "<input type='text' name='tag' placeholder='Enter tag with correct case' class='autocomplete_tags' autocomplete='off'>";
$html .= "<input type='submit' value='Set Tag Case'>"; $html .= "<input type='submit' value='Set Tag Case'>";
$html .= "</form>\n"; $html .= "</form>\n";
$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/>";
$h_reason = "<input type='text' name='reason' placeholder='Ban reason (leave blank to not ban)'>"; }
} if (class_exists("ImageBan")) {
$html = make_form(make_link("admin/delete_by_query"), "POST") . " $h_reason = "<input type='text' name='reason' placeholder='Ban reason (leave blank to not ban)'>";
}
$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
<input type='submit' id='dbqsubmit' disabled='true' value='Delete All These Images'> <input type='submit' id='dbqsubmit' disabled='true' value='Delete All These Images'>
</form> </form>
"; ";
return $html; return $html;
} }
} }

24
ext/alias_editor/info.php Normal file
View 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;
}

View File

@ -1,178 +1,162 @@
<?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 */ {
public $oldtag; /** @var string */
/** @var string */ public $oldtag;
public $newtag; /** @var string */
public $newtag;
/** public function __construct(string $oldtag, string $newtag)
* @param string $oldtag {
* @param string $newtag $this->oldtag = trim($oldtag);
*/ $this->newtag = trim($newtag);
public function __construct($oldtag, $newtag) { }
$this->oldtag = trim($oldtag);
$this->newtag = trim($newtag);
}
} }
class AddAliasException extends SCoreException {} class AddAliasException extends SCoreException
{
class AliasEditor extends Extension {
public function onPageRequest(PageRequestEvent $event) {
global $config, $database, $page, $user;
if($event->page_matches("alias")) {
if($event->get_arg(0) == "add") {
if($user->can("manage_alias_list")) {
if(isset($_POST['oldtag']) && isset($_POST['newtag'])) {
try {
$aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']);
send_event($aae);
$page->set_mode("redirect");
$page->set_redirect(make_link("alias/list"));
}
catch(AddAliasException $ex) {
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
}
}
}
}
else if($event->get_arg(0) == "remove") {
if($user->can("manage_alias_list")) {
if(isset($_POST['oldtag'])) {
$database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", array("oldtag" => $_POST['oldtag']));
log_info("alias_editor", "Deleted alias for ".$_POST['oldtag'], true);
$page->set_mode("redirect");
$page->set_redirect(make_link("alias/list"));
}
}
}
else if($event->get_arg(0) == "list") {
$page_number = $event->get_arg(1);
if(is_null($page_number) || !is_numeric($page_number)) {
$page_number = 0;
}
else if ($page_number <= 0) {
$page_number = 0;
}
else {
$page_number--;
}
$alias_per_page = $config->get_int('alias_items_per_page', 30);
$query = "SELECT oldtag, newtag FROM aliases ORDER BY newtag ASC LIMIT :limit OFFSET :offset";
$alias = $database->get_pairs($query,
array("limit"=>$alias_per_page, "offset"=>$page_number * $alias_per_page)
);
$total_pages = ceil($database->get_one("SELECT COUNT(*) FROM aliases") / $alias_per_page);
$this->theme->display_aliases($alias, $page_number + 1, $total_pages);
}
else if($event->get_arg(0) == "export") {
$page->set_mode("data");
$page->set_type("text/csv");
$page->set_filename("aliases.csv");
$page->set_data($this->get_alias_csv($database));
}
else if($event->get_arg(0) == "import") {
if($user->can("manage_alias_list")) {
if(count($_FILES) > 0) {
$tmp = $_FILES['alias_file']['tmp_name'];
$contents = file_get_contents($tmp);
$this->add_alias_csv($database, $contents);
log_info("alias_editor", "Imported aliases from file", true); # FIXME: how many?
$page->set_mode("redirect");
$page->set_redirect(make_link("alias/list"));
}
else {
$this->theme->display_error(400, "No File Specified", "You have to upload a file");
}
}
else {
$this->theme->display_error(401, "Admins Only", "Only admins can edit the alias list");
}
}
}
}
public function onAddAlias(AddAliasEvent $event) {
global $database;
$pair = array("oldtag" => $event->oldtag, "newtag" => $event->newtag);
if($database->get_row("SELECT * FROM aliases WHERE oldtag=:oldtag AND lower(newtag)=lower(:newtag)", $pair)) {
throw new AddAliasException("That alias already exists");
}
else if($database->get_row("SELECT * FROM aliases WHERE oldtag=:newtag", array("newtag" => $event->newtag))) {
throw new AddAliasException("{$event->newtag} is itself an alias");
}
else {
$database->execute("INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)", $pair);
log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}", true);
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event) {
global $user;
if($user->can("manage_alias_list")) {
$event->add_link("Alias Editor", make_link("alias/list"));
}
}
/**
* @param Database $database
* @return string
*/
private function get_alias_csv(Database $database) {
$csv = "";
$aliases = $database->get_pairs("SELECT oldtag, newtag FROM aliases ORDER BY newtag");
foreach($aliases as $old => $new) {
$csv .= "\"$old\",\"$new\"\n";
}
return $csv;
}
/**
* @param Database $database
* @param string $csv
*/
private function add_alias_csv(Database $database, /*string*/ $csv) {
$csv = str_replace("\r", "\n", $csv);
foreach(explode("\n", $csv) as $line) {
$parts = str_getcsv($line);
if(count($parts) == 2) {
try {
$aae = new AddAliasEvent($parts[0], $parts[1]);
send_event($aae);
}
catch(AddAliasException $ex) {
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
}
}
}
}
/**
* Get the priority for this extension.
*
* Add alias *after* mass tag editing, else the MTE will
* search for the images and be redirected to the alias,
* missing out the images tagged with the old tag.
*
* @return int
*/
public function get_priority() {return 60;}
} }
class AliasEditor extends Extension
{
public function onPageRequest(PageRequestEvent $event)
{
global $config, $database, $page, $user;
if ($event->page_matches("alias")) {
if ($event->get_arg(0) == "add") {
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
if (isset($_POST['oldtag']) && isset($_POST['newtag'])) {
try {
$aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']);
send_event($aae);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("alias/list"));
} catch (AddAliasException $ex) {
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
}
}
}
} elseif ($event->get_arg(0) == "remove") {
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
if (isset($_POST['oldtag'])) {
$database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", ["oldtag" => $_POST['oldtag']]);
log_info("alias_editor", "Deleted alias for ".$_POST['oldtag'], "Deleted alias");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("alias/list"));
}
}
} elseif ($event->get_arg(0) == "list") {
$page_number = $event->get_arg(1);
if (is_null($page_number) || !is_numeric($page_number)) {
$page_number = 0;
} elseif ($page_number <= 0) {
$page_number = 0;
} else {
$page_number--;
}
$alias_per_page = $config->get_int('alias_items_per_page', 30);
$query = "SELECT oldtag, newtag FROM aliases ORDER BY newtag ASC LIMIT :limit OFFSET :offset";
$alias = $database->get_pairs(
$query,
["limit"=>$alias_per_page, "offset"=>$page_number * $alias_per_page]
);
$total_pages = ceil($database->get_one("SELECT COUNT(*) FROM aliases") / $alias_per_page);
$this->theme->display_aliases($alias, $page_number + 1, $total_pages);
} elseif ($event->get_arg(0) == "export") {
$page->set_mode(PageMode::DATA);
$page->set_type("text/csv");
$page->set_filename("aliases.csv");
$page->set_data($this->get_alias_csv($database));
} elseif ($event->get_arg(0) == "import") {
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
if (count($_FILES) > 0) {
$tmp = $_FILES['alias_file']['tmp_name'];
$contents = file_get_contents($tmp);
$this->add_alias_csv($database, $contents);
log_info("alias_editor", "Imported aliases from file", "Imported aliases"); # FIXME: how many?
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("alias/list"));
} else {
$this->theme->display_error(400, "No File Specified", "You have to upload a file");
}
} else {
$this->theme->display_error(401, "Admins Only", "Only admins can edit the alias list");
}
}
}
}
public function onAddAlias(AddAliasEvent $event)
{
global $database;
$pair = ["oldtag" => $event->oldtag, "newtag" => $event->newtag];
if ($database->get_row("SELECT * FROM aliases WHERE oldtag=:oldtag AND lower(newtag)=lower(:newtag)", $pair)) {
throw new AddAliasException("That alias already exists");
} elseif ($database->get_row("SELECT * FROM aliases WHERE oldtag=:newtag", ["newtag" => $event->newtag])) {
throw new AddAliasException("{$event->newtag} is itself an alias");
} else {
$database->execute("INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)", $pair);
log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}", "Added alias");
}
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
if ($event->parent=="tags") {
$event->add_nav_link("aliases", new Link('alias/list'), "Aliases", NavLink::is_active(["alias"]));
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
$event->add_link("Alias Editor", make_link("alias/list"));
}
}
private function get_alias_csv(Database $database): string
{
$csv = "";
$aliases = $database->get_pairs("SELECT oldtag, newtag FROM aliases ORDER BY newtag");
foreach ($aliases as $old => $new) {
$csv .= "\"$old\",\"$new\"\n";
}
return $csv;
}
private function add_alias_csv(Database $database, string $csv)
{
$csv = str_replace("\r", "\n", $csv);
foreach (explode("\n", $csv) as $line) {
$parts = str_getcsv($line);
if (count($parts) == 2) {
try {
$aae = new AddAliasEvent($parts[0], $parts[1]);
send_event($aae);
} catch (AddAliasException $ex) {
$this->theme->display_error(500, "Error adding alias", $ex->getMessage());
}
}
}
}
/**
* Get the priority for this extension.
*
* Add alias *after* mass tag editing, else the MTE will
* search for the images and be redirected to the alias,
* missing out the images tagged with the old tag.
*/
public function get_priority(): int
{
return 60;
}
}

View File

@ -1,104 +1,107 @@
<?php <?php
class AliasEditorTest extends ShimmiePHPUnitTestCase { class AliasEditorTest extends ShimmiePHPUnitTestCase
public function testAliasList() { {
$this->get_page('alias/list'); public function testAliasList()
$this->assert_response(200); {
$this->assert_title("Alias List"); $this->get_page('alias/list');
} $this->assert_response(200);
$this->assert_title("Alias List");
}
public function testAliasListReadOnly() { public function testAliasListReadOnly()
// Check that normal users can't add aliases. {
$this->log_in_as_user(); // Check that normal users can't add aliases.
$this->get_page('alias/list'); $this->log_in_as_user();
$this->assert_title("Alias List"); $this->get_page('alias/list');
$this->assert_no_text("Add"); $this->assert_title("Alias List");
} $this->assert_no_text("Add");
}
public function testAliasEditor() { public function testAliasEditor()
/* {
********************************************************************** /*
* FIXME: TODO: **********************************************************************
* For some reason the alias tests always fail when they are running * FIXME: TODO:
* inside the TravisCI VM environment. I have tried to determine * For some reason the alias tests always fail when they are running
* the exact cause of this, but have been unable to pin it down. * inside the TravisCI VM environment. I have tried to determine
* * the exact cause of this, but have been unable to pin it down.
* For now, I am commenting them out until I have more time to *
* dig into this and determine exactly what is happening. * For now, I am commenting them out until I have more time to
* * dig into this and determine exactly what is happening.
********************************************************************* *
*/ *********************************************************************
$this->markTestIncomplete(); */
$this->markTestIncomplete();
$this->log_in_as_admin(); $this->log_in_as_admin();
# test one to one # test one to one
$this->get_page('alias/list'); $this->get_page('alias/list');
$this->assert_title("Alias List"); $this->assert_title("Alias List");
$this->set_field('oldtag', "test1"); $this->set_field('oldtag', "test1");
$this->set_field('newtag', "test2"); $this->set_field('newtag', "test2");
$this->clickSubmit('Add'); $this->clickSubmit('Add');
$this->assert_no_text("Error adding alias"); $this->assert_no_text("Error adding alias");
$this->get_page('alias/list'); $this->get_page('alias/list');
$this->assert_text("test1"); $this->assert_text("test1");
$this->get_page("alias/export/aliases.csv"); $this->get_page("alias/export/aliases.csv");
$this->assert_text("test1,test2"); $this->assert_text("test1,test2");
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1"); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
$this->get_page("post/view/$image_id"); # check that the tag has been replaced $this->get_page("post/view/$image_id"); # check that the tag has been replaced
$this->assert_title("Image $image_id: test2"); $this->assert_title("Image $image_id: test2");
$this->get_page("post/list/test1/1"); # searching for an alias should find the master tag $this->get_page("post/list/test1/1"); # searching for an alias should find the master tag
$this->assert_title("Image $image_id: test2"); $this->assert_title("Image $image_id: test2");
$this->get_page("post/list/test2/1"); # check that searching for the main tag still works $this->get_page("post/list/test2/1"); # check that searching for the main tag still works
$this->assert_title("Image $image_id: test2"); $this->assert_title("Image $image_id: test2");
$this->delete_image($image_id); $this->delete_image($image_id);
$this->get_page('alias/list'); $this->get_page('alias/list');
$this->click("Remove"); $this->click("Remove");
$this->get_page('alias/list'); $this->get_page('alias/list');
$this->assert_title("Alias List"); $this->assert_title("Alias List");
$this->assert_no_text("test1"); $this->assert_no_text("test1");
# test one to many # test one to many
$this->get_page('alias/list'); $this->get_page('alias/list');
$this->assert_title("Alias List"); $this->assert_title("Alias List");
$this->set_field('oldtag', "onetag"); $this->set_field('oldtag', "onetag");
$this->set_field('newtag', "multi tag"); $this->set_field('newtag', "multi tag");
$this->click("Add"); $this->click("Add");
$this->get_page('alias/list'); $this->get_page('alias/list');
$this->assert_text("multi"); $this->assert_text("multi");
$this->assert_text("tag"); $this->assert_text("tag");
$this->get_page("alias/export/aliases.csv"); $this->get_page("alias/export/aliases.csv");
$this->assert_text("onetag,multi tag"); $this->assert_text("onetag,multi tag");
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "onetag"); $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "onetag");
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "onetag"); $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "onetag");
// FIXME: known broken // FIXME: known broken
//$this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases //$this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases
//$this->assert_title("onetag"); //$this->assert_title("onetag");
//$this->assert_no_text("No Images Found"); //$this->assert_no_text("No Images Found");
$this->get_page("post/list/multi/1"); $this->get_page("post/list/multi/1");
$this->assert_title("multi"); $this->assert_title("multi");
$this->assert_no_text("No Images Found"); $this->assert_no_text("No Images Found");
$this->get_page("post/list/multi%20tag/1"); $this->get_page("post/list/multi%20tag/1");
$this->assert_title("multi tag"); $this->assert_title("multi tag");
$this->assert_no_text("No Images Found"); $this->assert_no_text("No Images Found");
$this->delete_image($image_id_1); $this->delete_image($image_id_1);
$this->delete_image($image_id_2); $this->delete_image($image_id_2);
$this->get_page('alias/list'); $this->get_page('alias/list');
$this->click("Remove"); $this->click("Remove");
$this->get_page('alias/list'); $this->get_page('alias/list');
$this->assert_title("Alias List"); $this->assert_title("Alias List");
$this->assert_no_text("test1"); $this->assert_no_text("test1");
$this->log_out(); $this->log_out();
$this->get_page('alias/list'); $this->get_page('alias/list');
$this->assert_title("Alias List"); $this->assert_title("Alias List");
$this->assert_no_text("Add"); $this->assert_no_text("Add");
} }
} }

View File

@ -1,22 +1,20 @@
<?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 public function display_aliases(array $aliases, int $pageNumber, int $totalPages): void
* @param int $totalPages {
*/ global $page, $user;
public function display_aliases($aliases, $pageNumber, $totalPages) {
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>
".make_form(make_link("alias/add"))." ".make_form(make_link("alias/add"))."
<td><input type='text' name='oldtag' class='autocomplete_tags' autocomplete='off'></td> <td><input type='text' name='oldtag' class='autocomplete_tags' autocomplete='off'></td>
@ -25,20 +23,19 @@ 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"))."
<input type='hidden' name='oldtag' value='$h_old'> <input type='hidden' name='oldtag' value='$h_old'>
@ -46,10 +43,10 @@ class AliasEditorTheme extends Themelet {
</form> </form>
</td> </td>
"; ";
} }
$h_aliases .= "</tr>"; $h_aliases .= "</tr>";
} }
$html = " $html = "
<table id='aliases' class='sortable zebra'> <table id='aliases' class='sortable zebra'>
<thead><tr><th>From</th><th>To</th>$h_action</tr></thead> <thead><tr><th>From</th><th>To</th>$h_action</tr></thead>
<tbody>$h_aliases</tbody> <tbody>$h_aliases</tbody>
@ -58,22 +55,21 @@ class AliasEditorTheme extends Themelet {
<p><a href='".make_link("alias/export/aliases.csv")."' download='aliases.csv'>Download as CSV</a></p> <p><a href='".make_link("alias/export/aliases.csv")."' download='aliases.csv'>Download as CSV</a></p>
"; ";
$bulk_html = " $bulk_html = "
".make_form(make_link("alias/import"), 'post', true)." ".make_form(make_link("alias/import"), 'post', true)."
<input type='file' name='alias_file'> <input type='file' name='alias_file'>
<input type='submit' value='Upload List'> <input type='submit' value='Upload List'>
</form> </form>
"; ";
$page->set_title("Alias List"); $page->set_title("Alias List");
$page->set_heading("Alias List"); $page->set_heading("Alias List");
$page->add_block(new NavBlock()); $page->add_block(new NavBlock());
$page->add_block(new Block("Aliases", $html)); $page->add_block(new Block("Aliases", $html));
if($can_manage) { if ($can_manage) {
$page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51)); $page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51));
} }
$this->display_paginator($page, "alias/list", null, $pageNumber, $totalPages); $this->display_paginator($page, "alias/list", null, $pageNumber, $totalPages);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
}
}

View 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.";
}

View File

@ -1,49 +1,38 @@
<?php <?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 ArrowkeyNavigation extends Extension {
/**
* Adds functionality for post/view on images.
*
* @param DisplayingImageEvent $event
*/
public function onDisplayingImage(DisplayingImageEvent $event) {
$prev_url = make_http(make_link("post/prev/".$event->image->id));
$next_url = make_http(make_link("post/next/".$event->image->id));
$this->add_arrowkeys_code($prev_url, $next_url);
}
/** class ArrowkeyNavigation extends Extension
* Adds functionality for post/list. {
* /**
* @param PageRequestEvent $event * Adds functionality for post/view on images.
*/ */
public function onPageRequest(PageRequestEvent $event) { public function onDisplayingImage(DisplayingImageEvent $event)
if($event->page_matches("post/list")) { {
$pageinfo = $this->get_list_pageinfo($event); $prev_url = make_http(make_link("post/prev/".$event->image->id));
$prev_url = make_http(make_link("post/list/".$pageinfo["prev"])); $next_url = make_http(make_link("post/next/".$event->image->id));
$next_url = make_http(make_link("post/list/".$pageinfo["next"])); $this->add_arrowkeys_code($prev_url, $next_url);
$this->add_arrowkeys_code($prev_url, $next_url); }
}
}
/** /**
* Adds the javascript to the page with the given urls. * Adds functionality for post/list.
* */
* @param string $prev_url public function onPageRequest(PageRequestEvent $event)
* @param string $next_url {
*/ if ($event->page_matches("post/list")) {
private function add_arrowkeys_code($prev_url, $next_url) { $pageinfo = $this->get_list_pageinfo($event);
global $page; $prev_url = make_http(make_link("post/list/".$pageinfo["prev"]));
$next_url = make_http(make_link("post/list/".$pageinfo["next"]));
$this->add_arrowkeys_code($prev_url, $next_url);
}
}
$page->add_html_header("<script type=\"text/javascript\"> /**
* Adds the javascript to the page with the given urls.
*/
private function add_arrowkeys_code(string $prev_url, string $next_url)
{
global $page;
$page->add_html_header("<script type=\"text/javascript\">
(function($){ (function($){
$(document).keyup(function(e) { $(document).keyup(function(e) {
if($(e.target).is('input', 'textarea')){ return; } if($(e.target).is('input', 'textarea')){ return; }
@ -53,49 +42,53 @@ class ArrowkeyNavigation extends Extension {
}); });
})(jQuery); })(jQuery);
</script>", 60); </script>", 60);
} }
/** /**
* Returns info about the current page number. * Returns info about the current page number.
* */
* @param PageRequestEvent $event private function get_list_pageinfo(PageRequestEvent $event): array
* @return array {
*/ global $config, $database;
private function get_list_pageinfo(PageRequestEvent $event) {
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
View 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

View File

@ -1,9 +1,10 @@
<?php <?php
class ArtistTest extends ShimmiePHPUnitTestCase { class ArtistTest extends ShimmiePHPUnitTestCase
public function testSearch() { {
# FIXME: check that the results are there public function testSearch()
$this->get_page("post/list/author=bob/1"); {
#$this->assert_response(200); # FIXME: check that the results are there
} $this->get_page("post/list/author=bob/1");
#$this->assert_response(200);
}
} }

View File

@ -1,13 +1,10 @@
<?php <?php
class ArtistsTheme extends Themelet { class ArtistsTheme extends Themelet
{
/** public function get_author_editor_html(string $author): string
* @param string $author {
* @return string $h_author = html_escape($author);
*/ return "
public function get_author_editor_html(/*string*/ $author) {
$h_author = html_escape($author);
return "
<tr> <tr>
<th>Author</th> <th>Author</th>
<td> <td>
@ -16,105 +13,104 @@ class ArtistsTheme extends Themelet {
</td> </td>
</tr> </tr>
"; ";
} }
/** public function sidebar_options(string $mode, ?int $artistID=null, $is_admin=false): void
* @param string $mode {
* @param null|int $artistID global $page, $user;
* @param bool $is_admin
*/
public function sidebar_options(/*string*/ $mode, $artistID=NULL, $is_admin=FALSE) {
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'];
$artistNotes = $artist['notes']; $artistNotes = $artist['notes'];
$artistID = $artist['id']; $artistID = $artist['id'];
// aliases // aliases
$aliasesString = ""; $aliasesString = "";
$aliasesIDsString = ""; $aliasesIDsString = "";
foreach ($aliases as $alias) { foreach ($aliases as $alias) {
$aliasesString .= $alias["alias_name"]." "; $aliasesString .= $alias["alias_name"]." ";
$aliasesIDsString .= $alias["alias_id"]." "; $aliasesIDsString .= $alias["alias_id"]." ";
} }
$aliasesString = rtrim($aliasesString); $aliasesString = rtrim($aliasesString);
$aliasesIDsString = rtrim($aliasesIDsString); $aliasesIDsString = rtrim($aliasesIDsString);
// members // members
$membersString = ""; $membersString = "";
$membersIDsString = ""; $membersIDsString = "";
foreach ($members as $member) { foreach ($members as $member) {
$membersString .= $member["name"]." "; $membersString .= $member["name"]." ";
$membersIDsString .= $member["id"]." "; $membersIDsString .= $member["id"]." ";
} }
$membersString = rtrim($membersString); $membersString = rtrim($membersString);
$membersIDsString = rtrim($membersIDsString); $membersIDsString = rtrim($membersIDsString);
// urls // urls
$urlsString = ""; $urlsString = "";
$urlsIDsString = ""; $urlsIDsString = "";
foreach ($urls as $url) { foreach ($urls as $url) {
$urlsString .= $url["url"]."\n"; $urlsString .= $url["url"]."\n";
$urlsIDsString .= $url["id"]." "; $urlsIDsString .= $url["id"]." ";
} }
$urlsString = substr($urlsString, 0, strlen($urlsString) -1); $urlsString = substr($urlsString, 0, strlen($urlsString) -1);
$urlsIDsString = rtrim($urlsIDsString); $urlsIDsString = rtrim($urlsIDsString);
$html = ' $html = '
<form method="POST" action="'.make_link("artist/edited/".$artist['id']).'"> <form method="POST" action="'.make_link("artist/edited/".$artist['id']).'">
'.$user->get_auth_html().' '.$user->get_auth_html().'
<table> <table>
@ -132,14 +128,15 @@ class ArtistsTheme extends Themelet {
</form> </form>
'; ';
global $page; global $page;
$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'>
".$user->get_auth_html()." ".$user->get_auth_html()."
<table> <table>
<tr><td>Name:</td><td><input type='text' name='name' /></td></tr> <tr><td>Name:</td><td><input type='text' name='name' /></td></tr>
@ -151,86 +148,95 @@ class ArtistsTheme extends Themelet {
</table> </table>
"; ";
$page->set_title("Artists"); $page->set_title("Artists");
$page->set_heading("Artists"); $page->set_heading("Artists");
$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'>".
"<thead><tr>". "<thead><tr>".
"<th>Name</th>". "<th>Name</th>".
"<th>Type</th>". "<th>Type</th>".
"<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>";
$html .= "</tr></thead>"; } // space for edit link
$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>";
$user_link = "<a href='".make_link("user/".$artist['user_name'])."'>".$artist['user_name']."</a>"; $user_link = "<a href='".make_link("user/".$artist['user_name'])."'>".$artist['user_name']."</a>";
$edit_link = "<a href='".make_link($editionLinkActionArray[$artist['type']].$artist['id'])."'>Edit</a>"; $edit_link = "<a href='".make_link($editionLinkActionArray[$artist['type']].$artist['id'])."'>Edit</a>";
$del_link = "<a href='".make_link($deletionLinkActionArray[$artist['type']].$artist['id'])."'>Delete</a>"; $del_link = "<a href='".make_link($deletionLinkActionArray[$artist['type']].$artist['id'])."'>Delete</a>";
$html .= "<tr>". $html .= "<tr>".
"<td class='left'>".$elementLink; "<td class='left'>".$elementLink;
//if ($artist['type'] == 'member') //if ($artist['type'] == 'member')
// $html .= " (member of ".$artist_link.")"; // $html .= " (member of ".$artist_link.")";
//if ($artist['type'] == 'alias') //if ($artist['type'] == 'alias')
// $html .= " (alias for ".$artist_link.")"; // $html .= " (alias for ".$artist_link.")";
$html .= "</td>". $html .= "</td>".
"<td>".$typeTextArray[$artist['type']]."</td>". "<td>".$typeTextArray[$artist['type']]."</td>".
"<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>";
} }
$html .= "</tbody></table>"; $html .= "</tbody></table>";
$page->set_title("Artists"); $page->set_title("Artists");
$page->set_heading("Artists"); $page->set_heading("Artists");
$page->add_block(new Block("Artists", $html, "main", 10)); $page->add_block(new Block("Artists", $html, "main", 10));
$this->display_paginator($page, "artist/list", null, $pageNumber, $totalPages); $this->display_paginator($page, "artist/list", null, $pageNumber, $totalPages);
} }
public function show_new_alias_composer($artistID) { public function show_new_alias_composer($artistID)
global $user; {
global $user;
$html = ' $html = '
<form method="POST" action='.make_link("artist/alias/add").'> <form method="POST" action='.make_link("artist/alias/add").'>
'.$user->get_auth_html().' '.$user->get_auth_html().'
<table> <table>
@ -241,14 +247,15 @@ class ArtistsTheme extends Themelet {
</form> </form>
'; ';
global $page; global $page;
$page->add_block(new Block("Artist Aliases", $html, "main", 20)); $page->add_block(new Block("Artist Aliases", $html, "main", 20));
} }
public function show_new_member_composer($artistID) { public function show_new_member_composer($artistID)
global $user; {
global $user;
$html = ' $html = '
<form method="POST" action='.make_link("artist/member/add").'> <form method="POST" action='.make_link("artist/member/add").'>
'.$user->get_auth_html().' '.$user->get_auth_html().'
<table> <table>
@ -259,14 +266,15 @@ class ArtistsTheme extends Themelet {
</form> </form>
'; ';
global $page; global $page;
$page->add_block(new Block("Artist members", $html, "main", 30)); $page->add_block(new Block("Artist members", $html, "main", 30));
} }
public function show_new_url_composer($artistID) { public function show_new_url_composer($artistID)
global $user; {
global $user;
$html = ' $html = '
<form method="POST" action='.make_link("artist/url/add").'> <form method="POST" action='.make_link("artist/url/add").'>
'.$user->get_auth_html().' '.$user->get_auth_html().'
<table> <table>
@ -277,253 +285,274 @@ class ArtistsTheme extends Themelet {
</form> </form>
'; ';
global $page; global $page;
$page->add_block(new Block("Artist URLs", $html, "main", 40)); $page->add_block(new Block("Artist URLs", $html, "main", 40));
} }
public function show_alias_editor($alias) { 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>
'; ';
global $page; global $page;
$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>
'; ';
global $page; global $page;
$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>
'; ';
global $page; global $page;
$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>";
$html = "<table id='poolsList' class='zebra'> $html = "<table id='poolsList' class='zebra'>
<thead> <thead>
<tr> <tr>
<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>
<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>";
$html .= "</tr>"; }
if ($userIsAdmin) {
$html .= "<td></td>";
}
$html .= "</tr>";
$html .= $this->render_aliases($aliases, $userIsLogged, $userIsAdmin); $html .= $this->render_aliases($aliases, $userIsLogged, $userIsAdmin);
$html .= $this->render_members($members, $userIsLogged, $userIsAdmin); $html .= $this->render_members($members, $userIsLogged, $userIsAdmin);
$html .= $this->render_urls($urls, $userIsLogged, $userIsAdmin); $html .= $this->render_urls($urls, $userIsLogged, $userIsAdmin);
$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>";
//TODO how will notes be edited? On edit artist? (should there be an editartist?) or on a editnotes? }
//same question for deletion if ($userIsAdmin) {
$html .= "</tr> $html .= "<td></td>";
}
//TODO how will notes be edited? On edit artist? (should there be an editartist?) or on a editnotes?
//same question for deletion
$html .= "</tr>
</table>"; </table>";
$page->set_title("Artist"); $page->set_title("Artist");
$page->set_heading("Artist"); $page->set_heading("Artist");
$page->add_block(new Block("Artist", $html, "main", 10)); $page->add_block(new Block("Artist", $html, "main", 10));
//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">'.
'<a href="$image_link">'.$thumb_html.'</a>'. '<a href="$image_link">'.$thumb_html.'</a>'.
'</span>'; '</span>';
} }
$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 $html = "";
* @param $userIsAdmin if (count($aliases) > 0) {
* @return string $aliasViewLink = str_replace("_", " ", $aliases[0]['alias_name']); // no link anymore
*/ $aliasEditLink = "<a href='" . make_link("artist/alias/edit/" . $aliases[0]['alias_id']) . "'>Edit</a>";
private function render_aliases($aliases, $userIsLogged, $userIsAdmin) { $aliasDeleteLink = "<a href='" . make_link("artist/alias/delete/" . $aliases[0]['alias_id']) . "'>Delete</a>";
$html = "";
if(count($aliases) > 0) {
$aliasViewLink = str_replace("_", " ", $aliases[0]['alias_name']); // no link anymore
$aliasEditLink = "<a href='" . make_link("artist/alias/edit/" . $aliases[0]['alias_id']) . "'>Edit</a>";
$aliasDeleteLink = "<a href='" . make_link("artist/alias/delete/" . $aliases[0]['alias_id']) . "'>Delete</a>";
$html .= "<tr> $html .= "<tr>
<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>";
if (count($aliases) > 1) { if (count($aliases) > 1) {
for ($i = 1; $i < count($aliases); $i++) { for ($i = 1; $i < count($aliases); $i++) {
$aliasViewLink = str_replace("_", " ", $aliases[$i]['alias_name']); // no link anymore $aliasViewLink = str_replace("_", " ", $aliases[$i]['alias_name']); // no link anymore
$aliasEditLink = "<a href='" . make_link("artist/alias/edit/" . $aliases[$i]['alias_id']) . "'>Edit</a>"; $aliasEditLink = "<a href='" . make_link("artist/alias/edit/" . $aliases[$i]['alias_id']) . "'>Edit</a>";
$aliasDeleteLink = "<a href='" . make_link("artist/alias/delete/" . $aliases[$i]['alias_id']) . "'>Delete</a>"; $aliasDeleteLink = "<a href='" . make_link("artist/alias/delete/" . $aliases[$i]['alias_id']) . "'>Delete</a>";
$html .= "<tr> $html .= "<tr>
<td class='left'>&nbsp;</td> <td class='left'>&nbsp;</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) }
$html .= "<td class='left'>" . $aliasDeleteLink . "</td>"; if ($userIsAdmin) {
$html .= "<td class='left'>" . $aliasDeleteLink . "</td>";
}
$html .= "</tr>"; $html .= "</tr>";
} }
} }
} }
return $html; return $html;
} }
/** private function render_members(array $members, bool $userIsLogged, bool $userIsAdmin): string
* @param $members {
* @param $userIsLogged $html = "";
* @param $userIsAdmin if (count($members) > 0) {
* @return string $memberViewLink = str_replace("_", " ", $members[0]['name']); // no link anymore
*/ $memberEditLink = "<a href='" . make_link("artist/member/edit/" . $members[0]['id']) . "'>Edit</a>";
private function render_members($members, $userIsLogged, $userIsAdmin) { $memberDeleteLink = "<a href='" . make_link("artist/member/delete/" . $members[0]['id']) . "'>Delete</a>";
$html = "";
if(count($members) > 0) {
$memberViewLink = str_replace("_", " ", $members[0]['name']); // no link anymore
$memberEditLink = "<a href='" . make_link("artist/member/edit/" . $members[0]['id']) . "'>Edit</a>";
$memberDeleteLink = "<a href='" . make_link("artist/member/delete/" . $members[0]['id']) . "'>Delete</a>";
$html .= "<tr> $html .= "<tr>
<td class='left'>Members:</td> <td class='left'>Members:</td>
<td class='left'>" . $memberViewLink . "</td>"; <td class='left'>" . $memberViewLink . "</td>";
if ($userIsLogged) if ($userIsLogged) {
$html .= "<td class='left'>" . $memberEditLink . "</td>"; $html .= "<td class='left'>" . $memberEditLink . "</td>";
if ($userIsAdmin) }
$html .= "<td class='left'>" . $memberDeleteLink . "</td>"; if ($userIsAdmin) {
$html .= "<td class='left'>" . $memberDeleteLink . "</td>";
}
$html .= "</tr>"; $html .= "</tr>";
if (count($members) > 1) { if (count($members) > 1) {
for ($i = 1; $i < count($members); $i++) { for ($i = 1; $i < count($members); $i++) {
$memberViewLink = str_replace("_", " ", $members[$i]['name']); // no link anymore $memberViewLink = str_replace("_", " ", $members[$i]['name']); // no link anymore
$memberEditLink = "<a href='" . make_link("artist/member/edit/" . $members[$i]['id']) . "'>Edit</a>"; $memberEditLink = "<a href='" . make_link("artist/member/edit/" . $members[$i]['id']) . "'>Edit</a>";
$memberDeleteLink = "<a href='" . make_link("artist/member/delete/" . $members[$i]['id']) . "'>Delete</a>"; $memberDeleteLink = "<a href='" . make_link("artist/member/delete/" . $members[$i]['id']) . "'>Delete</a>";
$html .= "<tr> $html .= "<tr>
<td class='left'>&nbsp;</td> <td class='left'>&nbsp;</td>
<td class='left'>" . $memberViewLink . "</td>"; <td class='left'>" . $memberViewLink . "</td>";
if ($userIsLogged) if ($userIsLogged) {
$html .= "<td class='left'>" . $memberEditLink . "</td>"; $html .= "<td class='left'>" . $memberEditLink . "</td>";
if ($userIsAdmin) }
$html .= "<td class='left'>" . $memberDeleteLink . "</td>"; if ($userIsAdmin) {
$html .= "<td class='left'>" . $memberDeleteLink . "</td>";
}
$html .= "</tr>"; $html .= "</tr>";
} }
} }
} }
return $html; return $html;
} }
/** private function render_urls(array $urls, bool $userIsLogged, bool $userIsAdmin): string
* @param $urls {
* @param $userIsLogged $html = "";
* @param $userIsAdmin if (count($urls) > 0) {
* @return string $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>";
private function render_urls($urls, $userIsLogged, $userIsAdmin) { $urlDeleteLink = "<a href='" . make_link("artist/url/delete/" . $urls[0]['id']) . "'>Delete</a>";
$html = "";
if(count($urls) > 0) {
$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>";
$urlDeleteLink = "<a href='" . make_link("artist/url/delete/" . $urls[0]['id']) . "'>Delete</a>";
$html .= "<tr> $html .= "<tr>
<td class='left'>URLs:</td> <td class='left'>URLs:</td>
<td class='left'>" . $urlViewLink . "</td>"; <td class='left'>" . $urlViewLink . "</td>";
if ($userIsLogged) if ($userIsLogged) {
$html .= "<td class='left'>" . $urlEditLink . "</td>"; $html .= "<td class='left'>" . $urlEditLink . "</td>";
}
if ($userIsAdmin) if ($userIsAdmin) {
$html .= "<td class='left'>" . $urlDeleteLink . "</td>"; $html .= "<td class='left'>" . $urlDeleteLink . "</td>";
}
$html .= "</tr>"; $html .= "</tr>";
if (count($urls) > 1) { if (count($urls) > 1) {
for ($i = 1; $i < count($urls); $i++) { for ($i = 1; $i < count($urls); $i++) {
$urlViewLink = "<a href='" . str_replace("_", " ", $urls[$i]['url']) . "' target='_blank'>" . str_replace("_", " ", $urls[$i]['url']) . "</a>"; $urlViewLink = "<a href='" . str_replace("_", " ", $urls[$i]['url']) . "' target='_blank'>" . str_replace("_", " ", $urls[$i]['url']) . "</a>";
$urlEditLink = "<a href='" . make_link("artist/url/edit/" . $urls[$i]['id']) . "'>Edit</a>"; $urlEditLink = "<a href='" . make_link("artist/url/edit/" . $urls[$i]['id']) . "'>Edit</a>";
$urlDeleteLink = "<a href='" . make_link("artist/url/delete/" . $urls[$i]['id']) . "'>Delete</a>"; $urlDeleteLink = "<a href='" . make_link("artist/url/delete/" . $urls[$i]['id']) . "'>Delete</a>";
$html .= "<tr> $html .= "<tr>
<td class='left'>&nbsp;</td> <td class='left'>&nbsp;</td>
<td class='left'>" . $urlViewLink . "</td>"; <td class='left'>" . $urlViewLink . "</td>";
if ($userIsLogged) if ($userIsLogged) {
$html .= "<td class='left'>" . $urlEditLink . "</td>"; $html .= "<td class='left'>" . $urlEditLink . "</td>";
}
if ($userIsAdmin) if ($userIsAdmin) {
$html .= "<td class='left'>" . $urlDeleteLink . "</td>"; $html .= "<td class='left'>" . $urlDeleteLink . "</td>";
}
$html .= "</tr>"; $html .= "</tr>";
} }
return $html; return $html;
} }
} }
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
View 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.";
}

View File

@ -1,47 +1,66 @@
<?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;
}
//$limit = 0; $page->set_mode(PageMode::DATA);
$cache_key = "autocomplete-" . strtolower($_GET["s"]); $page->set_type("application/json");
$limitSQL = "";
$SQLarr = array("search"=>$_GET["s"]."%");
if(isset($_GET["limit"]) && $_GET["limit"] !== 0){
$limitSQL = "LIMIT :limit";
$SQLarr['limit'] = $_GET["limit"];
$cache_key .= "-" . $_GET["limit"];
}
$res = $database->cache->get($cache_key); $s = strtolower($_GET["s"]);
if(!$res) { if (
$res = $database->get_pairs($database->scoreql_to_sql(" $s == '' ||
$s[0] == '_' ||
$s[0] == '%' ||
strlen($s) > 32
) {
$page->set_data("{}");
return;
}
//$limit = 0;
$cache_key = "autocomplete-$s";
$limitSQL = "";
$s = str_replace('_', '\_', $s);
$s = str_replace('%', '\%', $s);
$SQLarr = ["search"=>"$s%"]; #, "cat_search"=>"%:$s%"];
if (isset($_GET["limit"]) && $_GET["limit"] !== 0) {
$limitSQL = "LIMIT :limit";
$SQLarr['limit'] = $_GET["limit"];
$cache_key .= "-" . $_GET["limit"];
}
$res = $database->cache->get($cache_key);
if (!$res) {
$res = $database->get_pairs(
$database->scoreql_to_sql("
SELECT tag, count 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_data(json_encode($res));
$page->set_type("application/json"); }
$page->set_data(json_encode($res));
}
$this->theme->build_autocomplete($page); $this->theme->build_autocomplete($page);
} }
} }

View File

@ -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();

View File

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

36
ext/ban_words/info.php Normal file
View 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\"";
}

View File

@ -1,29 +1,11 @@
<?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) { {
global $config; public function onInitExt(InitExtEvent $event)
$config->set_default_string('banned_words', " {
global $config;
$config->set_default_string('banned_words', "
a href= a href=
anal anal
blowjob blowjob
@ -51,86 +33,87 @@ very nice site
viagra viagra
xanax xanax
"); ");
} }
public function onCommentPosting(CommentPostingEvent $event) { public function onCommentPosting(CommentPostingEvent $event)
global $user; {
if(!$user->can("bypass_comment_checks")) { global $user;
$this->test_text($event->comment, new CommentPostingException("Comment contains banned terms")); if (!$user->can(Permissions::BYPASS_COMMENT_CHECKS)) {
} $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->add_label("One per line, lines that start with slashes are treated as regex<br/>"); $sb = new SetupBlock("Banned Phrases");
$sb->add_longtext_option("banned_words"); $sb->add_label("One per line, lines that start with slashes are treated as regex<br/>");
$failed = array(); $sb->add_longtext_option("banned_words");
foreach($this->get_words() as $word) { $failed = [];
if($word[0] == '/') { foreach ($this->get_words() as $word) {
if(preg_match($word, "") === false) { if ($word[0] == '/') {
$failed[] = $word; if (preg_match($word, "") === false) {
} $failed[] = $word;
} }
} }
if($failed) { }
$sb->add_label("Failed regexes: ".join(", ", $failed)); if ($failed) {
} $sb->add_label("Failed regexes: ".join(", ", $failed));
$event->panel->add_block($sb); }
} $event->panel->add_block($sb);
}
/** /**
* Throws if the comment contains banned words. * Throws if the comment contains banned words.
* @param string $comment */
* @param CommentPostingException|SCoreException $ex private function test_text(string $comment, Exception $ex): void
* @throws CommentPostingException|SCoreException if the comment contains banned words. {
*/ $comment = strtolower($comment);
private function test_text($comment, $ex) {
$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[] {
*/ global $config;
private function get_words() { $words = [];
global $config;
$words = array();
$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;
} }
$words[] = $word; $words[] = $word;
} }
return $words; return $words;
} }
public function get_priority() {return 30;} public function get_priority(): int
{
return 30;
}
} }

View File

@ -1,33 +1,34 @@
<?php <?php
class BanWordsTest extends ShimmiePHPUnitTestCase { class BanWordsTest extends ShimmiePHPUnitTestCase
public function check_blocked($image_id, $words) { {
global $user; public function check_blocked($image_id, $words)
try { {
send_event(new CommentPostingEvent($image_id, $user, $words)); global $user;
$this->fail("Exception not thrown"); try {
} send_event(new CommentPostingEvent($image_id, $user, $words));
catch(CommentPostingException $e) { $this->fail("Exception not thrown");
$this->assertEquals($e->getMessage(), "Comment contains banned terms"); } catch (CommentPostingException $e) {
} $this->assertEquals($e->getMessage(), "Comment contains banned terms");
} }
}
public function testWordBan() { public function testWordBan()
global $config; {
$config->set_string("banned_words", "viagra\nporn\n\n/http:.*\.cn\//"); global $config;
$config->set_string("banned_words", "viagra\nporn\n\n/http:.*\.cn\//");
$this->log_in_as_user(); $this->log_in_as_user();
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot");
$this->check_blocked($image_id, "kittens and viagra"); $this->check_blocked($image_id, "kittens and viagra");
$this->check_blocked($image_id, "kittens and ViagrA"); $this->check_blocked($image_id, "kittens and ViagrA");
$this->check_blocked($image_id, "kittens and viagra!"); $this->check_blocked($image_id, "kittens and viagra!");
$this->check_blocked($image_id, "some link to http://something.cn/"); $this->check_blocked($image_id, "some link to http://something.cn/");
$this->get_page('comment/list'); $this->get_page('comment/list');
$this->assert_title('Comments'); $this->assert_title('Comments');
$this->assert_no_text('viagra'); $this->assert_no_text('viagra');
$this->assert_no_text('ViagrA'); $this->assert_no_text('ViagrA');
$this->assert_no_text('http://something.cn/'); $this->assert_no_text('http://something.cn/');
} }
} }

40
ext/bbcode/info.php Normal file
View 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>&gt;&gt;123 (link to image #123)
</ul>";
}

View File

@ -1,184 +1,162 @@
<?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>&gt;&gt;123 (link to image #123)
* </ul>
*/
class BBCode extends FormatterExtension {
/**
* @param string $text
* @return string
*/
public function format(/*string*/ $text) {
$text = $this->extract_code($text);
foreach(array(
"b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4",
) as $el) {
$text = preg_replace("!\[$el\](.*?)\[/$el\]!s", "<$el>$1</$el>", $text);
}
$text = preg_replace('!^&gt;&gt;([^\d].+)!', '<blockquote><small>$1</small></blockquote>', $text);
$text = preg_replace('!&gt;&gt;(\d+)(#c?\d+)?!s', '<a class="shm-clink" data-clink-sel="$2" href="'.make_link('post/view/$1$2').'">&gt;&gt;$1$2</a>', $text);
$text = preg_replace('!\[anchor=(.*?)\](.*?)\[/anchor\]!s', '<span class="anchor">$2 <a class="alink" href="#bb-$1" name="bb-$1" title="link to this anchor"> ¶ </a></span>', $text); // add "bb-" to avoid clashing with eg #top
$text = preg_replace('!\[url=site://(.*?)(#c\d+)?\](.*?)\[/url\]!s', '<a class="shm-clink" data-clink-sel="$2" href="'.make_link('$1$2').'">$3</a>', $text);
$text = preg_replace('!\[url\]site://(.*?)(#c\d+)?\[/url\]!s', '<a class="shm-clink" data-clink-sel="$2" href="'.make_link('$1$2').'">$1$2</a>', $text);
$text = preg_replace('!\[url=((?:https?|ftp|irc|mailto)://.*?)\](.*?)\[/url\]!s', '<a href="$1">$2</a>', $text);
$text = preg_replace('!\[url\]((?:https?|ftp|irc|mailto)://.*?)\[/url\]!s', '<a href="$1">$1</a>', $text);
$text = preg_replace('!\[email\](.*?)\[/email\]!s', '<a href="mailto:$1">$1</a>', $text);
$text = preg_replace('!\[img\](https?:\/\/.*?)\[/img\]!s', '<img src="$1">', $text);
$text = preg_replace('!\[\[([^\|\]]+)\|([^\]]+)\]\]!s', '<a href="'.make_link('wiki/$1').'">$2</a>', $text);
$text = preg_replace('!\[\[([^\]]+)\]\]!s', '<a href="'.make_link('wiki/$1').'">$1</a>', $text);
$text = preg_replace("!\n\s*\n!", "\n\n", $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><em>\\1 said:</em><br><small>\\2</small></blockquote>", $text);
while(preg_match("/\[list\](.*?)\[\/list\]/s", $text))
$text = preg_replace("/\[list\](.*?)\[\/list\]/s", "<ul>\\1</ul>", $text);
while(preg_match("/\[ul\](.*?)\[\/ul\]/s", $text))
$text = preg_replace("/\[ul\](.*?)\[\/ul\]/s", "<ul>\\1</ul>", $text);
while(preg_match("/\[ol\](.*?)\[\/ol\]/s", $text))
$text = preg_replace("/\[ol\](.*?)\[\/ol\]/s", "<ol>\\1</ol>", $text);
$text = preg_replace("/\[li\](.*?)\[\/li\]/s", "<li>\\1</li>", $text);
$text = preg_replace("#\[\*\]#s", "<li>", $text);
$text = preg_replace("#<br><(li|ul|ol|/ul|/ol)>#s", "<\\1>", $text);
$text = preg_replace("#\[align=(left|center|right)\](.*?)\[\/align\]#s", "<div style='text-align:\\1;'>\\2</div>", $text);
$text = $this->filter_spoiler($text);
$text = $this->insert_code($text);
return $text;
}
/** class BBCode extends FormatterExtension
* @param string $text {
* @return string public function format(string $text): string
*/ {
public function strip(/*string*/ $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",
"code", "url", "email", "li", ] as $el) {
) as $el) { $text = preg_replace("!\[$el\](.*?)\[/$el\]!s", "<$el>$1</$el>", $text);
$text = preg_replace("!\[$el\](.*?)\[/$el\]!s", '$1', $text); }
} $text = preg_replace('!^&gt;&gt;([^\d].+)!', '<blockquote><small>$1</small></blockquote>', $text);
$text = preg_replace("!\[anchor=(.*?)\](.*?)\[/anchor\]!s", '$2', $text); $text = preg_replace('!&gt;&gt;(\d+)(#c?\d+)?!s', '<a class="shm-clink" data-clink-sel="$2" href="'.make_link('post/view/$1$2').'">&gt;&gt;$1$2</a>', $text);
$text = preg_replace("!\[url=(.*?)\](.*?)\[/url\]!s", '$2', $text); $text = preg_replace('!\[anchor=(.*?)\](.*?)\[/anchor\]!s', '<span class="anchor">$2 <a class="alink" href="#bb-$1" name="bb-$1" title="link to this anchor"> ¶ </a></span>', $text); // add "bb-" to avoid clashing with eg #top
$text = preg_replace("!\[img\](.*?)\[/img\]!s", "", $text); $text = preg_replace('!\[url=site://(.*?)(#c\d+)?\](.*?)\[/url\]!s', '<a class="shm-clink" data-clink-sel="$2" href="'.make_link('$1$2').'">$3</a>', $text);
$text = preg_replace("!\[\[([^\|\]]+)\|([^\]]+)\]\]!s", '$2', $text); $text = preg_replace('!\[url\]site://(.*?)(#c\d+)?\[/url\]!s', '<a class="shm-clink" data-clink-sel="$2" href="'.make_link('$1$2').'">$1$2</a>', $text);
$text = preg_replace("!\[\[([^\]]+)\]\]!s", '$1', $text); $text = preg_replace('!\[url=((?:https?|ftp|irc|mailto)://.*?)\](.*?)\[/url\]!s', '<a href="$1">$2</a>', $text);
$text = preg_replace("!\[quote\](.*?)\[/quote\]!s", "", $text); $text = preg_replace('!\[url\]((?:https?|ftp|irc|mailto)://.*?)\[/url\]!s', '<a href="$1">$1</a>', $text);
$text = preg_replace("!\[quote=(.*?)\](.*?)\[/quote\]!s", "", $text); $text = preg_replace('!\[email\](.*?)\[/email\]!s', '<a href="mailto:$1">$1</a>', $text);
$text = preg_replace("!\[/?(list|ul|ol)\]!", "", $text); $text = preg_replace('!\[img\](https?:\/\/.*?)\[/img\]!s', '<img src="$1">', $text);
$text = preg_replace("!\[\*\](.*?)!s", '$1', $text); $text = preg_replace('!\[\[([^\|\]]+)\|([^\]]+)\]\]!s', '<a href="'.make_link('wiki/$1').'">$2</a>', $text);
$text = $this->strip_spoiler($text); $text = preg_replace('!\[\[([^\]]+)\]\]!s', '<a href="'.make_link('wiki/$1').'">$1</a>', $text);
return $text; $text = preg_replace("!\n\s*\n!", "\n\n", $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><em>\\1 said:</em><br><small>\\2</small></blockquote>", $text);
while (preg_match("/\[list\](.*?)\[\/list\]/s", $text)) {
$text = preg_replace("/\[list\](.*?)\[\/list\]/s", "<ul>\\1</ul>", $text);
}
while (preg_match("/\[ul\](.*?)\[\/ul\]/s", $text)) {
$text = preg_replace("/\[ul\](.*?)\[\/ul\]/s", "<ul>\\1</ul>", $text);
}
while (preg_match("/\[ol\](.*?)\[\/ol\]/s", $text)) {
$text = preg_replace("/\[ol\](.*?)\[\/ol\]/s", "<ol>\\1</ol>", $text);
}
$text = preg_replace("/\[li\](.*?)\[\/li\]/s", "<li>\\1</li>", $text);
$text = preg_replace("#\[\*\]#s", "<li>", $text);
$text = preg_replace("#<br><(li|ul|ol|/ul|/ol)>#s", "<\\1>", $text);
$text = preg_replace("#\[align=(left|center|right)\](.*?)\[\/align\]#s", "<div style='text-align:\\1;'>\\2</div>", $text);
$text = $this->filter_spoiler($text);
$text = $this->insert_code($text);
return $text;
}
/** public function strip(string $text): string
* @param string $text {
* @return string foreach ([
*/ "b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4",
private function filter_spoiler(/*string*/ $text) { "code", "url", "email", "li",
return str_replace( ] as $el) {
array("[spoiler]","[/spoiler]"), $text = preg_replace("!\[$el\](.*?)\[/$el\]!s", '$1', $text);
array("<span style=\"background-color:#000; color:#000;\">","</span>"), }
$text); $text = preg_replace("!\[anchor=(.*?)\](.*?)\[/anchor\]!s", '$2', $text);
} $text = preg_replace("!\[url=(.*?)\](.*?)\[/url\]!s", '$2', $text);
$text = preg_replace("!\[img\](.*?)\[/img\]!s", "", $text);
$text = preg_replace("!\[\[([^\|\]]+)\|([^\]]+)\]\]!s", '$2', $text);
$text = preg_replace("!\[\[([^\]]+)\]\]!s", '$1', $text);
$text = preg_replace("!\[quote\](.*?)\[/quote\]!s", "", $text);
$text = preg_replace("!\[quote=(.*?)\](.*?)\[/quote\]!s", "", $text);
$text = preg_replace("!\[/?(list|ul|ol)\]!", "", $text);
$text = preg_replace("!\[\*\](.*?)!s", '$1', $text);
$text = $this->strip_spoiler($text);
return $text;
}
/** private function filter_spoiler(string $text): string
* @param string $text {
* @return string return str_replace(
*/ ["[spoiler]","[/spoiler]"],
private function strip_spoiler(/*string*/ $text) { ["<span style=\"background-color:#000; color:#000;\">","</span>"],
$l1 = strlen("[spoiler]"); $text
$l2 = strlen("[/spoiler]"); );
while(true) { }
$start = strpos($text, "[spoiler]");
if($start === false) break;
$end = strpos($text, "[/spoiler]"); private function strip_spoiler(string $text): string
if($end === false) break; {
$l1 = strlen("[spoiler]");
$l2 = strlen("[/spoiler]");
while (true) {
$start = strpos($text, "[spoiler]");
if ($start === false) {
break;
}
if($end < $start) break; $end = strpos($text, "[/spoiler]");
if ($end === false) {
break;
}
$beginning = substr($text, 0, $start); if ($end < $start) {
$middle = str_rot13(substr($text, $start+$l1, ($end-$start-$l1))); break;
$ending = substr($text, $end + $l2, (strlen($text)-$end+$l2)); }
$text = $beginning . $middle . $ending; $beginning = substr($text, 0, $start);
} $middle = str_rot13(substr($text, $start+$l1, ($end-$start-$l1)));
return $text; $ending = substr($text, $end + $l2, (strlen($text)-$end+$l2));
}
/** $text = $beginning . $middle . $ending;
* @param string $text }
* @return string return $text;
*/ }
private function extract_code(/*string*/ $text) {
# at the end of this function, the only code! blocks should be
# the ones we've added -- others may contain malicious content,
# which would only appear after decoding
$text = str_replace("[code!]", "[code]", $text);
$text = str_replace("[/code!]", "[/code]", $text);
$l1 = strlen("[code]"); private function extract_code(string $text): string
$l2 = strlen("[/code]"); {
while(true) { # at the end of this function, the only code! blocks should be
$start = strpos($text, "[code]"); # the ones we've added -- others may contain malicious content,
if($start === false) break; # which would only appear after decoding
$text = str_replace("[code!]", "[code]", $text);
$text = str_replace("[/code!]", "[/code]", $text);
$end = strpos($text, "[/code]", $start); $l1 = strlen("[code]");
if($end === false) break; $l2 = strlen("[/code]");
while (true) {
$start = strpos($text, "[code]");
if ($start === false) {
break;
}
if($end < $start) break; $end = strpos($text, "[/code]", $start);
if ($end === false) {
break;
}
$beginning = substr($text, 0, $start); if ($end < $start) {
$middle = base64_encode(substr($text, $start+$l1, ($end-$start-$l1))); break;
$ending = substr($text, $end + $l2, (strlen($text)-$end+$l2)); }
$text = $beginning . "[code!]" . $middle . "[/code!]" . $ending; $beginning = substr($text, 0, $start);
} $middle = base64_encode(substr($text, $start+$l1, ($end-$start-$l1)));
return $text; $ending = substr($text, $end + $l2, (strlen($text)-$end+$l2));
}
/** $text = $beginning . "[code!]" . $middle . "[/code!]" . $ending;
* @param string $text }
* @return string return $text;
*/ }
private function insert_code(/*string*/ $text) {
$l1 = strlen("[code!]");
$l2 = strlen("[/code!]");
while(true) {
$start = strpos($text, "[code!]");
if($start === false) break;
$end = strpos($text, "[/code!]"); private function insert_code(string $text): string
if($end === false) break; {
$l1 = strlen("[code!]");
$l2 = strlen("[/code!]");
while (true) {
$start = strpos($text, "[code!]");
if ($start === false) {
break;
}
$beginning = substr($text, 0, $start); $end = strpos($text, "[/code!]");
$middle = base64_decode(substr($text, $start+$l1, ($end-$start-$l1))); if ($end === false) {
$ending = substr($text, $end + $l2, (strlen($text)-$end+$l2)); break;
}
$text = $beginning . "<pre>" . $middle . "</pre>" . $ending; $beginning = substr($text, 0, $start);
} $middle = base64_decode(substr($text, $start+$l1, ($end-$start-$l1)));
return $text; $ending = substr($text, $end + $l2, (strlen($text)-$end+$l2));
}
$text = $beginning . "<pre>" . $middle . "</pre>" . $ending;
}
return $text;
}
} }

18
ext/bbcode/script.js Normal file
View 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());
}
});
});

View File

@ -1,85 +1,110 @@
<?php <?php
class BBCodeTest extends ShimmiePHPUnitTestCase { class BBCodeTest extends ShimmiePHPUnitTestCase
public function testBasics() { {
$this->assertEquals( public function testBasics()
$this->filter("[b]bold[/b][i]italic[/i]"), {
"<b>bold</b><i>italic</i>"); $this->assertEquals(
} $this->filter("[b]bold[/b][i]italic[/i]"),
"<b>bold</b><i>italic</i>"
);
}
public function testStacking() { public function testStacking()
$this->assertEquals( {
$this->filter("[b]B[/b][i]I[/i][b]B[/b]"), $this->assertEquals(
"<b>B</b><i>I</i><b>B</b>"); $this->filter("[b]B[/b][i]I[/i][b]B[/b]"),
$this->assertEquals( "<b>B</b><i>I</i><b>B</b>"
$this->filter("[b]bold[i]bolditalic[/i]bold[/b]"), );
"<b>bold<i>bolditalic</i>bold</b>"); $this->assertEquals(
} $this->filter("[b]bold[i]bolditalic[/i]bold[/b]"),
"<b>bold<i>bolditalic</i>bold</b>"
);
}
public function testFailure() { public function testFailure()
$this->assertEquals( {
$this->filter("[b]bold[i]italic"), $this->assertEquals(
"[b]bold[i]italic"); $this->filter("[b]bold[i]italic"),
} "[b]bold[i]italic"
);
}
public function testCode() { public function testCode()
$this->assertEquals( {
$this->filter("[code][b]bold[/b][/code]"), $this->assertEquals(
"<pre>[b]bold[/b]</pre>"); $this->filter("[code][b]bold[/b][/code]"),
} "<pre>[b]bold[/b]</pre>"
);
}
public function testNestedList() { public function testNestedList()
$this->assertEquals( {
$this->filter("[list][*]a[list][*]a[*]b[/list][*]b[/list]"), $this->assertEquals(
"<ul><li>a<ul><li>a<li>b</ul><li>b</ul>"); $this->filter("[list][*]a[list][*]a[*]b[/list][*]b[/list]"),
$this->assertEquals( "<ul><li>a<ul><li>a<li>b</ul><li>b</ul>"
$this->filter("[ul][*]a[ol][*]a[*]b[/ol][*]b[/ul]"), );
"<ul><li>a<ol><li>a<li>b</ol><li>b</ul>"); $this->assertEquals(
} $this->filter("[ul][*]a[ol][*]a[*]b[/ol][*]b[/ul]"),
"<ul><li>a<ol><li>a<li>b</ol><li>b</ul>"
);
}
public function testSpoiler() { public function testSpoiler()
$this->assertEquals( {
$this->filter("[spoiler]ShishNet[/spoiler]"), $this->assertEquals(
"<span style=\"background-color:#000; color:#000;\">ShishNet</span>"); $this->filter("[spoiler]ShishNet[/spoiler]"),
$this->assertEquals( "<span style=\"background-color:#000; color:#000;\">ShishNet</span>"
$this->strip("[spoiler]ShishNet[/spoiler]"), );
"FuvfuArg"); $this->assertEquals(
#$this->assertEquals( $this->strip("[spoiler]ShishNet[/spoiler]"),
# $this->filter("[spoiler]ShishNet"), "FuvfuArg"
# "[spoiler]ShishNet"); );
} #$this->assertEquals(
# $this->filter("[spoiler]ShishNet"),
# "[spoiler]ShishNet");
}
public function testURL() { public function testURL()
$this->assertEquals( {
$this->filter("[url]http://shishnet.org[/url]"), $this->assertEquals(
"<a href=\"http://shishnet.org\">http://shishnet.org</a>"); $this->filter("[url]http://shishnet.org[/url]"),
$this->assertEquals( "<a href=\"http://shishnet.org\">http://shishnet.org</a>"
$this->filter("[url=http://shishnet.org]ShishNet[/url]"), );
"<a href=\"http://shishnet.org\">ShishNet</a>"); $this->assertEquals(
$this->assertEquals( $this->filter("[url=http://shishnet.org]ShishNet[/url]"),
$this->filter("[url=javascript:alert(\"owned\")]click to fail[/url]"), "<a href=\"http://shishnet.org\">ShishNet</a>"
"[url=javascript:alert(\"owned\")]click to fail[/url]"); );
} $this->assertEquals(
$this->filter("[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->filter("[email]spam@shishnet.org[/email]"), $this->assertEquals(
"<a href=\"mailto:spam@shishnet.org\">spam@shishnet.org</a>"); $this->filter("[email]spam@shishnet.org[/email]"),
} "<a href=\"mailto:spam@shishnet.org\">spam@shishnet.org</a>"
);
}
public function testAnchor() { public function testAnchor()
$this->assertEquals( {
$this->filter("[anchor=rules]Rules[/anchor]"), $this->assertEquals(
'<span class="anchor">Rules <a class="alink" href="#bb-rules" name="bb-rules" title="link to this anchor"> ¶ </a></span>'); $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>'
);
}
private function filter($in) { private function filter($in)
$bb = new BBCode(); {
return $bb->format($in); $bb = new BBCode();
} return $bb->format($in);
}
private function strip($in) { private function strip($in)
$bb = new BBCode(); {
return $bb->strip($in); $bb = new BBCode();
} return $bb->strip($in);
}
} }

21
ext/blocks/info.php Normal file
View 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)";
}

View File

@ -1,17 +1,12 @@
<?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) { {
global $config, $database; public function onInitExt(InitExtEvent $event)
if($config->get_int("ext_blocks_version") < 1) { {
$database->create_table("blocks", " global $config, $database;
if ($config->get_int("ext_blocks_version") < 1) {
$database->create_table("blocks", "
id SCORE_AIPK, id SCORE_AIPK,
pages VARCHAR(128) NOT NULL, pages VARCHAR(128) NOT NULL,
title VARCHAR(128) NOT NULL, title 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; {
if($user->can("manage_blocks")) { global $user;
$event->add_link("Blocks Editor", make_link("blocks/list")); if ($event->parent==="system") {
} if ($user->can(Permissions::MANAGE_BLOCKS)) {
} $event->add_nav_link("blocks", new Link('blocks/list'), "Blocks Editor");
}
}
}
public function onPageRequest(PageRequestEvent $event) { public function onUserBlockBuilding(UserBlockBuildingEvent $event)
global $database, $page, $user; {
global $user;
if ($user->can(Permissions::MANAGE_BLOCKS)) {
$event->add_link("Blocks Editor", make_link("blocks/list"));
}
}
$blocks = $database->cache->get("blocks"); public function onPageRequest(PageRequestEvent $event)
if($blocks === false) { {
$blocks = $database->get_all("SELECT * FROM blocks"); global $database, $page, $user;
$database->cache->set("blocks", $blocks, 600);
}
foreach($blocks as $block) {
$path = implode("/", $event->args);
if(strlen($path) < 4000 && fnmatch($block['pages'], $path)) {
$b = new Block($block['title'], $block['content'], $block['area'], $block['priority']);
$b->is_content = false;
$page->add_block($b);
}
}
if($event->page_matches("blocks") && $user->can("manage_blocks")) { $blocks = $database->cache->get("blocks");
if($event->get_arg(0) == "add") { if ($blocks === false) {
if($user->check_auth_token()) { $blocks = $database->get_all("SELECT * FROM blocks");
$database->execute(" $database->cache->set("blocks", $blocks, 600);
}
foreach ($blocks as $block) {
$path = implode("/", $event->args);
if (strlen($path) < 4000 && fnmatch($block['pages'], $path)) {
$b = new Block($block['title'], $block['content'], $block['area'], $block['priority']);
$b->is_content = false;
$page->add_block($b);
}
}
if ($event->page_matches("blocks") && $user->can(Permissions::MANAGE_BLOCKS)) {
if ($event->get_arg(0) == "add") {
if ($user->check_auth_token()) {
$database->execute("
INSERT INTO blocks (pages, title, area, priority, content) 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")); }
} }
} }
}
} }

View File

@ -1,10 +1,11 @@
<?php <?php
class BlocksTest extends ShimmiePHPUnitTestCase { class BlocksTest extends ShimmiePHPUnitTestCase
public function testBlocks() { {
$this->log_in_as_admin(); public function testBlocks()
$this->get_page("blocks/list"); {
$this->assert_response(200); $this->log_in_as_admin();
$this->assert_title("Blocks"); $this->get_page("blocks/list");
} $this->assert_response(200);
$this->assert_title("Blocks");
}
} }

View File

@ -1,46 +1,47 @@
<?php <?php
class BlocksTheme extends Themelet { class BlocksTheme extends Themelet
public function display_blocks($blocks) { {
global $page; public function display_blocks($blocks)
{
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>";
$html .= "<th>Title</th><td><input type='text' name='title' value='".html_escape($block['title'])."'></td>"; $html .= "<th>Title</th><td><input type='text' name='title' value='".html_escape($block['title'])."'></td>";
$html .= "<th>Area</th><td><input type='text' name='area' value='".html_escape($block['area'])."'></td>"; $html .= "<th>Area</th><td><input type='text' name='area' value='".html_escape($block['area'])."'></td>";
$html .= "<th>Priority</th><td><input type='text' name='priority' value='".html_escape($block['priority'])."'></td>"; $html .= "<th>Priority</th><td><input type='text' name='priority' value='".html_escape($block['priority'])."'></td>";
$html .= "<th>Pages</th><td><input type='text' name='pages' value='".html_escape($block['pages'])."'></td>"; $html .= "<th>Pages</th><td><input type='text' name='pages' value='".html_escape($block['pages'])."'></td>";
$html .= "<th>Delete</th><td><input type='checkbox' name='delete'></td>"; $html .= "<th>Delete</th><td><input type='checkbox' name='delete'></td>";
$html .= "<td><input type='submit' value='Save'></td>"; $html .= "<td><input type='submit' value='Save'></td>";
$html .= "</tr>"; $html .= "</tr>";
$html .= "<tr>"; $html .= "<tr>";
$html .= "<td colspan='11'><textarea rows='5' name='content'>".html_escape($block['content'])."</textarea></td>"; $html .= "<td colspan='11'><textarea rows='5' name='content'>".html_escape($block['content'])."</textarea></td>";
$html .= "</tr>\n"; $html .= "</tr>\n";
$html .= "<tr>"; $html .= "<tr>";
$html .= "<td colspan='11'>&nbsp;</td>"; $html .= "<td colspan='11'>&nbsp;</td>";
$html .= "</tr>\n"; $html .= "</tr>\n";
$html .= "</form>\n"; $html .= "</form>\n";
} }
$html .= make_form(make_link("blocks/add")); $html .= make_form(make_link("blocks/add"));
$html .= "<tr>"; $html .= "<tr>";
$html .= "<th>Title</th><td><input type='text' name='title' value=''></td>"; $html .= "<th>Title</th><td><input type='text' name='title' value=''></td>";
$html .= "<th>Area</th><td><select name='area'><option>left<option>main</select></td>"; $html .= "<th>Area</th><td><select name='area'><option>left<option>main</select></td>";
$html .= "<th>Priority</th><td><input type='text' name='priority' value='50'></td>"; $html .= "<th>Priority</th><td><input type='text' name='priority' value='50'></td>";
$html .= "<th>Pages</th><td><input type='text' name='pages' value='post/list*'></td>"; $html .= "<th>Pages</th><td><input type='text' name='pages' value='post/list*'></td>";
$html .= "<td colspan='3'><input type='submit' value='Add'></td>"; $html .= "<td colspan='3'><input type='submit' value='Add'></td>";
$html .= "</tr>"; $html .= "</tr>";
$html .= "<tr>"; $html .= "<tr>";
$html .= "<td colspan='11'><textarea rows='5' name='content'></textarea></td>"; $html .= "<td colspan='11'><textarea rows='5' name='content'></textarea></td>";
$html .= "</tr>\n"; $html .= "</tr>\n";
$html .= "</form>"; $html .= "</form>";
$html .= "</table>"; $html .= "</table>";
$page->set_title("Blocks"); $page->set_title("Blocks");
$page->set_heading("Blocks"); $page->set_heading("Blocks");
$page->add_block(new NavBlock()); $page->add_block(new NavBlock());
$page->add_block(new Block("Block Editor", $html)); $page->add_block(new Block("Block Editor", $html));
} }
} }

22
ext/blotter/info.php Normal file
View 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";
}

View File

@ -1,132 +1,152 @@
<?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. /**
* * I love re-using this installer don't I...
* Development TODO at http://github.com/zshall/shimmie2/issues */
*/ global $config;
class Blotter extends Extension { $version = $config->get_int("blotter_version", 0);
public function onInitExt(InitExtEvent $event) { /**
/** * If this version is less than "1", it's time to install.
* I love re-using this installer don't I... *
*/ * REMINDER: If I change the database tables, I must change up version by 1.
global $config; */
$version = $config->get_int("blotter_version", 0); if ($version < 1) {
/** /**
* If this version is less than "1", it's time to install. * Installer
* */
* REMINDER: If I change the database tables, I must change up version by 1. global $database, $config;
*/ $database->create_table("blotter", "
if($version < 1) {
/**
* Installer
*/
global $database, $config;
$database->create_table("blotter", "
id SCORE_AIPK, id SCORE_AIPK,
entry_date SCORE_DATETIME DEFAULT SCORE_NOW, entry_date SCORE_DATETIME DEFAULT SCORE_NOW,
entry_text TEXT NOT NULL, entry_text TEXT NOT NULL,
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(), ?, ?)",
log_info("blotter", "Installed tables for blotter extension."); ["Installed the blotter extension!", "Y"]
$config->set_int("blotter_version", 1); );
} log_info("blotter", "Installed tables for blotter extension.");
// Set default config: $config->set_int("blotter_version", 1);
$config->set_default_int("blotter_recent", 5); }
$config->set_default_string("blotter_color", "FF0000"); // Set default config:
$config->set_default_string("blotter_position", "subheading"); $config->set_default_int("blotter_recent", 5);
} $config->set_default_string("blotter_color", "FF0000");
$config->set_default_string("blotter_position", "subheading");
}
public function onSetupBuilding(SetupBuildingEvent $event) { public function onSetupBuilding(SetupBuildingEvent $event)
$sb = new SetupBlock("Blotter"); {
$sb->add_int_option("blotter_recent", "<br />Number of recent entries to display: "); $sb = new SetupBlock("Blotter");
$sb->add_text_option("blotter_color", "<br />Color of important updates: (ABCDEF format) "); $sb->add_int_option("blotter_recent", "<br />Number of recent entries to display: ");
$sb->add_choice_option("blotter_position", array("Top of page" => "subheading", "In navigation bar" => "left"), "<br>Position: "); $sb->add_text_option("blotter_color", "<br />Color of important updates: (ABCDEF format) ");
$event->panel->add_block($sb); $sb->add_choice_option("blotter_position", ["Top of page" => "subheading", "In navigation bar" => "left"], "<br>Position: ");
} $event->panel->add_block($sb);
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event) { public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
global $user; {
if($user->is_admin()) { global $user;
$event->add_link("Blotter Editor", make_link("blotter/editor")); if ($event->parent==="system") {
} if ($user->can(Permissions::BLOTTER_ADMIN)) {
} $event->add_nav_link("blotter", new Link('blotter/editor'), "Blotter Editor");
}
}
}
public function onPageRequest(PageRequestEvent $event) {
global $page, $database, $user;
if($event->page_matches("blotter")) {
switch($event->get_arg(0)) {
case "editor":
/**
* Displays the blotter editor.
*/
if(!$user->is_admin()) {
$this->theme->display_permission_denied();
} else {
$entries = $database->get_all("SELECT * FROM blotter ORDER BY id DESC");
$this->theme->display_editor($entries);
}
break;
case "add":
/**
* Adds an entry
*/
if(!$user->is_admin() || !$user->check_auth_token()) {
$this->theme->display_permission_denied();
} else {
$entry_text = $_POST['entry_text'];
if($entry_text == "") { die("No entry message!"); }
if(isset($_POST['important'])) { $important = 'Y'; } else { $important = 'N'; }
// Now insert into db:
$database->execute("INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), ?, ?)",
array($entry_text, $important));
log_info("blotter", "Added Message: $entry_text");
$page->set_mode("redirect");
$page->set_redirect(make_link("blotter/editor"));
}
break;
case "remove":
/**
* Removes an entry
*/
if(!$user->is_admin() || !$user->check_auth_token()) {
$this->theme->display_permission_denied();
} else {
$id = int_escape($_POST['id']);
if(!isset($id)) { die("No ID!"); }
$database->Execute("DELETE FROM blotter WHERE id=:id", array("id"=>$id));
log_info("blotter", "Removed Entry #$id");
$page->set_mode("redirect");
$page->set_redirect(make_link("blotter/editor"));
}
break;
case "list":
/**
* Displays all blotter entries
*/
$entries = $database->get_all("SELECT * FROM blotter ORDER BY id DESC");
$this->theme->display_blotter_page($entries);
break;
}
}
/**
* Finally, display the blotter on whatever page we're viewing.
*/
$this->display_blotter();
}
private function display_blotter() { public function onUserBlockBuilding(UserBlockBuildingEvent $event)
global $database, $config; {
$limit = $config->get_int("blotter_recent", 5); global $user;
$sql = 'SELECT * FROM blotter ORDER BY id DESC LIMIT '.intval($limit); if ($user->can(Permissions::BLOTTER_ADMIN)) {
$entries = $database->get_all($sql); $event->add_link("Blotter Editor", make_link("blotter/editor"));
$this->theme->display_blotter($entries); }
} }
public function onPageRequest(PageRequestEvent $event)
{
global $page, $database, $user;
if ($event->page_matches("blotter")) {
switch ($event->get_arg(0)) {
case "editor":
/**
* Displays the blotter editor.
*/
if (!$user->can(Permissions::BLOTTER_ADMIN)) {
$this->theme->display_permission_denied();
} else {
$entries = $database->get_all("SELECT * FROM blotter ORDER BY id DESC");
$this->theme->display_editor($entries);
}
break;
case "add":
/**
* Adds an entry
*/
if (!$user->can(Permissions::BLOTTER_ADMIN) || !$user->check_auth_token()) {
$this->theme->display_permission_denied();
} else {
$entry_text = $_POST['entry_text'];
if ($entry_text == "") {
die("No entry message!");
}
if (isset($_POST['important'])) {
$important = 'Y';
} else {
$important = 'N';
}
// Now insert into db:
$database->execute(
"INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), ?, ?)",
[$entry_text, $important]
);
log_info("blotter", "Added Message: $entry_text");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("blotter/editor"));
}
break;
case "remove":
/**
* Removes an entry
*/
if (!$user->can(Permissions::BLOTTER_ADMIN) || !$user->check_auth_token()) {
$this->theme->display_permission_denied();
} else {
$id = int_escape($_POST['id']);
if (!isset($id)) {
die("No ID!");
}
$database->Execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]);
log_info("blotter", "Removed Entry #$id");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("blotter/editor"));
}
break;
case "list":
/**
* Displays all blotter entries
*/
$entries = $database->get_all("SELECT * FROM blotter ORDER BY id DESC");
$this->theme->display_blotter_page($entries);
break;
}
}
/**
* Finally, display the blotter on whatever page we're viewing.
*/
$this->display_blotter();
}
private function display_blotter()
{
global $database, $config;
$limit = $config->get_int("blotter_recent", 5);
$sql = 'SELECT * FROM blotter ORDER BY id DESC LIMIT '.intval($limit);
$entries = $database->get_all($sql);
$this->theme->display_blotter($entries);
}
} }

View File

@ -1,34 +1,38 @@
<?php <?php
class BlotterTest extends ShimmiePHPUnitTestCase { class BlotterTest extends ShimmiePHPUnitTestCase
public function testLogin() { {
$this->log_in_as_admin(); public function testLogin()
//$this->assert_text("Blotter Editor"); {
//$this->click("Blotter Editor"); $this->log_in_as_admin();
//$this->log_out(); //$this->assert_text("Blotter Editor");
} //$this->click("Blotter Editor");
//$this->log_out();
}
public function testDenial() { public function testDenial()
$this->get_page("blotter/editor"); {
$this->assert_response(403); $this->get_page("blotter/editor");
$this->get_page("blotter/add"); $this->assert_response(403);
$this->assert_response(403); $this->get_page("blotter/add");
$this->get_page("blotter/remove"); $this->assert_response(403);
$this->assert_response(403); $this->get_page("blotter/remove");
} $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");
//$this->set_field("entry_text", "blotter testing"); //$this->set_field("entry_text", "blotter testing");
//$this->click("Add"); //$this->click("Add");
//$this->assert_text("blotter testing"); //$this->assert_text("blotter testing");
$this->get_page("blotter"); $this->get_page("blotter");
//$this->assert_text("blotter testing"); //$this->assert_text("blotter testing");
$this->get_page("blotter/editor"); $this->get_page("blotter/editor");
//$this->click("Remove"); //$this->click("Remove");
//$this->assert_no_text("blotter testing"); //$this->assert_no_text("blotter testing");
} }
} }

View File

@ -1,45 +1,50 @@
<?php <?php
class BlotterTheme extends Themelet { class BlotterTheme extends Themelet
public function display_editor($entries) { {
global $page; public function display_editor($entries)
$html = $this->get_html_for_blotter_editor($entries); {
$page->set_title("Blotter Editor"); global $page;
$page->set_heading("Blotter Editor"); $html = $this->get_html_for_blotter_editor($entries);
$page->add_block(new Block("Welcome to the Blotter Editor!", $html, "main", 10)); $page->set_title("Blotter Editor");
$page->add_block(new Block("Navigation", "<a href='".make_link()."'>Index</a>", "left", 0)); $page->set_heading("Blotter Editor");
} $page->add_block(new Block("Welcome to the Blotter Editor!", $html, "main", 10));
$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; {
$html = $this->get_html_for_blotter_page($entries); global $page;
$page->set_title("Blotter"); $html = $this->get_html_for_blotter_page($entries);
$page->set_heading("Blotter"); $page->set_title("Blotter");
$page->add_block(new Block("Blotter Entries", $html, "main", 10)); $page->set_heading("Blotter");
} $page->add_block(new Block("Blotter Entries", $html, "main", 10));
}
public function display_blotter($entries) { public function display_blotter($entries)
global $page, $config; {
$html = $this->get_html_for_blotter($entries); global $page, $config;
$position = $config->get_string("blotter_position", "subheading"); $html = $this->get_html_for_blotter($entries);
$page->add_block(new Block(null, $html, $position, 20)); $position = $config->get_string("blotter_position", "subheading");
} $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;
/** /**
* Long function name, but at least I won't confuse it with something else ^_^ * Long function name, but at least I won't confuse it with something else ^_^
*/ */
// Add_new stuff goes here. // Add_new stuff goes here.
$table_header = " $table_header = "
<tr> <tr>
<th>Date</th> <th>Date</th>
<th>Message</th> <th>Message</th>
<th>Important?</th> <th>Important?</th>
<th>Action</th> <th>Action</th>
</tr>"; </tr>";
$add_new = " $add_new = "
<tr class='even'> <tr class='even'>
".make_form(make_link("blotter/add"))." ".make_form(make_link("blotter/add"))."
<td colspan='2'><textarea style='text-align:left;' name='entry_text' rows='2' /></textarea></td> <td colspan='2'><textarea style='text-align:left;' name='entry_text' rows='2' /></textarea></td>
@ -49,21 +54,25 @@ class BlotterTheme extends Themelet {
</tr>"; </tr>";
// Now, time for entries list. // Now, time for entries list.
$table_rows = ""; $table_rows = "";
$num_entries = count($entries); $num_entries = count($entries);
for ($i = 0 ; $i < $num_entries ; $i++) { for ($i = 0 ; $i < $num_entries ; $i++) {
/** /**
* Add table rows * Add table rows
*/ */
$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 .=
"<tr> "<tr>
<td>$entry_date</td> <td>$entry_date</td>
<td>$entry_text</td> <td>$entry_text</td>
<td>$important</td> <td>$important</td>
@ -74,9 +83,9 @@ class BlotterTheme extends Themelet {
</form> </form>
</td> </td>
</tr>"; </tr>";
} }
$html = " $html = "
<table id='blotter_entries' class='zebra'> <table id='blotter_entries' class='zebra'>
<thead>$table_header</thead> <thead>$table_header</thead>
<tbody>$add_new</tbody> <tbody>$add_new</tbody>
@ -87,82 +96,83 @@ class BlotterTheme extends Themelet {
<b>Help:</b><br /> <b>Help:</b><br />
<blockquote>Add entries to the blotter, and they will be displayed.</blockquote>"; <blockquote>Add entries to the blotter, and they will be displayed.</blockquote>";
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.
global $config; */
$i_color = $config->get_string("blotter_color", "#FF0000"); global $config;
$html = "<pre>"; $i_color = $config->get_string("blotter_color", "#FF0000");
$html = "<pre>";
$num_entries = count($entries); $num_entries = count($entries);
for ($i = 0 ; $i < $num_entries ; $i++) { for ($i = 0 ; $i < $num_entries ; $i++) {
/** /**
* Blotter entries * Blotter entries
*/ */
// Reset variables: // Reset variables:
$i_open = ""; $i_open = "";
$i_close = ""; $i_close = "";
//$id = $entries[$i]['id']; //$id = $entries[$i]['id'];
$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>";
} }
$html .= "{$i_open}{$clean_date} - {$entry_text}{$i_close}<br /><br />"; $html .= "{$i_open}{$clean_date} - {$entry_text}{$i_close}<br /><br />";
} }
$html .= "</pre>"; $html .= "</pre>";
return $html; return $html;
} }
private function get_html_for_blotter($entries) { private function get_html_for_blotter($entries)
global $config; {
$i_color = $config->get_string("blotter_color", "#FF0000"); global $config;
$position = $config->get_string("blotter_position", "subheading"); $i_color = $config->get_string("blotter_color", "#FF0000");
$entries_list = ""; $position = $config->get_string("blotter_position", "subheading");
$num_entries = count($entries); $entries_list = "";
for ($i = 0 ; $i < $num_entries ; $i++) { $num_entries = count($entries);
/** for ($i = 0 ; $i < $num_entries ; $i++) {
* Blotter entries /**
*/ * Blotter entries
// Reset variables: */
$i_open = ""; // Reset variables:
$i_close = ""; $i_open = "";
//$id = $entries[$i]['id']; $i_close = "";
$messy_date = $entries[$i]['entry_date']; //$id = $entries[$i]['id'];
$clean_date = date("m/d/y", strtotime($messy_date)); $messy_date = $entries[$i]['entry_date'];
$entry_text = $entries[$i]['entry_text']; $clean_date = date("m/d/y", strtotime($messy_date));
if($entries[$i]['important'] == 'Y') { $entry_text = $entries[$i]['entry_text'];
$i_open = "<font color='#{$i_color}'>"; if ($entries[$i]['important'] == 'Y') {
$i_close="</font>"; $i_open = "<font color='#{$i_color}'>";
} $i_close="</font>";
$entries_list .= "<li>{$i_open}{$clean_date} - {$entry_text}{$i_close}</li>"; }
} $entries_list .= "<li>{$i_open}{$clean_date} - {$entry_text}{$i_close}</li>";
}
$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>"; }
}
$html = " $html = "
<div id='blotter1' class='shm-blotter1'> <div id='blotter1' class='shm-blotter1'>
<span>$out_text</span> <span>$out_text</span>
{$pos_break} {$pos_break}
@ -173,6 +183,6 @@ class BlotterTheme extends Themelet {
</div> </div>
<div id='blotter2' class='shm-blotter2'>$in_text</div> <div id='blotter2' class='shm-blotter2'>$in_text</div>
"; ";
return $html; return $html;
} }
} }

View 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";
}

View File

@ -1,43 +1,33 @@
<?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) { {
global $config; public function onInitExt(InitExtEvent $event)
$config->set_default_string("search_suggestions_results_order", 'a'); {
} global $config;
$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 = "
<SearchPlugin xmlns='http://www.mozilla.org/2006/browser/search/' xmlns:os='http://a9.com/-/spec/opensearch/1.1/'> <SearchPlugin xmlns='http://www.mozilla.org/2006/browser/search/' xmlns:os='http://a9.com/-/spec/opensearch/1.1/'>
<os:ShortName>$search_title</os:ShortName> <os:ShortName>$search_title</os:ShortName>
<os:InputEncoding>UTF-8</os:InputEncoding> <os:InputEncoding>UTF-8</os:InputEncoding>
@ -50,55 +40,53 @@ class BrowserSearch extends Extension {
</SearchPlugin> </SearchPlugin>
"; ";
// 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 (
$event->page_matches("browser_search") &&
!$config->get_bool("disable_search_suggestions")
) {
// We have to build some json stuff
$tag_search = $event->get_arg(0);
else if( // Now to get DB results
$event->page_matches("browser_search") && if ($config->get_string("search_suggestions_results_order") == "a") {
!$config->get_bool("disable_search_suggestions") $tags = $database->execute("SELECT tag FROM tags WHERE tag LIKE ? AND count > 0 ORDER BY tag ASC LIMIT 30", [$tag_search."%"]);
) { } else {
// We have to build some json stuff $tags = $database->execute("SELECT tag FROM tags WHERE tag LIKE ? AND count > 0 ORDER BY count DESC LIMIT 30", [$tag_search."%"]);
$tag_search = $event->get_arg(0); }
// Now to get DB results
if($config->get_string("search_suggestions_results_order") == "a") {
$tags = $database->execute("SELECT tag FROM tags WHERE tag LIKE ? AND count > 0 ORDER BY tag ASC LIMIT 30",array($tag_search."%"));
} else {
$tags = $database->execute("SELECT tag FROM tags WHERE tag LIKE ? AND count > 0 ORDER BY count DESC LIMIT 30",array($tag_search."%"));
}
// And to do stuff with it. We want our output to look like: // And to do stuff with it. We want our output to look like:
// ["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['Alphabetical'] = 'a'; $sort_by = [];
$sort_by['Tag Count'] = 't'; $sort_by['Alphabetical'] = 'a';
$sort_by['Tag Count'] = 't';
$sb = new SetupBlock("Browser Search"); $sb = new SetupBlock("Browser Search");
$sb->add_bool_option("disable_search_suggestions", "Disable search suggestions: "); $sb->add_bool_option("disable_search_suggestions", "Disable search suggestions: ");
$sb->add_label("<br>"); $sb->add_label("<br>");
$sb->add_choice_option("search_suggestions_results_order", $sort_by, "Sort the suggestions by:"); $sb->add_choice_option("search_suggestions_results_order", $sort_by, "Sort the suggestions by:");
$event->panel->add_block($sb); $event->panel->add_block($sb);
} }
} }

View File

@ -1,8 +1,9 @@
<?php <?php
class BrowserSearchTest extends ShimmiePHPUnitTestCase { class BrowserSearchTest extends ShimmiePHPUnitTestCase
public function testBasic() { {
$this->get_page("browser_search/please_dont_use_this_tag_as_it_would_break_stuff__search.xml"); public function testBasic()
$this->get_page("browser_search/test"); {
} $this->get_page("browser_search/please_dont_use_this_tag_as_it_would_break_stuff__search.xml");
$this->get_page("browser_search/test");
}
} }

23
ext/bulk_actions/info.php Normal file
View 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
View 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
View 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("");
});

View File

@ -0,0 +1,10 @@
.selected {
outline: 3px solid blue;
}
.bulk_action {
margin-top: 8pt;
}
.bulk_selector_controls table td {
width: 33%;
}

View 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
View 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
";
}

View File

@ -1,74 +1,62 @@
<?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->results = array(); $this->dir = $dir;
} $this->results = [];
}
} }
class BulkAdd extends Extension { class BulkAdd extends Extension
public function onPageRequest(PageRequestEvent $event) { {
global $page, $user; public function onPageRequest(PageRequestEvent $event)
if($event->page_matches("bulk_add")) { {
if($user->is_admin() && $user->check_auth_token() && isset($_POST['dir'])) { global $page, $user;
set_time_limit(0); if ($event->page_matches("bulk_add")) {
$bae = new BulkAddEvent($_POST['dir']); if ($user->can(Permissions::BULK_ADD) && $user->check_auth_token() && isset($_POST['dir'])) {
send_event($bae); set_time_limit(0);
if(is_array($bae->results)) { $bae = new BulkAddEvent($_POST['dir']);
foreach($bae->results as $result) { send_event($bae);
$this->theme->add_status("Adding files", $result); foreach ($bae->results as $result) {
} $this->theme->add_status("Adding files", $result);
} else if(strlen($bae->results) > 0) { }
$this->theme->add_status("Adding files", $bae->results); $this->theme->display_upload_results($page);
} }
$this->theme->display_upload_results($page); }
} }
}
}
public function onCommand(CommandEvent $event) { public function onCommand(CommandEvent $event)
if($event->cmd == "help") { {
print "\tbulk-add [directory]\n"; if ($event->cmd == "help") {
print "\t\tImport this directory\n\n"; print "\tbulk-add [directory]\n";
} print "\t\tImport this directory\n\n";
if($event->cmd == "bulk-add") { }
if(count($event->args) == 1) { if ($event->cmd == "bulk-add") {
$bae = new BulkAddEvent($event->args[0]); if (count($event->args) == 1) {
send_event($bae); $bae = new BulkAddEvent($event->args[0]);
print(implode("\n", $bae->results)); send_event($bae);
} print(implode("\n", $bae->results));
} }
} }
}
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)) { {
$event->results = add_dir($event->dir); if (is_dir($event->dir) && is_readable($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