Merge remote-tracking branch 'upstream/develop' into custom_ratings

This commit is contained in:
matthew 2019-06-27 08:41:18 -05:00
commit 8e3b8a7a1b
180 changed files with 6574 additions and 3058 deletions

View File

@ -3,6 +3,7 @@
$finder = PhpCsFixer\Finder::create()
->exclude('ext/amazon_s3/lib')
->exclude('vendor')
->exclude('data')
->in(__DIR__)
;

View File

@ -99,24 +99,24 @@ For example, one can override the default anonymous "allow nothing"
permissions like so:
```php
new UserClass("anonymous", "base", array(
"create_comment" => True,
"edit_image_tag" => True,
"edit_image_source" => True,
"create_image_report" => True,
));
new UserClass("anonymous", "base", [
Permissions::CREATE_COMMENT => True,
Permissions::EDIT_IMAGE_TAG => True,
Permissions::EDIT_IMAGE_SOURCE => True,
Permissions::CREATE_IMAGE_REPORT => True,
]);
```
For a moderator class, being a regular user who can delete images and comments:
```php
new UserClass("moderator", "user", array(
"delete_image" => True,
"delete_comment" => True,
));
new UserClass("moderator", "user", [
Permissions::DELETE_IMAGE => True,
Permissions::DELETE_COMMENT => True,
]);
```
For a list of permissions, see `core/userclass.php`
For a list of permissions, see `core/permissions.php`
# Development Info

View File

@ -31,7 +31,7 @@
"ifixit/php-akismet" : "1.*",
"google/recaptcha" : "~1.1",
"dapphp/securimage" : "3.6.*",
"shish/libcontext-php" : "dev-master",
"shish/eventtracer-php" : "dev-master",
"enshrined/svg-sanitize" : "0.8.2",
"bower-asset/jquery" : "1.12.3",
@ -42,7 +42,7 @@
},
"require-dev" : {
"phpunit/phpunit" : "6.*"
"phpunit/phpunit" : "7.*"
},
"suggest": {

560
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,20 +4,25 @@
* actually do anything as far as the app is concerned
*/
global $config, $database, $user, $page, $_shm_ctx;
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/shish/libcontext-php/context.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
$_shm_ctx->log_start("Opening files");
$_tracer->begin("Bootstrap");
$_tracer->begin("Opening files");
$_shm_files = array_merge(
zglob("core/*.php"),
zglob("core/{".ENABLED_MODS."}/*.php"),
@ -30,23 +35,27 @@ foreach ($_shm_files as $_shm_filename) {
}
unset($_shm_files);
unset($_shm_filename);
$_shm_ctx->log_endok();
$_tracer->end();
// connect to the database
$_shm_ctx->log_start("Connecting to DB");
$_tracer->begin("Connecting to DB");
$database = new Database();
$config = new DatabaseConfig($database);
$_shm_ctx->log_endok();
$_tracer->end();
// load the theme parts
$_shm_ctx->log_start("Loading themelets");
$_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();
$_shm_ctx->log_endok();
$_tracer->end();
// hook up event handlers
$_tracer->begin("Loading extensions");
_load_event_listeners();
$_tracer->end();
send_event(new InitExtEvent());
$_tracer->end();

View File

@ -20,6 +20,8 @@
ob_start();
date_default_timezone_set('UTC');
define("DATABASE_TIMEOUT", 10000);
?>
<!DOCTYPE html>
<html>
@ -58,6 +60,9 @@ date_default_timezone_set('UTC');
<?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";
@ -110,7 +115,7 @@ function do_install()
{ // {{{
if (file_exists("data/config/auto_install.conf.php")) {
require_once "data/config/auto_install.conf.php";
} elseif (@$_POST["database_type"] == "sqlite") {
} 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'])) {
@ -121,7 +126,6 @@ function do_install()
}
define("CACHE_DSN", null);
define("DEBUG_SQL", false);
define("DATABASE_KA", true);
install_process();
} // }}}
@ -153,9 +157,9 @@ function ask_questions()
$drivers = PDO::getAvailableDrivers();
if (
!in_array("mysql", $drivers) &&
!in_array("pgsql", $drivers) &&
!in_array("sqlite", $drivers)
!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
@ -163,9 +167,9 @@ function ask_questions()
";
}
$db_m = in_array("mysql", $drivers) ? '<option value="mysql">MySQL</option>' : "";
$db_p = in_array("pgsql", $drivers) ? '<option value="pgsql">PostgreSQL</option>' : "";
$db_s = in_array("sqlite", $drivers) ? '<option value="sqlite">SQLite</option>' : "";
$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) : "";

View File

@ -58,7 +58,7 @@ class BaseThemelet
$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'));
$tsize = get_thumbnail_size($config->get_int(ImageConfig::THUMB_WIDTH), $config->get_int(ImageConfig::THUMB_WIDTH));
}
$custom_classes = "";

View File

@ -170,7 +170,7 @@ class Cache
{
$matches = [];
$c = null;
if ($dsn && preg_match("#(.*)://(.*)#", $dsn, $matches)) {
if ($dsn && preg_match("#(.*)://(.*)#", $dsn, $matches) && !isset($_GET['DISABLE_CACHE'])) {
if ($matches[1] == "memcache") {
$c = new MemcacheCache($matches[2]);
} elseif ($matches[1] == "memcached") {
@ -188,34 +188,34 @@ class Cache
public function get(string $key)
{
global $_tracer;
$_tracer->begin("Cache Query", ["key"=>$key]);
$val = $this->engine->get($key);
if ((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
$hit = $val === false ? "hit" : "miss";
file_put_contents("data/cache.log", "Cache $hit: $key\n", FILE_APPEND);
}
if ($val !== false) {
$res = "hit";
$this->hits++;
return $val;
} else {
$res = "miss";
$this->misses++;
return false;
}
$_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);
if ((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
file_put_contents("data/cache.log", "Cache set: $key ($time)\n", FILE_APPEND);
}
$_tracer->end();
}
public function delete(string $key)
{
global $_tracer;
$_tracer->begin("Cache Delete", ["key"=>$key]);
$this->engine->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);
}
$_tracer->end();
}
public function get_hits(): int

View File

@ -144,6 +144,13 @@ abstract class BaseConfig implements Config
}
}
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))) {
@ -170,6 +177,11 @@ abstract class BaseConfig implements Config
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);

View File

@ -1,9 +1,17 @@
<?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
@ -72,7 +80,7 @@ class Database
// 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") {
if (version_compare(PHP_VERSION, "6.9.9") == 1 && $this->get_driver_name() == DatabaseDriver::SQLITE) {
$ka = false;
}
@ -96,11 +104,11 @@ class Database
throw new SCoreException("Can't figure out database engine");
}
if ($db_proto === "mysql") {
if ($db_proto === DatabaseDriver::MYSQL) {
$this->engine = new MySQL();
} elseif ($db_proto === "pgsql") {
} elseif ($db_proto === DatabaseDriver::PGSQL) {
$this->engine = new PostgreSQL();
} elseif ($db_proto === "sqlite") {
} elseif ($db_proto === DatabaseDriver::SQLITE) {
$this->engine = new SQLite();
} else {
die('Unknown PDO driver: '.$db_proto);
@ -159,6 +167,19 @@ class Database
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;
} else if ($input===false) {
return $this->engine->BOOL_N;
}
return $input;
}
public function get_driver_name(): string
{
if (is_null($this->engine)) {
@ -167,35 +188,16 @@ class Database
return $this->engine->name;
}
private function count_execs(string $sql, array $inputarray): void
private function count_time(string $method, float $start, string $query, ?array $args): void
{
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);
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]);
}
if (!is_array($inputarray)) {
$this->query_count++;
}
# handle 2-dimensional input arrays
elseif (is_array(reset($inputarray))) {
$this->query_count += sizeof($inputarray);
} else {
$this->query_count++;
}
}
private function count_time(string $method, float $start): void
{
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;
$this->query_count++;
$this->dbtime += $dur;
}
public function execute(string $query, array $args=[]): PDOStatement
@ -204,7 +206,6 @@ class Database
if (is_null($this->db)) {
$this->connect_db();
}
$this->count_execs($query, $args);
$stmt = $this->db->prepare(
"-- " . str_replace("%2F", "/", urlencode(@$_GET['q'])). "\n" .
$query
@ -225,7 +226,7 @@ class Database
return $stmt;
} catch (PDOException $pdoe) {
throw new SCoreException($pdoe->getMessage()."<p><b>Query:</b> ".$query);
}
}
}
/**
@ -235,7 +236,18 @@ class Database
{
$_start = microtime(true);
$data = $this->execute($query, $args)->fetchAll();
$this->count_time("get_all", $_start);
$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;
}
@ -246,7 +258,7 @@ class Database
{
$_start = microtime(true);
$row = $this->execute($query, $args)->fetch();
$this->count_time("get_row", $_start);
$this->count_time("get_row", $_start, $query, $args);
return $row ? $row : null;
}
@ -256,27 +268,32 @@ class Database
public function get_col(string $query, array $args=[]): array
{
$_start = microtime(true);
$stmt = $this->execute($query, $args);
$res = [];
foreach ($stmt as $row) {
$res[] = $row[0];
}
$this->count_time("get_col", $_start);
$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 the first row => the second row.
* 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);
$stmt = $this->execute($query, $args);
$res = [];
foreach ($stmt as $row) {
$res[$row[0]] = $row[1];
}
$this->count_time("get_pairs", $_start);
$res = $this->execute($query, $args)->fetchAll(PDO::FETCH_KEY_PAIR);
$this->count_time("get_pairs", $_start, $query, $args);
return $res;
}
@ -287,7 +304,7 @@ class Database
{
$_start = microtime(true);
$row = $this->execute($query, $args)->fetch();
$this->count_time("get_one", $_start);
$this->count_time("get_one", $_start, $query, $args);
return $row[0];
}
@ -296,7 +313,7 @@ class Database
*/
public function get_last_insert_id(string $seq): int
{
if ($this->engine->name == "pgsql") {
if ($this->engine->name == DatabaseDriver::PGSQL) {
return $this->db->lastInsertId($seq);
} else {
return $this->db->lastInsertId();
@ -326,15 +343,15 @@ class Database
$this->connect_db();
}
if ($this->engine->name === "mysql") {
if ($this->engine->name === DatabaseDriver::MYSQL) {
return count(
$this->get_all("SHOW TABLES")
);
} elseif ($this->engine->name === "pgsql") {
} 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 === "sqlite") {
} elseif ($this->engine->name === DatabaseDriver::SQLITE) {
return count(
$this->get_all("SELECT name FROM sqlite_master WHERE type = 'table'")
);

View File

@ -1,9 +1,24 @@
<?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)
{
}
@ -22,7 +37,10 @@ class DBEngine
class MySQL extends DBEngine
{
/** @var string */
public $name = "mysql";
public $name = DatabaseDriver::MYSQL;
public $BOOL_Y = 'Y';
public $BOOL_N = 'N';
public function init(PDO $db)
{
@ -31,15 +49,15 @@ class MySQL extends DBEngine
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", "'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);
$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;
}
@ -53,8 +71,13 @@ class MySQL extends DBEngine
class PostgreSQL extends DBEngine
{
/** @var string */
public $name = "pgsql";
public $name = DatabaseDriver::PGSQL;
public $BOOL_Y = 't';
public $BOOL_N = 'f';
public function init(PDO $db)
{
@ -63,20 +86,20 @@ class PostgreSQL extends DBEngine
} else {
$db->exec("SET application_name TO 'shimmie [local]';");
}
$db->exec("SET statement_timeout TO 10000;");
$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", "'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);
$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;
}
@ -136,7 +159,11 @@ function _ln($n)
class SQLite extends DBEngine
{
/** @var string */
public $name = "sqlite";
public $name = DatabaseDriver::SQLITE;
public $BOOL_Y = 'Y';
public $BOOL_N = 'N';
public function init(PDO $db)
{
@ -156,14 +183,14 @@ class SQLite extends DBEngine
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", "'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);
$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;
}

View File

@ -6,6 +6,8 @@
*/
abstract class Event
{
public $stop_processing = false;
public function __construct()
{
}
@ -58,7 +60,7 @@ class PageRequestEvent extends Event
// if path is not specified, use the default front page
if (empty($path)) { /* empty is faster than strlen */
$path = $config->get_string('front_page');
$path = $config->get_string(SetupConfig::FRONT_PAGE);
}
// break the path into parts

View File

@ -182,7 +182,7 @@ abstract class DataHandlerExtension extends Extension
// even more hax..
$event->metadata['tags'] = $existing->get_tag_list();
$image = $this->create_image_from_data(warehouse_path("images", $event->metadata['hash']), $event->metadata);
$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");
@ -192,13 +192,14 @@ abstract class DataHandlerExtension extends Extension
send_event($ire);
$event->image_id = $image_id;
} else {
$image = $this->create_image_from_data(warehouse_path("images", $event->hash), $event->metadata);
$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'])) {
@ -222,13 +223,13 @@ abstract class DataHandlerExtension extends Extension
$result = false;
if ($this->supported_ext($event->type)) {
if ($event->force) {
$result = $this->create_thumb($event->hash);
$result = $this->create_thumb($event->hash, $event->type);
} else {
$outname = warehouse_path("thumbs", $event->hash);
$outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash);
if (file_exists($outname)) {
return;
}
$result = $this->create_thumb($event->hash);
$result = $this->create_thumb($event->hash, $event->type);
}
}
if ($result) {
@ -256,5 +257,5 @@ abstract class DataHandlerExtension extends Extension
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): bool;
abstract protected function create_thumb(string $hash, string $type): bool;
}

View File

@ -11,6 +11,8 @@ class ImageAdditionEvent extends Event
/** @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
@ -40,15 +42,19 @@ 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)
public function __construct(Image $image, bool $force = false)
{
$this->image = $image;
$this->force = $force;
}
}

View File

@ -10,6 +10,9 @@
*/
class Image
{
public const IMAGE_DIR = "images";
public const THUMBNAIL_DIR = "thumbs";
private static $tag_n = 0; // temp hack
public static $order_sql = null; // this feels ugly
@ -51,6 +54,19 @@ class Image
/** @var boolean */
public $locked = false;
/** @var boolean */
public $lossless = null;
/** @var boolean */
public $video = null;
/** @var boolean */
public $audio = null;
/** @var int */
public $length = null;
/**
* One will very rarely construct an image directly, more common
* would be to use Image::by_id, Image::by_hash, etc.
@ -100,6 +116,43 @@ class Image
}
}
private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags=[]): iterable
{
global $database, $user, $config;
if ($start < 0) {
$start = 0;
}
if ($limit!=null && $limit < 1) {
$limit = 1;
}
if (SPEED_HAX) {
if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > 3) {
throw new SCoreException("Anonymous users may only search for up to 3 tags at a time");
}
}
list($tag_conditions, $img_conditions) = self::terms_to_conditions($tags);
$result = Image::get_accelerated_result($tag_conditions, $img_conditions, $start, $limit);
if (!$result) {
$querylet = Image::build_search_querylet($tag_conditions, $img_conditions);
$querylet->append(new Querylet(" ORDER BY ".(Image::$order_sql ?: "images.".$config->get_string("index_order"))));
if($limit!=null) {
$querylet->append(new Querylet(" LIMIT :limit ", ["limit" => $limit]));
}
$querylet->append(new Querylet(" OFFSET :offset ", ["offset"=>$start]));
#var_dump($querylet->sql); var_dump($querylet->variables);
$result = $database->get_all_iterable($querylet->sql, $querylet->variables);
}
Image::$order_sql = null;
return $result;
}
/**
* Search for an array of images
*
@ -108,92 +161,30 @@ class Image
*/
public static function find_images(int $start, int $limit, array $tags=[]): array
{
global $database, $user, $config;
$result = self::find_images_internal($start, $limit, $tags);
$images = [];
if ($start < 0) {
$start = 0;
}
if ($limit < 1) {
$limit = 1;
}
if (SPEED_HAX) {
if (!$user->can("big_search") and count($tags) > 3) {
throw new SCoreException("Anonymous users may only search for up to 3 tags at a time");
}
}
$result = null;
if (SEARCH_ACCEL) {
$result = Image::get_accelerated_result($tags, $start, $limit);
}
if (!$result) {
$querylet = Image::build_search_querylet($tags);
$querylet->append(new Querylet(" ORDER BY ".(Image::$order_sql ?: "images.".$config->get_string("index_order"))));
$querylet->append(new Querylet(" LIMIT :limit OFFSET :offset", ["limit"=>$limit, "offset"=>$start]));
#var_dump($querylet->sql); var_dump($querylet->variables);
$result = $database->execute($querylet->sql, $querylet->variables);
}
while ($row = $result->fetch()) {
foreach ($result as $row) {
$images[] = new Image($row);
}
Image::$order_sql = null;
return $images;
}
/**
* Search for an array of image IDs
*
* #param string[] $tags
* #return int[]
* Search for an array of images, returning a iterable object of Image
*/
public static function find_image_ids(int $start, int $limit, array $tags=[]): array
public static function find_images_iterable(int $start = 0, ?int $limit = null, array $tags=[]): Generator
{
global $database, $user, $config;
$images = [];
if ($start < 0) {
$start = 0;
$result = self::find_images_internal($start, $limit, $tags);
foreach ($result as $row) {
yield new Image($row);
}
if ($limit < 1) {
$limit = 1;
}
if (SPEED_HAX) {
if (!$user->can("big_search") and count($tags) > 3) {
throw new SCoreException("Anonymous users may only search for up to 3 tags at a time");
}
}
$result = null;
if (SEARCH_ACCEL) {
$result = Image::get_accelerated_result($tags, $start, $limit);
}
if (!$result) {
$querylet = Image::build_search_querylet($tags);
$querylet->append(new Querylet(" ORDER BY ".(Image::$order_sql ?: "images.".$config->get_string("index_order"))));
$querylet->append(new Querylet(" LIMIT :limit OFFSET :offset", ["limit"=>$limit, "offset"=>$start]));
#var_dump($querylet->sql); var_dump($querylet->variables);
$result = $database->execute($querylet->sql, $querylet->variables);
}
while ($row = $result->fetch()) {
$images[] = $row["id"];
}
Image::$order_sql = null;
return $images;
}
/*
* Accelerator stuff
*/
public static function get_acceleratable(array $tags): ?array
public static function get_acceleratable(array $tag_conditions): ?array
{
$ret = [
"yays" => [],
@ -201,16 +192,17 @@ class Image
];
$yays = 0;
$nays = 0;
foreach ($tags as $tag) {
if (!preg_match("/^-?[a-zA-Z0-9_'-]+$/", $tag)) {
foreach ($tag_conditions as $tq) {
if (strpos($tq->tag, "*") !== false) {
// can't deal with wildcards
return null;
}
if ($tag[0] == "-") {
$nays++;
$ret["nays"][] = substr($tag, 1);
} else {
if ($tq->positive) {
$yays++;
$ret["yays"][] = $tag;
$ret["yays"][] = $tq->tag;
} else {
$nays++;
$ret["nays"][] = $tq->tag;
}
}
if ($yays > 1 || $nays > 0) {
@ -219,11 +211,15 @@ class Image
return null;
}
public static function get_accelerated_result(array $tags, int $offset, int $limit): ?PDOStatement
public static function get_accelerated_result(array $tag_conditions, array $img_conditions, int $offset, ?int $limit): ?PDOStatement
{
if (!SEARCH_ACCEL || !empty($img_conditions) || isset($_GET['DISABLE_ACCEL'])) {
return null;
}
global $database;
$req = Image::get_acceleratable($tags);
$req = Image::get_acceleratable($tag_conditions);
if (!$req) {
return null;
}
@ -231,8 +227,8 @@ class Image
$req["limit"] = $limit;
$response = Image::query_accelerator($req);
$list = implode(",", $response);
if ($list) {
if ($response) {
$list = implode(",", $response);
$result = $database->execute("SELECT * FROM images WHERE id IN ($list) ORDER BY images.id DESC");
} else {
$result = $database->execute("SELECT * FROM images WHERE 1=0 ORDER BY images.id DESC");
@ -240,9 +236,13 @@ class Image
return $result;
}
public static function get_accelerated_count(array $tags): ?int
public static function get_accelerated_count(array $tag_conditions, array $img_conditions): ?int
{
$req = Image::get_acceleratable($tags);
if (!SEARCH_ACCEL || !empty($img_conditions) || isset($_GET['DISABLE_ACCEL'])) {
return null;
}
$req = Image::get_acceleratable($tag_conditions);
if (!$req) {
return null;
}
@ -253,17 +253,21 @@ class Image
public static function query_accelerator($req)
{
global $_tracer;
$fp = @fsockopen("127.0.0.1", 21212);
if (!$fp) {
return null;
}
fwrite($fp, json_encode($req));
$req_str = json_encode($req);
$_tracer->begin("Accelerator Query", ["req"=>$req_str]);
fwrite($fp, $req_str);
$data = "";
while (($buffer = fgets($fp, 4096)) !== false) {
$data .= $buffer;
}
$_tracer->end();
if (!feof($fp)) {
die("Error: unexpected fgets() fail in query_accelerator($req)\n");
die("Error: unexpected fgets() fail in query_accelerator($req_str)\n");
}
fclose($fp);
return json_decode($data);
@ -295,9 +299,10 @@ class Image
["tag"=>$tags[0]]
);
} else {
$total = Image::get_accelerated_count($tags);
list($tag_conditions, $img_conditions) = self::terms_to_conditions($tags);
$total = Image::get_accelerated_count($tag_conditions, $img_conditions);
if (is_null($total)) {
$querylet = Image::build_search_querylet($tags);
$querylet = Image::build_search_querylet($tag_conditions, $img_conditions);
$total = $database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables);
}
}
@ -318,6 +323,49 @@ class Image
return ceil(Image::count_images($tags) / $config->get_int('index_images'));
}
private static function terms_to_conditions(array $terms): array
{
$tag_conditions = [];
$img_conditions = [];
/*
* Turn a bunch of strings into a bunch of TagCondition
* and ImgCondition objects
*/
$stpe = new SearchTermParseEvent(null, $terms);
send_event($stpe);
if ($stpe->is_querylet_set()) {
foreach ($stpe->get_querylets() as $querylet) {
$img_conditions[] = new ImgCondition($querylet, true);
}
}
foreach ($terms as $term) {
$positive = true;
if (is_string($term) && !empty($term) && ($term[0] == '-')) {
$positive = false;
$term = substr($term, 1);
}
if (strlen($term) === 0) {
continue;
}
$stpe = new SearchTermParseEvent($term, $terms);
send_event($stpe);
if ($stpe->is_querylet_set()) {
foreach ($stpe->get_querylets() as $querylet) {
$img_conditions[] = new ImgCondition($querylet, $positive);
}
} else {
// if the whole match is wild, skip this
if (str_replace("*", "", $term) != "") {
$tag_conditions[] = new TagCondition($term, $positive);
}
}
}
return [$tag_conditions, $img_conditions];
}
/*
* Accessors & mutators
*/
@ -352,7 +400,8 @@ class Image
');
} else {
$tags[] = 'id'. $gtlt . $this->id;
$querylet = Image::build_search_querylet($tags);
list($tag_conditions, $img_conditions) = self::terms_to_conditions($tags);
$querylet = Image::build_search_querylet($tag_conditions, $img_conditions);
$querylet->append_sql(' ORDER BY images.id '.$dir.' LIMIT 1');
$row = $database->get_row($querylet->sql, $querylet->variables);
}
@ -427,7 +476,7 @@ class Image
*/
public function get_image_link(): string
{
return $this->get_link('image_ilink', '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext');
return $this->get_link(ImageConfig::ILINK, '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext');
}
/**
@ -444,8 +493,8 @@ class Image
public function get_thumb_link(): string
{
global $config;
$ext = $config->get_string("thumb_type");
return $this->get_link('image_tlink', '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext);
$ext = $config->get_string(ImageConfig::THUMB_TYPE);
return $this->get_link(ImageConfig::TLINK, '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext);
}
/**
@ -476,7 +525,7 @@ class Image
public function get_tooltip(): string
{
global $config;
$tt = $this->parse_link_template($config->get_string('image_tip'), "no_escape");
$tt = $this->parse_link_template($config->get_string(ImageConfig::TIP), "no_escape");
// Removes the size tag if the file is an mp3
if ($this->ext === 'mp3') {
@ -502,7 +551,7 @@ class Image
*/
public function get_image_filename(): string
{
return warehouse_path("images", $this->hash);
return warehouse_path(self::IMAGE_DIR, $this->hash);
}
/**
@ -510,7 +559,7 @@ class Image
*/
public function get_thumb_filename(): string
{
return warehouse_path("thumbs", $this->hash);
return warehouse_path(self::THUMBNAIL_DIR, $this->hash);
}
/**
@ -590,7 +639,7 @@ class Image
public function delete_tags_from_image(): void
{
global $database;
if ($database->get_driver_name() == "mysql") {
if ($database->get_driver_name() == DatabaseDriver::MYSQL) {
//mysql < 5.6 has terrible subquery optimization, using EXISTS / JOIN fixes this
$database->execute(
"
@ -764,7 +813,7 @@ class Image
$tmpl = str_replace('$size', "{$this->width}x{$this->height}", $tmpl);
$tmpl = str_replace('$filesize', to_shorthand_int($this->filesize), $tmpl);
$tmpl = str_replace('$filename', $_escape($base_fname), $tmpl);
$tmpl = str_replace('$title', $_escape($config->get_string("title")), $tmpl);
$tmpl = str_replace('$title', $_escape($config->get_string(SetupConfig::TITLE)), $tmpl);
$tmpl = str_replace('$date', $_escape(autodate($this->posted, false)), $tmpl);
// nothing seems to use this, sending the event out to 50 exts is a lot of overhead
@ -774,16 +823,17 @@ class Image
$tmpl = $plte->link;
}
static $flexihash = null;
static $fh_last_opts = null;
static $flexihashes = [];
$matches = [];
if (preg_match("/(.*){(.*)}(.*)/", $tmpl, $matches)) {
$pre = $matches[1];
$opts = $matches[2];
$post = $matches[3];
if ($opts != $fh_last_opts) {
$fh_last_opts = $opts;
if(isset($flexihashes[$opts])) {
$flexihash = $flexihashes[$opts];
}
else {
$flexihash = new Flexihash\Flexihash();
foreach (explode(",", $opts) as $opt) {
$parts = explode("=", $opt);
@ -799,6 +849,7 @@ class Image
}
$flexihash->addTarget($opt_val, $opt_weight);
}
$flexihashes[$opts] = $flexihash;
}
// $choice = $flexihash->lookup($pre.$post);
@ -813,57 +864,17 @@ class Image
/**
* #param string[] $terms
*/
private static function build_search_querylet(array $terms): Querylet
private static function build_search_querylet(array $tag_conditions, array $img_conditions): Querylet
{
global $database;
$tag_querylets = [];
$img_querylets = [];
$positive_tag_count = 0;
$negative_tag_count = 0;
/*
* Turn a bunch of strings into a bunch of TagQuerylet
* and ImgQuerylet objects
*/
$stpe = new SearchTermParseEvent(null, $terms);
send_event($stpe);
if ($stpe->is_querylet_set()) {
foreach ($stpe->get_querylets() as $querylet) {
$img_querylets[] = new ImgQuerylet($querylet, true);
}
}
foreach ($terms as $term) {
$positive = true;
if (is_string($term) && !empty($term) && ($term[0] == '-')) {
$positive = false;
$term = substr($term, 1);
}
if (strlen($term) === 0) {
continue;
}
$stpe = new SearchTermParseEvent($term, $terms);
send_event($stpe);
if ($stpe->is_querylet_set()) {
foreach ($stpe->get_querylets() as $querylet) {
$img_querylets[] = new ImgQuerylet($querylet, $positive);
}
foreach ($tag_conditions as $tq) {
if ($tq->positive) {
$positive_tag_count++;
} else {
// if the whole match is wild, skip this;
// if not, translate into SQL
if (str_replace("*", "", $term) != "") {
$term = str_replace('_', '\_', $term);
$term = str_replace('%', '\%', $term);
$term = str_replace('*', '%', $term);
$tag_querylets[] = new TagQuerylet($term, $positive);
if ($positive) {
$positive_tag_count++;
} else {
$negative_tag_count++;
}
}
$negative_tag_count++;
}
}
@ -888,41 +899,20 @@ class Image
");
}
// one positive tag (a common case), do an optimised search
elseif ($positive_tag_count === 1 && $negative_tag_count === 0) {
# "LIKE" to account for wildcards
$query = new Querylet($database->scoreql_to_sql("
SELECT *
FROM (
SELECT images.*
FROM images
JOIN image_tags ON images.id=image_tags.image_id
JOIN tags ON image_tags.tag_id=tags.id
WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:tag)
GROUP BY images.id
) AS images
WHERE 1=1
"), ["tag"=>$tag_querylets[0]->tag]);
}
// more than one positive tag, or more than zero negative tags
else {
if ($database->get_driver_name() === "mysql") {
$query = Image::build_ugly_search_querylet($tag_querylets);
} else {
$query = Image::build_accurate_search_querylet($tag_querylets);
}
$query = Image::build_accurate_search_querylet($tag_conditions);
}
/*
* Merge all the image metadata searches into one generic querylet
* and append to the base querylet with "AND blah"
*/
if (!empty($img_querylets)) {
if (!empty($img_conditions)) {
$n = 0;
$img_sql = "";
$img_vars = [];
foreach ($img_querylets as $iq) {
foreach ($img_conditions as $iq) {
if ($n++ > 0) {
$img_sql .= " AND";
}
@ -940,47 +930,30 @@ class Image
}
/**
* WARNING: this description is no longer accurate, though it does get across
* the general idea - the actual method has a few extra optimisations
*
* "foo bar -baz user=foo" becomes
*
* SELECT * FROM images WHERE
* images.id IN (SELECT image_id FROM image_tags WHERE tag='foo')
* AND images.id IN (SELECT image_id FROM image_tags WHERE tag='bar')
* AND NOT images.id IN (SELECT image_id FROM image_tags WHERE tag='baz')
* AND images.id IN (SELECT id FROM images WHERE owner_name='foo')
*
* This is:
* A) Incredibly simple:
* Each search term maps to a list of image IDs
* B) Runs really fast on a good database:
* These lists are calculated once, and the set intersection taken
* C) Runs really slow on bad databases:
* All the subqueries are executed every time for every row in the
* images table. Yes, MySQL does suck this much.
*
* #param TagQuerylet[] $tag_querylets
* #param TagQuerylet[] $tag_conditions
*/
private static function build_accurate_search_querylet(array $tag_querylets): Querylet
private static function build_accurate_search_querylet(array $tag_conditions): Querylet
{
global $database;
$positive_tag_id_array = [];
$positive_wildcard_id_array = [];
$negative_tag_id_array = [];
foreach ($tag_querylets as $tq) {
foreach ($tag_conditions as $tq) {
$tag_ids = $database->get_col(
$database->scoreql_to_sql("
SELECT id
FROM tags
WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:tag)
"),
["tag" => $tq->tag]
["tag" => Tag::sqlify($tq->tag)]
);
$tag_count = count($tag_ids);
if ($tq->positive) {
$positive_tag_id_array = array_merge($positive_tag_id_array, $tag_ids);
if (count($tag_ids) == 0) {
if ($tag_count== 0) {
# one of the positive tags had zero results, therefor there
# can be no results; "where 1=0" should shortcut things
return new Querylet("
@ -988,115 +961,71 @@ class Image
FROM images
WHERE 1=0
");
} elseif($tag_count==1) {
// All wildcard terms that qualify for a single tag can be treated the same as non-wildcards
$positive_tag_id_array[] = $tag_ids[0];
} else {
// Terms that resolve to multiple tags act as an OR within themselves
// and as an AND in relation to all other terms,
$positive_wildcard_id_array[] = $tag_ids;
}
} else {
// Unlike positive criteria, negative criteria are all handled in an OR fashion,
// so we can just compile them all into a single sub-query.
$negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids);
}
}
assert($positive_tag_id_array || $negative_tag_id_array, @$_GET['q']);
$wheres = [];
if (!empty($positive_tag_id_array)) {
$positive_tag_id_list = join(', ', $positive_tag_id_array);
$wheres[] = "tag_id IN ($positive_tag_id_list)";
}
if (!empty($negative_tag_id_array)) {
$sql = "";
assert($positive_tag_id_array || $positive_wildcard_id_array || $negative_tag_id_array, @$_GET['q']);
if(!empty($positive_tag_id_array) || !empty($positive_wildcard_id_array)) {
$inner_joins = [];
if (!empty($positive_tag_id_array)) {
foreach($positive_tag_id_array as $tag) {
$inner_joins[] = "= $tag";
}
}
if(!empty($positive_wildcard_id_array)) {
foreach ($positive_wildcard_id_array as $tags) {
$positive_tag_id_list = join(', ', $tags);
$inner_joins[] = "IN ($positive_tag_id_list)";
}
}
$first = array_shift($inner_joins);
$sub_query = "SELECT it.image_id FROM image_tags it ";
$i = 0;
foreach ($inner_joins as $inner_join) {
$i++;
$sub_query .= " INNER JOIN image_tags it$i ON it$i.image_id = it.image_id AND it$i.tag_id $inner_join ";
}
if(!empty($negative_tag_id_array)) {
$negative_tag_id_list = join(', ', $negative_tag_id_array);
$sub_query .= " LEFT JOIN image_tags negative ON negative.image_id = it.image_id AND negative.tag_id IN ($negative_tag_id_list) ";
}
$sub_query .= "WHERE it.tag_id $first ";
if(!empty($negative_tag_id_array)) {
$sub_query .= " AND negative.image_id IS NULL";
}
$sub_query .= " GROUP BY it.image_id ";
$sql = "
SELECT images.*
FROM images INNER JOIN (
$sub_query
) a on a.image_id = images.id
";
} elseif(!empty($negative_tag_id_array)) {
$negative_tag_id_list = join(', ', $negative_tag_id_array);
$wheres[] = "tag_id NOT IN ($negative_tag_id_list)";
}
$wheres_str = join(" AND ", $wheres);
return new Querylet("
SELECT images.*
FROM images
WHERE images.id IN (
SELECT image_id
FROM image_tags
WHERE $wheres_str
GROUP BY image_id
HAVING COUNT(image_id) >= :search_score
)
", ["search_score"=>count($positive_tag_id_array)]);
}
/**
* this function exists because mysql is a turd, see the docs for
* build_accurate_search_querylet() for a full explanation
*
* #param TagQuerylet[] $tag_querylets
*/
private static function build_ugly_search_querylet(array $tag_querylets): Querylet
{
global $database;
$positive_tag_count = 0;
foreach ($tag_querylets as $tq) {
if ($tq->positive) {
$positive_tag_count++;
}
$sql = "
SELECT images.*
FROM images LEFT JOIN image_tags negative ON negative.image_id = images.id AND negative.tag_id in ($negative_tag_id_list)
WHERE negative.image_id IS NULL
";
} else {
throw new SCoreException("No criteria specified");
}
// only negative tags - shortcut to fail
if ($positive_tag_count == 0) {
// TODO: This isn't currently implemented.
// SEE: https://github.com/shish/shimmie2/issues/66
return new Querylet("
SELECT images.*
FROM images
WHERE 1=0
");
}
// merge all the tag querylets into one generic one
$sql = "0";
$terms = [];
foreach ($tag_querylets as $tq) {
$sign = $tq->positive ? "+" : "-";
$sql .= ' '.$sign.' IF(SUM(tag LIKE :tag'.Image::$tag_n.'), 1, 0)';
$terms['tag'.Image::$tag_n] = $tq->tag;
Image::$tag_n++;
}
$tag_search = new Querylet($sql, $terms);
$tag_id_array = [];
foreach ($tag_querylets as $tq) {
$tag_ids = $database->get_col(
$database->scoreql_to_sql("
SELECT id
FROM tags
WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:tag)
"),
["tag" => $tq->tag]
);
$tag_id_array = array_merge($tag_id_array, $tag_ids);
if ($tq->positive && count($tag_ids) == 0) {
# one of the positive tags had zero results, therefor there
# can be no results; "where 1=0" should shortcut things
return new Querylet("
SELECT images.*
FROM images
WHERE 1=0
");
}
}
Image::$tag_n = 0;
return new Querylet('
SELECT *
FROM (
SELECT images.*, ('.$tag_search->sql.') AS score
FROM images
LEFT JOIN image_tags ON image_tags.image_id = images.id
JOIN tags ON image_tags.tag_id = tags.id
WHERE tags.id IN (' . join(', ', $tag_id_array) . ')
GROUP BY images.id
HAVING score = :score
) AS images
WHERE 1=1
', array_merge(
$tag_search->variables,
["score"=>$positive_tag_count]
));
return new Querylet($sql);
}
}

View File

@ -7,11 +7,12 @@
* 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("images", $event->hash);
$target = warehouse_path(Image::IMAGE_DIR, $event->hash);
if (!@copy($event->tmpname, $target)) {
$errors = error_get_last();
throw new UploadException(
@ -24,7 +25,8 @@ function move_upload_to_archive(DataUploadEvent $event): void
/**
* Add a directory full of images
*
* #return string[]
* @param string $base
* @return array
*/
function add_dir(string $base): array
{
@ -48,6 +50,14 @@ function add_dir(string $base): array
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));
@ -65,10 +75,15 @@ function add_image(string $tmpname, string $filename, string $tags): void
send_event($event);
}
function get_extension_from_mime(String $file_path): ?String
/**
* 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
{
global $config;
$mime = mime_content_type($file_path);
if (!empty($mime)) {
$ext = get_extension($mime);
@ -80,14 +95,17 @@ function get_extension_from_mime(String $file_path): ?String
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
* into the configured thumbnail square, with ratio intact.
* Optionally uses the High-DPI scaling setting to adjust the final resolution.
*
* #return int[]
* @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): array
function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_scaling = false): array
{
global $config;
@ -105,363 +123,97 @@ function get_thumbnail_size(int $orig_width, int $orig_height): array
$orig_height = $orig_width * 5;
}
$max_width = $config->get_int('thumb_width');
$max_height = $config->get_int('thumb_height');
$xscale = ($max_height / $orig_height);
$yscale = ($max_width / $orig_width);
$scale = ($xscale < $yscale) ? $xscale : $yscale;
if($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);
}
if ($scale > 1 && $config->get_bool('thumb_upscale')) {
$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 [(int)($orig_width*$scale), (int)($orig_height*$scale)];
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];
}
/**
* Given a full size pair of dimensions, return a pair scaled down to fit
* into the configured thumbnail square, with ratio intact, using thumb_scaling
* Fetches the thumbnails height and width settings and applies the High-DPI scaling setting before returning the dimensions.
*
* #return int[]
* @return array [width, height]
*/
function get_thumbnail_size_scaled(int $orig_width, int $orig_height): 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;
}
$max_size = get_thumbnail_max_size_scaled();
$max_width = $max_size[0];
$max_height = $max_size[1];
$xscale = ($max_height / $orig_height);
$yscale = ($max_width / $orig_width);
$scale = ($xscale < $yscale) ? $xscale : $yscale;
if ($scale > 1 && $config->get_bool('thumb_upscale')) {
return [(int)$orig_width, (int)$orig_height];
} else {
return [(int)($orig_width*$scale), (int)($orig_height*$scale)];
}
}
function get_thumbnail_max_size_scaled(): array
{
global $config;
$scaling = $config->get_int("thumb_scaling");
$max_width = $config->get_int('thumb_width') * ($scaling/100);
$max_height = $config->get_int('thumb_height') * ($scaling/100);
$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_thumbnail_convert($hash): bool
{
function create_image_thumb(string $hash, string $type, string $engine = null) {
global $config;
$inname = warehouse_path("images", $hash);
$outname = warehouse_path("thumbs", $hash);
$q = $config->get_int("thumb_quality");
$convert = $config->get_string("thumb_convert_path");
if ($convert==null||$convert=="") {
return false;
}
// ffff imagemagick fails sometimes, not sure why
//$format = "'%s' '%s[0]' -format '%%[fx:w] %%[fx:h]' info:";
//$cmd = sprintf($format, $convert, $inname);
//$size = shell_exec($cmd);
//$size = explode(" ", trim($size));
$inname = warehouse_path(Image::IMAGE_DIR, $hash);
$outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
$tsize = get_thumbnail_max_size_scaled();
$w = $tsize[0];
$h = $tsize[1];
// running the call with cmd.exe requires quoting for our paths
$type = $config->get_string('thumb_type');
$options = "";
if (!$config->get_bool('thumb_upscale')) {
$options .= "\>";
if(empty($engine)) {
$engine = $config->get_string(ImageConfig::THUMB_ENGINE);
}
$bg = "black";
if ($type=="webp") {
$bg = "none";
}
$format = '"%s" -flatten -strip -thumbnail %ux%u%s -quality %u -background %s "%s[0]" %s:"%s"';
$cmd = sprintf($format, $convert, $w, $h, $options, $q, $bg, $inname, $type, $outname);
$cmd = str_replace("\"convert\"", "convert", $cmd); // quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
exec($cmd, $output, $ret);
log_debug('handle_pixel', "Generating thumbnail with command `$cmd`, returns $ret");
if ($config->get_bool("thumb_optim", false)) {
exec("jpegoptim $outname", $output, $ret);
$output_format = $config->get_string(ImageConfig::THUMB_TYPE);
if($output_format=="webp") {
$output_format = Media::WEBP_LOSSY;
}
return true;
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)
));
}
function create_thumbnail_ffmpeg($hash): bool
const TIME_UNITS = ["s"=>60,"m"=>60,"h"=>24,"d"=>365,"y"=>PHP_INT_MAX];
function format_milliseconds(int $input): string
{
global $config;
$output = "";
$ffmpeg = $config->get_string("thumb_ffmpeg_path");
if ($ffmpeg==null||$ffmpeg=="") {
return false;
$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;
}
$inname = warehouse_path("images", $hash);
$outname = warehouse_path("thumbs", $hash);
$orig_size = video_size($inname);
$scaled_size = get_thumbnail_size_scaled($orig_size[0], $orig_size[1]);
$codec = "mjpeg";
$quality = $config->get_int("thumb_quality");
if ($config->get_string("thumb_type")=="webp") {
$codec = "libwebp";
} else {
// mjpeg quality ranges from 2-31, with 2 being the best quality.
$quality = floor(31 - (31 * ($quality/100)));
if ($quality<2) {
$quality = 2;
}
}
$args = [
escapeshellarg($ffmpeg),
"-y", "-i", escapeshellarg($inname),
"-vf", "thumbnail,scale={$scaled_size[0]}:{$scaled_size[1]}",
"-f", "image2",
"-vframes", "1",
"-c:v", $codec,
"-q:v", $quality,
escapeshellarg($outname),
];
$cmd = escapeshellcmd(implode(" ", $args));
exec($cmd, $output, $ret);
if ((int)$ret == (int)0) {
log_debug('imageboard/misc', "Generating thumbnail with command `$cmd`, returns $ret");
return true;
} else {
log_error('imageboard/misc', "Generating thumbnail with command `$cmd`, returns $ret");
return false;
}
}
function video_size(string $filename): array
{
global $config;
$ffmpeg = $config->get_string("thumb_ffmpeg_path");
$cmd = escapeshellcmd(implode(" ", [
escapeshellarg($ffmpeg),
"-y", "-i", escapeshellarg($filename),
"-vstats"
]));
$output = shell_exec($cmd . " 2>&1");
// error_log("Getting size with `$cmd`");
$regex_sizes = "/Video: .* ([0-9]{1,4})x([0-9]{1,4})/";
if (preg_match($regex_sizes, $output, $regs)) {
if (preg_match("/displaymatrix: rotation of (90|270).00 degrees/", $output)) {
$size = [$regs[2], $regs[1]];
} else {
$size = [$regs[1], $regs[2]];
}
} else {
$size = [1, 1];
}
log_debug('imageboard/misc', "Getting video size with `$cmd`, returns $output -- $size[0], $size[1]");
return $size;
}
/**
* Check Memory usage limits
*
* Old check: $memory_use = (filesize($image_filename)*2) + ($width*$height*4) + (4*1024*1024);
* New check: $memory_use = $width * $height * ($bits_per_channel) * channels * 2.5
*
* It didn't make sense to compute the memory usage based on the NEW size for the image. ($width*$height*4)
* We need to consider the size that we are GOING TO instead.
*
* The factor of 2.5 is simply a rough guideline.
* http://stackoverflow.com/questions/527532/reasonable-php-memory-limit-for-image-resize
*/
function calc_memory_use(array $info): int
{
if (isset($info['bits']) && isset($info['channels'])) {
$memory_use = ($info[0] * $info[1] * ($info['bits'] / 8) * $info['channels'] * 2.5) / 1024;
} else {
// If we don't have bits and channel info from the image then assume default values
// of 8 bits per color and 4 channels (R,G,B,A) -- ie: regular 24-bit color
$memory_use = ($info[0] * $info[1] * 1 * 4 * 2.5) / 1024;
}
return (int)$memory_use;
}
function image_resize_gd(
String $image_filename,
array $info,
int $new_width,
int $new_height,
string $output_filename=null,
string $output_type=null,
int $output_quality = 80
) {
$width = $info[0];
$height = $info[1];
if ($output_type==null) {
/* If not specified, output to the same format as the original image */
switch ($info[2]) {
case IMAGETYPE_GIF: $output_type = "gif"; break;
case IMAGETYPE_JPEG: $output_type = "jpeg"; break;
case IMAGETYPE_PNG: $output_type = "png"; break;
case IMAGETYPE_WEBP: $output_type = "webp"; break;
case IMAGETYPE_BMP: $output_type = "bmp"; break;
default: throw new ImageResizeException("Failed to save the new image - Unsupported image type.");
}
}
$memory_use = calc_memory_use($info);
$memory_limit = get_memory_limit();
if ($memory_use > $memory_limit) {
throw new InsufficientMemoryException("The image is too large to resize given the memory limits. ($memory_use > $memory_limit)");
}
$image = imagecreatefromstring(file_get_contents($image_filename));
$image_resized = imagecreatetruecolor($new_width, $new_height);
try {
if ($image===false) {
throw new ImageResizeException("Could not load image: ".$image_filename);
}
if ($image_resized===false) {
throw new ImageResizeException("Could not create output image with dimensions $new_width c $new_height ");
}
// Handle transparent images
switch ($info[2]) {
case IMAGETYPE_GIF:
$transparency = imagecolortransparent($image);
$palletsize = imagecolorstotal($image);
// If we have a specific transparent color
if ($transparency >= 0 && $transparency < $palletsize) {
// Get the original image's transparent color's RGB values
$transparent_color = imagecolorsforindex($image, $transparency);
// Allocate the same color in the new image resource
$transparency = imagecolorallocate($image_resized, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']);
if ($transparency===false) {
throw new ImageResizeException("Unable to allocate transparent color");
}
// Completely fill the background of the new image with allocated color.
if (imagefill($image_resized, 0, 0, $transparency)===false) {
throw new ImageResizeException("Unable to fill new image with transparent color");
}
// Set the background color for new image to transparent
imagecolortransparent($image_resized, $transparency);
}
break;
case IMAGETYPE_PNG:
case IMAGETYPE_WEBP:
//
// More info here: http://stackoverflow.com/questions/279236/how-do-i-resize-pngs-with-transparency-in-php
//
if (imagealphablending($image_resized, false)===false) {
throw new ImageResizeException("Unable to disable image alpha blending");
}
if (imagesavealpha($image_resized, true)===false) {
throw new ImageResizeException("Unable to enable image save alpha");
}
$transparent_color = imagecolorallocatealpha($image_resized, 255, 255, 255, 127);
if ($transparent_color===false) {
throw new ImageResizeException("Unable to allocate transparent color");
}
if (imagefilledrectangle($image_resized, 0, 0, $new_width, $new_height, $transparent_color)===false) {
throw new ImageResizeException("Unable to fill new image with transparent color");
}
break;
}
// Actually resize the image.
if (imagecopyresampled(
$image_resized,
$image,
0,
0,
0,
0,
$new_width,
$new_height,
$width,
$height
)===false) {
throw new ImageResizeException("Unable to copy resized image data to new image");
}
$result = false;
switch ($output_type) {
case "bmp":
$result = imagebmp($image_resized, $output_filename, true);
break;
case "webp":
$result = imagewebp($image_resized, $output_filename, $output_quality);
break;
case "jpg":
case "jpeg":
$result = imagejpeg($image_resized, $output_filename, $output_quality);
break;
case "png":
$result = imagepng($image_resized, $output_filename, 9);
break;
case "gif":
$result = imagegif($image_resized, $output_filename);
break;
default:
throw new ImageResizeException("Failed to save the new image - Unsupported image type: $output_type");
}
if ($result==false) {
throw new ImageResizeException("Failed to save the new image, function returned false when saving type: $output_type");
}
} finally {
imagedestroy($image);
imagedestroy($image_resized);
}
}
function is_animated_gif(String $image_filename)
{
$isanigif = 0;
if (($fh = @fopen($image_filename, 'rb'))) {
//check if gif is animated (via http://www.php.net/manual/en/function.imagecreatefromgif.php#104473)
while (!feof($fh) && $isanigif < 2) {
$chunk = fread($fh, 1024 * 100);
$isanigif += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
}
}
return ($isanigif == 0);
}
return trim($output);
}

View File

@ -29,7 +29,7 @@ class Querylet
}
}
class TagQuerylet
class TagCondition
{
/** @var string */
public $tag;
@ -43,7 +43,7 @@ class TagQuerylet
}
}
class ImgQuerylet
class ImgCondition
{
/** @var Querylet */
public $qlet;

View File

@ -100,4 +100,12 @@ class Tag
return $tag_array;
}
public static function sqlify(string $term): string
{
$term = str_replace('_', '\_', $term);
$term = str_replace('%', '\%', $term);
$term = str_replace('*', '%', $term);
return $term;
}
}

View File

@ -26,6 +26,13 @@
* 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
@ -40,7 +47,7 @@ class Page
/** @name Overall */
//@{
/** @var string */
public $mode = "page";
public $mode = PageMode::PAGE;
/** @var string */
public $type = "text/html; charset=utf-8";
@ -69,9 +76,14 @@ class Page
/** @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.
*/
@ -80,12 +92,18 @@ class Page
$this->data = $data;
}
public function set_file(string $file): void
{
$this->file = $file;
}
/**
* Set the recommended download filename.
*/
public function set_filename(string $filename): void
public function set_filename(string $filename, string $disposition = "attachment"): void
{
$this->filename = $filename;
$this->disposition = $disposition;
}
@ -165,7 +183,7 @@ class Page
/**
* Add a line to the HTML head section.
*/
public function add_html_header(string $line, int $position=50): void
public function add_html_header(string $line, int $position = 50): void
{
while (isset($this->html_headers[$position])) {
$position++;
@ -176,7 +194,7 @@ class Page
/**
* Add a http header to be sent to the client.
*/
public function add_http_header(string $line, int $position=50): void
public function add_http_header(string $line, int $position = 50): void
{
while (isset($this->http_headers[$position])) {
$position++;
@ -191,13 +209,13 @@ class Page
*/
public function add_cookie(string $name, string $value, int $time, string $path): void
{
$full_name = COOKIE_PREFIX."_".$name;
$full_name = COOKIE_PREFIX . "_" . $name;
$this->cookies[] = [$full_name, $value, $time, $path];
}
public function get_cookie(string $name): ?string
{
$full_name = COOKIE_PREFIX."_".$name;
$full_name = COOKIE_PREFIX . "_" . $name;
if (isset($_COOKIE[$full_name])) {
return $_COOKIE[$full_name];
} else {
@ -246,8 +264,8 @@ class Page
global $page, $user;
header("HTTP/1.0 {$this->code} Shimmie");
header("Content-type: ".$this->type);
header("X-Powered-By: SCore-".SCORE_VERSION);
header("Content-type: " . $this->type);
header("X-Powered-By: SCore-" . SCORE_VERSION);
if (!headers_sent()) {
foreach ($this->http_headers as $head) {
@ -261,7 +279,7 @@ class Page
}
switch ($this->mode) {
case "page":
case PageMode::PAGE:
if (CACHE_HTTP) {
header("Vary: Cookie, Accept-Encoding");
if ($user->is_anonymous() && $_SERVER["REQUEST_METHOD"] == "GET") {
@ -281,20 +299,135 @@ class Page
$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);
$layout->display_page($page, $nav_links, $sub_links);
break;
case "data":
header("Content-Length: ".strlen($this->data));
case PageMode::DATA:
header("Content-Length: " . strlen($this->data));
if (!is_null($this->filename)) {
header('Content-Disposition: attachment; filename='.$this->filename);
header('Content-Disposition: ' . $this->disposition . '; 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>';
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";
@ -318,7 +451,7 @@ class Page
global $config;
$data_href = get_base_href();
$theme_name = $config->get_string('theme', 'default');
$theme_name = $config->get_string(SetupConfig::THEME, 'default');
$this->add_html_header("<script type='text/javascript'>base_href = '$data_href';</script>", 40);
@ -335,7 +468,7 @@ class Page
/*** Generate CSS cache files ***/
$css_latest = $config_latest;
$css_files = array_merge(
zglob("ext/{".ENABLED_EXTS."}/style.css"),
zglob("ext/{" . ENABLED_EXTS . "}/style.css"),
zglob("themes/$theme_name/style.css")
);
foreach ($css_files as $css) {
@ -348,7 +481,7 @@ class Page
foreach ($css_files as $file) {
$file_data = file_get_contents($file);
$pattern = '/url[\s]*\([\s]*["\']?([^"\'\)]+)["\']?[\s]*\)/';
$replace = 'url("../../../'.dirname($file).'/$1")';
$replace = 'url("../../../' . dirname($file) . '/$1")';
$file_data = preg_replace($pattern, $replace, $file_data);
$css_data .= $file_data . "\n";
}
@ -366,7 +499,7 @@ class Page
"vendor/bower-asset/js-cookie/src/js.cookie.js",
"ext/handle_static/modernizr-3.3.1.custom.js",
],
zglob("ext/{".ENABLED_EXTS."}/script.js"),
zglob("ext/{" . ENABLED_EXTS . "}/script.js"),
zglob("themes/$theme_name/script.js")
);
foreach ($js_files as $js) {
@ -384,3 +517,99 @@ class Page
$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;
}

70
core/permissions.php Normal file
View File

@ -0,0 +1,70 @@
<?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 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";
}

View File

@ -251,21 +251,44 @@ if (!function_exists('mb_strlen')) {
}
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', 'video/x-flv' => 'flv',
'svg' => 'image/svg+xml', 'pdf' => 'application/pdf',
'zip' => 'application/zip', 'gz' => 'application/x-gzip',
'tar' => 'application/x-tar', 'bz' => 'application/x-bzip',
'bz2' => 'application/x-bzip2', 'txt' => 'text/plain',
'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html',
'css' => 'text/css', 'js' => 'text/javascript',
'xml' => 'text/xml', 'xsl' => 'application/xsl+xml',
'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav',
'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg',
'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php',
'mp4' => 'video/mp4', 'ogv' => 'video/ogg', 'webm' => 'video/webm',
'webp' => 'image/webp', 'bmp' =>'image/x-ms-bmp', 'psd' => 'image/vnd.adobe.photoshop',
'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'
];
@ -729,3 +752,53 @@ function validate_input(array $inputs): array
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));
}

View File

@ -9,9 +9,7 @@ $_shm_event_listeners = [];
function _load_event_listeners(): void
{
global $_shm_event_listeners, $_shm_ctx;
$_shm_ctx->log_start("Loading extensions");
global $_shm_event_listeners;
$cache_path = data_path("cache/shm_event_listeners.php");
if (COMPILE_ELS && file_exists($cache_path)) {
@ -23,8 +21,13 @@ function _load_event_listeners(): void
_dump_event_listeners($_shm_event_listeners, $cache_path);
}
}
}
$_shm_ctx->log_endok();
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
@ -105,35 +108,31 @@ $_shm_event_count = 0;
*/
function send_event(Event $event): void
{
global $_shm_event_listeners, $_shm_event_count, $_shm_ctx;
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 context gets called the time starts to add up
$ctx_enabled = constant('CONTEXT');
if ($ctx_enabled) {
$_shm_ctx->log_start(get_class($event));
}
// 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 ($ctx_enabled) {
$_shm_ctx->log_start(get_class($listener));
}
if ($tracer_enabled) $_tracer->begin(get_class($listener));
if (method_exists($listener, $method_name)) {
$listener->$method_name($event);
}
if ($ctx_enabled) {
$_shm_ctx->log_endok();
if ($tracer_enabled) $_tracer->end();
if($event->stop_processing===true) {
break;
}
}
$_shm_event_count++;
if ($ctx_enabled) {
$_shm_ctx->log_endok();
}
if ($tracer_enabled) $_tracer->end();
}

View File

@ -27,12 +27,10 @@ function _d(string $name, $value): void
}
_d("DATABASE_DSN", null); // string PDO database connection details
_d("DATABASE_KA", true); // string Keep database connection alive
_d("DATABASE_TIMEOUT", 10000);// int Time to wait for each statement to complete
_d("CACHE_DSN", null); // string cache connection details
_d("DEBUG", false); // boolean print various debugging details
_d("DEBUG_SQL", false); // boolean dump SQL queries to data/sql.log
_d("DEBUG_CACHE", false); // boolean dump cache queries to data/cache.log
_d("COVERAGE", false); // boolean activate xdebug coverage monitor
_d("CONTEXT", null); // string file to log performance data into
_d("CACHE_HTTP", false); // boolean output explicit HTTP caching headers
_d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes
_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance
@ -42,11 +40,12 @@ _d("SEARCH_ACCEL", false); // boolean use search accelerator
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
_d("VERSION", '2.7-beta'); // string shimmie version
_d("TIMEZONE", null); // string timezone
_d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,handle_static,comment,tag_list,index,tag_edit,alias_editor"); // extensions to always enable
_d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,handle_static,comment,tag_list,index,tag_edit,alias_editor,media,help_pages,system"); // extensions to always enable
_d("EXTRA_EXTS", ""); // string optional extra extensions
_d("BASE_URL", null); // string force a specific base URL (default is auto-detect)
_d("MIN_PHP_VERSION", '7.1');// string minimum supported PHP version
_d("SLOW_PAGES", null); // float log pages which take more time than this
_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");
/*

View File

@ -45,4 +45,77 @@ class PolyfillsTest extends \PHPUnit\Framework\TestCase
$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/\\/\/")
);
}
}

67
core/tests/util.test.php Normal file
View File

@ -0,0 +1,67 @@
<?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)
);
}
}

View File

@ -3,6 +3,23 @@
* 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.
@ -14,7 +31,7 @@ function make_link(?string $page=null, ?string $query=null): string
global $config;
if (is_null($page)) {
$page = $config->get_string('main_page');
$page = $config->get_string(SetupConfig::MAIN_PAGE);
}
if (!is_null(BASE_URL)) {

View File

@ -69,7 +69,7 @@ class User
global $config, $database;
$row = $database->cache->get("user-session:$name-$session");
if (!$row) {
if ($database->get_driver_name() === "mysql") {
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";

View File

@ -72,129 +72,138 @@ class UserClass
// action = create / view / edit / delete
// object = image / user / tag / setting
new UserClass("base", null, [
"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)
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)
"manage_extension_list" => false,
"manage_alias_list" => false,
"mass_tag_edit" => false,
Permissions::MANAGE_EXTENSION_LIST => false,
Permissions::MANAGE_ALIAS_LIST => false,
Permissions::MASS_TAG_EDIT => false,
"view_ip" => false, # view IP addresses associated with things
"ban_ip" => false,
Permissions::VIEW_IP => false, # view IP addresses associated with things
Permissions::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,
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,
"create_comment" => false,
"delete_comment" => false,
"bypass_comment_checks" => false, # spam etc
Permissions::CREATE_COMMENT => false,
Permissions::DELETE_COMMENT => false,
Permissions::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,
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,
"ban_image" => false,
Permissions::BAN_IMAGE => false,
"view_eventlog" => false,
"ignore_downtime" => false,
Permissions::VIEW_EVENTLOG => false,
Permissions::IGNORE_DOWNTIME => false,
"create_image_report" => false,
"view_image_report" => false, # deal with reported images
Permissions::CREATE_IMAGE_REPORT => false,
Permissions::VIEW_IMAGE_REPORT => false, # deal with reported images
"edit_wiki_page" => false,
"delete_wiki_page" => false,
Permissions::EDIT_WIKI_PAGE => false,
Permissions::DELETE_WIKI_PAGE => false,
"manage_blocks" => false,
Permissions::MANAGE_BLOCKS => false,
"manage_admintools" => false,
Permissions::MANAGE_ADMINTOOLS => false,
"view_other_pms" => false,
"edit_feature" => false,
"bulk_edit_vote" => false,
"edit_other_vote" => false,
"view_sysinfo" => false,
Permissions::VIEW_OTHER_PMS => false,
Permissions::EDIT_FEATURE => false,
Permissions::BULK_EDIT_VOTE => false,
Permissions::EDIT_OTHER_VOTE => false,
Permissions::VIEW_SYSINTO => false,
"hellbanned" => false,
"view_hellbanned" => false,
Permissions::HELLBANNED => false,
Permissions::VIEW_HELLBANNED => false,
"protected" => false, # only admins can modify protected users (stops a moderator changing an admin's password)
Permissions::PROTECTED => false, # only admins can modify protected users (stops a moderator changing an admin's password)
"edit_image_rating" => false,
"bulk_edit_image_rating" => false,
Permissions::EDIT_IMAGE_RATING => false,
Permissions::BULK_EDIT_IMAGE_RATING => false,
Permissions::VIEW_TRASH => false,
Permissions::PERFORM_BULK_ACTIONS => false,
]);
new UserClass("anonymous", "base", [
]);
new UserClass("user", "base", [
"big_search" => true,
"create_image" => true,
"create_comment" => true,
"edit_image_tag" => true,
"edit_image_source" => true,
"create_image_report" => true,
"edit_image_rating" => true,
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", [
"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,
"edit_image_rating" => true,
"bulk_edit_image_rating" => true,
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::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,
]);
new UserClass("hellbanned", "user", [
"hellbanned" => true,
Permissions::HELLBANNED => true,
]);
@include_once "data/config/user-classes.conf.php";

View File

@ -1,10 +1,11 @@
<?php
require_once "vendor/shish/libcontext-php/context.php";
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Misc *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
const DATA_DIR = "data";
function mtimefile(string $file): string
{
$data_href = get_base_href();
@ -15,7 +16,7 @@ function mtimefile(string $file): string
function get_theme(): string
{
global $config;
$theme = $config->get_string("theme", "default");
$theme = $config->get_string(SetupConfig::THEME, "default");
if (!file_exists("themes/$theme")) {
$theme = "default";
}
@ -78,7 +79,7 @@ function get_memory_limit(): int
// thumbnail generation requires lots of memory
$default_limit = 8*1024*1024; // 8 MB of memory is PHP's default.
$shimmie_limit = parse_shorthand_int($config->get_int("thumb_mem_limit"));
$shimmie_limit = parse_shorthand_int($config->get_int(MediaConfig::MEM_LIMIT));
if ($shimmie_limit < 3*1024*1024) {
// we aren't going to fit, override
@ -159,25 +160,40 @@ function format_text(string $string): string
return $tfe->formatted;
}
function warehouse_path(string $base, string $hash, bool $create=true): string
/**
* 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
{
$ab = substr($hash, 0, 2);
$cd = substr($hash, 2, 2);
if (WH_SPLITS == 2) {
$pa = 'data/'.$base.'/'.$ab.'/'.$cd.'/'.$hash;
} else {
$pa = 'data/'.$base.'/'.$ab.'/'.$hash;
$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;
}
function data_path(string $filename): string
/**
* Determines the path to the specified file in the data folder.
*/
function data_path(string $filename, bool $create = true): string
{
$filename = "data/" . $filename;
if (!file_exists(dirname($filename))) {
$filename = join_path("data", $filename);
if ($create&&!file_exists(dirname($filename))) {
mkdir(dirname($filename), 0755, true);
}
return $filename;
@ -281,21 +297,57 @@ function manual_include(string $fname): ?string
function path_to_tags(string $path): string
{
$matches = [];
$tags = "";
if (preg_match("/\d+ - (.*)\.([a-zA-Z0-9]+)/", basename($path), $matches)) {
$tags = $matches[1];
$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);
$dir_tags = dirname($path);
$dir_tags = str_replace("/", " ", $dir_tags);
$dir_tags = str_replace("__", " ", $dir_tags);
$dir_tags = trim($dir_tags);
if ($dir_tags != "") {
$tags = trim($tags)." ".trim($dir_tags);
$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;
}
$tags = trim($tags);
return $tags;
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;
}
@ -338,19 +390,6 @@ function get_debug_info(): string
return $debug;
}
function log_slow(): void
{
global $_shm_load_start;
if (!is_null(SLOW_PAGES)) {
$_time = microtime(true) - $_shm_load_start;
if ($_time > SLOW_PAGES) {
$_query = _get_query();
$_dbg = get_debug_info();
file_put_contents("data/slow-pages.log", "$_time $_query $_dbg\n", FILE_APPEND | LOCK_EX);
}
}
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Request initialisation stuff *
@ -375,7 +414,7 @@ date and you should plan on moving elsewhere.
function _sanitise_environment(): void
{
global $_shm_ctx;
global $_tracer;
if (TIMEZONE) {
date_default_timezone_set(TIMEZONE);
@ -387,10 +426,7 @@ function _sanitise_environment(): void
error_reporting(E_ALL);
}
$_shm_ctx = new Context();
if (CONTEXT) {
$_shm_ctx->set_log(CONTEXT);
}
$_tracer = new EventTracer();
if (COVERAGE) {
_start_coverage();
@ -552,8 +588,8 @@ 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("ban_ip") ? ", <a href='".make_link("ip_ban/list", "ip=$ip&reason=$u_reason&end=$u_end#add")."'>Ban</a>" : "";
$ip = $user->can("view_ip") ? $ip.$ban : "";
$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;
}

View File

@ -54,7 +54,7 @@ class AdminPage extends Extension
global $page, $user;
if ($event->page_matches("admin")) {
if (!$user->can("manage_admintools")) {
if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) {
$this->theme->display_permission_denied();
} else {
if ($event->count_args() == 0) {
@ -70,7 +70,7 @@ class AdminPage extends Extension
}
if ($aae->redirect) {
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("admin"));
}
}
@ -108,10 +108,20 @@ class AdminPage extends Extension
$this->theme->display_form();
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
global $user;
if($event->parent==="system") {
if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
$event->add_nav_link("admin", new Link('admin'), "Board Admin");
}
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
if ($user->can("manage_admintools")) {
if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
$event->add_link("Board Admin", make_link("admin"));
}
}
@ -137,6 +147,7 @@ class AdminPage extends Extension
global $page;
$query = $_POST['query'];
$reason = @$_POST['reason'];
assert(strlen($query) > 1);
$images = Image::find_images(0, 1000000, Tag::explode($query));
@ -146,10 +157,10 @@ class AdminPage extends Extension
if ($reason && class_exists("ImageBan")) {
send_event(new AddImageHashBanEvent($image->hash, $reason));
}
send_event(new ImageDeletionEvent($image));
send_event(new ImageDeletionEvent($image, true));
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/list"));
return false;
}
@ -201,14 +212,14 @@ class AdminPage extends Extension
$database = $matches['dbname'];
switch ($software) {
case 'mysql':
case DatabaseDriver::MYSQL:
$cmd = "mysqldump -h$hostname -u$username -p$password $database";
break;
case 'pgsql':
case DatabaseDriver::PGSQL:
putenv("PGPASSWORD=$password");
$cmd = "pg_dump -h $hostname -U $username $database";
break;
case 'sqlite':
case DatabaseDriver::SQLITE:
$cmd = "sqlite3 $database .dump";
break;
default:
@ -218,7 +229,7 @@ class AdminPage extends Extension
//FIXME: .SQL dump is empty if cmd doesn't exist
if ($cmd) {
$page->set_mode("data");
$page->set_mode(PageMode::DATA);
$page->set_type("application/x-unknown");
$page->set_filename('shimmie-'.date('Ymd').'.sql');
$page->set_data(shell_exec($cmd));
@ -237,13 +248,13 @@ class AdminPage extends Extension
$zip = new ZipArchive;
if ($zip->open($filename, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE) === true) {
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->close();
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link($filename)); //TODO: Delete file after downloaded?
return false; // we do want a redirect, but a manual one
@ -257,7 +268,7 @@ class AdminPage extends Extension
//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);
if ($matches['proto'] == "mysql") {
if ($matches['proto'] == DatabaseDriver::MYSQL) {
$tables = $database->get_col("SELECT TABLE_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = :db
@ -280,9 +291,9 @@ class AdminPage extends Extension
$i++;
}
$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
} elseif ($matches['proto'] == "sqlite") {
} elseif ($matches['proto'] == DatabaseDriver::SQLITE) {
//TODO: Make this work with SQLite
}
return true;

View File

@ -45,7 +45,7 @@ class AdminPageTheme extends Themelet
$html .= $this->button("Download all images", "download_all_images", false);
}
$html .= $this->button("Download database contents", "database_dump", false);
if ($database->get_driver_name() == "mysql") {
if ($database->get_driver_name() == DatabaseDriver::MYSQL) {
$html .= $this->button("Reset image IDs", "reset_image_ids", true);
}
$page->add_block(new Block("Misc Admin Tools", $html));
@ -55,16 +55,18 @@ class AdminPageTheme extends Themelet
$html .= "<input type='submit' value='Set Tag Case'>";
$html .= "</form>\n";
$page->add_block(new Block("Set Tag Case", $html));
}
public function dbq_html($terms)
{
$h_terms = html_escape($terms);
$h_reason = "";
if(ext_is_live("Trash")) {
$warning = "This delete method will bypass the trash<br/>";
}
if (class_exists("ImageBan")) {
$h_reason = "<input type='text' name='reason' placeholder='Ban reason (leave blank to not ban)'>";
}
$html = make_form(make_link("admin/delete_by_query"), "POST") . "
$html = $warning.make_form(make_link("admin/delete_by_query"), "POST") . "
<input type='button' class='shm-unlocker' data-unlock-sel='#dbqsubmit' value='Unlock'>
<input type='hidden' name='query' value='$h_terms'>
$h_reason
@ -73,4 +75,6 @@ class AdminPageTheme extends Themelet
";
return $html;
}
}

View File

@ -36,12 +36,12 @@ class AliasEditor extends Extension
if ($event->page_matches("alias")) {
if ($event->get_arg(0) == "add") {
if ($user->can("manage_alias_list")) {
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
if (isset($_POST['oldtag']) && isset($_POST['newtag'])) {
try {
$aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']);
send_event($aae);
$page->set_mode("redirect");
$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());
@ -49,12 +49,12 @@ class AliasEditor extends Extension
}
}
} elseif ($event->get_arg(0) == "remove") {
if ($user->can("manage_alias_list")) {
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("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("alias/list"));
}
}
@ -80,18 +80,18 @@ class AliasEditor extends Extension
$this->theme->display_aliases($alias, $page_number + 1, $total_pages);
} elseif ($event->get_arg(0) == "export") {
$page->set_mode("data");
$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("manage_alias_list")) {
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("redirect");
$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");
@ -117,10 +117,17 @@ class AliasEditor extends Extension
}
}
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("manage_alias_list")) {
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
$event->add_link("Alias Editor", make_link("alias/list"));
}
}

View File

@ -11,7 +11,7 @@ class AliasEditorTheme extends Themelet
{
global $page, $user;
$can_manage = $user->can("manage_alias_list");
$can_manage = $user->can(Permissions::MANAGE_ALIAS_LIST);
if ($can_manage) {
$h_action = "<th width='10%'>Action</th>";
$h_add = "

View File

@ -47,12 +47,23 @@ class Artists extends Extension
public function onSearchTermParse(SearchTermParseEvent $event)
{
$matches = [];
if (preg_match("/^author[=|:](.*)$/i", $event->term, $matches)) {
if (preg_match("/^(author|artist)[=|:](.*)$/i", $event->term, $matches)) {
$char = $matches[1];
$event->add_querylet(new Querylet("Author = :author_char", ["author_char"=>$char]));
}
}
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
{
if($event->key===HelpPages::SEARCH) {
$block = new Block();
$block->header = "Artist";
$block->body = $this->theme->get_help_html();
$event->add_block($block);
}
}
public function onInitExt(InitExtEvent $event)
{
global $config, $database;
@ -172,7 +183,7 @@ class Artists extends Extension
}
case "new_artist":
{
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/new"));
break;
}
@ -183,7 +194,7 @@ class Artists extends Extension
if ($newArtistID == -1) {
$this->theme->display_error(400, "Error", "Error when entering artist data.");
} else {
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$newArtistID));
}
} else {
@ -238,7 +249,7 @@ class Artists extends Extension
case "edit_artist":
{
$artistID = $_POST['artist_id'];
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/edit/".$artistID));
break;
}
@ -246,14 +257,14 @@ class Artists extends Extension
{
$artistID = int_escape($_POST['id']);
$this->update_artist();
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "nuke_artist":
{
$artistID = $_POST['artist_id'];
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/nuke/".$artistID));
break;
}
@ -261,7 +272,7 @@ class Artists extends Extension
{
$artistID = $event->get_arg(1);
$this->delete_artist($artistID); // this will delete the artist, its alias, its urls and its members
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/list"));
break;
}
@ -291,7 +302,7 @@ class Artists extends Extension
{
$artistID = $_POST['artistID'];
$this->add_alias();
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
@ -300,7 +311,7 @@ class Artists extends Extension
$aliasID = $event->get_arg(2);
$artistID = $this->get_artistID_by_aliasID($aliasID);
$this->delete_alias($aliasID);
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
@ -316,7 +327,7 @@ class Artists extends Extension
$this->update_alias();
$aliasID = int_escape($_POST['aliasID']);
$artistID = $this->get_artistID_by_aliasID($aliasID);
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
@ -332,7 +343,7 @@ class Artists extends Extension
{
$artistID = $_POST['artistID'];
$this->add_urls();
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
@ -341,7 +352,7 @@ class Artists extends Extension
$urlID = $event->get_arg(2);
$artistID = $this->get_artistID_by_urlID($urlID);
$this->delete_url($urlID);
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
@ -357,7 +368,7 @@ class Artists extends Extension
$this->update_url();
$urlID = int_escape($_POST['urlID']);
$artistID = $this->get_artistID_by_urlID($urlID);
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
@ -372,7 +383,7 @@ class Artists extends Extension
{
$artistID = $_POST['artistID'];
$this->add_members();
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
@ -381,7 +392,7 @@ class Artists extends Extension
$memberID = int_escape($event->get_arg(2));
$artistID = $this->get_artistID_by_memberID($memberID);
$this->delete_member($memberID);
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
@ -397,7 +408,7 @@ class Artists extends Extension
$this->update_member();
$memberID = int_escape($_POST['memberID']);
$artistID = $this->get_artistID_by_memberID($memberID);
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}

View File

@ -545,4 +545,14 @@ class ArtistsTheme extends Themelet
}
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>
';
}
}

View File

@ -21,7 +21,7 @@ class AutoComplete extends Extension
return;
}
$page->set_mode("data");
$page->set_mode(PageMode::DATA);
$page->set_type("application/json");
$s = strtolower($_GET["s"]);
@ -38,7 +38,9 @@ class AutoComplete extends Extension
//$limit = 0;
$cache_key = "autocomplete-$s";
$limitSQL = "";
$SQLarr = ["search"=>"$s%"];
$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"];
@ -51,7 +53,8 @@ class AutoComplete extends Extension
$database->scoreql_to_sql("
SELECT tag, count
FROM tags
WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:search)
WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:search)
-- OR SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:cat_search)
AND count > 0
ORDER BY count DESC
$limitSQL"),

View File

@ -58,7 +58,7 @@ xanax
public function onCommentPosting(CommentPostingEvent $event)
{
global $user;
if (!$user->can("bypass_comment_checks")) {
if (!$user->can(Permissions::BYPASS_COMMENT_CHECKS)) {
$this->test_text($event->comment, new CommentPostingException("Comment contains banned terms"));
}
}

View File

@ -26,10 +26,20 @@ class Blocks extends Extension
}
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
global $user;
if($event->parent==="system") {
if ($user->can(Permissions::MANAGE_BLOCKS)) {
$event->add_nav_link("blocks", new Link('blocks/list'), "Blocks Editor");
}
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
if ($user->can("manage_blocks")) {
if ($user->can(Permissions::MANAGE_BLOCKS)) {
$event->add_link("Blocks Editor", make_link("blocks/list"));
}
}
@ -52,7 +62,7 @@ class Blocks extends Extension
}
}
if ($event->page_matches("blocks") && $user->can("manage_blocks")) {
if ($event->page_matches("blocks") && $user->can(Permissions::MANAGE_BLOCKS)) {
if ($event->get_arg(0) == "add") {
if ($user->check_auth_token()) {
$database->execute("
@ -61,7 +71,7 @@ class Blocks extends Extension
", [$_POST['pages'], $_POST['title'], $_POST['area'], (int)$_POST['priority'], $_POST['content']]);
log_info("blocks", "Added Block #".($database->get_last_insert_id('blocks_id_seq'))." (".$_POST['title'].")");
$database->cache->delete("blocks");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("blocks/list"));
}
}
@ -81,7 +91,7 @@ class Blocks extends Extension
log_info("blocks", "Updated Block #".$_POST['id']." (".$_POST['title'].")");
}
$database->cache->delete("blocks");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("blocks/list"));
}
} elseif ($event->get_arg(0) == "list") {

View File

@ -56,6 +56,17 @@ class Blotter extends Extension
$event->panel->add_block($sb);
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
global $user;
if($event->parent==="system") {
if ($user->is_admin()) {
$event->add_nav_link("blotter", new Link('blotter/editor'), "Blotter Editor");
}
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
@ -102,7 +113,7 @@ class Blotter extends Extension
[$entry_text, $important]
);
log_info("blotter", "Added Message: $entry_text");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("blotter/editor"));
}
break;
@ -119,7 +130,7 @@ class Blotter extends Extension
}
$database->Execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]);
log_info("blotter", "Removed Entry #$id");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("blotter/editor"));
}
break;

View File

@ -27,14 +27,14 @@ class BrowserSearch extends Extension
// Add in header code to let the browser know that the search plugin exists
// We need to build the data for the header
$search_title = $config->get_string('title');
$search_title = $config->get_string(SetupConfig::TITLE);
$search_file_url = make_link('browser_search/please_dont_use_this_tag_as_it_would_break_stuff__search.xml');
$page->add_html_header("<link rel='search' type='application/opensearchdescription+xml' title='$search_title' href='$search_file_url'>");
// The search.xml file that is generated on the fly
if ($event->page_matches("browser_search/please_dont_use_this_tag_as_it_would_break_stuff__search.xml")) {
// First, we need to build all the variables we'll need
$search_title = $config->get_string('title');
$search_title = $config->get_string(SetupConfig::TITLE);
$search_form_url = make_link('post/list/{searchTerms}');
$suggenton_url = make_link('browser_search/')."{searchTerms}";
$icon_b64 = base64_encode(file_get_contents("ext/handle_static/static/favicon.ico"));
@ -54,7 +54,7 @@ class BrowserSearch extends Extension
";
// And now to send it to the browser
$page->set_mode("data");
$page->set_mode(PageMode::DATA);
$page->set_type("text/xml");
$page->set_data($xml);
} elseif (
@ -85,7 +85,7 @@ class BrowserSearch extends Extension
// And now for the final output
$json_string = "[\"$tag_search\",[\"$json_tag_list\"],[],[]]";
$page->set_mode("data");
$page->set_mode(PageMode::DATA);
$page->set_data($json_string);
}
}

View File

@ -14,22 +14,31 @@ class BulkActionBlockBuildingEvent extends Event
/** @var array */
public $actions = [];
public function add_action(String $action, string $button_text, String $confirmation_message = "", String $block = "", int $position = 40)
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 = "";
}
array_push(
$this->actions,
[
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
]
);
];
}
}
@ -42,7 +51,7 @@ class BulkActionEvent extends Event
/** @var PageRequestEvent */
public $page_request;
public function __construct(String $action, PageRequestEvent $pageRequestEvent, array $items)
public function __construct(String $action, PageRequestEvent $pageRequestEvent, Generator $items)
{
$this->action = $action;
$this->page_request = $pageRequestEvent;
@ -58,6 +67,8 @@ class BulkActions extends Extension
if ($user->is_logged_in()) {
$babbe = new BulkActionBlockBuildingEvent();
$babbe->search_terms = $event->search_terms;
send_event($babbe);
if (sizeof($babbe->actions) == 0) {
@ -74,16 +85,23 @@ class BulkActions extends Extension
{
global $user;
if ($user->can("delete_image")) {
$event->add_action("bulk_delete", "Delete", "Delete selected images?", "", 10);
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("bulk_edit_image_tag")) {
$event->add_action("bulk_tag", "Tag", "", $this->theme->render_tag_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("bulk_edit_image_source")) {
$event->add_action("bulk_source", "Set Source", "", $this->theme->render_source_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);
}
}
@ -93,7 +111,7 @@ class BulkActions extends Extension
switch ($event->action) {
case "bulk_delete":
if ($user->can("delete_image")) {
if ($user->can(Permissions::DELETE_IMAGE)) {
$i = $this->delete_items($event->items);
flash_message("Deleted $i items");
}
@ -102,7 +120,7 @@ class BulkActions extends Extension
if (!isset($_POST['bulk_tags'])) {
return;
}
if ($user->can("bulk_edit_image_tag")) {
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") {
@ -117,7 +135,7 @@ class BulkActions extends Extension
if (!isset($_POST['bulk_source'])) {
return;
}
if ($user->can("bulk_edit_image_source")) {
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");
@ -129,49 +147,33 @@ class BulkActions extends Extension
public function onPageRequest(PageRequestEvent $event)
{
global $page, $user;
if ($event->page_matches("bulk_action") && $user->is_admin()) {
if ($event->page_matches("bulk_action") && $user->can(Permissions::PERFORM_BULK_ACTIONS)) {
if (!isset($_POST['bulk_action'])) {
return;
}
$action = $_POST['bulk_action'];
$items = [];
$items = null;
if (isset($_POST['bulk_selected_ids']) && $_POST['bulk_selected_ids'] != "") {
$data = json_decode($_POST['bulk_selected_ids']);
if (is_array($data)) {
foreach ($data as $id) {
if (is_numeric($id)) {
array_push($items, int_escape($id));
}
}
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 != "") {
$n = 0;
$tags = Tag::explode($query);
while (true) {
$results = Image::find_image_ids($n, 100, $tags);
if (count($results) == 0) {
break;
}
reset($results); // rewind to first element in array.
$items = array_merge($items, $results);
$n += count($results);
}
$items = $this->yield_search_results($query);
}
}
if (sizeof($items) > 0) {
reset($items); // rewind to first element in array.
if (is_iterable($items)) {
$newEvent = new BulkActionEvent($action, $event, $items);
send_event($newEvent);
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
if (!isset($_SERVER['HTTP_REFERER'])) {
$_SERVER['HTTP_REFERER'] = make_link();
}
@ -179,31 +181,50 @@ class BulkActions extends Extension
}
}
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(array $items): int
private function delete_items(iterable $items): int
{
$total = 0;
foreach ($items as $id) {
foreach ($items as $image) {
try {
$image = Image::by_id($id);
if ($image==null) {
continue;
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 $id: " . $e->getMessage(), "error");
flash_message("Error while removing {$image->id}: " . $e->getMessage(), "error");
}
}
return $total;
}
private function tag_items(array $items, string $tags, bool $replace): int
private function tag_items(iterable $items, string $tags, bool $replace): int
{
$tags = Tag::explode($tags);
@ -219,28 +240,21 @@ class BulkActions extends Extension
$total = 0;
if ($replace) {
foreach ($items as $id) {
$image = Image::by_id($id);
if ($image==null) {
continue;
}
foreach ($items as $image) {
send_event(new TagSetEvent($image, $tags));
$total++;
}
} else {
foreach ($items as $id) {
$image = Image::by_id($id);
if ($image==null) {
continue;
}
foreach ($items as $image) {
$img_tags = array_map("strtolower",$image->get_tag_array());
$img_tags = [];
if (!empty($neg_tag_array)) {
$img_tags = array_merge($pos_tag_array, $image->get_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, $image->get_tag_array());
$img_tags = array_merge($tags, $img_tags);
}
send_event(new TagSetEvent($image, $img_tags));
$total++;
@ -250,23 +264,17 @@ class BulkActions extends Extension
return $total;
}
private function set_source(array $items, String $source): int
private function set_source(iterable $items, String $source): int
{
$total = 0;
foreach ($items as $id) {
foreach ($items as $image) {
try {
$image = Image::by_id($id);
if ($image==null) {
continue;
}
send_event(new SourceSetEvent($image, $source));
$total++;
} catch (Exception $e) {
flash_message("Error while setting source for $id: " . $e->getMessage(), "error");
flash_message("Error while setting source for {$image->id}: " . $e->getMessage(), "error");
}
}
return $total;
}
}

View File

@ -37,7 +37,7 @@ function validate_selections(form, confirmationMessage) {
function activate_bulk_selector () {
set_selected_items([]);
if(!bulk_selector_initialized) {
$("a.shm-thumb").each(
$(".shm-thumb").each(
function (index, block) {
add_selector_button($(block));
}
@ -67,11 +67,11 @@ function get_selected_items() {
}
function set_selected_items(items) {
$("a.shm-thumb").removeClass('selected');
$(".shm-thumb").removeClass('selected');
$(items).each(
function(index,item) {
$('a.shm-thumb[data-post-id="' + item + '"]').addClass('selected');
$('.shm-thumb[data-post-id="' + item + '"]').addClass('selected');
}
);
@ -109,7 +109,7 @@ function toggle_selection( id ) {
function select_all() {
var items = [];
$("a.shm-thumb").each(
$(".shm-thumb").each(
function ( index, block ) {
block = $(block);
var id = block.data("post-id");
@ -122,7 +122,7 @@ function select_all() {
function select_invert() {
var currentItems = get_selected_items();
var items = [];
$("a.shm-thumb").each(
$(".shm-thumb").each(
function ( index, block ) {
block = $(block);
var id = block.data("post-id");
@ -141,7 +141,7 @@ function select_none() {
function select_range(start, end) {
var data = get_selected_items();
var selecting = false;
$("a.shm-thumb").each(
$(".shm-thumb").each(
function ( index, block ) {
block = $(block);
var id = block.data("post-id");

View File

@ -2,14 +2,14 @@
class BulkActionsTheme extends Themelet
{
public function display_selector(Page $page, $actions, $query)
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 Selector'/>
<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 Selector'/>
<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>
@ -36,7 +36,7 @@ class BulkActionsTheme extends Themelet
"<input type='hidden' name='bulk_selected_ids' />" .
"<input type='hidden' name='bulk_action' value='" . $action["action"] . "' />" .
$action["block"] .
"<input type='submit' name='submit_button' value='" . $action["button_text"] . "'/>" .
"<input type='submit' name='submit_button' accesskey='{$action["access_key"]}' value='" . $action["button_text"] . "'/>" .
"</form></div>";
}
@ -47,6 +47,15 @@ class BulkActionsTheme extends Themelet
$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>" .

View File

@ -81,7 +81,7 @@ class BulkAddCSV extends Extension
send_event($ratingevent);
}
if (file_exists($thumbfile)) {
copy($thumbfile, warehouse_path("thumbs", $event->hash));
copy($thumbfile, warehouse_path(Image::THUMBNAIL_DIR, $event->hash));
}
}
}

View File

@ -157,6 +157,21 @@ class CommentList extends Extension
}
}
public function onPageNavBuilding(PageNavBuildingEvent $event)
{
$event->add_nav_link("comment", new Link('comment/list'), "Comments");
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
if($event->parent=="comment") {
$event->add_nav_link("comment_list", new Link('comment/list'), "All");
$event->add_nav_link("comment_help", new Link('ext_doc/comment'), "Help");
}
}
public function onPageRequest(PageRequestEvent $event)
{
if ($event->page_matches("comment")) {
@ -178,7 +193,7 @@ class CommentList extends Extension
$i_iid = int_escape($_POST['image_id']);
$cpe = new CommentPostingEvent($_POST['image_id'], $user, $_POST['comment']);
send_event($cpe);
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/$i_iid#comment_on_$i_iid"));
} catch (CommentPostingException $ex) {
$this->theme->display_error(403, "Comment Blocked", $ex->getMessage());
@ -189,12 +204,12 @@ class CommentList extends Extension
private function onPageRequest_delete(PageRequestEvent $event)
{
global $user, $page;
if ($user->can("delete_comment")) {
if ($user->can(Permissions::DELETE_COMMENT)) {
// FIXME: post, not args
if ($event->count_args() === 3) {
send_event(new CommentDeletionEvent($event->get_arg(1)));
flash_message("Deleted comment");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
if (!empty($_SERVER['HTTP_REFERER'])) {
$page->set_redirect($_SERVER['HTTP_REFERER']);
} else {
@ -209,7 +224,7 @@ class CommentList extends Extension
private function onPageRequest_bulk_delete()
{
global $user, $database, $page;
if ($user->can("delete_comment") && !empty($_POST["ip"])) {
if ($user->can(Permissions::DELETE_COMMENT) && !empty($_POST["ip"])) {
$ip = $_POST['ip'];
$comment_ids = $database->get_col("
@ -224,7 +239,7 @@ class CommentList extends Extension
}
flash_message("Deleted $num comments");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("admin"));
} else {
$this->theme->display_permission_denied();
@ -288,7 +303,7 @@ class CommentList extends Extension
$this->theme->display_image_comments(
$event->image,
$this->get_comments($event->image->id),
$user->can("create_comment")
$user->can(Permissions::CREATE_COMMENT)
);
}
@ -351,6 +366,16 @@ class CommentList extends Extension
}
}
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
{
if($event->key===HelpPages::SEARCH) {
$block = new Block();
$block->header = "Comments";
$block->body = $this->theme->get_help_html();
$event->add_block($block);
}
}
// page building {{{
private function build_page(int $current_page)
{
@ -399,7 +424,7 @@ class CommentList extends Extension
}
}
$this->theme->display_comment_list($images, $current_page, $total_pages, $user->can("create_comment"));
$this->theme->display_comment_list($images, $current_page, $total_pages, $user->can(Permissions::CREATE_COMMENT));
}
// }}}
@ -480,14 +505,14 @@ class CommentList extends Extension
global $config, $database;
// sqlite fails at intervals
if ($database->get_driver_name() === "sqlite") {
if ($database->get_driver_name() === DatabaseDriver::SQLITE) {
return false;
}
$window = int_escape($config->get_int('comment_window'));
$max = int_escape($config->get_int('comment_limit'));
if ($database->get_driver_name() == "mysql") {
if ($database->get_driver_name() == DatabaseDriver::MYSQL) {
$window_sql = "interval $window minute";
} else {
$window_sql = "interval '$window minute'";
@ -574,7 +599,7 @@ class CommentList extends Extension
{
global $database, $page;
if (!$user->can("bypass_comment_checks")) {
if (!$user->can(Permissions::BYPASS_COMMENT_CHECKS)) {
// will raise an exception if anything is wrong
$this->comment_checks($image_id, $user, $comment);
}
@ -600,7 +625,7 @@ class CommentList extends Extension
global $config, $page;
// basic sanity checks
if (!$user->can("create_comment")) {
if (!$user->can(Permissions::CREATE_COMMENT)) {
throw new CommentPostingException("Anonymous posting has been disabled");
} elseif (is_null(Image::by_id($image_id))) {
throw new CommentPostingException("The image does not exist");

View File

@ -218,9 +218,9 @@ class CommentListTheme extends Themelet
if (!array_key_exists($comment->poster_ip, $this->anon_map)) {
$this->anon_map[$comment->poster_ip] = $this->anon_id;
}
#if($user->can("view_ip")) {
#if($user->can(UserAbilities::VIEW_IP)) {
#$style = " style='color: ".$this->get_anon_colour($comment->poster_ip).";'";
if ($user->can("view_ip") || $config->get_bool("comment_samefags_public", false)) {
if ($user->can(Permissions::VIEW_IP) || $config->get_bool("comment_samefags_public", false)) {
if ($this->anon_map[$comment->poster_ip] != $this->anon_id) {
$anoncode2 = '<sup>('.$this->anon_map[$comment->poster_ip].')</sup>';
}
@ -248,9 +248,9 @@ class CommentListTheme extends Themelet
$h_avatar = "<img src=\"//www.gravatar.com/avatar/$hash.jpg?cacheBreak=$cb\"><br>";
}
$h_reply = " - <a href='javascript: replyTo($i_image_id, $i_comment_id, \"$h_name\")'>Reply</a>";
$h_ip = $user->can("view_ip") ? "<br>".show_ip($comment->poster_ip, "Comment posted {$comment->posted}") : "";
$h_ip = $user->can(Permissions::VIEW_IP) ? "<br>".show_ip($comment->poster_ip, "Comment posted {$comment->posted}") : "";
$h_del = "";
if ($user->can("delete_comment")) {
if ($user->can(Permissions::DELETE_COMMENT)) {
$comment_preview = substr(html_unescape($tfe->stripped), 0, 50);
$j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview");
$h_delete_script = html_escape("return confirm($j_delete_confirm_message);");
@ -290,4 +290,28 @@ class CommentListTheme extends Themelet
</div>
';
}
public function get_help_html()
{
return '<p>Search for images containing a certain number of comments, or comments by a particular individual.</p>
<div class="command_example">
<pre>comments=1</pre>
<p>Returns images with exactly 1 comment.</p>
</div>
<div class="command_example">
<pre>comments>0</pre>
<p>Returns images with 1 or more comments. </p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
<div class="command_example">
<pre>commented_by:username</pre>
<p>Returns images that have been commented on by "username". </p>
</div>
<div class="command_example">
<pre>commented_by_userno:123</pre>
<p>Returns images that have been commented on by user 123. </p>
</div>
';
}
}

View File

@ -1,42 +1,52 @@
<?php
/*
* Name: Cron Uploader
* Author: YaoiFox <admin@yaoifox.com>
* Authors: YaoiFox <admin@yaoifox.com>, Matthew Barbour <matthew@darkholme.net>
* Link: http://www.yaoifox.com/
* License: GPLv2
* Description: Uploads images automatically using Cron Jobs
* Documentation: Installation guide: activate this extension and navigate to www.yoursite.com/cron_upload
*/
class CronUploader extends Extension
{
// TODO: Checkbox option to only allow localhost + a list of additional IP adresses that can be set in /cron_upload
// TODO: Change logging to MySQL + display log at /cron_upload
// TODO: Move stuff to theme.php
const QUEUE_DIR = "queue";
const UPLOADED_DIR = "uploaded";
const FAILED_DIR = "failed_to_upload";
const CONFIG_KEY = "cron_uploader_key";
const CONFIG_COUNT = "cron_uploader_count";
const CONFIG_DIR = "cron_uploader_dir";
/**
* Lists all log events this session
* @var string
*/
private $upload_info = "";
/**
* Lists all files & info required to upload.
* @var array
*/
private $image_queue = [];
/**
* Cron Uploader root directory
* @var string
*/
private $root_dir = "";
/**
* Key used to identify uploader
* @var string
*/
private $upload_key = "";
/**
* Checks if the cron upload page has been accessed
* and initializes the upload.
@ -44,39 +54,50 @@ class CronUploader extends Extension
public function onPageRequest(PageRequestEvent $event)
{
global $config, $user;
if ($event->page_matches("cron_upload")) {
$this->upload_key = $config->get_string("cron_uploader_key", "");
$this->upload_key = $config->get_string(self::CONFIG_KEY, "");
// If the key is in the url, upload
if ($this->upload_key != "" && $event->get_arg(0) == $this->upload_key) {
// log in as admin
$this->process_upload(); // Start upload
$this->set_dir();
$lockfile = fopen($this->root_dir . "/.lock", "w");
if (!flock($lockfile, LOCK_EX | LOCK_NB)) {
throw new Exception("Cron upload process is already running");
}
try {
$this->process_upload(); // Start upload
} finally {
flock($lockfile, LOCK_UN);
fclose($lockfile);
}
} elseif ($user->is_admin()) {
$this->set_dir();
$this->display_documentation();
}
}
}
private function display_documentation()
{
global $page;
$this->set_dir(); // Determines path to cron_uploader_dir
$queue_dir = $this->root_dir . "/queue";
$uploaded_dir = $this->root_dir . "/uploaded";
$failed_dir = $this->root_dir . "/failed_to_upload";
$queue_dir = $this->root_dir . "/" . self::QUEUE_DIR;
$uploaded_dir = $this->root_dir . "/" . self::UPLOADED_DIR;
$failed_dir = $this->root_dir . "/" . self::FAILED_DIR;
$queue_dirinfo = $this->scan_dir($queue_dir);
$uploaded_dirinfo = $this->scan_dir($uploaded_dir);
$failed_dirinfo = $this->scan_dir($failed_dir);
$cron_url = make_http(make_link("/cron_upload/" . $this->upload_key));
$cron_cmd = "curl --silent $cron_url";
$log_path = $this->root_dir . "/uploads.log";
$info_html = "<b>Information</b>
<br>
<table style='width:470px;'>
@ -105,7 +126,7 @@ class CronUploader extends Extension
<br>Cron Command: <input type='text' size='60' value='$cron_cmd'><br>
Create a cron job with the command above.<br/>
Read the documentation if you're not sure what to do.<br>";
$install_html = "
This cron uploader is fairly easy to use but has to be configured first.
<br />1. Install & activate this plugin.
@ -130,8 +151,10 @@ class CronUploader extends Extension
<br />So when you want to manually upload an image, all you have to do is open the link once.
<br />This link can be found under 'Cron Command' in the board config, just remove the 'wget ' part and only the url remains.
<br />(<b>$cron_url</b>)";
$page->set_title("Cron Uploader");
$page->set_heading("Cron Uploader");
$block = new Block("Cron Uploader", $info_html, "main", 10);
$block_install = new Block("Installation Guide", $install_html, "main", 20);
$page->add_block($block);
@ -142,36 +165,37 @@ class CronUploader extends Extension
{
global $config;
// Set default values
$this->upload_key = $config->get_string("cron_uploader_key", "");
if (strlen($this->upload_key)<=0) {
$config->set_default_int(self::CONFIG_COUNT, 1);
$this->set_dir();
$this->upload_key = $config->get_string(self::CONFIG_KEY, "");
if (empty($this->upload_key)) {
$this->upload_key = $this->generate_key();
$config->set_default_int('cron_uploader_count', 1);
$config->set_default_string('cron_uploader_key', $this->upload_key);
$this->set_dir();
$config->set_string(self::CONFIG_KEY, $this->upload_key);
}
}
public function onSetupBuilding(SetupBuildingEvent $event)
{
$this->set_dir();
$cron_url = make_http(make_link("/cron_upload/" . $this->upload_key));
$cron_cmd = "curl --silent $cron_url";
$documentation_link = make_http(make_link("cron_upload"));
$sb = new SetupBlock("Cron Uploader");
$sb->add_label("<b>Settings</b><br>");
$sb->add_int_option("cron_uploader_count", "How many to upload each time");
$sb->add_text_option("cron_uploader_dir", "<br>Set Cron Uploader root directory<br>");
$sb->add_label("<br>Cron Command: <input type='text' size='60' value='$cron_cmd'><br>
$sb->add_int_option(self::CONFIG_COUNT, "How many to upload each time");
$sb->add_text_option(self::CONFIG_DIR, "<br>Set Cron Uploader root directory<br>");
$sb->add_label("<br>Cron Command: <input type='text' size='60' readonly='readonly' value='" . html_escape($cron_cmd) . "'><br>
Create a cron job with the command above.<br/>
<a href='$documentation_link'>Read the documentation</a> if you're not sure what to do.");
$event->panel->add_block($sb);
}
/*
* Generates a unique key for the website to prevent unauthorized access.
*/
@ -180,14 +204,14 @@ class CronUploader extends Extension
$length = 20;
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
for ($i = 0; $i < $length; $i ++) {
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters [rand(0, strlen($characters) - 1)];
}
return $randomString;
}
/*
* Set the directory for the image queue. If no directory was given, set it to the default directory.
*/
@ -195,50 +219,50 @@ class CronUploader extends Extension
{
global $config;
// Determine directory (none = default)
$dir = $config->get_string("cron_uploader_dir", "");
$dir = $config->get_string(self::CONFIG_DIR, "");
// Sets new default dir if not in config yet/anymore
if ($dir == "") {
$dir = data_path("cron_uploader");
$config->set_string('cron_uploader_dir', $dir);
$config->set_string(self::CONFIG_DIR, $dir);
}
// Make the directory if it doesn't exist yet
if (!is_dir($dir . "/queue/")) {
mkdir($dir . "/queue/", 0775, true);
if (!is_dir($dir . "/" . self::QUEUE_DIR . "/")) {
mkdir($dir . "/" . self::QUEUE_DIR . "/", 0775, true);
}
if (!is_dir($dir . "/uploaded/")) {
mkdir($dir . "/uploaded/", 0775, true);
if (!is_dir($dir . "/" . self::UPLOADED_DIR . "/")) {
mkdir($dir . "/" . self::UPLOADED_DIR . "/", 0775, true);
}
if (!is_dir($dir . "/failed_to_upload/")) {
mkdir($dir . "/failed_to_upload/", 0775, true);
if (!is_dir($dir . "/" . self::FAILED_DIR . "/")) {
mkdir($dir . "/" . self::FAILED_DIR . "/", 0775, true);
}
$this->root_dir = $dir;
return $dir;
}
/**
* Returns amount of files & total size of dir.
*/
public function scan_dir(string $path): array
{
$bytestotal=0;
$nbfiles=0;
$bytestotal = 0;
$nbfiles = 0;
$ite=new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
foreach (new RecursiveIteratorIterator($ite) as $filename=>$cur) {
$ite = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
foreach (new RecursiveIteratorIterator($ite) as $filename => $cur) {
$filesize = $cur->getSize();
$bytestotal += $filesize;
$nbfiles++;
}
$size_mb = $bytestotal / 1048576; // to mb
$size_mb = number_format($size_mb, 2, '.', '');
return ['total_files'=>$nbfiles,'total_mb'=>$size_mb];
return ['total_files' => $nbfiles, 'total_mb' => $size_mb];
}
/**
* Uploads the image & handles everything
*/
@ -246,24 +270,24 @@ class CronUploader extends Extension
{
global $config, $database;
set_time_limit(0);
//set_time_limit(0);
$output_subdir = date('Ymd-His', time())."/";
$this->set_dir();
$output_subdir = date('Ymd-His', time()) . "/";
$this->generate_image_queue();
// Gets amount of imgs to upload
if ($upload_count == 0) {
$upload_count = $config->get_int("cron_uploader_count", 1);
$upload_count = $config->get_int(self::CONFIG_COUNT, 1);
}
// Throw exception if there's nothing in the queue
if (count($this->image_queue) == 0) {
$this->add_upload_info("Your queue is empty so nothing could be uploaded.");
$this->handle_log();
return false;
}
// Randomize Images
//shuffle($this->image_queue);
@ -271,18 +295,17 @@ class CronUploader extends Extension
$added = 0;
$failed = 0;
$failedItems = [];
// Upload the file(s)
for ($i = 0; $i < $upload_count && sizeof($this->image_queue)>0; $i++) {
for ($i = 0; $i < $upload_count && sizeof($this->image_queue) > 0; $i++) {
$img = array_pop($this->image_queue);
try {
$database->beginTransaction();
$this->add_upload_info("Adding file: {$img[1]} - tags: {$img[2]}");
$result = $this->add_image($img[0], $img[1], $img[2]);
$database->commit();
$this->move_uploaded($img[0], $img[1], $output_subdir, false);
if ($result==null) {
if ($result->merged) {
$merged++;
} else {
$added++;
@ -290,13 +313,9 @@ class CronUploader extends Extension
} catch (Exception $e) {
$failed++;
$this->move_uploaded($img[0], $img[1], $output_subdir, true);
$msgNumber = $this->add_upload_info("(".gettype($e).") ".$e->getMessage());
$msgNumber = $this->add_upload_info("(" . gettype($e) . ") " . $e->getMessage());
$msgNumber = $this->add_upload_info($e->getTraceAsString());
if (strpos($e->getMessage(), 'SQLSTATE') !== false) {
// Postgres invalidates the transaction if there is an SQL error,
// so all subsequence transactions will fail.
break;
}
try {
$database->rollback();
} catch (Exception $e) {
@ -310,85 +329,94 @@ class CronUploader extends Extension
$msgNumber = $this->add_upload_info("Items failed: $failed");
// Display & save upload log
$this->handle_log();
return true;
}
private function move_uploaded($path, $filename, $output_subdir, $corrupt = false)
{
global $config;
// Create
$newDir = $this->root_dir;
$relativeDir = dirname(substr($path, strlen($this->root_dir) + 7));
// Determine which dir to move to
if ($corrupt) {
// Move to corrupt dir
$newDir .= "/failed_to_upload/".$output_subdir.$relativeDir;
$newDir .= "/" . self::FAILED_DIR . "/" . $output_subdir . $relativeDir;
$info = "ERROR: Image was not uploaded.";
} else {
$newDir .= "/uploaded/".$output_subdir.$relativeDir;
$newDir .= "/" . self::UPLOADED_DIR . "/" . $output_subdir . $relativeDir;
$info = "Image successfully uploaded. ";
}
$newDir = str_replace("//", "/", $newDir."/");
$newDir = str_replace("//", "/", $newDir . "/");
if (!is_dir($newDir)) {
mkdir($newDir, 0775, true);
}
// move file to correct dir
rename($path, $newDir.$filename);
rename($path, $newDir . $filename);
$this->add_upload_info($info . "Image \"$filename\" moved from queue to \"$newDir\".");
}
/**
* Generate the necessary DataUploadEvent for a given image and tags.
*/
private function add_image(string $tmpname, string $filename, string $tags)
private function add_image(string $tmpname, string $filename, string $tags): DataUploadEvent
{
assert(file_exists($tmpname));
$tagArray = Tag::explode($tags);
if (count($tagArray)==0) {
$tagArray[] = "tagme";
}
$pathinfo = pathinfo($filename);
$metadata = [];
$metadata ['filename'] = $pathinfo ['basename'];
if (array_key_exists('extension', $pathinfo)) {
$metadata ['extension'] = $pathinfo ['extension'];
}
$metadata ['tags'] = Tag::explode($tags);
$metadata ['tags'] = $tagArray; // doesn't work when not logged in here, handled below
$metadata ['source'] = null;
$event = new DataUploadEvent($tmpname, $metadata);
send_event($event);
// Generate info message
$infomsg = ""; // Will contain info message
if ($event->image_id == -1) {
throw new Exception("File type not recognised. Filename: {$filename}");
} elseif ($event->image_id == null) {
$infomsg = "Image merged. Filename: {$filename}";
} elseif ($event->merged === true) {
$infomsg = "Image merged. ID: {$event->image_id} Filename: {$filename}";
} else {
$infomsg = "Image uploaded. ID: {$event->image_id} - Filename: {$filename}";
}
$msgNumber = $this->add_upload_info($infomsg);
return $event->image_id;
// Set tags
$img = Image::by_id($event->image_id);
$img->set_tags(array_merge($tagArray, $img->get_tag_array()));
return $event;
}
private function generate_image_queue(): void
{
$base = $this->root_dir . "/queue";
if (! is_dir($base)) {
$base = $this->root_dir . "/" . self::QUEUE_DIR;
if (!is_dir($base)) {
$this->add_upload_info("Image Queue Directory could not be found at \"$base\".");
return;
}
$ite=new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS);
foreach (new RecursiveIteratorIterator($ite) as $fullpath=>$cur) {
$ite = new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS);
foreach (new RecursiveIteratorIterator($ite) as $fullpath => $cur) {
if (!is_link($fullpath) && !is_dir($fullpath)) {
$pathinfo = pathinfo($fullpath);
@ -396,62 +424,62 @@ class CronUploader extends Extension
$tags = path_to_tags($relativePath);
$img = [
0 => $fullpath,
1 => $pathinfo ["basename"],
2 => $tags
0 => $fullpath,
1 => $pathinfo ["basename"],
2 => $tags
];
array_push($this->image_queue, $img);
}
}
}
/**
* Adds a message to the info being published at the end
*/
private function add_upload_info(string $text, int $addon = 0): int
{
$info = $this->upload_info;
$time = "[" .date('Y-m-d H:i:s'). "]";
$time = "[" . date('Y-m-d H:i:s') . "]";
// If addon function is not used
if ($addon == 0) {
$this->upload_info .= "$time $text\r\n";
$this->upload_info .= "$time $text\r\n";
// Returns the number of the current line
$currentLine = substr_count($this->upload_info, "\n") -1;
$currentLine = substr_count($this->upload_info, "\n") - 1;
return $currentLine;
}
// else if addon function is used, select the line & modify it
$lines = substr($info, "\n"); // Seperate the string to array in lines
$lines[$addon] = "$lines[$addon] $text"; // Add the content to the line
$this->upload_info = implode("\n", $lines); // Put string back together & update
return $addon; // Return line number
}
/**
* This is run at the end to display & save the log.
*/
private function handle_log()
{
global $page;
// Display message
$page->set_mode("data");
$page->set_mode(PageMode::DATA);
$page->set_type("text/plain");
$page->set_data($this->upload_info);
// Save log
$log_path = $this->root_dir . "/uploads.log";
if (file_exists($log_path)) {
$prev_content = file_get_contents($log_path);
} else {
$prev_content = "";
}
$content = $prev_content ."\r\n".$this->upload_info;
$content = $prev_content . "\r\n" . $this->upload_info;
file_put_contents($log_path, $content);
}
}

View File

@ -65,7 +65,7 @@ class custom_html_headers extends Extension
global $config, $page;
// get config values
$site_title = $config->get_string("title");
$site_title = $config->get_string(SetupConfig::TITLE);
$sitename_in_title = $config->get_int("sitename_in_title");
// if feature is enabled & sitename isn't already in title

View File

@ -60,7 +60,7 @@ class DanbooruApi extends Extension
private function api_danbooru(PageRequestEvent $event)
{
global $page;
$page->set_mode("data");
$page->set_mode(PageMode::DATA);
if (($event->get_arg(1) == 'add_post') || (($event->get_arg(1) == 'post') && ($event->get_arg(2) == 'create.xml'))) {
// No XML data is returned from this function
@ -80,7 +80,7 @@ class DanbooruApi extends Extension
// This redirects that to http://shimmie/post/view/123
elseif (($event->get_arg(1) == 'post') && ($event->get_arg(2) == 'show')) {
$fixedlocation = make_link("post/view/" . $event->get_arg(3));
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($fixedlocation);
}
}
@ -297,7 +297,7 @@ class DanbooruApi extends Extension
// Now we check if a file was uploaded or a url was provided to transload
// Much of this code is borrowed from /ext/upload
if (!$user->can("create_image")) {
if (!$user->can(Permissions::CREATE_IMAGE)) {
$page->set_code(409);
$page->add_http_header("X-Danbooru-Errors: authentication error");
return;

View File

@ -32,7 +32,7 @@ class Downtime extends Extension
global $config, $page, $user;
if ($config->get_bool("downtime")) {
if (!$user->can("ignore_downtime") && !$this->is_safe_page($event)) {
if (!$user->can(Permissions::IGNORE_DOWNTIME) && !$this->is_safe_page($event)) {
$msg = $config->get_string("downtime_message");
$this->theme->display_message($msg);
if (!defined("UNITTEST")) { // hax D:

View File

@ -21,12 +21,12 @@ class DowntimeTheme extends Themelet
public function display_message(string $message)
{
global $config, $user, $page;
$theme_name = $config->get_string('theme');
$theme_name = $config->get_string(SetupConfig::THEME);
$data_href = get_base_href();
$login_link = make_link("user_admin/login");
$auth = $user->get_auth_html();
$page->set_mode('data');
$page->set_mode(PageMode::DATA);
$page->set_code(503);
$page->set_data(
<<<EOD

View File

@ -18,7 +18,7 @@ class EmoticonListTheme extends Themelet
}
$html .= "</tr></table>";
$html .= "</body></html>";
$page->set_mode("data");
$page->set_mode(PageMode::DATA);
$page->set_data($html);
}
}

View File

@ -18,16 +18,28 @@ class ET extends Extension
{
global $user;
if ($event->page_matches("system_info")) {
if ($user->can("view_sysinfo")) {
if ($user->can(Permissions::VIEW_SYSINTO)) {
$this->theme->display_info_page($this->get_info());
}
}
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
global $user;
if($event->parent==="system") {
if ($user->can(Permissions::VIEW_SYSINTO)) {
$event->add_nav_link("system_info", new Link('system_info'), "System Info", null, 10);
}
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
if ($user->can("view_sysinfo")) {
if ($user->can(Permissions::VIEW_SYSINTO)) {
$event->add_link("System Info", make_link("system_info"));
}
}
@ -40,8 +52,8 @@ class ET extends Extension
global $config, $database;
$info = [];
$info['site_title'] = $config->get_string("title");
$info['site_theme'] = $config->get_string("theme");
$info['site_title'] = $config->get_string(SetupConfig::TITLE);
$info['site_theme'] = $config->get_string(SetupConfig::THEME);
$info['site_url'] = "http://" . $_SERVER["HTTP_HOST"] . get_base_href();
$info['sys_shimmie'] = VERSION;
@ -52,14 +64,17 @@ class ET extends Extension
$info['sys_disk'] = to_shorthand_int(disk_total_space("./") - disk_free_space("./")) . " / " .
to_shorthand_int(disk_total_space("./"));
$info['sys_server'] = isset($_SERVER["SERVER_SOFTWARE"]) ? $_SERVER["SERVER_SOFTWARE"] : 'unknown';
$info['thumb_engine'] = $config->get_string("thumb_engine");
$info['thumb_quality'] = $config->get_int('thumb_quality');
$info['thumb_width'] = $config->get_int('thumb_width');
$info['thumb_height'] = $config->get_int('thumb_height');
$info['thumb_scaling'] = $config->get_int('thumb_scaling');
$info['thumb_type'] = $config->get_string('thumb_type');
$info['thumb_mem'] = $config->get_int("thumb_mem_limit");
$info[MediaConfig::FFMPEG_PATH] = $config->get_string(MediaConfig::FFMPEG_PATH);
$info[MediaConfig::CONVERT_PATH] = $config->get_string(MediaConfig::CONVERT_PATH);
$info[MediaConfig::MEM_LIMIT] = $config->get_int(MediaConfig::MEM_LIMIT);
$info[ImageConfig::THUMB_ENGINE] = $config->get_string(ImageConfig::THUMB_ENGINE);
$info[ImageConfig::THUMB_QUALITY] = $config->get_int(ImageConfig::THUMB_QUALITY);
$info[ImageConfig::THUMB_WIDTH] = $config->get_int(ImageConfig::THUMB_WIDTH);
$info[ImageConfig::THUMB_HEIGHT] = $config->get_int(ImageConfig::THUMB_HEIGHT);
$info[ImageConfig::THUMB_SCALING] = $config->get_int(ImageConfig::THUMB_SCALING);
$info[ImageConfig::THUMB_TYPE] = $config->get_string(ImageConfig::THUMB_TYPE);
$info['stat_images'] = $database->get_one("SELECT COUNT(*) FROM images");
$info['stat_comments'] = $database->get_one("SELECT COUNT(*) FROM comments");

View File

@ -35,14 +35,16 @@ Database: {$info['sys_db']}
Server: {$info['sys_server']}
Disk use: {$info['sys_disk']}
Media System:
Memory Limit: {$info[MediaConfig::MEM_LIMIT]}
Thumbnail Generation:
Engine: {$info['thumb_engine']}
Type: {$info['thumb_type']}
Memory: {$info['thumb_mem']}
Quality: {$info['thumb_quality']}
Width: {$info['thumb_width']}
Height: {$info['thumb_height']}
Scaling: {$info['thumb_scaling']}
Engine: {$info[ImageConfig::THUMB_ENGINE]}
Type: {$info[ImageConfig::THUMB_TYPE]}
Quality: {$info[ImageConfig::THUMB_QUALITY]}
Width: {$info[ImageConfig::THUMB_WIDTH]}
Height: {$info[ImageConfig::THUMB_HEIGHT]}
Scaling: {$info[ImageConfig::THUMB_SCALING]}
Shimmie stats:
Images: {$info['stat_images']}

View File

@ -22,8 +22,7 @@ class ExtensionInfo
public $ext_name;
public $name;
public $link;
public $author;
public $email;
public $authors;
public $description;
public $documentation;
public $version;
@ -39,8 +38,9 @@ class ExtensionInfo
$this->ext_name = $matches[1];
$this->name = $this->ext_name;
$this->enabled = $this->is_enabled($this->ext_name);
$this->authors = [];
for ($i=0; $i<$number_of_lines; $i++) {
for ($i = 0; $i < $number_of_lines; $i++) {
$line = $lines[$i];
if (preg_match("/Name: (.*)/", $line, $matches)) {
$this->name = $matches[1];
@ -53,25 +53,29 @@ class ExtensionInfo
}
} elseif (preg_match("/Version: (.*)/", $line, $matches)) {
$this->version = $matches[1];
} elseif (preg_match("/Author: (.*) [<\(](.*@.*)[>\)]/", $line, $matches)) {
$this->author = $matches[1];
$this->email = $matches[2];
} elseif (preg_match("/Author: (.*)/", $line, $matches)) {
$this->author = $matches[1];
} elseif (preg_match("/Authors?: (.*)/", $line, $matches)) {
$author_list = explode(',', $matches[1]);
foreach ($author_list as $author) {
if (preg_match("/(.*) [<\(](.*@.*)[>\)]/", $author, $matches)) {
$this->authors[] = new ExtensionAuthor($matches[1], $matches[2]);
} else {
$this->authors[] = new ExtensionAuthor($author, null);
}
}
} elseif (preg_match("/(.*)Description: ?(.*)/", $line, $matches)) {
$this->description = $matches[2];
$start = $matches[1]." ";
$start = $matches[1] . " ";
$start_len = strlen($start);
while (substr($lines[$i+1], 0, $start_len) == $start) {
$this->description .= " ".substr($lines[$i+1], $start_len);
while (substr($lines[$i + 1], 0, $start_len) == $start) {
$this->description .= " " . substr($lines[$i + 1], $start_len);
$i++;
}
} elseif (preg_match("/(.*)Documentation: ?(.*)/", $line, $matches)) {
$this->documentation = $matches[2];
$start = $matches[1]." ";
$start = $matches[1] . " ";
$start_len = strlen($start);
while (substr($lines[$i+1], 0, $start_len) == $start) {
$this->documentation .= " ".substr($lines[$i+1], $start_len);
while (substr($lines[$i + 1], 0, $start_len) == $start) {
$this->documentation .= " " . substr($lines[$i + 1], $start_len);
$i++;
}
$this->documentation = str_replace('$site', make_http(get_base_href()), $this->documentation);
@ -96,18 +100,30 @@ class ExtensionInfo
}
}
class ExtensionAuthor
{
public $name;
public $email;
public function __construct(string $name, ?string $email)
{
$this->name = $name;
$this->email = $email;
}
}
class ExtManager extends Extension
{
public function onPageRequest(PageRequestEvent $event)
{
global $page, $user;
if ($event->page_matches("ext_manager")) {
if ($user->can("manage_extension_list")) {
if ($user->can(Permissions::MANAGE_EXTENSION_LIST)) {
if ($event->get_arg(0) == "set" && $user->check_auth_token()) {
if (is_writable("data/config")) {
$this->set_things($_POST);
log_warning("ext_manager", "Active extensions changed", "Active extensions changed");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("ext_manager"));
} else {
$this->theme->display_error(
@ -146,11 +162,22 @@ class ExtManager extends Extension
}
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
global $user;
if($event->parent==="system") {
if ($user->can(Permissions::MANAGE_EXTENSION_LIST)) {
$event->add_nav_link("ext_manager", new Link('ext_manager'), "Extension Manager");
} else {
$event->add_nav_link("ext_doc", new Link('ext_doc'), "Board Help");
}
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
if ($user->can("manage_extension_list")) {
if ($user->can(Permissions::MANAGE_EXTENSION_LIST)) {
$event->add_link("Extension Manager", make_link("ext_manager"));
} else {
$event->add_link("Help", make_link("ext_doc"));
@ -166,7 +193,7 @@ class ExtManager extends Extension
if ($all) {
$exts = zglob("ext/*/main.php");
} else {
$exts = zglob("ext/{".ENABLED_EXTS."}/main.php");
$exts = zglob("ext/{" . ENABLED_EXTS . "}/main.php");
}
foreach ($exts as $main) {
$extensions[] = new ExtensionInfo($main);
@ -200,16 +227,14 @@ class ExtManager extends Extension
{
file_put_contents(
"data/config/extensions.conf.php",
'<'.'?php'."\n".
'define("EXTRA_EXTS", "'.implode(",", $extras).'");'."\n".
'?'.">"
'<' . '?php' . "\n" .
'define("EXTRA_EXTS", "' . implode(",", $extras) . '");' . "\n" .
'?' . ">"
);
// when the list of active extensions changes, we can be
// pretty sure that the list of who reacts to what will
// change too
if (file_exists("data/cache/event_listeners.php")) {
unlink("data/cache/event_listeners.php");
}
_clear_cached_event_listeners();
}
}

View File

@ -9,7 +9,7 @@ class ExtManagerTheme extends Themelet
{
$h_en = $editable ? "<th>Enabled</th>" : "";
$html = "
".make_form(make_link("ext_manager/set"))."
" . make_form(make_link("ext_manager/set")) . "
<table id='extensions' class='zebra sortable'>
<thead>
<tr>
@ -26,17 +26,17 @@ class ExtManagerTheme extends Themelet
continue;
}
$h_name = html_escape(empty($extension->name) ? $extension->ext_name : $extension->name);
$h_name = html_escape(empty($extension->name) ? $extension->ext_name : $extension->name);
$h_description = html_escape($extension->description);
$h_link = make_link("ext_doc/".url_escape($extension->ext_name));
$h_enabled = ($extension->enabled === true ? " checked='checked'" : ($extension->enabled === false ? "" : " disabled checked='checked'"));
$h_enabled_box = $editable ? "<td><input type='checkbox' name='ext_".html_escape($extension->ext_name)."' id='ext_".html_escape($extension->ext_name)."'$h_enabled></td>" : "";
$h_docs = ($extension->documentation ? "<a href='$h_link'>■</a>" : ""); //TODO: A proper "docs" symbol would be preferred here.
$h_link = make_link("ext_doc/" . url_escape($extension->ext_name));
$h_enabled = ($extension->enabled === true ? " checked='checked'" : ($extension->enabled === false ? "" : " disabled checked='checked'"));
$h_enabled_box = $editable ? "<td><input type='checkbox' name='ext_" . html_escape($extension->ext_name) . "' id='ext_" . html_escape($extension->ext_name) . "'$h_enabled></td>" : "";
$h_docs = ($extension->documentation ? "<a href='$h_link'>■</a>" : ""); //TODO: A proper "docs" symbol would be preferred here.
$html .= "
<tr data-ext='{$extension->ext_name}'>
{$h_enabled_box}
<td><label for='ext_".html_escape($extension->ext_name)."'>{$h_name}</label></td>
<td><label for='ext_" . html_escape($extension->ext_name) . "'>{$h_name}</label></td>
<td>{$h_docs}</td>
<td style='text-align: left;'>{$h_description}</td>
</tr>";
@ -116,15 +116,23 @@ class ExtManagerTheme extends Themelet
public function display_doc(Page $page, ExtensionInfo $info)
{
$author = "";
if ($info->author) {
if ($info->email) {
$author = "<br><b>Author:</b> <a href=\"mailto:".html_escape($info->email)."\">".html_escape($info->author)."</a>";
} else {
$author = "<br><b>Author:</b> ".html_escape($info->author);
if (count($info->authors) > 0) {
$author = "<br /><b>Author";
if (count($info->authors) > 1) {
$author .= "s";
}
$author .= ":</b>";
foreach ($info->authors as $auth) {
if (!empty($auth->email)) {
$author .= "<a href=\"mailto:" . html_escape($auth->email) . "\">" . html_escape($auth->name) . "</a>";
} else {
$author .= html_escape($auth->name);
}
}
}
$version = ($info->version) ? "<br><b>Version:</b> ".html_escape($info->version) : "";
$link = ($info->link) ? "<br><b>Home Page:</b> <a href=\"".html_escape($info->link)."\">Link</a>" : "";
$version = ($info->version) ? "<br><b>Version:</b> " . html_escape($info->version) : "";
$link = ($info->link) ? "<br><b>Home Page:</b> <a href=\"" . html_escape($info->link) . "\">Link</a>" : "";
$doc = $info->documentation;
$html = "
<div style='margin: auto; text-align: left; width: 512px;'>
@ -133,10 +141,10 @@ class ExtManagerTheme extends Themelet
$link
<p>$doc
<hr>
<p><a href='".make_link("ext_manager")."'>Back to the list</a>
<p><a href='" . make_link("ext_manager") . "'>Back to the list</a>
</div>";
$page->set_title("Documentation for ".html_escape($info->name));
$page->set_title("Documentation for " . html_escape($info->name));
$page->set_heading(html_escape($info->name));
$page->add_block(new NavBlock());
$page->add_block(new Block("Documentation", $html));

View File

@ -81,7 +81,7 @@ class Favorites extends Extension
log_debug("favourite", "Favourite removed for $image_id", "Favourite removed");
}
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/$image_id"));
}
}
@ -155,6 +155,30 @@ class Favorites extends Extension
}
}
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
{
if($event->key===HelpPages::SEARCH) {
$block = new Block();
$block->header = "Favorites";
$block->body = $this->theme->get_help_html();
$event->add_block($block);
}
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
global $user;
if($event->parent=="posts") {
$event->add_nav_link("posts_favorites", new Link("post/list/favorited_by={$user->name}/1"), "My Favorites");
}
if($event->parent==="user") {
if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
$username = url_escape($user->name);
$event->add_nav_link("favorites", new Link("post/list/favorited_by=$username/1"), "My Favorites");
}
}
}
private function install()
{

View File

@ -34,4 +34,28 @@ class FavoritesTheme extends Themelet
$page->add_block(new Block("Favorited By", $html, "left", 25));
}
public function get_help_html()
{
return '<p>Search for images that have been favorited a certain number of times, or favorited by a particular individual.</p>
<div class="command_example">
<pre>favorites=1</pre>
<p>Returns images that have been favorited once.</p>
</div>
<div class="command_example">
<pre>favorites>0</pre>
<p>Returns images that have been favorited 1 or more times</p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
<div class="command_example">
<pre>favorited_by:username</pre>
<p>Returns images that have been favorited by "username". </p>
</div>
<div class="command_example">
<pre>favorited_by_userno:123</pre>
<p>Returns images that have been favorited by user 123. </p>
</div>
';
}
}

View File

@ -32,12 +32,12 @@ class Featured extends Extension
global $config, $page, $user;
if ($event->page_matches("featured_image")) {
if ($event->get_arg(0) == "set" && $user->check_auth_token()) {
if ($user->can("edit_feature") && isset($_POST['image_id'])) {
if ($user->can(Permissions::EDIT_FEATURE) && isset($_POST['image_id'])) {
$id = int_escape($_POST['image_id']);
if ($id > 0) {
$config->set_int("featured_id", $id);
log_info("featured", "Featured image set to $id", "Featured image set");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/$id"));
}
}
@ -45,7 +45,7 @@ class Featured extends Extension
if ($event->get_arg(0) == "download") {
$image = Image::by_id($config->get_int("featured_id"));
if (!is_null($image)) {
$page->set_mode("data");
$page->set_mode(PageMode::DATA);
$page->set_type($image->get_mime_type());
$page->set_data(file_get_contents($image->get_image_filename()));
}
@ -86,7 +86,7 @@ class Featured extends Extension
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
{
global $user;
if ($user->can("edit_feature")) {
if ($user->can(Permissions::EDIT_FEATURE)) {
$event->add_part($this->theme->get_buttons_html($event->image->id));
}
}

View File

@ -139,7 +139,7 @@ class Forum extends Extension
$redirectTo = "forum/view/".$newThreadID."/1";
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link($redirectTo));
break;
@ -151,7 +151,7 @@ class Forum extends Extension
$this->delete_post($postID);
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("forum/view/".$threadID));
break;
case "nuke":
@ -161,7 +161,7 @@ class Forum extends Extension
$this->delete_thread($threadID);
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("forum/index"));
break;
case "answer":
@ -176,11 +176,11 @@ class Forum extends Extension
}
$this->save_new_post($threadID, $user);
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("forum/view/".$threadID."/".$total_pages));
break;
default:
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("forum/index"));
//$this->theme->display_error(400, "Invalid action", "You should check forum/index.");
break;

View File

@ -14,7 +14,7 @@ class Handle404 extends Extension
{
global $config, $page;
// hax.
if ($page->mode == "page" && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) {
if ($page->mode == PageMode::PAGE && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) {
$h_pagename = html_escape(implode('/', $event->args));
log_debug("handle_404", "Hit 404: $h_pagename");
$page->set_code(404);

View File

@ -8,12 +8,32 @@
class FlashFileHandler extends DataHandlerExtension
{
protected function create_thumb(string $hash): bool
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
switch ($event->ext) {
case "swf":
$event->lossless = true;
$event->video = true;
$info = getimagesize($event->file_name);
if (!$info) {
return null;
}
$event->width = $info[0];
$event->height = $info[1];
break;
}
}
protected function create_thumb(string $hash, string $type): bool
{
global $config;
if (!create_thumbnail_ffmpeg($hash)) {
copy("ext/handle_flash/thumb.jpg", warehouse_path("thumbs", $hash));
if (!Media::create_thumbnail_ffmpeg($hash)) {
copy("ext/handle_flash/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash));
}
return true;
}
@ -35,13 +55,7 @@ class FlashFileHandler extends DataHandlerExtension
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
$image->source = $metadata['source'];
$info = getimagesize($filename);
if (!$info) {
return null;
}
$image->width = $info[0];
$image->height = $info[1];
return $image;
}

View File

@ -5,65 +5,54 @@
* Description: Handle windows icons
*/
class IcoFileHandler extends Extension
class IcoFileHandler extends DataHandlerExtension
{
public function onDataUpload(DataUploadEvent $event)
const SUPPORTED_EXTENSIONS = ["ico", "ani", "cur"];
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
if ($this->supported_ext($event->type) && $this->check_contents($event->tmpname)) {
$hash = $event->hash;
$ha = substr($hash, 0, 2);
move_upload_to_archive($event);
send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
$image = $this->create_image_from_data("images/$ha/$hash", $event->metadata);
if (is_null($image)) {
throw new UploadException("Icon handler failed to create image object from data");
if(in_array($event->ext, self::SUPPORTED_EXTENSIONS)) {
$event->lossless = true;
$event->video = false;
$event->audio = false;
$fp = fopen($event->file_name, "r");
try {
unpack("Snull/Stype/Scount", fread($fp, 6));
$subheader = unpack("Cwidth/Cheight/Ccolours/Cnull/Splanes/Sbpp/Lsize/loffset", fread($fp, 16));
} finally {
fclose($fp);
}
$iae = new ImageAdditionEvent($image);
send_event($iae);
$event->image_id = $iae->image->id;
$width = $subheader['width'];
$height = $subheader['height'];
$event->width = $width == 0 ? 256 : $width;
$event->height = $height == 0 ? 256 : $height;
}
}
public function onDisplayingImage(DisplayingImageEvent $event)
protected function supported_ext(string $ext): bool
{
global $page;
if ($this->supported_ext($event->image->ext)) {
$this->theme->display_image($page, $event->image);
}
return in_array(strtolower($ext), self::SUPPORTED_EXTENSIONS);
}
private function supported_ext(string $ext): bool
{
$exts = ["ico", "ani", "cur"];
return in_array(strtolower($ext), $exts);
}
private function create_image_from_data(string $filename, array $metadata)
protected function create_image_from_data(string $filename, array $metadata)
{
$image = new Image();
$fp = fopen($filename, "r");
$header = unpack("Snull/Stype/Scount", fread($fp, 6));
$subheader = unpack("Cwidth/Cheight/Ccolours/Cnull/Splanes/Sbpp/Lsize/loffset", fread($fp, 16));
fclose($fp);
$width = $subheader['width'];
$height = $subheader['height'];
$image->width = $width == 0 ? 256 : $width;
$image->height = $height == 0 ? 256 : $height;
$image->filesize = $metadata['size'];
$image->hash = $metadata['hash'];
$image->filename = $metadata['filename'];
$image->ext = $metadata['extension'];
$image->filesize = $metadata['size'];
$image->hash = $metadata['hash'];
$image->filename = $metadata['filename'];
$image->ext = $metadata['extension'];
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
$image->source = $metadata['source'];
$image->source = $metadata['source'];
return $image;
}
private function check_contents(string $file): bool
protected function check_contents(string $file): bool
{
if (!file_exists($file)) {
return false;
@ -74,27 +63,14 @@ class IcoFileHandler extends Extension
return ($header['null'] == 0 && ($header['type'] == 0 || $header['type'] == 1));
}
private function create_thumb(string $hash): bool
protected function create_thumb(string $hash, string $type): bool
{
global $config;
$inname = warehouse_path("images", $hash);
$outname = warehouse_path("thumbs", $hash);
$tsize = get_thumbnail_size_scaled($width, $height);
$w = $tsize[0];
$h = $tsise[1];
$q = $config->get_int("thumb_quality");
$mem = $config->get_int("thumb_mem_limit") / 1024 / 1024; // IM takes memory in MB
if ($config->get_bool("ico_convert")) {
// "-limit memory $mem" broken?
exec("convert {$inname}[0] -geometry {$w}x{$h} -quality {$q} jpg:$outname");
} else {
copy($inname, $outname);
try {
create_image_thumb($hash, $type, MediaEngine::IMAGICK);
return true;
} catch (MediaException $e) {
log_warning("handle_ico", "Could not generate thumbnail. " . $e->getMessage());
return false;
}
return true;
}
}

View File

@ -7,9 +7,22 @@
class MP3FileHandler extends DataHandlerExtension
{
protected function create_thumb(string $hash): bool
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
copy("ext/handle_mp3/thumb.jpg", warehouse_path("thumbs", $hash));
switch ($event->ext) {
case "mp3":
$event->audio = true;
$event->video = false;
$event->lossless = false;
break;
}
// TODO: Buff out audio format support, length scanning
}
protected function create_thumb(string $hash, string $type): bool
{
copy("ext/handle_mp3/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash));
return true;
}
@ -37,6 +50,7 @@ class MP3FileHandler extends DataHandlerExtension
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
$image->source = $metadata['source'];
return $image;
}

View File

@ -8,25 +8,56 @@
class PixelFileHandler extends DataHandlerExtension
{
const SUPPORTED_EXTENSIONS = ["jpg", "jpeg", "gif", "png", "webp"];
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
if(in_array($event->ext, Media::LOSSLESS_FORMATS)) {
$event->lossless = true;
} elseif($event->ext=="webp") {
$event->lossless = Media::is_lossless_webp($event->file_name);
}
if(in_array($event->ext,self::SUPPORTED_EXTENSIONS)) {
if($event->lossless==null) {
$event->lossless = false;
}
$event->audio = false;
switch ($event->ext) {
case "gif":
$event->video = Media::is_animated_gif($event->file_name);
break;
case "webp":
$event->video = Media::is_animated_webp($event->file_name);
break;
default:
$event->video = false;
break;
}
$info = getimagesize($event->file_name);
if (!$info) {
return null;
}
$event->width = $info[0];
$event->height = $info[1];
}
}
protected function supported_ext(string $ext): bool
{
$exts = ["jpg", "jpeg", "gif", "png", "webp"];
$ext = (($pos = strpos($ext, '?')) !== false) ? substr($ext, 0, $pos) : $ext;
return in_array(strtolower($ext), $exts);
return in_array(strtolower($ext), self::SUPPORTED_EXTENSIONS);
}
protected function create_image_from_data(string $filename, array $metadata)
{
$image = new Image();
$info = getimagesize($filename);
if (!$info) {
return null;
}
$image->width = $info[0];
$image->height = $info[1];
$image->filesize = $metadata['size'];
$image->hash = $metadata['hash'];
$image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename'];
@ -53,26 +84,24 @@ class PixelFileHandler extends DataHandlerExtension
return false;
}
protected function create_thumb(string $hash): bool
protected function create_thumb(string $hash, string $type): bool
{
global $config;
$inname = warehouse_path("images", $hash);
$outname = warehouse_path("thumbs", $hash);
$ok = false;
switch ($config->get_string("thumb_engine")) {
default:
case 'gd':
$ok = $this->make_thumb_gd($inname, $outname);
break;
case 'convert':
$ok = create_thumbnail_convert($hash);
break;
try {
create_image_thumb($hash, $type);
return true;
} catch (InsufficientMemoryException $e) {
$tsize = get_thumbnail_max_size_scaled();
$thumb = imagecreatetruecolor($tsize[0], min($tsize[1], 64));
$white = imagecolorallocate($thumb, 255, 255, 255);
$black = imagecolorallocate($thumb, 0, 0, 0);
imagefill($thumb, 0, 0, $white);
log_warning("handle_pixel", "Insufficient memory while creating thumbnail: ".$e->getMessage());
imagestring($thumb, 5, 10, 24, "Image Too Large :(", $black);
return true;
} catch (Exception $e) {
log_error("handle_pixel", "Error while creating thumbnail: ".$e->getMessage());
return false;
}
return $ok;
}
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
@ -89,38 +118,4 @@ class PixelFileHandler extends DataHandlerExtension
", 20);
}
// GD thumber {{{
private function make_thumb_gd(string $inname, string $outname): bool
{
global $config;
try {
$info = getimagesize($inname);
$tsize = get_thumbnail_size_scaled($info[0], $info[1]);
$image = image_resize_gd(
$inname,
$info,
$tsize[0],
$tsize[1],
$outname,
$config->get_string('thumb_type'),
$config->get_int('thumb_quality')
);
} catch (InsufficientMemoryException $e) {
$tsize = get_thumbnail_max_size_scaled();
$thumb = imagecreatetruecolor($tsize[0], min($tsize[1], 64));
$white = imagecolorallocate($thumb, 255, 255, 255);
$black = imagecolorallocate($thumb, 0, 0, 0);
imagefill($thumb, 0, 0, $white);
log_warning("handle_pixel", "Insufficient memory while creating thumbnail: ".$e->getMessage());
imagestring($thumb, 5, 10, 24, "Image Too Large :(", $black);
return true;
} catch (Exception $e) {
log_error("handle_pixel", "Error while creating thumbnail: ".$e->getMessage());
return false;
}
return true;
}
// }}}
}

View File

@ -7,7 +7,7 @@ class PixelFileHandlerTheme extends Themelet
global $config;
$u_ilink = $image->get_image_link();
if ($config->get_bool("image_show_meta") && function_exists("exif_read_data")) {
if ($config->get_bool(ImageConfig::SHOW_META) && function_exists(ImageIO::EXIF_READ_FUNCTION)) {
# FIXME: only read from jpegs?
$exif = @exif_read_data($image->get_image_filename(), 0, true);
if ($exif) {

View File

@ -14,10 +14,10 @@ class HandleStatic extends Extension
{
global $config, $page;
// hax.
if ($page->mode == "page" && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) {
if ($page->mode == PageMode::PAGE && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) {
$h_pagename = html_escape(implode('/', $event->args));
$f_pagename = preg_replace("/[^a-z_\-\.]+/", "_", $h_pagename);
$theme_name = $config->get_string("theme", "default");
$theme_name = $config->get_string(SetupConfig::THEME, "default");
$theme_file = "themes/$theme_name/static/$f_pagename";
$static_file = "ext/handle_static/static/$f_pagename";
@ -27,7 +27,7 @@ class HandleStatic extends Extension
$page->add_http_header("Cache-control: public, max-age=600");
$page->add_http_header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 600) . ' GMT');
$page->set_mode("data");
$page->set_mode(PageMode::DATA);
$page->set_data(file_get_contents($filename));
if (endsWith($filename, ".ico")) {
$page->set_type("image/x-icon");

View File

@ -7,12 +7,13 @@ TD>INPUT[type="text"],
TD>INPUT[type="password"],
TD>INPUT[type="email"],
TD>SELECT,
TD>TEXTAREA {width: 100%;}
TD>TEXTAREA,
TD>BUTTON {width: 100%;}
TABLE.form {width: 300px;}
TABLE.form TD, TABLE.form TH {vertical-align: middle;}
TABLE.form TBODY TD {text-align: left;}
TABLE.form TBODY TH {text-align: right; padding-right: 4px; width: 1%;}
TABLE.form TBODY TH {text-align: right; padding-right: 4px; width: 1%; white-space: nowrap;}
TABLE.form TD + TH {padding-left: 8px;}
*[onclick],

View File

@ -10,6 +10,24 @@ use enshrined\svgSanitize\Sanitizer;
class SVGFileHandler extends DataHandlerExtension
{
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
switch ($event->ext) {
case "svg":
$event->lossless = true;
$event->video = false;
$event->audio = false;
$msp = new MiniSVGParser($event->file_name);
$event->width = $msp->width;
$event->height = $msp->height;
break;
}
}
public function onDataUpload(DataUploadEvent $event)
{
if ($this->supported_ext($event->type) && $this->check_contents($event->tmpname)) {
@ -19,25 +37,30 @@ class SVGFileHandler extends DataHandlerExtension
$sanitizer->removeRemoteReferences(true);
$dirtySVG = file_get_contents($event->tmpname);
$cleanSVG = $sanitizer->sanitize($dirtySVG);
file_put_contents(warehouse_path("images", $hash), $cleanSVG);
file_put_contents(warehouse_path(Image::IMAGE_DIR, $hash), $cleanSVG);
send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
$image = $this->create_image_from_data(warehouse_path("images", $hash), $event->metadata);
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $hash), $event->metadata);
if (is_null($image)) {
throw new UploadException("SVG handler failed to create image object from data");
}
$iae = new ImageAdditionEvent($image);
send_event($iae);
$event->image_id = $iae->image->id;
$event->merged = $iae->merged;
}
}
protected function create_thumb(string $hash): bool
protected function create_thumb(string $hash, string $type): bool
{
if (!create_thumbnail_convert($hash)) {
copy("ext/handle_svg/thumb.jpg", warehouse_path("thumbs", $hash));
try {
create_image_thumb($hash, $type, MediaEngine::IMAGICK);
return true;
} catch (MediaException $e) {
log_warning("handle_svg", "Could not generate thumbnail. " . $e->getMessage());
copy("ext/handle_svg/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash));
return false;
}
return true;
}
public function onDisplayingImage(DisplayingImageEvent $event)
@ -57,11 +80,11 @@ class SVGFileHandler extends DataHandlerExtension
$hash = $image->hash;
$page->set_type("image/svg+xml");
$page->set_mode("data");
$page->set_mode(PageMode::DATA);
$sanitizer = new Sanitizer();
$sanitizer->removeRemoteReferences(true);
$dirtySVG = file_get_contents(warehouse_path("images", $hash));
$dirtySVG = file_get_contents(warehouse_path(Image::IMAGE_DIR, $hash));
$cleanSVG = $sanitizer->sanitize($dirtySVG);
$page->set_data($cleanSVG);
}
@ -77,10 +100,6 @@ class SVGFileHandler extends DataHandlerExtension
{
$image = new Image();
$msp = new MiniSVGParser($filename);
$image->width = $msp->width;
$image->height = $msp->height;
$image->filesize = $metadata['size'];
$image->hash = $metadata['hash'];
$image->filename = $metadata['filename'];

View File

@ -16,20 +16,21 @@
class VideoFileHandler extends DataHandlerExtension
{
const SUPPORTED_MIME = [
'video/webm',
'video/mp4',
'video/ogg',
'video/flv',
'video/x-flv'
];
const SUPPORTED_EXT = ["flv", "mp4", "m4v", "ogv", "webm"];
public function onInitExt(InitExtEvent $event)
{
global $config;
if ($config->get_int("ext_handle_video_version") < 1) {
if ($ffmpeg = shell_exec((PHP_OS == 'WINNT' ? 'where' : 'which') . ' ffmpeg')) {
//ffmpeg exists in PATH, check if it's executable, and if so, default to it instead of static
if (is_executable(strtok($ffmpeg, PHP_EOL))) {
$config->set_default_string('thumb_ffmpeg_path', 'ffmpeg');
}
} else {
$config->set_default_string('thumb_ffmpeg_path', '');
}
// This used to set the ffmpeg path. It does not do this anymore, that is now in the base graphic extension.
$config->set_int("ext_handle_video_version", 1);
log_info("handle_video", "extension installed");
}
@ -41,37 +42,83 @@ class VideoFileHandler extends DataHandlerExtension
public function onSetupBuilding(SetupBuildingEvent $event)
{
$sb = new SetupBlock("Video Options");
$sb->add_label("<br>Path to ffmpeg: ");
$sb->add_text_option("thumb_ffmpeg_path");
$sb->add_label("<br>");
$sb->add_bool_option("video_playback_autoplay", "Autoplay: ");
$sb->add_label("<br>");
$sb->add_bool_option("video_playback_loop", "Loop: ");
$event->panel->add_block($sb);
}
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
if(in_array($event->ext, self::SUPPORTED_EXT)) {
$event->video = true;
try {
$data = Media::get_ffprobe_data($event->file_name);
if(is_array($data)) {
if(array_key_exists("streams", $data)) {
$video = false;
$audio = true;
$streams = $data["streams"];
if (is_array($streams)) {
foreach ($streams as $stream) {
if(is_array($stream)) {
if (array_key_exists("codec_type", $stream)) {
$type = $stream["codec_type"];
switch ($type) {
case "audio":
$audio = true;
break;
case "video":
$video = true;
break;
}
}
if (array_key_exists("width", $stream) && !empty($stream["width"])
&& is_numeric($stream["width"]) && intval($stream["width"]) > ($event->width) ?? 0) {
$event->width = intval($stream["width"]);
}
if (array_key_exists("height", $stream) && !empty($stream["height"])
&& is_numeric($stream["height"]) && intval($stream["height"]) > ($event->height) ?? 0) {
$event->height = intval($stream["height"]);
}
}
}
$event->video = $video;
$event->audio = $audio;
}
}
if(array_key_exists("format", $data)&& is_array($data["format"])) {
$format = $data["format"];
if(array_key_exists("duration", $format) && is_numeric($format["duration"])) {
$event->length = floor(floatval($format["duration"]) * 1000);
}
}
}
} catch(MediaException $e) {
}
}
}
/**
* Generate the Thumbnail image for particular file.
*/
protected function create_thumb(string $hash): bool
protected function create_thumb(string $hash, string $type): bool
{
return create_thumbnail_ffmpeg($hash);
return Media::create_thumbnail_ffmpeg($hash);
}
protected function supported_ext(string $ext): bool
{
$exts = ["flv", "mp4", "m4v", "ogv", "webm"];
return in_array(strtolower($ext), $exts);
return in_array(strtolower($ext), self::SUPPORTED_EXT);
}
protected function create_image_from_data(string $filename, array $metadata): Image
{
$image = new Image();
$size = video_size($filename);
$image->width = $size[0];
$image->height = $size[1];
switch (getMimeType($filename)) {
case "video/webm":
$image->ext = "webm";
@ -103,13 +150,7 @@ class VideoFileHandler extends DataHandlerExtension
{
return (
file_exists($tmpname) &&
in_array(getMimeType($tmpname), [
'video/webm',
'video/mp4',
'video/ogg',
'video/flv',
'video/x-flv'
])
in_array(getMimeType($tmpname), self::SUPPORTED_MIME)
);
}
}

View File

@ -13,6 +13,15 @@ class VideoFileHandlerTheme extends Themelet
$loop = $config->get_bool("video_playback_loop");
$player = make_link('vendor/bower-asset/mediaelement/build/flashmediaelement.swf');
$width="auto";
if($image->width>1) {
$width = $image->width."px";
}
$height="auto";
if($image->height>1) {
$height = $image->height."px";
}
$html = "Video not playing? <a href='$ilink'>Click here</a> to download the file.<br/>";
//Browser media format support: https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
@ -48,7 +57,8 @@ class VideoFileHandlerTheme extends Themelet
$loop = ($loop ? ' loop' : '');
$html .= "
<video controls class='shm-main-image' id='main_image' alt='main image' {$autoplay} {$loop} style='max-width: 100%'>
<video controls class='shm-main-image' id='main_image' alt='main image' poster='$thumb_url' {$autoplay} {$loop}
style='height: $height; width: $width; max-width: 100%'>
<source src='{$ilink}' type='{$supportedExts[$ext]}'>
<!-- If browser doesn't support filetype, fallback to flash -->

View File

@ -9,9 +9,9 @@ class HellBan extends Extension
{
global $page, $user;
if ($user->can("hellbanned")) {
if ($user->can(Permissions::HELLBANNED)) {
$s = "";
} elseif ($user->can("view_hellbanned")) {
} elseif ($user->can(Permissions::VIEW_HELLBANNED)) {
$s = "DIV.hb, TR.hb TD {border: 1px solid red !important;}";
} else {
$s = ".hb {display: none !important;}";

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

94
ext/help_pages/main.php Normal file
View File

@ -0,0 +1,94 @@
<?php
/**
* Name: Help Pages
* Author: Matthew Barbour <matthew@darkholme.net>
* License: MIT
* Description: Provides documentation screens
*/
class HelpPageListBuildingEvent extends Event
{
public $pages = [];
public function add_page(string $key, string $name)
{
$this->pages[$key] = $name;
}
}
class HelpPageBuildingEvent extends Event
{
public $key;
public $blocks = [];
public function __construct(string $key)
{
$this->key = $key;
}
function add_block(Block $block, int $position = 50)
{
if(!array_key_exists("$position",$this->blocks))
{
$this->blocks["$position"] = [];
}
$this->blocks["$position"][] = $block;
}
}
class HelpPages extends Extension
{
public const SEARCH = "search";
public function onPageRequest(PageRequestEvent $event)
{
global $page, $user;
if ($event->page_matches("help")) {
$e = new HelpPageListBuildingEvent();
send_event($e);
$page->set_mode(PageMode::PAGE);
if ($event->count_args() == 0) {
$this->theme->display_list_page($e->pages);
} else {
$name = $event->get_arg(0);
$title = $name;
if(array_key_exists($name, $e->pages)) {
$title = $e->pages[$name];
}
$this->theme->display_help_page($title);
$hpbe = new HelpPageBuildingEvent($name);
send_event($hpbe);
asort($hpbe->blocks);
foreach ($hpbe->blocks as $key=>$value) {
foreach($value as $block) {
$page->add_block($block);
}
}
}
}
}
public function onHelpPageListBuilding(HelpPageListBuildingEvent $event)
{
$event->add_page("search", "Searching");
}
public function onPageNavBuilding(PageNavBuildingEvent $event)
{
$event->add_nav_link("help", new Link('help'), "Help");
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
$event->add_link("Help", make_link("help"));
}
}

13
ext/help_pages/style.css Normal file
View File

@ -0,0 +1,13 @@
.command_example {
margin: 12pt;
padding-left: 16pt;
}
.command_example pre {
padding:4pt;
border: dashed 2px black;
}
.command_example p {
padding-left: 16pt;
}

31
ext/help_pages/theme.php Normal file
View File

@ -0,0 +1,31 @@
<?php
class HelpPagesTheme extends Themelet
{
public function display_list_page(array $pages)
{
global $page;
$page->set_title("Help Pages");
$page->set_heading("Help Pages");
$nav_block = new Block("Help", "", "left", 0);
foreach ($pages as $link=>$desc) {
$link = make_link("help/{$link}");
$nav_block->body .= "<a href='{$link}'>".html_escape($desc)."</a><br/>";
}
$page->add_block($nav_block);
$page->add_block(new Block("Help Pages", "See list of pages to left"));
}
public function display_help_page(String $title)
{
global $page;
$page->set_title("Help - $title");
$page->set_heading("Help - $title");
}
}

View File

@ -22,8 +22,8 @@ class Home extends Extension
global $config, $page;
if ($event->page_matches("home")) {
$base_href = get_base_href();
$sitename = $config->get_string('title');
$theme_name = $config->get_string('theme');
$sitename = $config->get_string(SetupConfig::TITLE);
$theme_name = $config->get_string(SetupConfig::THEME);
$body = $this->get_body();
@ -52,7 +52,7 @@ class Home extends Extension
// returns just the contents of the body
global $config;
$base_href = get_base_href();
$sitename = $config->get_string('title');
$sitename = $config->get_string(SetupConfig::TITLE);
$contact_link = contact_link();
if (is_null($contact_link)) {
$contact_link = "";

View File

@ -4,7 +4,7 @@ class HomeTheme extends Themelet
{
public function display_page(Page $page, $sitename, $base_href, $theme_name, $body)
{
$page->set_mode("data");
$page->set_mode(PageMode::DATA);
$page->add_auto_html_headers();
$hh = $page->get_all_html_headers();
$page->set_data(

View File

@ -9,41 +9,75 @@
*/
abstract class ImageConfig {
const THUMB_ENGINE = 'thumb_engine';
const THUMB_WIDTH = 'thumb_width';
const THUMB_HEIGHT = 'thumb_height';
const THUMB_SCALING = 'thumb_scaling';
const THUMB_QUALITY = 'thumb_quality';
const THUMB_TYPE = 'thumb_type';
const SHOW_META = 'image_show_meta';
const ILINK = 'image_ilink';
const TLINK = 'image_tlink';
const TIP = 'image_tip';
const EXPIRES = 'image_expires';
const UPLOAD_COLLISION_HANDLER = 'upload_collision_handler';
const COLLISION_MERGE = 'merge';
const COLLISION_ERROR = 'error';
}
/**
* A class to handle adding / getting / removing image files from the disk.
*/
class ImageIO extends Extension
{
const COLLISION_OPTIONS = ['Error'=>ImageConfig::COLLISION_ERROR, 'Merge'=>ImageConfig::COLLISION_MERGE];
const EXIF_READ_FUNCTION = "exif_read_data";
const THUMBNAIL_ENGINES = [
'Built-in GD' => MediaEngine::GD,
'ImageMagick' => MediaEngine::IMAGICK
];
const THUMBNAIL_TYPES = [
'JPEG' => "jpg",
'WEBP (Not IE/Safari compatible)' => "webp"
];
public function onInitExt(InitExtEvent $event)
{
global $config;
$config->set_default_int('thumb_width', 192);
$config->set_default_int('thumb_height', 192);
$config->set_default_int('thumb_scaling', 100);
$config->set_default_int('thumb_quality', 75);
$config->set_default_string('thumb_type', 'jpg');
$config->set_default_int('thumb_mem_limit', parse_shorthand_int('8MB'));
$config->set_default_string('thumb_convert_path', 'convert');
$config->set_default_int(ImageConfig::THUMB_WIDTH, 192);
$config->set_default_int(ImageConfig::THUMB_HEIGHT, 192);
$config->set_default_int(ImageConfig::THUMB_SCALING, 100);
$config->set_default_int(ImageConfig::THUMB_QUALITY, 75);
$config->set_default_string(ImageConfig::THUMB_TYPE, 'jpg');
if (function_exists("exif_read_data")) {
$config->set_default_bool('image_show_meta', false);
if (function_exists(self::EXIF_READ_FUNCTION)) {
$config->set_default_bool(ImageConfig::SHOW_META, false);
}
$config->set_default_string('image_ilink', '');
$config->set_default_string('image_tlink', '');
$config->set_default_string('image_tip', '$tags // $size // $filesize');
$config->set_default_string('upload_collision_handler', 'error');
$config->set_default_int('image_expires', (60*60*24*31)); // defaults to one month
$config->set_default_string(ImageConfig::ILINK, '');
$config->set_default_string(ImageConfig::TLINK, '');
$config->set_default_string(ImageConfig::TIP, '$tags // $size // $filesize');
$config->set_default_string(ImageConfig::UPLOAD_COLLISION_HANDLER, ImageConfig::COLLISION_ERROR);
$config->set_default_int(ImageConfig::EXPIRES, (60*60*24*31)); // defaults to one month
}
public function onPageRequest(PageRequestEvent $event)
{
if ($event->page_matches("image/delete")) {
global $page, $user;
if ($user->can("delete_image") && isset($_POST['image_id']) && $user->check_auth_token()) {
if ($user->can(Permissions::DELETE_IMAGE) && isset($_POST['image_id']) && $user->check_auth_token()) {
$image = Image::by_id($_POST['image_id']);
if ($image) {
send_event(new ImageDeletionEvent($image));
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
if (isset($_SERVER['HTTP_REFERER']) && !strstr($_SERVER['HTTP_REFERER'], 'post/view')) {
$page->set_redirect($_SERVER['HTTP_REFERER']);
} else {
@ -53,10 +87,10 @@ class ImageIO extends Extension
}
} elseif ($event->page_matches("image/replace")) {
global $page, $user;
if ($user->can("replace_image") && isset($_POST['image_id']) && $user->check_auth_token()) {
if ($user->can(Permissions::REPLACE_IMAGE) && isset($_POST['image_id']) && $user->check_auth_token()) {
$image = Image::by_id($_POST['image_id']);
if ($image) {
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link('upload/replace/'.$image->id));
} else {
/* Invalid image ID */
@ -76,11 +110,11 @@ class ImageIO extends Extension
{
global $user;
if ($user->can("delete_image")) {
if ($user->can(Permissions::DELETE_IMAGE)) {
$event->add_part($this->theme->get_deleter_html($event->image->id));
}
/* In the future, could perhaps allow users to replace images that they own as well... */
if ($user->can("replace_image")) {
if ($user->can(Permissions::REPLACE_IMAGE)) {
$event->add_part($this->theme->get_replace_html($event->image->id));
}
}
@ -88,7 +122,7 @@ class ImageIO extends Extension
public function onImageAddition(ImageAdditionEvent $event)
{
try {
$this->add_image($event->image);
$this->add_image($event);
} catch (ImageAdditionException $e) {
throw new UploadException($e->error);
}
@ -125,60 +159,48 @@ class ImageIO extends Extension
$sb = new SetupBlock("Image Options");
$sb->position = 30;
// advanced only
//$sb->add_text_option("image_ilink", "Image link: ");
//$sb->add_text_option("image_tlink", "<br>Thumbnail link: ");
$sb->add_text_option("image_tip", "Image tooltip: ");
$sb->add_choice_option("upload_collision_handler", ['Error'=>'error', 'Merge'=>'merge'], "<br>Upload collision handler: ");
if (function_exists("exif_read_data")) {
$sb->add_bool_option("image_show_meta", "<br>Show metadata: ");
//$sb->add_text_option(ImageConfig::ILINK, "Image link: ");
//$sb->add_text_option(ImageConfig::TLINK, "<br>Thumbnail link: ");
$sb->add_text_option(ImageConfig::TIP, "Image tooltip: ");
$sb->add_choice_option(ImageConfig::UPLOAD_COLLISION_HANDLER, self::COLLISION_OPTIONS, "<br>Upload collision handler: ");
if (function_exists(self::EXIF_READ_FUNCTION)) {
$sb->add_bool_option(ImageConfig::SHOW_META, "<br>Show metadata: ");
}
$event->panel->add_block($sb);
$thumbers = [];
$thumbers['Built-in GD'] = "gd";
$thumbers['ImageMagick'] = "convert";
$thumb_types = [];
$thumb_types['JPEG'] = "jpg";
$thumb_types['WEBP'] = "webp";
$sb = new SetupBlock("Thumbnailing");
$sb->add_choice_option("thumb_engine", $thumbers, "Engine: ");
$sb->add_choice_option(ImageConfig::THUMB_ENGINE, self::THUMBNAIL_ENGINES, "Engine: ");
$sb->add_label("<br>");
$sb->add_choice_option("thumb_type", $thumb_types, "Filetype: ");
$sb->add_choice_option(ImageConfig::THUMB_TYPE, self::THUMBNAIL_TYPES, "Filetype: ");
$sb->add_label("<br>Size ");
$sb->add_int_option("thumb_width");
$sb->add_int_option(ImageConfig::THUMB_WIDTH);
$sb->add_label(" x ");
$sb->add_int_option("thumb_height");
$sb->add_int_option(ImageConfig::THUMB_HEIGHT);
$sb->add_label(" px at ");
$sb->add_int_option("thumb_quality");
$sb->add_int_option(ImageConfig::THUMB_QUALITY);
$sb->add_label(" % quality ");
$sb->add_label("<br>High-DPI scaling ");
$sb->add_int_option("thumb_scaling");
$sb->add_int_option(ImageConfig::THUMB_SCALING);
$sb->add_label("%");
if ($config->get_string("thumb_engine") == "convert") {
$sb->add_label("<br>ImageMagick Binary: ");
$sb->add_text_option("thumb_convert_path");
}
if ($config->get_string("thumb_engine") == "gd") {
$sb->add_shorthand_int_option("thumb_mem_limit", "<br>Max memory use: ");
}
$event->panel->add_block($sb);
}
// add image {{{
private function add_image(Image $image)
private function add_image(ImageAdditionEvent $event)
{
global $user, $database, $config;
$image = $event->image;
/*
* Validate things
*/
@ -191,8 +213,8 @@ class ImageIO extends Extension
*/
$existing = Image::by_hash($image->hash);
if (!is_null($existing)) {
$handler = $config->get_string("upload_collision_handler");
if ($handler == "merge" || isset($_GET['update'])) {
$handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER);
if ($handler == ImageConfig::COLLISION_MERGE || isset($_GET['update'])) {
$merged = array_merge($image->get_tag_array(), $existing->get_tag_array());
send_event(new TagSetEvent($existing, $merged));
if (isset($_GET['rating']) && isset($_GET['update']) && ext_is_live("Ratings")) {
@ -201,7 +223,9 @@ class ImageIO extends Extension
if (isset($_GET['source']) && isset($_GET['update'])) {
send_event(new SourceSetEvent($existing, $_GET['source']));
}
return null;
$event->merged = true;
$event->image = Image::by_id($existing->id);
return;
} else {
$error = "Image <a href='".make_link("post/view/{$existing->id}")."'>{$existing->id}</a> ".
"already has hash {$image->hash}:<p>".$this->theme->build_thumb_html($existing);
@ -217,12 +241,12 @@ class ImageIO extends Extension
)
VALUES (
:owner_id, :owner_ip, :filename, :filesize,
:hash, :ext, :width, :height, now(), :source
:hash, :ext, 0, 0, now(), :source
)",
[
"owner_id"=>$user->id, "owner_ip"=>$_SERVER['REMOTE_ADDR'], "filename"=>substr($image->filename, 0, 60), "filesize"=>$image->filesize,
"hash"=>$image->hash, "ext"=>strtolower($image->ext), "width"=>$image->width, "height"=>$image->height, "source"=>$image->source
]
"owner_id" => $user->id, "owner_ip" => $_SERVER['REMOTE_ADDR'], "filename" => substr($image->filename, 0, 255), "filesize" => $image->filesize,
"hash" => $image->hash, "ext" => strtolower($image->ext), "source" => $image->source
]
);
$image->id = $database->get_last_insert_id('images_id_seq');
@ -240,6 +264,13 @@ class ImageIO extends Extension
if ($image->source !== null) {
log_info("core-image", "Source for Image #{$image->id} set to: {$image->source}");
}
try {
Media::update_image_media_properties($image->hash, strtolower($image->ext));
} catch(MediaException $e) {
log_warning("add_image","Error while running update_image_media_properties: ".$e->getMessage());
}
}
// }}} end add
@ -251,15 +282,14 @@ class ImageIO extends Extension
global $page;
if (!is_null($image)) {
$page->set_mode("data");
if ($type == "thumb") {
$ext = $config->get_string("thumb_type");
$ext = $config->get_string(ImageConfig::THUMB_TYPE);
if (array_key_exists($ext, MIME_TYPE_MAP)) {
$page->set_type(MIME_TYPE_MAP[$ext]);
} else {
$page->set_type("image/jpeg");
}
$file = $image->get_thumb_filename();
} else {
$page->set_type($image->get_mime_type());
@ -274,26 +304,29 @@ class ImageIO extends Extension
$gmdate_mod = gmdate('D, d M Y H:i:s', filemtime($file)) . ' GMT';
if ($if_modified_since == $gmdate_mod) {
$page->set_mode(PageMode::DATA);
$page->set_code(304);
$page->set_data("");
} else {
$page->set_mode(PageMode::FILE);
$page->add_http_header("Last-Modified: $gmdate_mod");
if ($type != "thumb") {
$page->add_http_header("Content-Disposition: inline; filename=".$image->get_nice_image_name());
$page->set_filename($image->get_nice_image_name(), 'inline');
}
$page->set_data(file_get_contents($file));
$page->set_file($file);
if ($config->get_int("image_expires")) {
$expires = date(DATE_RFC1123, time() + $config->get_int("image_expires"));
if ($config->get_int(ImageConfig::EXPIRES)) {
$expires = date(DATE_RFC1123, time() + $config->get_int(ImageConfig::EXPIRES));
} else {
$expires = 'Fri, 2 Sep 2101 12:42:42 GMT'; // War was beginning
}
$page->add_http_header('Expires: '.$expires);
$page->add_http_header('Expires: ' . $expires);
}
} else {
$page->set_title("Not Found");
$page->set_heading("Not Found");
$page->add_block(new Block("Navigation", "<a href='".make_link()."'>Index</a>", "left", 0));
$page->add_block(new Block("Navigation", "<a href='" . make_link() . "'>Index</a>", "left", 0));
$page->add_block(new Block(
"Image not in database",
"The requested image was not found in the database"
@ -314,33 +347,50 @@ class ImageIO extends Extension
throw new ImageReplaceException("Image to replace does not exist!");
}
$duplicate = Image::by_hash($image->hash);
if(!is_null($duplicate) && $duplicate->id!=$id) {
$error = "Image <a href='" . make_link("post/view/{$duplicate->id}") . "'>{$duplicate->id}</a> " .
"already has hash {$image->hash}:<p>" . $this->theme->build_thumb_html($duplicate);
throw new ImageReplaceException($error);
}
if (strlen(trim($image->source)) == 0) {
$image->source = $existing->get_source();
}
// Update the data in the database.
$database->Execute(
"UPDATE images SET
filename = :filename, filesize = :filesize, hash = :hash,
ext = :ext, width = 0, height = 0, source = :source
WHERE
id = :id
",
[
"filename" => substr($image->filename, 0, 255),
"filesize" => $image->filesize,
"hash" => $image->hash,
"ext" => strtolower($image->ext),
"source" => $image->source,
"id" => $id,
]
);
/*
This step could be optional, ie: perhaps move the image somewhere
and have it stored in a 'replaced images' list that could be
inspected later by an admin?
*/
log_debug("image", "Removing image with hash ".$existing->hash);
log_debug("image", "Removing image with hash " . $existing->hash);
$existing->remove_image_only(); // Actually delete the old image file from disk
// Update the data in the database.
$database->Execute(
"UPDATE images SET
filename = :filename, filesize = :filesize, hash = :hash,
ext = :ext, width = :width, height = :height, source = :source
WHERE
id = :id
",
[
"filename"=>$image->filename, "filesize"=>$image->filesize, "hash"=>$image->hash,
"ext"=>strtolower($image->ext), "width"=>$image->width, "height"=>$image->height, "source"=>$image->source,
"id"=>$id
]
);
try {
Media::update_image_media_properties($image->hash, $image->ext);
} catch(MediaException $e) {
log_warning("image_replace","Error while running update_image_media_properties: ".$e->getMessage());
}
/* Generate new thumbnail */
send_event(new ThumbnailGenerationEvent($image->hash, strtolower($image->ext)));

View File

@ -64,7 +64,7 @@ class ImageBan extends Extension
global $database, $page, $user;
if ($event->page_matches("image_hash_ban")) {
if ($user->can("ban_image")) {
if ($user->can(Permissions::BAN_IMAGE)) {
if ($event->get_arg(0) == "add") {
$image = isset($_POST['image_id']) ? Image::by_id(int_escape($_POST['image_id'])) : null;
$hash = isset($_POST["hash"]) ? $_POST["hash"] : $image->hash;
@ -79,7 +79,7 @@ class ImageBan extends Extension
flash_message("Image deleted");
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER['HTTP_REFERER']);
}
} elseif ($event->get_arg(0) == "remove") {
@ -87,7 +87,7 @@ class ImageBan extends Extension
send_event(new RemoveImageHashBanEvent($_POST['hash']));
flash_message("Image ban removed");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER['HTTP_REFERER']);
}
} elseif ($event->get_arg(0) == "list") {
@ -103,10 +103,21 @@ class ImageBan extends Extension
}
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
global $user;
if($event->parent==="system") {
if ($user->can(Permissions::BAN_IMAGE)) {
$event->add_nav_link("image_bans", new Link('image_hash_ban/list/1'), "Image Bans", NavLink::is_active(["image_hash_ban"]));
}
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
if ($user->can("ban_image")) {
if ($user->can(Permissions::BAN_IMAGE)) {
$event->add_link("Image Bans", make_link("image_hash_ban/list/1"));
}
}
@ -130,7 +141,7 @@ class ImageBan extends Extension
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
{
global $user;
if ($user->can("ban_image")) {
if ($user->can(Permissions::BAN_IMAGE)) {
$event->add_part($this->theme->get_buttons_html($event->image));
}
}

View File

@ -233,16 +233,16 @@ class Index extends Extension
public function onPageRequest(PageRequestEvent $event)
{
global $database, $page;
global $database, $page, $user;
if ($event->page_matches("post/list")) {
if (isset($_GET['search'])) {
// implode(explode()) to resolve aliases and sanitise
$search = url_escape(Tag::implode(Tag::explode($_GET['search'], false)));
if (empty($search)) {
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/list/1"));
} else {
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link('post/list/'.$search.'/1'));
}
return;
@ -257,13 +257,32 @@ class Index extends Extension
try {
#log_debug("index", "Search for ".Tag::implode($search_terms), false, array("terms"=>$search_terms));
$total_pages = Image::count_pages($search_terms);
if (SPEED_HAX && $count_search_terms === 0 && ($page_number < 10)) { // extra caching for the first few post/list pages
$images = $database->cache->get("post-list:$page_number");
if (!$images) {
$images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms);
$database->cache->set("post-list:$page_number", $images, 60);
$images = [];
if (SPEED_HAX) {
if (!$user->can("big_search")) {
$fast_page_limit = 500;
if ($total_pages > $fast_page_limit) $total_pages = $fast_page_limit;
if ($page_number > $fast_page_limit) {
$this->theme->display_error(
404, "Search limit hit",
"Only $fast_page_limit pages of results are searchable - " .
"if you want to find older results, use more specific search terms"
);
return;
}
}
} else {
if ($count_search_terms === 0 && ($page_number < 10)) {
// extra caching for the first few post/list pages
$images = $database->cache->get("post-list:$page_number");
if (!$images) {
$images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms);
$database->cache->set("post-list:$page_number", $images, 60);
}
}
}
if (!$images) {
$images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms);
}
} catch (SearchTermParseException $stpe) {
@ -278,7 +297,7 @@ class Index extends Extension
$this->theme->display_intro($page);
send_event(new PostListBuildingEvent($search_terms));
} elseif ($count_search_terms > 0 && $count_images === 1 && $page_number === 1) {
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link('post/view/'.$images[0]->id));
} else {
$plbe = new PostListBuildingEvent($search_terms);
@ -313,6 +332,29 @@ class Index extends Extension
}
}
public function onPageNavBuilding(PageNavBuildingEvent $event)
{
$event->add_nav_link("posts", new Link('post/list'), "Posts", NavLink::is_active(["post","view"]),20);
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
if($event->parent=="posts") {
$event->add_nav_link("posts_all", new Link('post/list'), "All");
}
}
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
{
if($event->key===HelpPages::SEARCH) {
$block = new Block();
$block->header = "General";
$block->body = $this->theme->get_help_html();
$event->add_block($block, 0);
}
}
public function onSearchTermParse(SearchTermParseEvent $event)
{
$matches = [];
@ -361,6 +403,7 @@ class Index extends Extension
$event->add_querylet(new Querylet('images.source LIKE :src', ["src"=>"%$source%"]));
}
} elseif (preg_match("/^posted([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])([0-9-]*)$/i", $event->term, $matches)) {
// TODO Make this able to search = without needing a time component.
$cmp = ltrim($matches[1], ":") ?: "=";
$val = $matches[2];
$event->add_querylet(new Querylet("images.posted $cmp :posted{$this->stpen}", ["posted{$this->stpen}"=>$val]));

View File

@ -157,7 +157,7 @@ class IndexTest extends ShimmiePHPUnitTestCase
global $database;
$db = $database->get_driver_name();
if ($db == "pgsql" || $db == "sqlite") {
if ($db == DatabaseDriver::PGSQL || $db == DatabaseDriver::SQLITE) {
$this->markTestIncomplete();
}

View File

@ -110,7 +110,7 @@ and of course start organising your images :-)
global $config;
if (count($this->search_terms) == 0) {
$page_title = $config->get_string('title');
$page_title = $config->get_string(SetupConfig::TITLE);
} else {
$search_string = implode(' ', $this->search_terms);
$page_title = html_escape($search_string);
@ -144,4 +144,201 @@ and of course start organising your images :-)
$this->display_paginator($page, "post/list", null, $this->page_number, $this->total_pages, true);
}
}
public function get_help_html()
{
return '<p>Searching is largely based on tags, with a number of special keywords available that allow searching based on properties of the images.</p>
<div class="command_example">
<pre>tagname</pre>
<p>Returns images that are tagged with "tagname".</p>
</div>
<div class="command_example">
<pre>tagname othertagname</pre>
<p>Returns images that are tagged with "tagname" and "othertagname".</p>
</div>
<p>Most tags and keywords can be prefaced with a negative sign (-) to indicate that you want to search for images that do not match something.</p>
<div class="command_example">
<pre>-tagname</pre>
<p>Returns images that are not tagged with "tagname".</p>
</div>
<div class="command_example">
<pre>-tagname -othertagname</pre>
<p>Returns images that are not tagged with "tagname" and "othertagname". This is different than without the negative sign, as images with "tagname" or "othertagname" can still be returned as long as the other one is not present.</p>
</div>
<div class="command_example">
<pre>tagname -othertagname</pre>
<p>Returns images that are tagged with "tagname", but are not tagged with "othertagname".</p>
</div>
<p>Wildcard searches are possible as well using * for "any one, more, or none" and ? for "any one".</p>
<div class="command_example">
<pre>tagn*</pre>
<p>Returns images that are tagged with "tagname", "tagnot", or anything else that starts with "tagn".</p>
</div>
<div class="command_example">
<pre>tagn?me</pre>
<p>Returns images that are tagged with "tagname", "tagnome", or anything else that starts with "tagn", has one character, and ends with "me".</p>
</div>
<div class="command_example">
<pre>tags=1</pre>
<p>Returns images with exactly 1 tag.</p>
</div>
<div class="command_example">
<pre>tags>0</pre>
<p>Returns images with 1 or more tags. </p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
<hr/>
<p>Search for images by aspect ratio</p>
<div class="command_example">
<pre>ratio=4:3</pre>
<p>Returns images with an aspect ratio of 4:3.</p>
</div>
<div class="command_example">
<pre>ratio>16:9</pre>
<p>Returns images with an aspect ratio greater than 16:9. </p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =. The relation is calculated by dividing width by height.</p>
<hr/>
<p>Search for images by file size</p>
<div class="command_example">
<pre>filesize=1</pre>
<p>Returns images exactly 1 byte in size.</p>
</div>
<div class="command_example">
<pre>filesize>100mb</pre>
<p>Returns images greater than 100 megabytes in size. </p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =. Supported suffixes are kb, mb, and gb. Uses multiples of 1024.</p>
<hr/>
<p>Search for images by MD5 hash</p>
<div class="command_example">
<pre>hash=0D3512CAA964B2BA5D7851AF5951F33B</pre>
<p>Returns image with an MD5 hash 0D3512CAA964B2BA5D7851AF5951F33B.</p>
</div>
<hr/>
<p>Search for images by file type</p>
<div class="command_example">
<pre>filetype=jpg</pre>
<p>Returns images that are of type "jpg".</p>
</div>
<hr/>
<p>Search for images by file name</p>
<div class="command_example">
<pre>filename=picasso.jpg</pre>
<p>Returns images that are named "picasso.jpg".</p>
</div>
<hr/>
<p>Search for images by source</p>
<div class="command_example">
<pre>source=http://google.com/</pre>
<p>Returns images with a source of "http://google.com/".</p>
</div>
<div class="command_example">
<pre>source=any</pre>
<p>Returns images with a source set.</p>
</div>
<div class="command_example">
<pre>source=none</pre>
<p>Returns images without a source set.</p>
</div>
<hr/>
<p>Search for images by date posted.</p>
<div class="command_example">
<pre>posted>=07-19-2019</pre>
<p>Returns images posted on or after 07-19-2019.</p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =. Date format is mm-dd-yyyy. Date posted includes time component, so = will not work unless the time is exact.</p>
<hr/>
<p>Search for images by image dimensions</p>
<div class="command_example">
<pre>size=640x480</pre>
<p>Returns images exactly 640 pixels wide by 480 pixels high.</p>
</div>
<div class="command_example">
<pre>size>1920x1080</pre>
<p>Returns images with a width larger than 1920 and a height larger than 1080.</p>
</div>
<div class="command_example">
<pre>width=1000</pre>
<p>Returns images exactly 1000 pixels wide.</p>
</div>
<div class="command_example">
<pre>height=1000</pre>
<p>Returns images exactly 1000 pixels high.</p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
<hr/>
<p>Sorting search results can be done using the pattern order:field_direction. _direction can be either _asc or _desc, indicating ascending (123) or descending (321) order.</p>
<div class="command_example">
<pre>order:id_asc</pre>
<p>Returns images sorted by ID, smallest first.</p>
</div>
<div class="command_example">
<pre>order:width_desc</pre>
<p>Returns images sorted by width, largest first.</p>
</div>
<p>These fields are supported:
<ul>
<li>id</li>
<li>width</li>
<li>height</li>
<li>filesize</li>
<li>filename</li>
</ul>
</p>
';
}
}

View File

@ -66,7 +66,7 @@ class IPBan extends Extension
{
if ($event->page_matches("ip_ban")) {
global $page, $user;
if ($user->can("ban_ip")) {
if ($user->can(Permissions::BAN_IP)) {
if ($event->get_arg(0) == "add" && $user->check_auth_token()) {
if (isset($_POST['ip']) && isset($_POST['reason']) && isset($_POST['end'])) {
if (empty($_POST['end'])) {
@ -77,7 +77,7 @@ class IPBan extends Extension
send_event(new AddIPBanEvent($_POST['ip'], $_POST['reason'], $end));
flash_message("Ban for {$_POST['ip']} added");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("ip_ban/list"));
}
} elseif ($event->get_arg(0) == "remove" && $user->check_auth_token()) {
@ -85,7 +85,7 @@ class IPBan extends Extension
send_event(new RemoveIPBanEvent($_POST['id']));
flash_message("Ban removed");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("ip_ban/list"));
}
} elseif ($event->get_arg(0) == "list") {
@ -105,10 +105,20 @@ class IPBan extends Extension
$event->panel->add_block($sb);
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
global $user;
if($event->parent==="system") {
if ($user->can(Permissions::BAN_IP)) {
$event->add_nav_link("ip_bans", new Link('ip_ban/list'), "IP Bans", NavLink::is_active(["ip_ban"]));
}
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
if ($user->can("ban_ip")) {
if ($user->can(Permissions::BAN_IP)) {
$event->add_link("IP Bans", make_link("ip_ban/list"));
}
}
@ -235,7 +245,7 @@ class IPBan extends Extension
{
global $config, $database;
$prefix = ($database->get_driver_name() == "sqlite" ? "bans." : "");
$prefix = ($database->get_driver_name() == DatabaseDriver::SQLITE ? "bans." : "");
$bans = $this->get_active_bans();

View File

@ -16,7 +16,7 @@ class IPBanTheme extends Themelet
{
global $database, $user;
$h_bans = "";
$prefix = ($database->get_driver_name() == "sqlite" ? "bans." : "");
$prefix = ($database->get_driver_name() == DatabaseDriver::SQLITE ? "bans." : "");
foreach ($bans as $ban) {
$end_human = date('Y-m-d', $ban[$prefix.'end_timestamp']);
$h_bans .= "

View File

@ -48,7 +48,7 @@ class LogDatabase extends Extension
{
global $database, $user;
if ($event->page_matches("log/view")) {
if ($user->can("view_eventlog")) {
if ($user->can(Permissions::VIEW_EVENTLOG)) {
$wheres = [];
$args = [];
$page_num = int_escape($event->get_arg(0));
@ -68,7 +68,7 @@ class LogDatabase extends Extension
$args["module"] = $_GET["module"];
}
if (!empty($_GET["user"])) {
if ($database->get_driver_name() == "pgsql") {
if ($database->get_driver_name() == DatabaseDriver::PGSQL) {
if (preg_match("#\d+\.\d+\.\d+\.\d+(/\d+)?#", $_GET["user"])) {
$wheres[] = "(username = :user1 OR text(address) = :user2)";
$args["user1"] = $_GET["user"];
@ -120,10 +120,20 @@ class LogDatabase extends Extension
}
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
global $user;
if($event->parent==="system") {
if ($user->can(Permissions::VIEW_EVENTLOG)) {
$event->add_nav_link("event_log", new Link('log/view'), "Event Log");
}
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
if ($user->can("view_eventlog")) {
if ($user->can(Permissions::VIEW_EVENTLOG)) {
$event->add_link("Event Log", make_link("log/view"));
}
}

View File

@ -35,7 +35,7 @@ class MailTest extends Extension
{
if ($event->page_matches("mail/test")) {
global $page;
$page->set_mode("data");
$page->set_mode(PageMode::DATA);
echo "Alert: uncomment this page's code on /ext/mail/main.php starting on line 33, and change the email address. Make sure you're using a server with a domain, not localhost.";
/*
echo "Preparing to send message:<br>";

View File

@ -64,7 +64,7 @@ class MassTagger extends Extension
}
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
if (!isset($_SERVER['HTTP_REFERER'])) {
$_SERVER['HTTP_REFERER'] = make_link();
}

1143
ext/media/main.php Normal file

File diff suppressed because it is too large Load Diff

47
ext/media/theme.php Normal file
View File

@ -0,0 +1,47 @@
<?php
class MediaTheme extends Themelet
{
public function display_form(array $types)
{
global $page, $database;
$html = "Use this to force scanning for media properties.";
$html .= make_form(make_link("admin/media_rescan"));
$html .= "<table class='form'>";
$html .= "<tr><th>Image Type</th><td><select name='media_rescan_type'><option value=''>All</option>";
foreach ($types as $type) {
$html .= "<option value='".$type["ext"]."'>".$type["ext"]." (".$type["count"].")</option>";
}
$html .= "</select></td></tr>";
$html .= "<tr><td colspan='2'><input type='submit' value='Scan Media Information'></td></tr>";
$html .= "</table></form>\n";
$page->add_block(new Block("Media Tools", $html));
}
public function get_buttons_html(int $image_id): string
{
return "
".make_form(make_link("media_rescan/"))."
<input type='hidden' name='image_id' value='$image_id'>
<input type='submit' value='Scan Media Properties'>
</form>
";
}
public function get_help_html()
{
return '<p>Search for items based on the type of media.</p>
<div class="command_example">
<pre>content:audio</pre>
<p>Returns items that contain audio, including videos and audio files.</p>
</div>
<div class="command_example">
<pre>content:video</pre>
<p>Returns items that contain video, including animated GIFs.</p>
</div>
<p>These search terms depend on the items being scanned for media content. Automatic scanning was implemented in mid-2019, so items uploaded before, or items uploaded on a system without ffmpeg, will require additional scanning before this will work.</p>
';
}
}

View File

@ -58,10 +58,20 @@ class NotATag extends Extension
}
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
global $user;
if($event->parent==="tags") {
if ($user->can(Permissions::BAN_IMAGE)) {
$event->add_nav_link("untags", new Link('untag/list/1'), "UnTags");
}
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
if ($user->can("ban_image")) {
if ($user->can(Permissions::BAN_IMAGE)) {
$event->add_link("UnTags", make_link("untag/list/1"));
}
}
@ -71,7 +81,7 @@ class NotATag extends Extension
global $database, $page, $user;
if ($event->page_matches("untag")) {
if ($user->can("ban_image")) {
if ($user->can(Permissions::BAN_IMAGE)) {
if ($event->get_arg(0) == "add") {
$tag = $_POST["tag"];
$redirect = isset($_POST['redirect']) ? $_POST['redirect'] : "DNP";
@ -81,14 +91,14 @@ class NotATag extends Extension
[$tag, $redirect]
);
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER['HTTP_REFERER']);
} elseif ($event->get_arg(0) == "remove") {
if (isset($_POST['tag'])) {
$database->Execute("DELETE FROM untags WHERE tag = ?", [$_POST['tag']]);
flash_message("Image ban removed");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER['HTTP_REFERER']);
}
} elseif ($event->get_arg(0) == "list") {

View File

@ -100,7 +100,7 @@ class Notes extends Extension
$this->revert_history($noteID, $reviewID);
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("note/updated"));
break;
case "add_note":
@ -108,7 +108,7 @@ class Notes extends Extension
$this->add_new_note();
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/".$_POST["image_id"]));
break;
case "add_request":
@ -116,7 +116,7 @@ class Notes extends Extension
$this->add_note_request();
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/".$_POST["image_id"]));
break;
case "nuke_notes":
@ -124,7 +124,7 @@ class Notes extends Extension
$this->nuke_notes();
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/".$_POST["image_id"]));
break;
case "nuke_requests":
@ -132,25 +132,25 @@ class Notes extends Extension
$this->nuke_requests();
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/".$_POST["image_id"]));
break;
case "edit_note":
if (!$user->is_anonymous()) {
$this->update_note();
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/" . $_POST["image_id"]));
}
break;
case "delete_note":
if ($user->is_admin()) {
$this->delete_note();
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/".$_POST["image_id"]));
}
break;
default:
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("note/list"));
break;
}
@ -210,12 +210,22 @@ class Notes extends Extension
}
$event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM notes WHERE user_id = $user_id)"));
} elseif (preg_match("/^notes_by_userno[=|:](\d+)$/i", $event->term, $matches)) {
$user_id = int_escape($matches[1]);
} elseif (preg_match("/^(notes_by_userno|notes_by_user_id)[=|:](\d+)$/i", $event->term, $matches)) {
$user_id = int_escape($matches[2]);
$event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM notes WHERE user_id = $user_id)"));
}
}
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
{
if($event->key===HelpPages::SEARCH) {
$block = new Block();
$block->header = "Notes";
$block->body = $this->theme->get_help_html();
$event->add_block($block);
}
}
/**
* HERE WE GET ALL NOTES FOR DISPLAYED IMAGE.

View File

@ -247,4 +247,29 @@ class NotesTheme extends Themelet
$this->display_paginator($page, "note/updated", null, $pageNumber, $totalPages);
}
public function get_help_html()
{
return '<p>Search for images with notes.</p>
<div class="command_example">
<pre>note=noted</pre>
<p>Returns images with a note matching "noted".</p>
</div>
<div class="command_example">
<pre>notes>0</pre>
<p>Returns images with 1 or more notes.</p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
<div class="command_example">
<pre>notes_by=username</pre>
<p>Returns images with note(s) by "username".</p>
</div>
<div class="command_example">
<pre>notes_by_user_id=123</pre>
<p>Returns images with note(s) by user 123.</p>
</div>
';
}
}

View File

@ -45,7 +45,7 @@ class NumericScore extends Extension
public function onUserPageBuilding(UserPageBuildingEvent $event)
{
global $user;
if ($user->can("edit_other_vote")) {
if ($user->can(Permissions::EDIT_OTHER_VOTE)) {
$this->theme->get_nuller($event->display_user);
}
@ -94,11 +94,11 @@ class NumericScore extends Extension
if (!is_null($score) && $image_id>0) {
send_event(new NumericScoreSetEvent($image_id, $user, $score));
}
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/$image_id"));
}
} elseif ($event->page_matches("numeric_score/remove_votes_on") && $user->check_auth_token()) {
if ($user->can("edit_other_vote")) {
if ($user->can(Permissions::EDIT_OTHER_VOTE)) {
$image_id = int_escape($_POST['image_id']);
$database->execute(
"DELETE FROM numeric_score_votes WHERE image_id=?",
@ -108,13 +108,13 @@ class NumericScore extends Extension
"UPDATE images SET numeric_score=0 WHERE id=?",
[$image_id]
);
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/$image_id"));
}
} elseif ($event->page_matches("numeric_score/remove_votes_by") && $user->check_auth_token()) {
if ($user->can("edit_other_vote")) {
if ($user->can(Permissions::EDIT_OTHER_VOTE)) {
$this->delete_votes_by(int_escape($_POST['user_id']));
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link());
}
} elseif ($event->page_matches("popular_by_day") || $event->page_matches("popular_by_month") || $event->page_matches("popular_by_year")) {
@ -228,6 +228,16 @@ class NumericScore extends Extension
$event->replace('$score', $event->image->numeric_score);
}
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
{
if($event->key===HelpPages::SEARCH) {
$block = new Block();
$block->header = "Numeric Score";
$block->body = $this->theme->get_help_html();
$event->add_block($block);
}
}
public function onSearchTermParse(SearchTermParseEvent $event)
{
$matches = [];
@ -294,6 +304,16 @@ class NumericScore extends Extension
}
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
if($event->parent=="posts") {
$event->add_nav_link("numeric_score_day", new Link('popular_by_day'), "Popular by Day");
$event->add_nav_link("numeric_score_month", new Link('popular_by_month'), "Popular by Month");
$event->add_nav_link("numeric_score_year", new Link('popular_by_year'), "Popular by Year");
}
}
private function install()
{
global $database;

View File

@ -32,7 +32,7 @@ class NumericScoreTheme extends Themelet
<input type='submit' value='Vote Down'>
</form>
";
if ($user->can("edit_other_vote")) {
if ($user->can(Permissions::EDIT_OTHER_VOTE)) {
$html .= "
<form action='".make_link("numeric_score/remove_votes_on")."' method='POST'>
".$user->get_auth_html()."
@ -87,8 +87,51 @@ class NumericScoreTheme extends Themelet
$nav_html = "<a href=".make_link().">Index</a>";
$page->set_heading($config->get_string('title'));
$page->set_heading($config->get_string(SetupConfig::TITLE));
$page->add_block(new Block("Navigation", $nav_html, "left", 10));
$page->add_block(new Block(null, $html, "main", 30));
}
public function get_help_html()
{
return '<p>Search for images that have received numeric scores by the score or by the scorer.</p>
<div class="command_example">
<pre>score=1</pre>
<p>Returns images with a score of 1.</p>
</div>
<div class="command_example">
<pre>score>0</pre>
<p>Returns images with a score of 1 or more.</p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
<div class="command_example">
<pre>upvoted_by=username</pre>
<p>Returns images upvoted by "username".</p>
</div>
<div class="command_example">
<pre>upvoted_by_id=123</pre>
<p>Returns images upvoted by user 123.</p>
</div>
<div class="command_example">
<pre>downvoted_by=username</pre>
<p>Returns images downvoted by "username".</p>
</div>
<div class="command_example">
<pre>downvoted_by_id=123</pre>
<p>Returns images downvoted by user 123.</p>
</div>
<div class="command_example">
<pre>order:score_desc</pre>
<p>Sorts the search results by score, descending.</p>
</div>
<div class="command_example">
<pre>order:score_asc</pre>
<p>Sorts the search results by score, ascending.</p>
</div>
';
}
}

View File

@ -12,7 +12,7 @@ class Oekaki extends Extension
global $user, $page;
if ($event->page_matches("oekaki")) {
if ($user->can("create_image")) {
if ($user->can(Permissions::CREATE_IMAGE)) {
if ($event->get_arg(0) == "create") {
$this->theme->display_page();
$this->theme->display_block();
@ -41,7 +41,7 @@ class Oekaki extends Extension
throw new UploadException("File type not recognised");
} else {
unlink($tmpname);
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/".$duev->image_id));
}
}
@ -84,7 +84,7 @@ class Oekaki extends Extension
public function onPostListBuilding(PostListBuildingEvent $event)
{
global $user;
if ($user->can("create_image")) {
if ($user->can(Permissions::CREATE_IMAGE)) {
$this->theme->display_block();
}
}

View File

@ -231,8 +231,8 @@ class _SafeOuroborosImage
$this->has_notes = false;
// thumb
$this->preview_height = $config->get_int('thumb_height');
$this->preview_width = $config->get_int('thumb_width');
$this->preview_height = $config->get_int(ImageConfig::THUMB_HEIGHT);
$this->preview_width = $config->get_int(ImageConfig::THUMB_WIDTH);
$this->preview_url = make_http($img->get_thumb_link());
// sample (use the full image here)
@ -404,13 +404,13 @@ class OuroborosAPI extends Extension
} elseif ($this->type == 'xml') {
$page->set_type('text/xml; charset=utf-8');
}
$page->set_mode('data');
$page->set_mode(PageMode::DATA);
$this->tryAuth();
if ($event->page_matches('post')) {
if ($this->match('create')) {
// Create
if ($user->can("create_image")) {
if ($user->can(Permissions::CREATE_IMAGE)) {
$md5 = !empty($_REQUEST['md5']) ? filter_var($_REQUEST['md5'], FILTER_SANITIZE_STRING) : null;
$this->postCreate(new OuroborosPost($_REQUEST['post']), $md5);
} else {
@ -464,7 +464,7 @@ class OuroborosAPI extends Extension
}
}
} elseif ($event->page_matches('post/show')) {
$page->set_mode('redirect');
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link(str_replace('post/show', 'post/view', implode('/', $event->args))));
$page->display();
die();
@ -481,8 +481,8 @@ class OuroborosAPI extends Extension
protected function postCreate(OuroborosPost $post, string $md5 = '')
{
global $config;
$handler = $config->get_string("upload_collision_handler");
if (!empty($md5) && !($handler == 'merge')) {
$handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER);
if (!empty($md5) && !($handler == ImageConfig::COLLISION_MERGE)) {
$img = Image::by_hash($md5);
if (!is_null($img)) {
$this->sendResponse(420, self::ERROR_POST_CREATE_DUPE);
@ -524,8 +524,8 @@ class OuroborosAPI extends Extension
if (!empty($meta['hash'])) {
$img = Image::by_hash($meta['hash']);
if (!is_null($img)) {
$handler = $config->get_string("upload_collision_handler");
if ($handler == "merge") {
$handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER);
if ($handler == ImageConfig::COLLISION_MERGE) {
$postTags = is_array($post->tags) ? $post->tags : Tag::explode($post->tags);
$merged = array_merge($postTags, $img->get_tag_array());
send_event(new TagSetEvent($img, $merged));

View File

@ -93,6 +93,19 @@ class PrivMsg extends Extension
}
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
global $user;
if($event->parent==="user") {
if (!$user->is_anonymous()) {
$count = $this->count_pms($user);
$h_count = $count > 0 ? " <span class='unread'>($count)</span>" : "";
$event->add_nav_link("pm", new Link('user#private-messages'), "Private Messages$h_count");
}
}
}
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
{
global $user;
@ -108,7 +121,7 @@ class PrivMsg extends Extension
global $page, $user;
$duser = $event->display_user;
if (!$user->is_anonymous() && !$duser->is_anonymous()) {
if (($user->id == $duser->id) || $user->can("view_other_pms")) {
if (($user->id == $duser->id) || $user->can(Permissions::VIEW_OTHER_PMS)) {
$this->theme->display_pms($page, $this->get_pms($duser));
}
if ($user->id != $duser->id) {
@ -128,7 +141,7 @@ class PrivMsg extends Extension
$pm = $database->get_row("SELECT * FROM private_message WHERE id = :id", ["id" => $pm_id]);
if (is_null($pm)) {
$this->theme->display_error(404, "No such PM", "There is no PM #$pm_id");
} elseif (($pm["to_id"] == $user->id) || $user->can("view_other_pms")) {
} elseif (($pm["to_id"] == $user->id) || $user->can(Permissions::VIEW_OTHER_PMS)) {
$from_user = User::by_id(int_escape($pm["from_id"]));
if ($pm["to_id"] == $user->id) {
$database->execute("UPDATE private_message SET is_read='Y' WHERE id = :id", ["id" => $pm_id]);
@ -145,11 +158,11 @@ class PrivMsg extends Extension
$pm = $database->get_row("SELECT * FROM private_message WHERE id = :id", ["id" => $pm_id]);
if (is_null($pm)) {
$this->theme->display_error(404, "No such PM", "There is no PM #$pm_id");
} elseif (($pm["to_id"] == $user->id) || $user->can("view_other_pms")) {
} elseif (($pm["to_id"] == $user->id) || $user->can(Permissions::VIEW_OTHER_PMS)) {
$database->execute("DELETE FROM private_message WHERE id = :id", ["id" => $pm_id]);
$database->cache->delete("pm-count-{$user->id}");
log_info("pm", "Deleted PM #$pm_id", "PM deleted");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER["HTTP_REFERER"]);
}
}
@ -162,7 +175,7 @@ class PrivMsg extends Extension
$message = $_POST["message"];
send_event(new SendPMEvent(new PM($from_id, $_SERVER["REMOTE_ADDR"], $to_id, $subject, $message)));
flash_message("PM sent");
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER["HTTP_REFERER"]);
}
break;

View File

@ -27,7 +27,7 @@ class PrivMsgTheme extends Themelet
$h_subject = "<b>$h_subject</b>";
$readYN = "N";
}
$hb = $from->can("hellbanned") ? "hb" : "";
$hb = $from->can(Permissions::HELLBANNED) ? "hb" : "";
$html .= "<tr class='$hb'>
<td>$readYN</td>
<td><a href='$pm_url'>$h_subject</a></td>

View File

@ -9,6 +9,18 @@
* Useful for related images like in a comic, etc.
*/
abstract class PoolsConfig
{
const MAX_IMPORT_RESULTS = "poolsMaxImportResults";
const IMAGES_PER_PAGE = "poolsImagesPerPage";
const LISTS_PER_PAGE = "poolsListsPerPage";
const UPDATED_PER_PAGE = "poolsUpdatedPerPage";
const INFO_ON_VIEW_IMAGE = "poolsInfoOnViewImage";
const ADDER_ON_VIEW_IMAGE = "poolsAdderOnViewImage";
const SHOW_NAV_LINKS = "poolsShowNavLinks";
const AUTO_INCREMENT_ORDER = "poolsAutoIncrementOrder";
}
/**
* This class is just a wrapper around SCoreException.
*/
@ -23,6 +35,41 @@ class PoolCreationException extends SCoreException
}
}
class PoolAddPostsEvent extends Event
{
public $pool_id;
public $posts = [];
public function __construct(int $pool_id, array $posts)
{
$this->pool_id = $pool_id;
$this->posts = $posts;
}
}
class PoolCreationEvent extends Event
{
public $title;
public $user;
public $public;
public $description;
public $new_id = -1;
public function __construct(string $title, User $pool_user = null, bool $public = false, string $description = "")
{
global $user;
$this->title = $title;
$this->user = $pool_user ?? $user;
$this->public = $public;
$this->description = $description;
}
}
class Pools extends Extension
{
public function onInitExt(InitExtEvent $event)
@ -30,14 +77,14 @@ class Pools extends Extension
global $config, $database;
// Set the defaults for the pools extension
$config->set_default_int("poolsMaxImportResults", 1000);
$config->set_default_int("poolsImagesPerPage", 20);
$config->set_default_int("poolsListsPerPage", 20);
$config->set_default_int("poolsUpdatedPerPage", 20);
$config->set_default_bool("poolsInfoOnViewImage", false);
$config->set_default_bool("poolsAdderOnViewImage", false);
$config->set_default_bool("poolsShowNavLinks", false);
$config->set_default_bool("poolsAutoIncrementOrder", false);
$config->set_default_int(PoolsConfig::MAX_IMPORT_RESULTS, 1000);
$config->set_default_int(PoolsConfig::IMAGES_PER_PAGE, 20);
$config->set_default_int(PoolsConfig::LISTS_PER_PAGE, 20);
$config->set_default_int(PoolsConfig::UPDATED_PER_PAGE, 20);
$config->set_default_bool(PoolsConfig::INFO_ON_VIEW_IMAGE, false);
$config->set_default_bool(PoolsConfig::ADDER_ON_VIEW_IMAGE, false);
$config->set_default_bool(PoolsConfig::SHOW_NAV_LINKS, false);
$config->set_default_bool(PoolsConfig::AUTO_INCREMENT_ORDER, false);
// Create the database tables
if ($config->get_int("ext_pools_version") < 1) {
@ -86,22 +133,39 @@ class Pools extends Extension
public function onSetupBuilding(SetupBuildingEvent $event)
{
$sb = new SetupBlock("Pools");
$sb->add_int_option("poolsMaxImportResults", "Max results on import: ");
$sb->add_int_option("poolsImagesPerPage", "<br>Images per page: ");
$sb->add_int_option("poolsListsPerPage", "<br>Index list items per page: ");
$sb->add_int_option("poolsUpdatedPerPage", "<br>Updated list items per page: ");
$sb->add_bool_option("poolsInfoOnViewImage", "<br>Show pool info on image: ");
$sb->add_bool_option("poolsShowNavLinks", "<br>Show 'Prev' & 'Next' links when viewing pool images: ");
$sb->add_bool_option("poolsAutoIncrementOrder", "<br>Autoincrement order when post is added to pool:");
//$sb->add_bool_option("poolsAdderOnViewImage", "<br>Show pool adder on image: ");
$sb->add_int_option(PoolsConfig::MAX_IMPORT_RESULTS, "Max results on import: ");
$sb->add_int_option(PoolsConfig::IMAGES_PER_PAGE, "<br>Images per page: ");
$sb->add_int_option(PoolsConfig::LISTS_PER_PAGE, "<br>Index list items per page: ");
$sb->add_int_option(PoolsConfig::UPDATED_PER_PAGE, "<br>Updated list items per page: ");
$sb->add_bool_option(PoolsConfig::INFO_ON_VIEW_IMAGE, "<br>Show pool info on image: ");
$sb->add_bool_option(PoolsConfig::SHOW_NAV_LINKS, "<br>Show 'Prev' & 'Next' links when viewing pool images: ");
$sb->add_bool_option(PoolsConfig::AUTO_INCREMENT_ORDER, "<br>Autoincrement order when post is added to pool:");
//$sb->add_bool_option(PoolsConfig::ADDER_ON_VIEW_IMAGE, "<br>Show pool adder on image: ");
$event->panel->add_block($sb);
}
public function onPageNavBuilding(PageNavBuildingEvent $event)
{
$event->add_nav_link("pool", new Link('pool/list'), "Pools");
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
if($event->parent=="pool") {
$event->add_nav_link("pool_list", new Link('pool/list'), "List");
$event->add_nav_link("pool_new", new Link('pool/new'), "Create");
$event->add_nav_link("pool_updated", new Link('pool/updated'), "Changes");
$event->add_nav_link("pool_help", new Link('ext_doc/pools'), "Help");
}
}
public function onPageRequest(PageRequestEvent $event)
{
global $page, $user;
global $page, $user, $database;
if ($event->page_matches("pool")) {
$pool_id = 0;
$pool = [];
@ -111,7 +175,7 @@ class Pools extends Extension
$pool_id = int_escape($_POST["pool_id"]);
$pool = $this->get_single_pool($pool_id);
}
// What action are we trying to perform?
switch ($event->get_arg(0)) {
case "list": //index
@ -129,9 +193,16 @@ class Pools extends Extension
case "create": // ADD _POST
try {
$newPoolID = $this->add_pool();
$page->set_mode("redirect");
$page->set_redirect(make_link("pool/view/".$newPoolID));
$title = $_POST["title"];
$event = new PoolCreationEvent(
$title,
$user,
$_POST["public"] === "Y",
$_POST["description"]);
send_event($event);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("pool/view/" . $event->new_id));
} catch (PoolCreationException $e) {
$this->theme->display_error(400, "Error", $e->error);
}
@ -150,7 +221,7 @@ class Pools extends Extension
if (!$user->is_anonymous()) {
$historyID = int_escape($event->get_arg(1));
$this->revert_history($historyID);
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("pool/updated"));
}
break;
@ -159,8 +230,8 @@ class Pools extends Extension
if ($this->have_permission($user, $pool)) {
$this->theme->edit_pool($page, $this->get_pool($pool_id), $this->edit_posts($pool_id));
} else {
$page->set_mode("redirect");
$page->set_redirect(make_link("pool/view/".$pool_id));
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("pool/view/" . $pool_id));
}
break;
@ -169,14 +240,14 @@ class Pools extends Extension
if ($this->have_permission($user, $pool)) {
$this->theme->edit_order($page, $this->get_pool($pool_id), $this->edit_order($pool_id));
} else {
$page->set_mode("redirect");
$page->set_redirect(make_link("pool/view/".$pool_id));
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("pool/view/" . $pool_id));
}
} else {
if ($this->have_permission($user, $pool)) {
$this->order_posts();
$page->set_mode("redirect");
$page->set_redirect(make_link("pool/view/".$pool_id));
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("pool/view/" . $pool_id));
} else {
$this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
}
@ -193,9 +264,13 @@ class Pools extends Extension
case "add_posts":
if ($this->have_permission($user, $pool)) {
$this->add_posts();
$page->set_mode("redirect");
$page->set_redirect(make_link("pool/view/".$pool_id));
$images = [];
foreach ($_POST['check'] as $imageID) {
$images[] = $imageID;
}
send_event(new PoolAddPostsEvent($pool_id, $images));
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("pool/view/" . $pool_id));
} else {
$this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
}
@ -204,8 +279,8 @@ class Pools extends Extension
case "remove_posts":
if ($this->have_permission($user, $pool)) {
$this->remove_posts();
$page->set_mode("redirect");
$page->set_redirect(make_link("pool/view/".$pool_id));
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("pool/view/" . $pool_id));
} else {
$this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
}
@ -215,8 +290,8 @@ class Pools extends Extension
case "edit_description":
if ($this->have_permission($user, $pool)) {
$this->edit_description();
$page->set_mode("redirect");
$page->set_redirect(make_link("pool/view/".$pool_id));
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("pool/view/" . $pool_id));
} else {
$this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
}
@ -228,7 +303,7 @@ class Pools extends Extension
// -> Only admins and owners may do this
if ($user->is_admin() || $user->id == $pool['user_id']) {
$this->nuke_pool($pool_id);
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("pool/list"));
} else {
$this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
@ -236,7 +311,7 @@ class Pools extends Extension
break;
default:
$page->set_mode("redirect");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("pool/list"));
break;
}
@ -260,11 +335,11 @@ class Pools extends Extension
{
global $config;
if ($config->get_bool("poolsInfoOnViewImage")) {
if ($config->get_bool(PoolsConfig::INFO_ON_VIEW_IMAGE)) {
$imageID = $event->image->id;
$poolsIDs = $this->get_pool_ids($imageID);
$show_nav = $config->get_bool("poolsShowNavLinks", false);
$show_nav = $config->get_bool(PoolsConfig::SHOW_NAV_LINKS, false);
$navInfo = [];
foreach ($poolsIDs as $poolID) {
@ -285,11 +360,11 @@ class Pools extends Extension
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
{
global $config, $database, $user;
if ($config->get_bool("poolsAdderOnViewImage") && !$user->is_anonymous()) {
if ($config->get_bool(PoolsConfig::ADDER_ON_VIEW_IMAGE) && !$user->is_anonymous()) {
if ($user->is_admin()) {
$pools = $database->get_all("SELECT * FROM pools");
} else {
$pools = $database->get_all("SELECT * FROM pools WHERE user_id=:id", ["id"=>$user->id]);
$pools = $database->get_all("SELECT * FROM pools WHERE user_id=:id", ["id" => $user->id]);
}
if (count($pools) > 0) {
$event->add_part($this->theme->get_adder_html($event->image, $pools));
@ -297,6 +372,17 @@ class Pools extends Extension
}
}
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
{
if($event->key===HelpPages::SEARCH) {
$block = new Block();
$block->header = "Pools";
$block->body = $this->theme->get_help_html();
$event->add_block($block);
}
}
public function onSearchTermParse(SearchTermParseEvent $event)
{
$matches = [];
@ -327,7 +413,7 @@ class Pools extends Extension
if (preg_match("/^pool[=|:]([^:]*|lastcreated):?([0-9]*)$/i", $event->term, $matches)) {
global $user;
$poolTag = (string) str_replace("_", " ", $matches[1]);
$poolTag = (string)str_replace("_", " ", $matches[1]);
$pool = null;
if ($poolTag == 'lastcreated') {
@ -350,6 +436,46 @@ class Pools extends Extension
}
}
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
{
global $user, $database;
$pools = $database->get_all("SELECT * FROM pools ORDER BY title ");
$event->add_action("bulk_pool_add_existing", "Add To (P)ool", "p","", $this->theme->get_bulk_pool_selector($pools));
$event->add_action("bulk_pool_add_new", "Create Pool", "","", $this->theme->get_bulk_pool_input($event->search_terms));
}
public function onBulkAction(BulkActionEvent $event)
{
global $user;
switch ($event->action) {
case "bulk_pool_add_existing":
if (!isset($_POST['bulk_pool_select'])) {
return;
}
$pool_id = intval($_POST['bulk_pool_select']);
$pool = $this->get_pool($pool_id);
if ($this->have_permission($user, $pool)) {
send_event(
new PoolAddPostsEvent($pool_id,iterator_map_to_array("image_to_id", $event->items)));
}
break;
case "bulk_pool_add_new":
if (!isset($_POST['bulk_pool_new'])) {
return;
}
$new_pool_title = $_POST['bulk_pool_new'];
$pce = new PoolCreationEvent($new_pool_title);
send_event($pce);
send_event(new PoolAddPostsEvent($pce->new_id, iterator_map_to_array("image_to_id", $event->items)));
break;
}
}
/* ------------------------------------------------- */
/* -------------- Private Functions -------------- */
/* ------------------------------------------------- */
@ -378,7 +504,7 @@ class Pools extends Extension
$pageNumber = clamp($pageNumber, 1, null) - 1;
$poolsPerPage = $config->get_int("poolsListsPerPage");
$poolsPerPage = $config->get_int(PoolsConfig::LISTS_PER_PAGE);
$order_by = "";
$order = $page->get_cookie("ui-order-pool");
@ -400,7 +526,7 @@ class Pools extends Extension
ON p.user_id = u.id
$order_by
LIMIT :l OFFSET :o
", ["l"=>$poolsPerPage, "o"=>$pageNumber * $poolsPerPage]);
", ["l" => $poolsPerPage, "o" => $pageNumber * $poolsPerPage]);
$totalPages = ceil($database->get_one("SELECT COUNT(*) FROM pools") / $poolsPerPage);
@ -411,31 +537,32 @@ class Pools extends Extension
/**
* HERE WE CREATE A NEW POOL
*/
private function add_pool(): int
public function onPoolCreation(PoolCreationEvent $event)
{
global $user, $database;
if ($user->is_anonymous()) {
throw new PoolCreationException("You must be registered and logged in to add a image.");
}
if (empty($_POST["title"])) {
if (empty($event->title)) {
throw new PoolCreationException("Pool title is empty.");
}
if ($this->get_single_pool_from_title($_POST["title"])) {
if ($this->get_single_pool_from_title($event->title)) {
throw new PoolCreationException("A pool using this title already exists.");
}
$public = $_POST["public"] === "Y" ? "Y" : "N";
$database->execute(
"
INSERT INTO pools (user_id, public, title, description, date)
VALUES (:uid, :public, :title, :desc, now())",
["uid"=>$user->id, "public"=>$public, "title"=>$_POST["title"], "desc"=>$_POST["description"]]
["uid" => $event->user->id, "public" => $event->public ? "Y" : "N", "title" => $event->title, "desc" => $event->description]
);
$poolID = $database->get_last_insert_id('pools_id_seq');
log_info("pools", "Pool {$poolID} created by {$user->name}");
return $poolID;
$event->new_id = $poolID;
}
/**
@ -446,7 +573,7 @@ class Pools extends Extension
private function get_pool(int $poolID): array
{
global $database;
return $database->get_all("SELECT * FROM pools WHERE id=:id", ["id"=>$poolID]);
return $database->get_all("SELECT * FROM pools WHERE id=:id", ["id" => $poolID]);
}
/**
@ -455,16 +582,16 @@ class Pools extends Extension
private function get_single_pool(int $poolID): array
{
global $database;
return $database->get_row("SELECT * FROM pools WHERE id=:id", ["id"=>$poolID]);
return $database->get_row("SELECT * FROM pools WHERE id=:id", ["id" => $poolID]);
}
/**
* Retrieve information about a pool given a pool title.
*/
private function get_single_pool_from_title(string $poolTitle): array
private function get_single_pool_from_title(string $poolTitle): ?array
{
global $database;
return $database->get_row("SELECT * FROM pools WHERE title=:title", ["title"=>$poolTitle]);
return $database->get_row("SELECT * FROM pools WHERE title=:title", ["title" => $poolTitle]);
}
/**
@ -474,7 +601,7 @@ class Pools extends Extension
private function get_pool_ids(int $imageID): array
{
global $database;
return $database->get_col("SELECT pool_id FROM pool_images WHERE image_id=:iid", ["iid"=>$imageID]);
return $database->get_col("SELECT pool_id FROM pool_images WHERE image_id=:iid", ["iid" => $imageID]);
}
/**
@ -483,7 +610,7 @@ class Pools extends Extension
private function get_last_userpool(int $userID): array
{
global $database;
return $database->get_row("SELECT * FROM pools WHERE user_id=:uid ORDER BY id DESC", ["uid"=>$userID]);
return $database->get_row("SELECT * FROM pools WHERE user_id=:uid ORDER BY id DESC", ["uid" => $userID]);
}
/**
@ -493,8 +620,8 @@ class Pools extends Extension
{
global $page, $config;
$poolsMaxResults = $config->get_int("poolsMaxImportResults", 1000);
$poolsMaxResults = $config->get_int(PoolsConfig::MAX_IMPORT_RESULTS, 1000);
$images = $images = Image::find_images(0, $poolsMaxResults, Tag::explode($_POST["pool_tag"]));
$this->theme->pool_result($page, $images, $this->get_pool($pool_id));
}
@ -503,41 +630,27 @@ class Pools extends Extension
/**
* HERE WE ADD CHECKED IMAGES FROM POOL AND UPDATE THE HISTORY
*
* TODO: Fix this so that the pool ID and images are passed as Arguments to the function.
*/
private function add_posts(): int
public function onPoolAddPosts(PoolAddPostsEvent $event)
{
global $database;
global $database, $user;
$poolID = int_escape($_POST['pool_id']);
$images = "";
$pool = $this->get_single_pool($event->pool_id);
if (!$this->have_permission($user, $pool)) {
return;
}
foreach ($_POST['check'] as $imageID) {
if (!$this->check_post($poolID, $imageID)) {
$database->execute(
"
INSERT INTO pool_images (pool_id, image_id)
VALUES (:pid, :iid)",
["pid"=>$poolID, "iid"=>$imageID]
);
$images .= " ".$imageID;
$images = " ";
foreach ($event->posts as $post_id) {
if ($this->add_post($event->pool_id, $post_id, false)) {
$images .= " " . $post_id;
}
}
if (!strlen($images) == 0) {
$count = int_escape($database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid"=>$poolID]));
$this->add_history($poolID, 1, $images, $count);
$count = int_escape($database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid" => $event->pool_id]));
$this->add_history($event->pool_id, 1, $images, $count);
}
$database->Execute(
"
UPDATE pools
SET posts=(SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid)
WHERE id=:pid",
["pid"=>$poolID]
);
return $poolID;
}
/**
@ -556,7 +669,7 @@ class Pools extends Extension
UPDATE pool_images
SET image_order = :ord
WHERE pool_id = :pid AND image_id = :iid",
["ord"=>$imageORDER, "pid"=>$poolID, "iid"=>$imageID]
["ord" => $imageORDER, "pid" => $poolID, "iid" => $imageID]
);
}
@ -576,11 +689,11 @@ class Pools extends Extension
$images = "";
foreach ($_POST['check'] as $imageID) {
$database->execute("DELETE FROM pool_images WHERE pool_id = :pid AND image_id = :iid", ["pid"=>$poolID, "iid"=>$imageID]);
$images .= " ".$imageID;
$database->execute("DELETE FROM pool_images WHERE pool_id = :pid AND image_id = :iid", ["pid" => $poolID, "iid" => $imageID]);
$images .= " " . $imageID;
}
$count = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid"=>$poolID]);
$count = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid" => $poolID]);
$this->add_history($poolID, 0, $images, $count);
return $poolID;
}
@ -593,7 +706,7 @@ class Pools extends Extension
global $database;
$poolID = int_escape($_POST['pool_id']);
$database->execute("UPDATE pools SET description=:dsc WHERE id=:pid", ["dsc"=>$_POST['description'], "pid"=>$poolID]);
$database->execute("UPDATE pools SET description=:dsc WHERE id=:pid", ["dsc" => $_POST['description'], "pid" => $poolID]);
return $poolID;
}
@ -605,7 +718,7 @@ class Pools extends Extension
private function check_post(int $poolID, int $imageID): bool
{
global $database;
$result = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid AND image_id=:iid", ["pid"=>$poolID, "iid"=>$imageID]);
$result = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid AND image_id=:iid", ["pid" => $poolID, "iid" => $imageID]);
return ($result != 0);
}
@ -621,7 +734,7 @@ class Pools extends Extension
if (empty($pool) || empty($imageID)) {
return null;
}
$result = $database->get_row(
"
SELECT (
@ -652,7 +765,7 @@ class Pools extends Extension
) AS next
LIMIT 1",
["pid"=>$pool['id'], "iid"=>$imageID]
["pid" => $pool['id'], "iid" => $imageID]
);
if (empty($result)) {
@ -682,46 +795,41 @@ class Pools extends Extension
$poolID = int_escape($poolID);
$pool = $this->get_pool($poolID);
$imagesPerPage = $config->get_int("poolsImagesPerPage");
$imagesPerPage = $config->get_int(PoolsConfig::IMAGES_PER_PAGE);
$query = "
INNER JOIN images AS i ON i.id = p.image_id
WHERE p.pool_id = :pid
";
// WE CHECK IF THE EXTENSION RATING IS INSTALLED, WHICH VERSION AND IF IT
// WORKS TO SHOW/HIDE SAFE, QUESTIONABLE, EXPLICIT AND UNRATED IMAGES FROM USER
if (ext_is_live("Ratings")) {
$rating = Ratings::privs_to_sql(Ratings::get_user_privs($user));
$query .= "AND i.rating IN (".Ratings::privs_to_sql(Ratings::get_user_privs($user)).")";
}
if (isset($rating) && !empty($rating)) {
$result = $database->get_all(
"
SELECT p.image_id
FROM pool_images AS p
INNER JOIN images AS i ON i.id = p.image_id
WHERE p.pool_id = :pid AND i.rating IN ($rating)
if(ext_is_live("trash")) {
$query .= $database->scoreql_to_sql(" AND trash = SCORE_BOOL_N ");
}
$result = $database->get_all(
"
SELECT p.image_id FROM pool_images p
$query
ORDER BY p.image_order ASC
LIMIT :l OFFSET :o",
["pid"=>$poolID, "l"=>$imagesPerPage, "o"=>$pageNumber * $imagesPerPage]
);
["pid" => $poolID, "l" => $imagesPerPage, "o" => $pageNumber * $imagesPerPage]
);
$totalPages = ceil($database->get_one(
$totalPages = ceil($database->get_one(
"
SELECT COUNT(*)
FROM pool_images AS p
INNER JOIN images AS i ON i.id = p.image_id
WHERE pool_id=:pid AND i.rating IN ($rating)",
["pid"=>$poolID]
SELECT COUNT(*) FROM pool_images p
$query",
["pid" => $poolID]
) / $imagesPerPage);
} else {
$result = $database->get_all(
"
SELECT image_id
FROM pool_images
WHERE pool_id=:pid
ORDER BY image_order ASC
LIMIT :l OFFSET :o",
["pid"=>$poolID, "l"=>$imagesPerPage, "o"=>$pageNumber * $imagesPerPage]
);
$totalPages = ceil($database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid"=>$poolID]) / $imagesPerPage);
}
$images = [];
foreach ($result as $singleResult) {
@ -740,14 +848,14 @@ class Pools extends Extension
{
global $database;
$result = $database->Execute("SELECT image_id FROM pool_images WHERE pool_id=:pid ORDER BY image_order ASC", ["pid"=>$poolID]);
$result = $database->Execute("SELECT image_id FROM pool_images WHERE pool_id=:pid ORDER BY image_order ASC", ["pid" => $poolID]);
$images = [];
while ($row = $result->fetch()) {
$image = Image::by_id($row["image_id"]);
$images[] = [$image];
}
return $images;
}
@ -761,21 +869,21 @@ class Pools extends Extension
{
global $database;
$result = $database->Execute("SELECT image_id FROM pool_images WHERE pool_id=:pid ORDER BY image_order ASC", ["pid"=>$poolID]);
$result = $database->Execute("SELECT image_id FROM pool_images WHERE pool_id=:pid ORDER BY image_order ASC", ["pid" => $poolID]);
$images = [];
while ($row = $result->fetch()) {
$image = $database->get_row(
"
SELECT * FROM images AS i
INNER JOIN pool_images AS p ON i.id = p.image_id
WHERE pool_id=:pid AND i.id=:iid",
["pid"=>$poolID, "iid"=>$row['image_id']]
["pid" => $poolID, "iid" => $row['image_id']]
);
$image = ($image ? new Image($image) : null);
$images[] = [$image];
}
return $images;
}
@ -787,15 +895,15 @@ class Pools extends Extension
{
global $user, $database;
$p_id = $database->get_one("SELECT user_id FROM pools WHERE id = :pid", ["pid"=>$poolID]);
$p_id = $database->get_one("SELECT user_id FROM pools WHERE id = :pid", ["pid" => $poolID]);
if ($user->is_admin()) {
$database->execute("DELETE FROM pool_history WHERE pool_id = :pid", ["pid"=>$poolID]);
$database->execute("DELETE FROM pool_images WHERE pool_id = :pid", ["pid"=>$poolID]);
$database->execute("DELETE FROM pools WHERE id = :pid", ["pid"=>$poolID]);
$database->execute("DELETE FROM pool_history WHERE pool_id = :pid", ["pid" => $poolID]);
$database->execute("DELETE FROM pool_images WHERE pool_id = :pid", ["pid" => $poolID]);
$database->execute("DELETE FROM pools WHERE id = :pid", ["pid" => $poolID]);
} elseif ($user->id == $p_id) {
$database->execute("DELETE FROM pool_history WHERE pool_id = :pid", ["pid"=>$poolID]);
$database->execute("DELETE FROM pool_images WHERE pool_id = :pid", ["pid"=>$poolID]);
$database->execute("DELETE FROM pools WHERE id = :pid AND user_id = :uid", ["pid"=>$poolID, "uid"=>$user->id]);
$database->execute("DELETE FROM pool_history WHERE pool_id = :pid", ["pid" => $poolID]);
$database->execute("DELETE FROM pool_images WHERE pool_id = :pid", ["pid" => $poolID]);
$database->execute("DELETE FROM pools WHERE id = :pid AND user_id = :uid", ["pid" => $poolID, "uid" => $user->id]);
}
}
@ -812,7 +920,7 @@ class Pools extends Extension
"
INSERT INTO pool_history (pool_id, user_id, action, images, count, date)
VALUES (:pid, :uid, :act, :img, :count, now())",
["pid"=>$poolID, "uid"=>$user->id, "act"=>$action, "img"=>$images, "count"=>$count]
["pid" => $poolID, "uid" => $user->id, "act" => $action, "img" => $images, "count" => $count]
);
}
@ -831,7 +939,7 @@ class Pools extends Extension
$pageNumber--;
}
$historiesPerPage = $config->get_int("poolsUpdatedPerPage");
$historiesPerPage = $config->get_int(PoolsConfig::UPDATED_PER_PAGE);
$history = $database->get_all("
SELECT h.id, h.pool_id, h.user_id, h.action, h.images,
@ -843,7 +951,7 @@ class Pools extends Extension
ON h.user_id = u.id
ORDER BY h.date DESC
LIMIT :l OFFSET :o
", ["l"=>$historiesPerPage, "o"=>$pageNumber * $historiesPerPage]);
", ["l" => $historiesPerPage, "o" => $pageNumber * $historiesPerPage]);
$totalPages = ceil($database->get_one("SELECT COUNT(*) FROM pool_history") / $historiesPerPage);
@ -856,7 +964,7 @@ class Pools extends Extension
private function revert_history(int $historyID)
{
global $database;
$status = $database->get_all("SELECT * FROM pool_history WHERE id=:hid", ["hid"=>$historyID]);
$status = $database->get_all("SELECT * FROM pool_history WHERE id=:hid", ["hid" => $historyID]);
foreach ($status as $entry) {
$images = trim($entry['images']);
@ -871,7 +979,7 @@ class Pools extends Extension
$imageID = $image;
$this->add_post($poolID, $imageID);
$imageArray .= " ".$imageID;
$imageArray .= " " . $imageID;
$newAction = 1;
}
} elseif ($entry['action'] == 1) {
@ -880,7 +988,7 @@ class Pools extends Extension
$imageID = $image;
$this->delete_post($poolID, $imageID);
$imageArray .= " ".$imageID;
$imageArray .= " " . $imageID;
$newAction = 0;
}
} else {
@ -889,7 +997,7 @@ class Pools extends Extension
continue; // go on to the next one.
}
$count = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid"=>$poolID]);
$count = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid" => $poolID]);
$this->add_history($poolID, $newAction, $imageArray, $count);
}
}
@ -898,18 +1006,18 @@ class Pools extends Extension
* HERE WE ADD A SIMPLE POST FROM POOL.
* USED WITH FOREACH IN revert_history() & onTagTermParse().
*/
private function add_post(int $poolID, int $imageID, bool $history=false, int $imageOrder=0)
private function add_post(int $poolID, int $imageID, bool $history = false, int $imageOrder = 0): bool
{
global $database, $config;
if (!$this->check_post($poolID, $imageID)) {
if ($config->get_bool("poolsAutoIncrementOrder") && $imageOrder === 0) {
if ($config->get_bool(PoolsConfig::AUTO_INCREMENT_ORDER) && $imageOrder === 0) {
$imageOrder = $database->get_one(
"
SELECT CASE WHEN image_order IS NOT NULL THEN MAX(image_order) + 1 ELSE 0 END
SELECT COALESCE(MAX(image_order),0) + 1
FROM pool_images
WHERE pool_id = :pid",
["pid"=>$poolID]
WHERE pool_id = :pid AND image_order IS NOT NULL",
["pid" => $poolID]
);
}
@ -917,31 +1025,43 @@ class Pools extends Extension
"
INSERT INTO pool_images (pool_id, image_id, image_order)
VALUES (:pid, :iid, :ord)",
["pid"=>$poolID, "iid"=>$imageID, "ord"=>$imageOrder]
["pid" => $poolID, "iid" => $imageID, "ord" => $imageOrder]
);
} else {
// If the post is already added, there is nothing else to do
return false;
}
$database->execute("UPDATE pools SET posts=(SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid) WHERE id=:pid", ["pid"=>$poolID]);
$this->update_count($poolID);
if ($history) {
$count = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid"=>$poolID]);
$count = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid" => $poolID]);
$this->add_history($poolID, 1, $imageID, $count);
}
return true;
}
private function update_count($pool_id)
{
global $database;
$database->execute("UPDATE pools SET posts=(SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid) WHERE id=:pid", ["pid" => $pool_id]);
}
/**
* HERE WE REMOVE A SIMPLE POST FROM POOL.
* USED WITH FOREACH IN revert_history() & onTagTermParse().
*/
private function delete_post(int $poolID, int $imageID, bool $history=false)
private function delete_post(int $poolID, int $imageID, bool $history = false)
{
global $database;
$database->execute("DELETE FROM pool_images WHERE pool_id = :pid AND image_id = :iid", ["pid"=>$poolID, "iid"=>$imageID]);
$database->execute("UPDATE pools SET posts=(SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid) WHERE id=:pid", ["pid"=>$poolID]);
$database->execute("DELETE FROM pool_images WHERE pool_id = :pid AND image_id = :iid", ["pid" => $poolID, "iid" => $imageID]);
$this->update_count($poolID);
if ($history) {
$count = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid"=>$poolID]);
$count = $database->get_one("SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid" => $poolID]);
$this->add_history($poolID, 0, $imageID, $count);
}
}

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