Compare commits

..

2 Commits

Author SHA1 Message Date
6631e2cb6f Add Rin to homepage counter 2023-10-23 23:10:45 -07:00
James Shiffer
7040b1b8e5 Fembooru changes 2022-05-14 16:44:50 -07:00
260 changed files with 3123 additions and 6788 deletions

View File

@ -10,9 +10,10 @@ jobs:
test: test:
name: PHP ${{ matrix.php }} / DB ${{ matrix.database }} name: PHP ${{ matrix.php }} / DB ${{ matrix.database }}
strategy: strategy:
max-parallel: 3
fail-fast: false fail-fast: false
matrix: matrix:
php: ['7.3', '7.4', '8.0'] php: ['7.3']
database: ['pgsql', 'mysql', 'sqlite'] database: ['pgsql', 'mysql', 'sqlite']
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -20,13 +21,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set Up Cache
uses: actions/cache@v2
with:
path: |
vendor
key: vendor-${{ matrix.php }}-${{ hashFiles('composer.lock') }}
- name: Set up PHP - name: Set up PHP
uses: shivammathur/setup-php@master uses: shivammathur/setup-php@master
with: with:
@ -62,7 +56,10 @@ jobs:
run: composer validate run: composer validate
- name: Install PHP dependencies - name: Install PHP dependencies
run: composer update && composer install --prefer-dist --no-progress run: composer install --prefer-dist --no-progress --no-suggest
- name: Install shimmie
run: php index.php
- name: Run test suite - name: Run test suite
run: | run: |
@ -78,16 +75,14 @@ jobs:
vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover
- name: Upload coverage - name: Upload coverage
if: matrix.php == '7.4'
run: | run: |
wget https://scrutinizer-ci.com/ocular.phar wget https://scrutinizer-ci.com/ocular.phar
php ocular.phar code-coverage:upload --format=php-clover data/coverage.clover php ocular.phar code-coverage:upload --format=php-clover data/coverage.clover
publish: publish:
name: Publish name: Publish
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: test needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/master' if: github.event_name == 'push'
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Publish to Registry - name: Publish to Registry

View File

@ -1,7 +1,7 @@
# "Build" shimmie (composer install - done in its own stage so that we don't # "Build" shimmie (composer install - done in its own stage so that we don't
# need to include all the composer fluff in the final image) # need to include all the composer fluff in the final image)
FROM debian:testing-slim AS app FROM debian:stable-slim AS app
RUN apt update && apt install -y composer php7.4-gd php7.4-dom php7.4-sqlite3 php-xdebug imagemagick RUN apt update && apt install -y composer php7.3-gd php7.3-dom php7.3-sqlite3 php-xdebug imagemagick
COPY composer.json composer.lock /app/ COPY composer.json composer.lock /app/
WORKDIR /app WORKDIR /app
RUN composer install --no-dev RUN composer install --no-dev
@ -10,8 +10,8 @@ COPY . /app/
# Tests in their own image. Really we should inherit from app and then # Tests in their own image. Really we should inherit from app and then
# `composer install` phpunit on top of that; but for some reason # `composer install` phpunit on top of that; but for some reason
# `composer install --no-dev && composer install` doesn't install dev # `composer install --no-dev && composer install` doesn't install dev
FROM debian:testing-slim AS tests FROM debian:stable-slim AS tests
RUN apt update && apt install -y composer php7.4-gd php7.4-dom php7.4-sqlite3 php-xdebug imagemagick RUN apt update && apt install -y composer php7.3-gd php7.3-dom php7.3-sqlite3 php-xdebug imagemagick
COPY composer.json composer.lock /app/ COPY composer.json composer.lock /app/
WORKDIR /app WORKDIR /app
RUN composer install RUN composer install
@ -25,7 +25,7 @@ RUN [ $RUN_TESTS = false ] || (\
echo '=== Cleaning ===' && rm -rf data) echo '=== Cleaning ===' && rm -rf data)
# Build su-exec so that our final image can be nicer # Build su-exec so that our final image can be nicer
FROM debian:testing-slim AS suexec FROM debian:stable-slim AS suexec
RUN apt-get update && apt-get install -y --no-install-recommends gcc libc-dev curl RUN apt-get update && apt-get install -y --no-install-recommends gcc libc-dev curl
RUN curl -k -o /usr/local/bin/su-exec.c https://raw.githubusercontent.com/ncopa/su-exec/master/su-exec.c; \ RUN curl -k -o /usr/local/bin/su-exec.c https://raw.githubusercontent.com/ncopa/su-exec/master/su-exec.c; \
gcc -Wall /usr/local/bin/su-exec.c -o/usr/local/bin/su-exec; \ gcc -Wall /usr/local/bin/su-exec.c -o/usr/local/bin/su-exec; \
@ -33,13 +33,13 @@ RUN curl -k -o /usr/local/bin/su-exec.c https://raw.githubusercontent.com/ncopa
chmod 0755 /usr/local/bin/su-exec; chmod 0755 /usr/local/bin/su-exec;
# Actually run shimmie # Actually run shimmie
FROM debian:testing-slim FROM debian:stable-slim
EXPOSE 8000 EXPOSE 8000
HEALTHCHECK --interval=5m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1 HEALTHCHECK --interval=5m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1
ENV UID=1000 \ ENV UID=1000 \
GID=1000 GID=1000
RUN apt update && apt install -y curl \ RUN apt update && apt install -y curl \
php7.4-cli php7.4-gd php7.4-pgsql php7.4-mysql php7.4-sqlite3 php7.4-zip php7.4-dom php7.4-mbstring \ php7.3-cli php7.3-gd php7.3-pgsql php7.3-mysql php7.3-sqlite3 php7.3-zip php7.3-dom php7.3-mbstring \
imagemagick zip unzip && \ imagemagick zip unzip && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
COPY --from=app /app /app COPY --from=app /app /app

View File

@ -6,10 +6,6 @@
"minimum-stability" : "dev", "minimum-stability" : "dev",
"repositories" : [ "repositories" : [
{
"type": "composer",
"url": "https://asset-packagist.org"
},
{ {
"type" : "package", "type" : "package",
"package" : { "package" : {
@ -25,29 +21,28 @@
], ],
"require" : { "require" : {
"php" : "^7.3 | ^8.0", "php" : "^7.3",
"ext-pdo": "*", "ext-pdo": "*",
"ext-json": "*", "ext-json": "*",
"ext-fileinfo": "*", "ext-fileinfo": "*",
"flexihash/flexihash" : "^2.0", "flexihash/flexihash" : "^2.0.0",
"ifixit/php-akismet" : "^1.0", "ifixit/php-akismet" : "1.*",
"google/recaptcha" : "^1.1", "google/recaptcha" : "~1.1",
"dapphp/securimage" : "^3.6", "dapphp/securimage" : "3.6.*",
"shish/eventtracer-php" : "^2.0", "shish/eventtracer-php" : "^2.0.0",
"shish/ffsphp" : "^1.0", "shish/ffsphp" : "^1.0.0",
"shish/microcrud" : "^2.0", "shish/microcrud" : "^2.0.0",
"shish/microhtml" : "^2.0", "shish/microhtml" : "^2.0.0",
"enshrined/svg-sanitize" : "^0.13", "enshrined/svg-sanitize" : "0.13.*",
"bower-asset/jquery" : "^1.12", "bower-asset/jquery" : "1.12.*",
"bower-asset/jquery-timeago" : "^1.5", "bower-asset/jquery-timeago" : "1.5.*",
"bower-asset/mediaelement" : "^2.21", "bower-asset/mediaelement" : "2.21.*",
"bower-asset/js-cookie" : "^2.1" "bower-asset/js-cookie" : "2.1.*"
}, },
"require-dev" : { "require-dev" : {
"phpunit/phpunit" : "^9.0"
}, },
"suggest": { "suggest": {
@ -63,5 +58,10 @@
"ext-zlib": "anti-spam", "ext-zlib": "anti-spam",
"ext-xml": "some extensions", "ext-xml": "some extensions",
"ext-gd": "GD-based thumbnailing" "ext-gd": "GD-based thumbnailing"
},"replace": {
"bower-asset/jquery": ">=1.11.0",
"bower-asset/inputmask": ">=3.2.0",
"bower-asset/punycode": ">=1.3.0",
"bower-asset/yii2-pjax": ">=2.0.0"
} }
} }

2065
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@ class BasePage
/** @var string */ /** @var string */
public $mode = PageMode::PAGE; public $mode = PageMode::PAGE;
/** @var string */ /** @var string */
private $mime; private $type = "text/html; charset=utf-8";
/** /**
* Set what this page should do; "page", "data", or "redirect". * Set what this page should do; "page", "data", or "redirect".
@ -36,14 +36,13 @@ class BasePage
/** /**
* Set the page's MIME type. * Set the page's MIME type.
*/ */
public function set_mime(string $mime): void public function set_type(string $type): void
{ {
$this->mime = $mime; $this->type = $type;
} }
public function __construct() public function __construct()
{ {
$this->mime = MimeType::add_parameters(MimeType::HTML, MimeType::CHARSET_UTF8);
if (@$_GET["flash"]) { if (@$_GET["flash"]) {
$this->flash[] = $_GET['flash']; $this->flash[] = $_GET['flash'];
unset($_GET["flash"]); unset($_GET["flash"]);
@ -244,7 +243,7 @@ class BasePage
{ {
if (!headers_sent()) { if (!headers_sent()) {
header("HTTP/1.0 {$this->code} Shimmie"); header("HTTP/1.0 {$this->code} Shimmie");
header("Content-type: " . $this->mime); header("Content-type: " . $this->type);
header("X-Powered-By: Shimmie-" . VERSION); header("X-Powered-By: Shimmie-" . VERSION);
foreach ($this->http_headers as $head) { foreach ($this->http_headers as $head) {
@ -298,7 +297,7 @@ class BasePage
if (isset($_SERVER['HTTP_RANGE'])) { if (isset($_SERVER['HTTP_RANGE'])) {
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2); list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (str_contains($range, ',')) { if (strpos($range, ',') !== false) {
header('HTTP/1.1 416 Requested Range Not Satisfiable'); header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size"); header("Content-Range: bytes $start-$end/$size");
break; break;
@ -335,7 +334,7 @@ class BasePage
break; break;
case PageMode::REDIRECT: case PageMode::REDIRECT:
if ($this->flash) { if ($this->flash) {
$this->redirect .= str_contains($this->redirect, "?") ? "&" : "?"; $this->redirect .= (strpos($this->redirect, "?") === false) ? "?" : "&";
$this->redirect .= "flash=" . url_escape(implode("\n", $this->flash)); $this->redirect .= "flash=" . url_escape(implode("\n", $this->flash));
} }
header('Location: ' . $this->redirect); header('Location: ' . $this->redirect);
@ -405,7 +404,6 @@ class BasePage
$js_latest = $config_latest; $js_latest = $config_latest;
$js_files = array_merge( $js_files = array_merge(
[ [
"vendor/bower-asset/jquery/dist/jquery.min.js",
"vendor/bower-asset/jquery-timeago/jquery.timeago.js", "vendor/bower-asset/jquery-timeago/jquery.timeago.js",
"vendor/bower-asset/js-cookie/src/js.cookie.js", "vendor/bower-asset/js-cookie/src/js.cookie.js",
"ext/static_files/modernizr-3.3.1.custom.js", "ext/static_files/modernizr-3.3.1.custom.js",
@ -560,7 +558,7 @@ EOD;
$contact = empty($contact_link) ? "" : "<br><a href='$contact_link'>Contact</a>"; $contact = empty($contact_link) ? "" : "<br><a href='$contact_link'>Contact</a>";
return " return "
Media &copy; their respective owners, Images &copy; their respective owners,
<a href=\"https://code.shishnet.org/shimmie2/\">Shimmie</a> &copy; <a href=\"https://code.shishnet.org/shimmie2/\">Shimmie</a> &copy;
<a href=\"https://www.shishnet.org/\">Shish</a> &amp; <a href=\"https://www.shishnet.org/\">Shish</a> &amp;
<a href=\"https://github.com/shish/shimmie2/graphs/contributors\">The Team</a> <a href=\"https://github.com/shish/shimmie2/graphs/contributors\">The Team</a>

View File

@ -53,9 +53,8 @@ class BaseThemelet
$h_tip = html_escape($image->get_tooltip()); $h_tip = html_escape($image->get_tooltip());
$h_tags = html_escape(strtolower($image->get_tag_list())); $h_tags = html_escape(strtolower($image->get_tag_list()));
// TODO: Set up a function for fetching what kind of files are currently thumbnailable $extArr = array_flip([EXTENSION_FLASH, EXTENSION_SVG, EXTENSION_MP3]); //List of thumbless filetypes
$mimeArr = array_flip([MimeType::MP3]); //List of thumbless filetypes if (!isset($extArr[$image->ext])) {
if (!isset($mimeArr[$image->get_mime()])) {
$tsize = get_thumbnail_size($image->width, $image->height); $tsize = get_thumbnail_size($image->width, $image->height);
} else { } else {
//Use max thumbnail size if using thumbless filetype //Use max thumbnail size if using thumbless filetype

View File

@ -1,65 +0,0 @@
<?php declare(strict_types=1);
// Provides mechanisms for cleanly executing command-line applications
// Was created to try to centralize a solution for whatever caused this:
// quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
class CommandBuilder
{
private $executable;
private $args = [];
public $output;
public function __construct(String $executable)
{
if (empty($executable)) {
throw new InvalidArgumentException("executable cannot be empty");
}
$this->executable = $executable;
}
public function add_flag(string $value): void
{
$this->args[] = $value;
}
public function add_escaped_arg(string $value): void
{
$this->args[] = escapeshellarg($value);
}
public function generate(): string
{
$command = escapeshellarg($this->executable);
if (!empty($this->args)) {
$command .= " ";
$command .= join(" ", $this->args);
}
return escapeshellcmd($command)." 2>&1";
}
public function combineOutput(string $empty_output = ""): string
{
if (empty($this->output)) {
return $empty_output;
} else {
return implode("\r\n", $this->output);
}
}
public function execute(bool $fail_on_non_zero_return = false): int
{
$cmd = $this->generate();
exec($cmd, $this->output, $ret);
$output = $this->combineOutput("nothing");
log_debug('command_builder', "Command `$cmd` returned $ret and outputted $output");
if ($fail_on_non_zero_return&&(int)$ret!==(int)0) {
throw new SCoreException("Command `$cmd` failed, returning $ret and outputting $output");
}
return $ret;
}
}

View File

@ -323,10 +323,10 @@ class DatabaseConfig extends BaseConfig
$params[] = ":sub_value"; $params[] = ":sub_value";
} }
$this->database->execute($query, $args); $this->database->Execute($query, $args);
$args["value"] =$this->values[$name]; $args["value"] =$this->values[$name];
$this->database->execute( $this->database->Execute(
"INSERT INTO {$this->table_name} (".join(",", $cols).") VALUES (".join(",", $params).")", "INSERT INTO {$this->table_name} (".join(",", $cols).") VALUES (".join(",", $params).")",
$args $args
); );
@ -334,6 +334,5 @@ class DatabaseConfig extends BaseConfig
// rather than deleting and having some other request(s) do a thundering // rather than deleting and having some other request(s) do a thundering
// herd of race-conditioned updates, just save the updated version once here // herd of race-conditioned updates, just save the updated version once here
$cache->set("config", $this->values); $cache->set("config", $this->values);
$this->database->notify("config");
} }
} }

View File

@ -33,6 +33,14 @@ class Database
*/ */
private $engine = null; private $engine = null;
/**
* A boolean flag to track if we already have an active transaction.
* (ie: True if beginTransaction() already called)
*
* @var bool
*/
public $transaction = false;
/** /**
* How many queries this DB object has run * How many queries this DB object has run
*/ */
@ -45,9 +53,13 @@ class Database
private function connect_db(): void private function connect_db(): void
{ {
$this->db = new PDO($this->dsn); $this->db = new PDO($this->dsn, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$this->connect_engine(); $this->connect_engine();
$this->engine->init($this->db); $this->engine->init($this->db);
$this->begin_transaction(); $this->begin_transaction();
} }
@ -75,19 +87,21 @@ class Database
public function begin_transaction(): void public function begin_transaction(): void
{ {
if ($this->is_transaction_open() === false) { if ($this->transaction === false) {
$this->db->beginTransaction(); $this->db->beginTransaction();
$this->transaction = true;
} }
} }
public function is_transaction_open(): bool public function is_transaction_open(): bool
{ {
return !is_null($this->db) && $this->db->inTransaction(); return !is_null($this->db) && $this->transaction === true;
} }
public function commit(): bool public function commit(): bool
{ {
if ($this->is_transaction_open()) { if ($this->is_transaction_open()) {
$this->transaction = false;
return $this->db->commit(); return $this->db->commit();
} else { } else {
throw new SCoreException("Unable to call commit() as there is no transaction currently open."); throw new SCoreException("Unable to call commit() as there is no transaction currently open.");
@ -97,6 +111,7 @@ class Database
public function rollback(): bool public function rollback(): bool
{ {
if ($this->is_transaction_open()) { if ($this->is_transaction_open()) {
$this->transaction = false;
return $this->db->rollback(); return $this->db->rollback();
} else { } else {
throw new SCoreException("Unable to call rollback() as there is no transaction currently open."); throw new SCoreException("Unable to call rollback() as there is no transaction currently open.");
@ -111,6 +126,19 @@ class Database
return $this->engine->scoreql_to_sql($input); return $this->engine->scoreql_to_sql($input);
} }
public function scoresql_value_prepare($input)
{
if (is_null($this->engine)) {
$this->connect_engine();
}
if ($input===true) {
return $this->engine->BOOL_Y;
} elseif ($input===false) {
return $this->engine->BOOL_N;
}
return $input;
}
public function get_driver_name(): string public function get_driver_name(): string
{ {
if (is_null($this->engine)) { if (is_null($this->engine)) {
@ -141,11 +169,6 @@ class Database
$this->engine->set_timeout($this->db, $time); $this->engine->set_timeout($this->db, $time);
} }
public function notify(string $channel, ?string $data=null): void
{
$this->engine->notify($this->db, $channel, $data);
}
public function execute(string $query, array $args = []): PDOStatement public function execute(string $query, array $args = []): PDOStatement
{ {
try { try {
@ -230,20 +253,6 @@ class Database
return $res; return $res;
} }
/**
* Execute an SQL query and return the the first column => the second column as an iterable object.
*/
public function get_pairs_iterable(string $query, array $args = []): Generator
{
$_start = microtime(true);
$stmt = $this->execute($query, $args);
$this->count_time("get_pairs_iterable", $_start, $query, $args);
foreach ($stmt as $row) {
yield $row[0] => $row[1];
}
}
/** /**
* Execute an SQL query and return a single value, or null. * Execute an SQL query and return a single value, or null.
*/ */
@ -327,29 +336,4 @@ class Database
{ {
return $this->db; return $this->db;
} }
public function standardise_boolean(string $table, string $column, bool $include_postgres=false): void
{
$d = $this->get_driver_name();
if ($d == DatabaseDriver::MYSQL) {
# In mysql, ENUM('Y', 'N') is secretly INTEGER where Y=1 and N=2.
# BOOLEAN is secretly TINYINT where true=1 and false=0.
# So we can cast directly from ENUM to BOOLEAN which gives us a
# column of values 'true' and 'invalid but who cares lol', which
# we can then UPDATE to be 'true' and 'false'.
$this->execute("ALTER TABLE $table MODIFY COLUMN $column BOOLEAN;");
$this->execute("UPDATE $table SET $column=0 WHERE $column=2;");
}
if ($d == DatabaseDriver::SQLITE) {
# SQLite doesn't care about column types at all, everything is
# text, so we can in-place replace a char with a bool
$this->execute("UPDATE $table SET $column = ($column IN ('Y', 1))");
}
if ($d == DatabaseDriver::PGSQL && $include_postgres) {
$this->execute("ALTER TABLE $table ADD COLUMN ${column}_b BOOLEAN DEFAULT FALSE NOT NULL");
$this->execute("UPDATE $table SET ${column}_b = ($column = 'Y')");
$this->execute("ALTER TABLE $table DROP COLUMN $column");
$this->execute("ALTER TABLE $table RENAME COLUMN ${column}_b TO $column");
}
}
} }

View File

@ -3,6 +3,9 @@ abstract class SCORE
{ {
const AIPK = "SCORE_AIPK"; const AIPK = "SCORE_AIPK";
const INET = "SCORE_INET"; const INET = "SCORE_INET";
const BOOL_Y = "SCORE_BOOL_Y";
const BOOL_N = "SCORE_BOOL_N";
const BOOL = "SCORE_BOOL";
} }
abstract class DBEngine abstract class DBEngine
@ -10,6 +13,9 @@ abstract class DBEngine
/** @var null|string */ /** @var null|string */
public $name = null; public $name = null;
public $BOOL_Y = null;
public $BOOL_N = null;
public function init(PDO $db) public function init(PDO $db)
{ {
} }
@ -27,8 +33,6 @@ abstract class DBEngine
abstract public function set_timeout(PDO $db, int $time); abstract public function set_timeout(PDO $db, int $time);
abstract public function get_version(PDO $db): string; abstract public function get_version(PDO $db): string;
abstract public function notify(PDO $db, string $channel, ?string $data=null): void;
} }
class MySQL extends DBEngine class MySQL extends DBEngine
@ -36,6 +40,9 @@ class MySQL extends DBEngine
/** @var string */ /** @var string */
public $name = DatabaseDriver::MYSQL; public $name = DatabaseDriver::MYSQL;
public $BOOL_Y = 'Y';
public $BOOL_N = 'N';
public function init(PDO $db) public function init(PDO $db)
{ {
$db->exec("SET NAMES utf8;"); $db->exec("SET NAMES utf8;");
@ -45,6 +52,9 @@ class MySQL extends DBEngine
{ {
$data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY auto_increment", $data); $data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY auto_increment", $data);
$data = str_replace(SCORE::INET, "VARCHAR(45)", $data); $data = str_replace(SCORE::INET, "VARCHAR(45)", $data);
$data = str_replace(SCORE::BOOL_Y, "'$this->BOOL_Y'", $data);
$data = str_replace(SCORE::BOOL_N, "'$this->BOOL_N'", $data);
$data = str_replace(SCORE::BOOL, "ENUM('Y', 'N')", $data);
return $data; return $data;
} }
@ -61,10 +71,6 @@ class MySQL extends DBEngine
// $db->exec("SET SESSION MAX_EXECUTION_TIME=".$time.";"); // $db->exec("SET SESSION MAX_EXECUTION_TIME=".$time.";");
} }
public function notify(PDO $db, string $channel, ?string $data=null): void
{
}
public function get_version(PDO $db): string public function get_version(PDO $db): string
{ {
return $db->query('select version()')->fetch()[0]; return $db->query('select version()')->fetch()[0];
@ -76,6 +82,9 @@ class PostgreSQL extends DBEngine
/** @var string */ /** @var string */
public $name = DatabaseDriver::PGSQL; public $name = DatabaseDriver::PGSQL;
public $BOOL_Y = "true";
public $BOOL_N = "false";
public function init(PDO $db) public function init(PDO $db)
{ {
if (array_key_exists('REMOTE_ADDR', $_SERVER)) { if (array_key_exists('REMOTE_ADDR', $_SERVER)) {
@ -92,6 +101,9 @@ class PostgreSQL extends DBEngine
{ {
$data = str_replace(SCORE::AIPK, "INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY", $data); $data = str_replace(SCORE::AIPK, "INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY", $data);
$data = str_replace(SCORE::INET, "INET", $data); $data = str_replace(SCORE::INET, "INET", $data);
$data = str_replace(SCORE::BOOL_Y, "true", $data);
$data = str_replace(SCORE::BOOL_N, "false", $data);
$data = str_replace(SCORE::BOOL, "BOOL", $data);
return $data; return $data;
} }
@ -106,15 +118,6 @@ class PostgreSQL extends DBEngine
$db->exec("SET statement_timeout TO ".$time.";"); $db->exec("SET statement_timeout TO ".$time.";");
} }
public function notify(PDO $db, string $channel, ?string $data=null): void
{
if ($data) {
$db->exec("NOTIFY $channel, '$data';");
} else {
$db->exec("NOTIFY $channel;");
}
}
public function get_version(PDO $db): string public function get_version(PDO $db): string
{ {
return $db->query('select version()')->fetch()[0]; return $db->query('select version()')->fetch()[0];
@ -172,6 +175,10 @@ class SQLite extends DBEngine
/** @var string */ /** @var string */
public $name = DatabaseDriver::SQLITE; public $name = DatabaseDriver::SQLITE;
public $BOOL_Y = 'Y';
public $BOOL_N = 'N';
public function init(PDO $db) public function init(PDO $db)
{ {
ini_set('sqlite.assoc_case', '0'); ini_set('sqlite.assoc_case', '0');
@ -192,6 +199,9 @@ class SQLite extends DBEngine
{ {
$data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY", $data); $data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY", $data);
$data = str_replace(SCORE::INET, "VARCHAR(45)", $data); $data = str_replace(SCORE::INET, "VARCHAR(45)", $data);
$data = str_replace(SCORE::BOOL_Y, "'$this->BOOL_Y'", $data);
$data = str_replace(SCORE::BOOL_N, "'$this->BOOL_N'", $data);
$data = str_replace(SCORE::BOOL, "CHAR(1)", $data);
return $data; return $data;
} }
@ -219,10 +229,6 @@ class SQLite extends DBEngine
// There doesn't seem to be such a thing for SQLite, so it does nothing // There doesn't seem to be such a thing for SQLite, so it does nothing
} }
public function notify(PDO $db, string $channel, ?string $data=null): void
{
}
public function get_version(PDO $db): string public function get_version(PDO $db): string
{ {
return $db->query('select sqlite_version()')->fetch()[0]; return $db->query('select sqlite_version()')->fetch()[0];

View File

@ -61,7 +61,7 @@ abstract class Extension
return 50; return 50;
} }
public static function determine_enabled_extensions(): void public static function determine_enabled_extensions()
{ {
self::$enabled_extensions = []; self::$enabled_extensions = [];
foreach (array_merge( foreach (array_merge(
@ -138,7 +138,6 @@ abstract class ExtensionInfo
public $license; public $license;
public $version; public $version;
public $dependencies = []; public $dependencies = [];
public $conflicts = [];
public $visibility; public $visibility;
public $description; public $description;
public $documentation; public $documentation;
@ -194,13 +193,6 @@ abstract class ExtensionInfo
if (!empty($this->db_support) && !in_array($database->get_driver_name(), $this->db_support)) { if (!empty($this->db_support) && !in_array($database->get_driver_name(), $this->db_support)) {
$this->support_info .= "Database not supported. "; $this->support_info .= "Database not supported. ";
} }
if (!empty($this->conflicts)) {
$intersects = array_intersect($this->conflicts, Extension::get_enabled_extensions());
if (!empty($intersects)) {
$this->support_info .= "Conflicts with other extension(s): " . join(", ", $intersects);
}
}
// Additional checks here as needed // Additional checks here as needed
$this->supported = empty($this->support_info); $this->supported = empty($this->support_info);
@ -243,7 +235,7 @@ abstract class ExtensionInfo
public static function load_all_extension_info() public static function load_all_extension_info()
{ {
foreach (get_subclasses_of("ExtensionInfo") as $class) { foreach (getSubclassesOf("ExtensionInfo") as $class) {
$extension_info = new $class(); $extension_info = new $class();
if (array_key_exists($extension_info->key, self::$all_info_by_key)) { if (array_key_exists($extension_info->key, self::$all_info_by_key)) {
throw new ScoreException("Extension Info $class with key $extension_info->key has already been loaded"); throw new ScoreException("Extension Info $class with key $extension_info->key has already been loaded");
@ -299,11 +291,11 @@ abstract class DataHandlerExtension extends Extension
public function onDataUpload(DataUploadEvent $event) public function onDataUpload(DataUploadEvent $event)
{ {
$supported_mime = $this->supported_mime($event->mime); $supported_ext = $this->supported_ext($event->type);
$check_contents = $this->check_contents($event->tmpname); $check_contents = $this->check_contents($event->tmpname);
if ($supported_mime && $check_contents) { if ($supported_ext && $check_contents) {
$this->move_upload_to_archive($event); $this->move_upload_to_archive($event);
send_event(new ThumbnailGenerationEvent($event->hash, $event->mime)); send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
/* Check if we are replacing an image */ /* Check if we are replacing an image */
if (!is_null($event->replace_id)) { if (!is_null($event->replace_id)) {
@ -313,20 +305,20 @@ abstract class DataHandlerExtension extends Extension
$existing = Image::by_id($event->replace_id); $existing = Image::by_id($event->replace_id);
if (is_null($existing)) { if (is_null($existing)) {
throw new UploadException("Post to replace does not exist!"); throw new UploadException("Image to replace does not exist!");
} }
if ($existing->hash === $event->metadata['hash']) { if ($existing->hash === $event->metadata['hash']) {
throw new UploadException("The uploaded post is the same as the one to replace."); throw new UploadException("The uploaded image is the same as the one to replace.");
} }
// even more hax.. // even more hax..
$event->metadata['tags'] = $existing->get_tag_list(); $event->metadata['tags'] = $existing->get_tag_list();
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $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)) { if (is_null($image)) {
throw new UploadException("Data handler failed to create post object from data"); throw new UploadException("Data handler failed to create image object from data");
} }
if (empty($image->get_mime())) { if (empty($image->ext)) {
throw new UploadException("Unable to determine MIME for ". $event->tmpname); throw new UploadException("Unable to determine extension for ". $event->tmpname);
} }
try { try {
send_event(new MediaCheckPropertiesEvent($image)); send_event(new MediaCheckPropertiesEvent($image));
@ -339,10 +331,10 @@ abstract class DataHandlerExtension extends Extension
} else { } else {
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata); $image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
if (is_null($image)) { if (is_null($image)) {
throw new UploadException("Data handler failed to create post object from data"); throw new UploadException("Data handler failed to create image object from data");
} }
if (empty($image->get_mime())) { if (empty($image->ext)) {
throw new UploadException("Unable to determine MIME for ". $event->tmpname); throw new UploadException("Unable to determine extension for ". $event->tmpname);
} }
try { try {
send_event(new MediaCheckPropertiesEvent($image)); send_event(new MediaCheckPropertiesEvent($image));
@ -366,7 +358,7 @@ abstract class DataHandlerExtension extends Extension
send_event(new LockSetEvent($image, !empty($locked))); send_event(new LockSetEvent($image, !empty($locked)));
} }
} }
} elseif ($supported_mime && !$check_contents) { } elseif ($supported_ext && !$check_contents) {
// We DO support this extension - but the file looks corrupt // We DO support this extension - but the file looks corrupt
throw new UploadException("Invalid or corrupted file"); throw new UploadException("Invalid or corrupted file");
} }
@ -375,15 +367,15 @@ abstract class DataHandlerExtension extends Extension
public function onThumbnailGeneration(ThumbnailGenerationEvent $event) public function onThumbnailGeneration(ThumbnailGenerationEvent $event)
{ {
$result = false; $result = false;
if ($this->supported_mime($event->mime)) { if ($this->supported_ext($event->type)) {
if ($event->force) { if ($event->force) {
$result = $this->create_thumb($event->hash, $event->mime); $result = $this->create_thumb($event->hash, $event->type);
} else { } else {
$outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash); $outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash);
if (file_exists($outname)) { if (file_exists($outname)) {
return; return;
} }
$result = $this->create_thumb($event->hash, $event->mime); $result = $this->create_thumb($event->hash, $event->type);
} }
} }
if ($result) { if ($result) {
@ -394,7 +386,7 @@ abstract class DataHandlerExtension extends Extension
public function onDisplayingImage(DisplayingImageEvent $event) public function onDisplayingImage(DisplayingImageEvent $event)
{ {
global $page; global $page;
if ($this->supported_mime($event->image->get_mime())) { if ($this->supported_ext($event->image->ext)) {
/** @noinspection PhpPossiblePolymorphicInvocationInspection */ /** @noinspection PhpPossiblePolymorphicInvocationInspection */
$this->theme->display_image($page, $event->image); $this->theme->display_image($page, $event->image);
} }
@ -402,23 +394,25 @@ abstract class DataHandlerExtension extends Extension
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event) public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{ {
if ($this->supported_mime($event->mime)) { if ($this->supported_ext($event->ext)) {
$this->media_check_properties($event); $this->media_check_properties($event);
} }
} }
protected function create_image_from_data(string $filename, array $metadata): Image protected function create_image_from_data(string $filename, array $metadata): Image
{ {
global $config;
$image = new Image(); $image = new Image();
$image->filesize = $metadata['size']; $image->filesize = $metadata['size'];
$image->hash = $metadata['hash']; $image->hash = $metadata['hash'];
$image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename']; $image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename'];
if ($config->get_bool("upload_use_mime")) {
if (array_key_exists("extension", $metadata)) { $image->ext = get_extension_for_file($filename);
$image->set_mime(MimeType::get_for_file($filename, $metadata["extension"])); }
} else { if (empty($image->ext)) {
$image->set_mime(MimeType::get_for_file($filename)); $image->ext = (($pos = strpos($metadata['extension'], '?')) !== false) ? substr($metadata['extension'], 0, $pos) : $metadata['extension'];
} }
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']); $image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
@ -429,35 +423,22 @@ abstract class DataHandlerExtension extends Extension
abstract protected function media_check_properties(MediaCheckPropertiesEvent $event): void; abstract protected function media_check_properties(MediaCheckPropertiesEvent $event): void;
abstract protected function check_contents(string $tmpname): bool; abstract protected function check_contents(string $tmpname): bool;
abstract protected function create_thumb(string $hash, string $mime): bool; abstract protected function create_thumb(string $hash, string $type): bool;
protected function supported_mime(string $mime): bool protected function supported_ext(string $ext): bool
{ {
return MimeType::matches_array($mime, $this->SUPPORTED_MIME); return in_array(get_mime_for_extension($ext), $this->SUPPORTED_MIME);
}
public static function get_all_supported_mimes(): array
{
$arr = [];
foreach (get_subclasses_of("DataHandlerExtension") as $handler) {
$handler = (new $handler());
$arr = array_merge($arr, $handler->SUPPORTED_MIME);
}
// Not sure how to handle this otherwise, don't want to set up a whole other event for this one class
if (class_exists("TranscodeImage")) {
$arr = array_merge($arr, TranscodeImage::get_enabled_mimes());
}
$arr = array_unique($arr);
return $arr;
} }
public static function get_all_supported_exts(): array public static function get_all_supported_exts(): array
{ {
$arr = []; $arr = [];
foreach (self::get_all_supported_mimes() as $mime) { foreach (getSubclassesOf("DataHandlerExtension") as $handler) {
$arr = array_merge($arr, FileExtension::get_all_for_mime($mime)); $handler = (new $handler());
foreach ($handler->SUPPORTED_MIME as $mime) {
$arr = array_merge($arr, get_all_extension_for_mime($mime));
}
} }
$arr = array_unique($arr); $arr = array_unique($arr);
return $arr; return $arr;

448
core/filetypes.php Normal file
View File

@ -0,0 +1,448 @@
<?php
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* MIME types and extension information and resolvers *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
const EXTENSION_ANI = 'ani';
const EXTENSION_ASC = 'asc';
const EXTENSION_ASF = 'asf';
const EXTENSION_AVI = 'avi';
const EXTENSION_BMP = 'bmp';
const EXTENSION_BZIP = 'bz';
const EXTENSION_BZIP2 = 'bz2';
const EXTENSION_CBR = 'cbr';
const EXTENSION_CBZ = 'cbz';
const EXTENSION_CBT = 'cbt';
const EXTENSION_CBA = 'cbA';
const EXTENSION_CB7 = 'cb7';
const EXTENSION_CSS = 'css';
const EXTENSION_CSV = 'csv';
const EXTENSION_CUR = 'cur';
const EXTENSION_FLASH = 'swf';
const EXTENSION_FLASH_VIDEO = 'flv';
const EXTENSION_GIF = 'gif';
const EXTENSION_GZIP = 'gz';
const EXTENSION_HTML = 'html';
const EXTENSION_HTM = 'htm';
const EXTENSION_ICO = 'ico';
const EXTENSION_JFIF = 'jfif';
const EXTENSION_JFI = 'jfi';
const EXTENSION_JPEG = 'jpeg';
const EXTENSION_JPG = 'jpg';
const EXTENSION_JS = 'js';
const EXTENSION_JSON = 'json';
const EXTENSION_MKV = 'mkv';
const EXTENSION_MP3 = 'mp3';
const EXTENSION_MP4 = 'mp4';
const EXTENSION_M4V = 'm4v';
const EXTENSION_M4A = 'm4a';
const EXTENSION_MPEG = 'mpeg';
const EXTENSION_MPG = 'mpg';
const EXTENSION_OGG = 'ogg';
const EXTENSION_OGG_VIDEO = 'ogv';
const EXTENSION_OGG_AUDIO = 'oga';
const EXTENSION_PDF = 'pdf';
const EXTENSION_PHP = 'php';
const EXTENSION_PHP5 = 'php5';
const EXTENSION_PNG = 'png';
const EXTENSION_PSD = 'psd';
const EXTENSION_MOV = 'mov';
const EXTENSION_RSS = 'rss';
const EXTENSION_SVG = 'svg';
const EXTENSION_TAR = 'tar';
const EXTENSION_TEXT = 'txt';
const EXTENSION_TIFF = 'tiff';
const EXTENSION_TIF = 'tif';
const EXTENSION_WAV = 'wav';
const EXTENSION_WEBM = 'webm';
const EXTENSION_WEBP = 'webp';
const EXTENSION_WMA = 'wma';
const EXTENSION_WMV = 'wmv';
const EXTENSION_XML = 'xml';
const EXTENSION_XSL = 'xsl';
const EXTENSION_ZIP = 'zip';
// Couldn't find a mimetype for ani, so made one up based on it being a riff container
const MIME_TYPE_ANI = 'application/riff+ani';
const MIME_TYPE_ASF = 'video/x-ms-asf';
const MIME_TYPE_AVI = 'video/x-msvideo';
// Went with mime types from http://fileformats.archiveteam.org/wiki/Comic_Book_Archive
const MIME_TYPE_COMIC_ZIP = 'application/vnd.comicbook+zip';
const MIME_TYPE_COMIC_RAR = 'application/vnd.comicbook-rar';
const MIME_TYPE_BMP = 'image/x-ms-bmp';
const MIME_TYPE_BZIP = 'application/x-bzip';
const MIME_TYPE_BZIP2 = 'application/x-bzip2';
const MIME_TYPE_CSS = 'text/css';
const MIME_TYPE_CSV = 'text/csv';
const MIME_TYPE_FLASH = 'application/x-shockwave-flash';
const MIME_TYPE_FLASH_VIDEO = 'video/x-flv';
const MIME_TYPE_GIF = 'image/gif';
const MIME_TYPE_GZIP = 'application/x-gzip';
const MIME_TYPE_HTML = 'text/html';
const MIME_TYPE_ICO = 'image/x-icon';
const MIME_TYPE_JPEG = 'image/jpeg';
const MIME_TYPE_JS = 'text/javascript';
const MIME_TYPE_JSON = 'application/json';
const MIME_TYPE_MKV = 'video/x-matroska';
const MIME_TYPE_MP3 = 'audio/mpeg';
const MIME_TYPE_MP4_AUDIO = 'audio/mp4';
const MIME_TYPE_MP4_VIDEO = 'video/mp4';
const MIME_TYPE_MPEG = 'video/mpeg';
const MIME_TYPE_OCTET_STREAM = 'application/octet-stream';
const MIME_TYPE_OGG = 'application/ogg';
const MIME_TYPE_OGG_VIDEO = 'video/ogg';
const MIME_TYPE_OGG_AUDIO = 'audio/ogg';
const MIME_TYPE_PDF = 'application/pdf';
const MIME_TYPE_PHP = 'text/x-php';
const MIME_TYPE_PNG = 'image/png';
const MIME_TYPE_PSD = 'image/vnd.adobe.photoshop';
const MIME_TYPE_QUICKTIME = 'video/quicktime';
const MIME_TYPE_RSS = 'application/rss+xml';
const MIME_TYPE_SVG = 'image/svg+xml';
const MIME_TYPE_TAR = 'application/x-tar';
const MIME_TYPE_TEXT = 'text/plain';
const MIME_TYPE_TIFF = 'image/tiff';
const MIME_TYPE_WAV = 'audio/x-wav';
const MIME_TYPE_WEBM = 'video/webm';
const MIME_TYPE_WEBP = 'image/webp';
const MIME_TYPE_WIN_BITMAP = 'image/x-win-bitmap';
const MIME_TYPE_XML = 'text/xml';
const MIME_TYPE_XML_APPLICATION = 'application/xml';
const MIME_TYPE_XSL = 'application/xsl+xml';
const MIME_TYPE_ZIP = 'application/zip';
const MIME_TYPE_MAP_NAME = 'name';
const MIME_TYPE_MAP_EXT = 'ext';
const MIME_TYPE_MAP_MIME = 'mime';
// Mime type map. Each entry in the MIME_TYPE_ARRAY represents a kind of file, identified by the "correct" mimetype as the key.
// The value for each entry is a map of twokeys, ext and mime.
// ext's value is an array of all of the extensions that the file type can use, with the "correct" one being first.
// mime's value is an array of all mime types that the file type is known to use, with the current "correct" one being first.
const MIME_TYPE_MAP = [
MIME_TYPE_ANI => [
MIME_TYPE_MAP_NAME => "ANI Cursor",
MIME_TYPE_MAP_EXT => [EXTENSION_ANI],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ANI],
],
MIME_TYPE_AVI => [
MIME_TYPE_MAP_NAME => "AVI",
MIME_TYPE_MAP_EXT => [EXTENSION_AVI],
MIME_TYPE_MAP_MIME => [MIME_TYPE_AVI,'video/avi','video/msvideo'],
],
MIME_TYPE_ASF => [
MIME_TYPE_MAP_NAME => "ASF/WMV",
MIME_TYPE_MAP_EXT => [EXTENSION_ASF,EXTENSION_WMA,EXTENSION_WMV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ASF,'audio/x-ms-wma','video/x-ms-wmv'],
],
MIME_TYPE_BMP => [
MIME_TYPE_MAP_NAME => "BMP",
MIME_TYPE_MAP_EXT => [EXTENSION_BMP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_BMP],
],
MIME_TYPE_BZIP => [
MIME_TYPE_MAP_NAME => "BZIP",
MIME_TYPE_MAP_EXT => [EXTENSION_BZIP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_BZIP],
],
MIME_TYPE_BZIP2 => [
MIME_TYPE_MAP_NAME => "BZIP2",
MIME_TYPE_MAP_EXT => [EXTENSION_BZIP2],
MIME_TYPE_MAP_MIME => [MIME_TYPE_BZIP2],
],
MIME_TYPE_COMIC_ZIP => [
MIME_TYPE_MAP_NAME => "CBZ",
MIME_TYPE_MAP_EXT => [EXTENSION_CBZ],
MIME_TYPE_MAP_MIME => [MIME_TYPE_COMIC_ZIP],
],
MIME_TYPE_CSS => [
MIME_TYPE_MAP_NAME => "Cascading Style Sheet",
MIME_TYPE_MAP_EXT => [EXTENSION_CSS],
MIME_TYPE_MAP_MIME => [MIME_TYPE_CSS],
],
MIME_TYPE_CSV => [
MIME_TYPE_MAP_NAME => "CSV",
MIME_TYPE_MAP_EXT => [EXTENSION_CSV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_CSV],
],
MIME_TYPE_FLASH => [
MIME_TYPE_MAP_NAME => "Flash",
MIME_TYPE_MAP_EXT => [EXTENSION_FLASH],
MIME_TYPE_MAP_MIME => [MIME_TYPE_FLASH],
],
MIME_TYPE_FLASH_VIDEO => [
MIME_TYPE_MAP_NAME => "Flash Video",
MIME_TYPE_MAP_EXT => [EXTENSION_FLASH_VIDEO],
MIME_TYPE_MAP_MIME => [MIME_TYPE_FLASH_VIDEO,'video/flv'],
],
MIME_TYPE_GIF => [
MIME_TYPE_MAP_NAME => "GIF",
MIME_TYPE_MAP_EXT => [EXTENSION_GIF],
MIME_TYPE_MAP_MIME => [MIME_TYPE_GIF],
],
MIME_TYPE_GZIP => [
MIME_TYPE_MAP_NAME => "GZIP",
MIME_TYPE_MAP_EXT => [EXTENSION_GZIP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TAR],
],
MIME_TYPE_HTML => [
MIME_TYPE_MAP_NAME => "HTML",
MIME_TYPE_MAP_EXT => [EXTENSION_HTM, EXTENSION_HTML],
MIME_TYPE_MAP_MIME => [MIME_TYPE_HTML],
],
MIME_TYPE_ICO => [
MIME_TYPE_MAP_NAME => "Icon",
MIME_TYPE_MAP_EXT => [EXTENSION_ICO, EXTENSION_CUR],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ICO, MIME_TYPE_WIN_BITMAP],
],
MIME_TYPE_JPEG => [
MIME_TYPE_MAP_NAME => "JPEG",
MIME_TYPE_MAP_EXT => [EXTENSION_JPG, EXTENSION_JPEG, EXTENSION_JFIF, EXTENSION_JFI],
MIME_TYPE_MAP_MIME => [MIME_TYPE_JPEG],
],
MIME_TYPE_JS => [
MIME_TYPE_MAP_NAME => "JavaScript",
MIME_TYPE_MAP_EXT => [EXTENSION_JS],
MIME_TYPE_MAP_MIME => [MIME_TYPE_JS],
],
MIME_TYPE_JSON => [
MIME_TYPE_MAP_NAME => "JSON",
MIME_TYPE_MAP_EXT => [EXTENSION_JSON],
MIME_TYPE_MAP_MIME => [MIME_TYPE_JSON],
],
MIME_TYPE_MKV => [
MIME_TYPE_MAP_NAME => "Matroska",
MIME_TYPE_MAP_EXT => [EXTENSION_MKV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MKV],
],
MIME_TYPE_MP3 => [
MIME_TYPE_MAP_NAME => "MP3",
MIME_TYPE_MAP_EXT => [EXTENSION_MP3],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP3],
],
MIME_TYPE_MP4_AUDIO => [
MIME_TYPE_MAP_NAME => "MP4 Audio",
MIME_TYPE_MAP_EXT => [EXTENSION_M4A],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP4_AUDIO,"audio/m4a"],
],
MIME_TYPE_MP4_VIDEO => [
MIME_TYPE_MAP_NAME => "MP4 Video",
MIME_TYPE_MAP_EXT => [EXTENSION_MP4,EXTENSION_M4V],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP4_VIDEO,'video/x-m4v'],
],
MIME_TYPE_MPEG => [
MIME_TYPE_MAP_NAME => "MPEG",
MIME_TYPE_MAP_EXT => [EXTENSION_MPG,EXTENSION_MPEG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MPEG],
],
MIME_TYPE_PDF => [
MIME_TYPE_MAP_NAME => "PDF",
MIME_TYPE_MAP_EXT => [EXTENSION_PDF],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PDF],
],
MIME_TYPE_PHP => [
MIME_TYPE_MAP_NAME => "PHP",
MIME_TYPE_MAP_EXT => [EXTENSION_PHP,EXTENSION_PHP5],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PHP],
],
MIME_TYPE_PNG => [
MIME_TYPE_MAP_NAME => "PNG",
MIME_TYPE_MAP_EXT => [EXTENSION_PNG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PNG],
],
MIME_TYPE_PSD => [
MIME_TYPE_MAP_NAME => "PSD",
MIME_TYPE_MAP_EXT => [EXTENSION_PSD],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PSD],
],
MIME_TYPE_OGG_AUDIO => [
MIME_TYPE_MAP_NAME => "Ogg Vorbis",
MIME_TYPE_MAP_EXT => [EXTENSION_OGG_AUDIO,EXTENSION_OGG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_OGG_AUDIO,MIME_TYPE_OGG],
],
MIME_TYPE_OGG_VIDEO => [
MIME_TYPE_MAP_NAME => "Ogg Theora",
MIME_TYPE_MAP_EXT => [EXTENSION_OGG_VIDEO],
MIME_TYPE_MAP_MIME => [MIME_TYPE_OGG_VIDEO],
],
MIME_TYPE_QUICKTIME => [
MIME_TYPE_MAP_NAME => "Quicktime",
MIME_TYPE_MAP_EXT => [EXTENSION_MOV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_QUICKTIME],
],
MIME_TYPE_RSS => [
MIME_TYPE_MAP_NAME => "RSS",
MIME_TYPE_MAP_EXT => [EXTENSION_RSS],
MIME_TYPE_MAP_MIME => [MIME_TYPE_RSS],
],
MIME_TYPE_SVG => [
MIME_TYPE_MAP_NAME => "SVG",
MIME_TYPE_MAP_EXT => [EXTENSION_SVG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_SVG],
],
MIME_TYPE_TAR => [
MIME_TYPE_MAP_NAME => "TAR",
MIME_TYPE_MAP_EXT => [EXTENSION_TAR],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TAR],
],
MIME_TYPE_TEXT => [
MIME_TYPE_MAP_NAME => "Text",
MIME_TYPE_MAP_EXT => [EXTENSION_TEXT, EXTENSION_ASC],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TEXT],
],
MIME_TYPE_TIFF => [
MIME_TYPE_MAP_NAME => "TIFF",
MIME_TYPE_MAP_EXT => [EXTENSION_TIF,EXTENSION_TIFF],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TIFF],
],
MIME_TYPE_WAV => [
MIME_TYPE_MAP_NAME => "Wave",
MIME_TYPE_MAP_EXT => [EXTENSION_WAV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_WAV],
],
MIME_TYPE_WEBM => [
MIME_TYPE_MAP_NAME => "WebM",
MIME_TYPE_MAP_EXT => [EXTENSION_WEBM],
MIME_TYPE_MAP_MIME => [MIME_TYPE_WEBM],
],
MIME_TYPE_WEBP => [
MIME_TYPE_MAP_NAME => "WebP",
MIME_TYPE_MAP_EXT => [EXTENSION_WEBP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_WEBP],
],
MIME_TYPE_XML => [
MIME_TYPE_MAP_NAME => "XML",
MIME_TYPE_MAP_EXT => [EXTENSION_XML],
MIME_TYPE_MAP_MIME => [MIME_TYPE_XML,MIME_TYPE_XML_APPLICATION],
],
MIME_TYPE_XSL => [
MIME_TYPE_MAP_NAME => "XSL",
MIME_TYPE_MAP_EXT => [EXTENSION_XSL],
MIME_TYPE_MAP_MIME => [MIME_TYPE_XSL],
],
MIME_TYPE_ZIP => [
MIME_TYPE_MAP_NAME => "ZIP",
MIME_TYPE_MAP_EXT => [EXTENSION_ZIP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ZIP],
],
];
/**
* Returns the mimetype that matches the provided extension.
*/
function get_mime_for_extension(string $ext): ?string
{
$ext = strtolower($ext);
foreach (MIME_TYPE_MAP as $key=>$value) {
if (in_array($ext, $value[MIME_TYPE_MAP_EXT])) {
return $key;
}
}
return null;
}
/**
* Returns the mimetype for the specified file, trying file inspection methods before falling back on extension-based detection.
* @param String $file
* @param String $ext The files extension, for if the current filename somehow lacks the extension
* @return String The extension that was found.
*/
function get_mime(string $file, string $ext=""): string
{
if (!file_exists($file)) {
throw new SCoreException("File not found: ".$file);
}
$type = false;
if (extension_loaded('fileinfo')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
try {
$type = finfo_file($finfo, $file);
} finally {
finfo_close($finfo);
}
} elseif (function_exists('mime_content_type')) {
// If anyone is still using mime_content_type()
$type = trim(mime_content_type($file));
}
if ($type===false || empty($type)) {
// Checking by extension is our last resort
if ($ext==null||strlen($ext) == 0) {
$ext = pathinfo($file, PATHINFO_EXTENSION);
}
$type = get_mime_for_extension($ext);
}
if ($type !== false && strlen($type) > 0) {
return $type;
}
return MIME_TYPE_OCTET_STREAM;
}
/**
* Returns the file extension associated with the specified mimetype.
*/
function get_extension(?string $mime_type): ?string
{
if (empty($mime_type)) {
return null;
}
if ($mime_type==MIME_TYPE_OCTET_STREAM) {
return null;
}
foreach (MIME_TYPE_MAP as $key=>$value) {
if (in_array($mime_type, $value[MIME_TYPE_MAP_MIME])) {
return $value[MIME_TYPE_MAP_EXT][0];
}
}
return null;
}
/**
* Returns all of the file extensions associated with the specified mimetype.
*/
function get_all_extension_for_mime(?string $mime_type): array
{
$output = [];
if (empty($mime_type)) {
return $output;
}
foreach (MIME_TYPE_MAP as $key=>$value) {
if (in_array($mime_type, $value[MIME_TYPE_MAP_MIME])) {
$output = array_merge($output, $value[MIME_TYPE_MAP_EXT]);
}
}
return $output;
}
/**
* Gets an the extension defined in MIME_TYPE_MAP for a file.
*
* @param String $file_path
* @return String The extension that was found, or null if one can not be found.
*/
function get_extension_for_file(String $file_path): ?String
{
$mime = get_mime($file_path);
if (!empty($mime)) {
if ($mime==MIME_TYPE_OCTET_STREAM) {
return null;
} else {
$ext = get_extension($mime);
}
if (!empty($ext)) {
return $ext;
}
}
return null;
}

View File

@ -91,7 +91,7 @@ class ThumbnailGenerationEvent extends Event
/** @var string */ /** @var string */
public $hash; public $hash;
/** @var string */ /** @var string */
public $mime; public $type;
/** @var bool */ /** @var bool */
public $force; public $force;
@ -101,11 +101,11 @@ class ThumbnailGenerationEvent extends Event
/** /**
* Request a thumbnail be made for an image object * Request a thumbnail be made for an image object
*/ */
public function __construct(string $hash, string $mime, bool $force=false) public function __construct(string $hash, string $type, bool $force=false)
{ {
parent::__construct(); parent::__construct();
$this->hash = $hash; $this->hash = $hash;
$this->mime = $mime; $this->type = $type;
$this->force = $force; $this->force = $force;
$this->generated = false; $this->generated = false;
} }

View File

@ -13,6 +13,8 @@ class Image
public const IMAGE_DIR = "images"; public const IMAGE_DIR = "images";
public const THUMBNAIL_DIR = "thumbs"; public const THUMBNAIL_DIR = "thumbs";
public static $order_sql = null; // this feels ugly
/** @var null|int */ /** @var null|int */
public $id = null; public $id = null;
@ -32,10 +34,7 @@ class Image
public $filename; public $filename;
/** @var string */ /** @var string */
private $ext; public $ext;
/** @var string */
private $mime;
/** @var string[]|null */ /** @var string[]|null */
public $tag_array; public $tag_array;
@ -61,9 +60,6 @@ class Image
/** @var boolean */ /** @var boolean */
public $video = null; public $video = null;
/** @var string */
public $video_codec = null;
/** @var boolean */ /** @var boolean */
public $image = null; public $image = null;
@ -84,10 +80,6 @@ class Image
{ {
if (!is_null($row)) { if (!is_null($row)) {
foreach ($row as $name => $value) { foreach ($row as $name => $value) {
if (is_numeric($name)) {
continue;
}
// some databases use table.name rather than name // some databases use table.name rather than name
$name = str_replace("images.", "", $name); $name = str_replace("images.", "", $name);
@ -160,9 +152,12 @@ class Image
} }
} }
$querylet = Image::build_search_querylet($tags, $limit, $start); $order = (Image::$order_sql ?: "images.".$config->get_string(IndexConfig::ORDER));
$querylet = Image::build_search_querylet($tags, $order, $limit, $start);
$result = $database->get_all_iterable($querylet->sql, $querylet->variables); $result = $database->get_all_iterable($querylet->sql, $querylet->variables);
Image::$order_sql = null;
return $result; return $result;
} }
@ -218,6 +213,27 @@ class Image
); );
} }
/**
* Counts consecutive days of image uploads
*/
public static function count_upload_streak(): int
{
$now = date_create();
$last_date = $now;
foreach (self::find_images_iterable() as $img) {
$next_date = date_create($img->posted);
if (date_diff($next_date, $last_date)->days > 0) {
break;
}
$last_date = $next_date;
}
if ($last_date === $now) {
return 0;
}
$diff_d = ($now->getTimestamp() - $last_date->getTimestamp()) / 86400;
return (int)ceil($diff_d);
}
/** /**
* Count the number of image results for a given search * Count the number of image results for a given search
* *
@ -228,11 +244,11 @@ class Image
global $cache, $database; global $cache, $database;
$tag_count = count($tags); $tag_count = count($tags);
if (SPEED_HAX && $tag_count === 0) { if ($tag_count === 0) {
// total number of images in the DB // total number of images in the DB
$total = self::count_total_images(); $total = self::count_total_images();
} elseif (SPEED_HAX && $tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) { } elseif ($tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) {
if (!str_starts_with($tags[0], "-")) { if (!startsWith($tags[0], "-")) {
// one tag - we can look that up directly // one tag - we can look that up directly
$total = self::count_tag($tags[0]); $total = self::count_tag($tags[0]);
} else { } else {
@ -278,19 +294,14 @@ class Image
{ {
$tag_conditions = []; $tag_conditions = [];
$img_conditions = []; $img_conditions = [];
$stpen = 0; // search term parse event number
$order = null;
/* /*
* Turn a bunch of strings into a bunch of TagCondition * Turn a bunch of strings into a bunch of TagCondition
* and ImgCondition objects * and ImgCondition objects
*/ */
/** @var $stpe SearchTermParseEvent */ $stpe = send_event(new SearchTermParseEvent(null, $terms));
$stpe = send_event(new SearchTermParseEvent($stpen++, null, $terms)); if ($stpe->is_querylet_set()) {
if ($stpe->order) { foreach ($stpe->get_querylets() as $querylet) {
$order = $stpe->order;
} elseif (!empty($stpe->querylets)) {
foreach ($stpe->querylets as $querylet) {
$img_conditions[] = new ImgCondition($querylet, true); $img_conditions[] = new ImgCondition($querylet, true);
} }
} }
@ -305,12 +316,9 @@ class Image
continue; continue;
} }
/** @var $stpe SearchTermParseEvent */ $stpe = send_event(new SearchTermParseEvent($term, $terms));
$stpe = send_event(new SearchTermParseEvent($stpen++, $term, $terms)); if ($stpe->is_querylet_set()) {
if ($stpe->order) { foreach ($stpe->get_querylets() as $querylet) {
$order = $stpe->order;
} elseif (!empty($stpe->querylets)) {
foreach ($stpe->querylets as $querylet) {
$img_conditions[] = new ImgCondition($querylet, $positive); $img_conditions[] = new ImgCondition($querylet, $positive);
} }
} else { } else {
@ -320,7 +328,7 @@ class Image
} }
} }
} }
return [$tag_conditions, $img_conditions, $order]; return [$tag_conditions, $img_conditions];
} }
/* /*
@ -357,9 +365,8 @@ class Image
'); ');
} else { } else {
$tags[] = 'id'. $gtlt . $this->id; $tags[] = 'id'. $gtlt . $this->id;
$tags[] = 'order:id_'. strtolower($dir);
$querylet = Image::build_search_querylet($tags); $querylet = Image::build_search_querylet($tags);
$querylet->append_sql(' LIMIT 1'); $querylet->append_sql(' ORDER BY images.id '.$dir.' LIMIT 1');
$row = $database->get_row($querylet->sql, $querylet->variables); $row = $database->get_row($querylet->sql, $querylet->variables);
} }
@ -396,7 +403,7 @@ class Image
SET owner_id=:owner_id SET owner_id=:owner_id
WHERE id=:id WHERE id=:id
", ["owner_id"=>$owner->id, "id"=>$this->id]); ", ["owner_id"=>$owner->id, "id"=>$this->id]);
log_info("core_image", "Owner for Post #{$this->id} set to {$owner->name}"); log_info("core_image", "Owner for Image #{$this->id} set to {$owner->name}");
} }
} }
@ -410,22 +417,22 @@ class Image
"INSERT INTO images( "INSERT INTO images(
owner_id, owner_ip, owner_id, owner_ip,
filename, filesize, filename, filesize,
hash, mime, ext, hash, ext,
width, height, width, height,
posted, source posted, source
) )
VALUES ( VALUES (
:owner_id, :owner_ip, :owner_id, :owner_ip,
:filename, :filesize, :filename, :filesize,
:hash, :mime, :ext, :hash, :ext,
0, 0, 0, 0,
now(), :source now(), :source
)", )",
[ [
"owner_id" => $user->id, "owner_ip" => $_SERVER['REMOTE_ADDR'], "owner_id" => $user->id, "owner_ip" => $_SERVER['REMOTE_ADDR'],
"filename" => $cut_name, "filesize" => $this->filesize, "filename" => $cut_name, "filesize" => $this->filesize,
"hash" => $this->hash, "mime" => strtolower($this->mime), "hash" => $this->hash, "ext" => strtolower($this->ext),
"ext" => strtolower($this->ext), "source" => $this->source "source" => $this->source
] ]
); );
$this->id = $database->get_last_insert_id('images_id_seq'); $this->id = $database->get_last_insert_id('images_id_seq');
@ -433,13 +440,12 @@ class Image
$database->execute( $database->execute(
"UPDATE images SET ". "UPDATE images SET ".
"filename = :filename, filesize = :filesize, hash = :hash, ". "filename = :filename, filesize = :filesize, hash = :hash, ".
"mime = :mime, ext = :ext, width = 0, height = 0, source = :source ". "ext = :ext, width = 0, height = 0, source = :source ".
"WHERE id = :id", "WHERE id = :id",
[ [
"filename" => $cut_name, "filename" => $cut_name,
"filesize" => $this->filesize, "filesize" => $this->filesize,
"hash" => $this->hash, "hash" => $this->hash,
"mime" => strtolower($this->mime),
"ext" => strtolower($this->ext), "ext" => strtolower($this->ext),
"source" => $this->source, "source" => $this->source,
"id" => $this->id, "id" => $this->id,
@ -450,18 +456,17 @@ class Image
$database->execute( $database->execute(
"UPDATE images SET ". "UPDATE images SET ".
"lossless = :lossless, ". "lossless = :lossless, ".
"video = :video, video_codec = :video_codec, audio = :audio,image = :image, ". "video = :video, audio = :audio,image = :image, ".
"height = :height, width = :width, ". "height = :height, width = :width, ".
"length = :length WHERE id = :id", "length = :length WHERE id = :id",
[ [
"id" => $this->id, "id" => $this->id,
"width" => $this->width ?? 0, "width" => $this->width ?? 0,
"height" => $this->height ?? 0, "height" => $this->height ?? 0,
"lossless" => $this->lossless, "lossless" => $database->scoresql_value_prepare($this->lossless),
"video" => $this->video, "video" => $database->scoresql_value_prepare($this->video),
"video_codec" => $this->video_codec, "image" => $database->scoresql_value_prepare($this->image),
"image" => $this->image, "audio" => $database->scoresql_value_prepare($this->audio),
"audio" => $this->audio,
"length" => $this->length "length" => $this->length
] ]
); );
@ -519,8 +524,7 @@ class Image
public function get_thumb_link(): string public function get_thumb_link(): string
{ {
global $config; global $config;
$mime = $config->get_string(ImageConfig::THUMB_MIME); $ext = $config->get_string(ImageConfig::THUMB_TYPE);
$ext = FileExtension::get_for_mime($mime);
return $this->get_link(ImageConfig::TLINK, '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext); return $this->get_link(ImageConfig::TLINK, '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext);
} }
@ -534,7 +538,7 @@ class Image
$image_link = $config->get_string($template); $image_link = $config->get_string($template);
if (!empty($image_link)) { if (!empty($image_link)) {
if (!str_contains($image_link, "://") && !str_starts_with($image_link, "/")) { if (!(strpos($image_link, "://") > 0) && !startsWith($image_link, "/")) {
$image_link = make_link($image_link); $image_link = make_link($image_link);
} }
$chosen = $image_link; $chosen = $image_link;
@ -558,19 +562,6 @@ class Image
return $plte->text; return $plte->text;
} }
/**
* Get the info for this image, formatted according to the
* configured template.
*/
public function get_info(): string
{
global $config;
$plte = new ParseLinkTemplateEvent($config->get_string(ImageConfig::INFO), $this);
send_event($plte);
return $plte->text;
}
/** /**
* Figure out where the full size image is on disk. * Figure out where the full size image is on disk.
*/ */
@ -596,38 +587,21 @@ class Image
} }
/** /**
* Get the image's extension. * Get the image's mime type.
*/
public function get_mime_type(): string
{
return get_mime($this->get_image_filename(), $this->get_ext());
}
/**
* Get the image's filename extension
*/ */
public function get_ext(): string public function get_ext(): string
{ {
return $this->ext; return $this->ext;
} }
/**
* Get the image's mime type.
*/
public function get_mime(): ?string
{
if ($this->mime===MimeType::WEBP&&$this->lossless) {
return MimeType::WEBP_LOSSLESS;
}
$m = $this->mime;
if (is_null($m)) {
$m = MimeMap::get_for_extension($this->ext)[0];
}
return $m;
}
/**
* Set the image's mime type.
*/
public function set_mime($mime): void
{
$this->mime = $mime;
$this->ext = FileExtension::get_for_mime($this->get_mime());
}
/** /**
* Get the image's source URL * Get the image's source URL
*/ */
@ -648,7 +622,7 @@ class Image
} }
if ($new_source != $old_source) { if ($new_source != $old_source) {
$database->execute("UPDATE images SET source=:source WHERE id=:id", ["source"=>$new_source, "id"=>$this->id]); $database->execute("UPDATE images SET source=:source WHERE id=:id", ["source"=>$new_source, "id"=>$this->id]);
log_info("core_image", "Source for Post #{$this->id} set to: $new_source (was $old_source)"); log_info("core_image", "Source for Image #{$this->id} set to: $new_source (was $old_source)");
} }
} }
@ -660,12 +634,16 @@ class Image
return $this->locked; return $this->locked;
} }
public function set_locked(bool $locked): void public function set_locked(bool $tf): void
{ {
global $database; global $database;
if ($locked !== $this->locked) { $ln = $tf ? "Y" : "N";
$database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn"=>$locked, "id"=>$this->id]); $sln = $database->scoreql_to_sql('SCORE_BOOL_'.$ln);
log_info("core_image", "Setting Post #{$this->id} lock to: $locked"); $sln = str_replace("'", "", $sln);
$sln = str_replace('"', "", $sln);
if (bool_escape($sln) !== $this->locked) {
$database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn"=>$sln, "id"=>$this->id]);
log_info("core_image", "Setting Image #{$this->id} lock to: $ln");
} }
} }
@ -720,7 +698,7 @@ class Image
$page->flash("Can't set a tag longer than 255 characters"); $page->flash("Can't set a tag longer than 255 characters");
continue; continue;
} }
if (str_starts_with($tag, "-")) { if (startsWith($tag, "-")) {
$page->flash("Can't set a tag which starts with a minus"); $page->flash("Can't set a tag which starts with a minus");
continue; continue;
} }
@ -782,7 +760,7 @@ class Image
); );
} }
log_info("core_image", "Tags for Post #{$this->id} set to: ".Tag::implode($tags)); log_info("core_image", "Tags for Image #{$this->id} set to: ".Tag::implode($tags));
$cache->delete("image-{$this->id}-tags"); $cache->delete("image-{$this->id}-tags");
} }
} }
@ -795,7 +773,7 @@ class Image
global $database; global $database;
$this->delete_tags_from_image(); $this->delete_tags_from_image();
$database->execute("DELETE FROM images WHERE id=:id", ["id"=>$this->id]); $database->execute("DELETE FROM images WHERE id=:id", ["id"=>$this->id]);
log_info("core_image", 'Deleted Post #'.$this->id.' ('.$this->hash.')'); log_info("core_image", 'Deleted Image #'.$this->id.' ('.$this->hash.')');
unlink($this->get_image_filename()); unlink($this->get_image_filename());
unlink($this->get_thumb_filename()); unlink($this->get_thumb_filename());
@ -807,7 +785,7 @@ class Image
*/ */
public function remove_image_only(): void public function remove_image_only(): void
{ {
log_info("core_image", 'Removed Post File ('.$this->hash.')'); log_info("core_image", 'Removed Image File ('.$this->hash.')');
@unlink($this->get_image_filename()); @unlink($this->get_image_filename());
@unlink($this->get_thumb_filename()); @unlink($this->get_thumb_filename());
} }
@ -833,14 +811,12 @@ class Image
* #param string[] $terms * #param string[] $terms
*/ */
private static function build_search_querylet( private static function build_search_querylet(
array $terms, array $tags,
?string $order=null,
?int $limit=null, ?int $limit=null,
?int $offset=null ?int $offset=null
): Querylet { ): Querylet {
global $config; list($tag_conditions, $img_conditions) = self::terms_to_conditions($tags);
list($tag_conditions, $img_conditions, $order) = self::terms_to_conditions($terms);
$order = ($order ?: "images.".$config->get_string(IndexConfig::ORDER));
$positive_tag_count = 0; $positive_tag_count = 0;
$negative_tag_count = 0; $negative_tag_count = 0;

View File

@ -33,6 +33,11 @@ function add_dir(string $base): array
/** /**
* Sends a DataUploadEvent for a file. * 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): int function add_image(string $tmpname, string $filename, string $tags): int
{ {
@ -70,12 +75,7 @@ function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_sca
$fit = $config->get_string(ImageConfig::THUMB_FIT); $fit = $config->get_string(ImageConfig::THUMB_FIT);
if (in_array($fit, [ if (in_array($fit, [Media::RESIZE_TYPE_FILL, Media::RESIZE_TYPE_STRETCH, Media::RESIZE_TYPE_FIT_BLUR])) {
Media::RESIZE_TYPE_FILL,
Media::RESIZE_TYPE_STRETCH,
Media::RESIZE_TYPE_FIT_BLUR,
Media::RESIZE_TYPE_FIT_BLUR_PORTRAIT
])) {
return [$config->get_int(ImageConfig::THUMB_WIDTH), $config->get_int(ImageConfig::THUMB_HEIGHT)]; return [$config->get_int(ImageConfig::THUMB_WIDTH), $config->get_int(ImageConfig::THUMB_HEIGHT)];
} }
@ -136,7 +136,7 @@ function get_thumbnail_max_size_scaled(): array
} }
function create_image_thumb(string $hash, string $mime, string $engine = null) function create_image_thumb(string $hash, string $type, string $engine = null)
{ {
global $config; global $config;
@ -147,7 +147,7 @@ function create_image_thumb(string $hash, string $mime, string $engine = null)
$inname, $inname,
$outname, $outname,
$tsize, $tsize,
$mime, $type,
$engine, $engine,
$config->get_string(ImageConfig::THUMB_FIT) $config->get_string(ImageConfig::THUMB_FIT)
); );
@ -155,7 +155,7 @@ function create_image_thumb(string $hash, string $mime, string $engine = null)
function create_scaled_image(string $inname, string $outname, array $tsize, string $mime, ?string $engine = null, ?string $resize_type = null) function create_scaled_image(string $inname, string $outname, array $tsize, string $type, ?string $engine = null, ?string $resize_type = null)
{ {
global $config; global $config;
if (empty($engine)) { if (empty($engine)) {
@ -165,44 +165,22 @@ function create_scaled_image(string $inname, string $outname, array $tsize, stri
$resize_type = $config->get_string(ImageConfig::THUMB_FIT); $resize_type = $config->get_string(ImageConfig::THUMB_FIT);
} }
$output_mime = $config->get_string(ImageConfig::THUMB_MIME); $output_format = $config->get_string(ImageConfig::THUMB_TYPE);
if ($output_format==EXTENSION_WEBP) {
$output_format = Media::WEBP_LOSSY;
}
send_event(new MediaResizeEvent( send_event(new MediaResizeEvent(
$engine, $engine,
$inname, $inname,
$mime, $type,
$outname, $outname,
$tsize[0], $tsize[0],
$tsize[1], $tsize[1],
$resize_type, $resize_type,
$output_mime, $output_format,
$config->get_string(ImageConfig::THUMB_ALPHA_COLOR),
$config->get_int(ImageConfig::THUMB_QUALITY), $config->get_int(ImageConfig::THUMB_QUALITY),
true, true,
true true
)); ));
} }
function redirect_to_next_image(Image $image): void
{
global $page;
if (isset($_GET['search'])) {
$search_terms = Tag::explode(Tag::decaret($_GET['search']));
$query = "search=" . url_escape($_GET['search']);
} else {
$search_terms = [];
$query = null;
}
$target_image = $image->get_next($search_terms);
if ($target_image == null) {
$redirect_target = referer_or(make_link("post/list"), ['post/view']);
} else {
$redirect_target = make_link("post/view/{$target_image->id}", null, $query);
}
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($redirect_target);
}

View File

@ -49,6 +49,7 @@ function get_dsn()
{ {
if (getenv("INSTALL_DSN")) { if (getenv("INSTALL_DSN")) {
$dsn = getenv("INSTALL_DSN"); $dsn = getenv("INSTALL_DSN");
;
} elseif (@$_POST["database_type"] == DatabaseDriver::SQLITE) { } elseif (@$_POST["database_type"] == DatabaseDriver::SQLITE) {
/** @noinspection PhpUnhandledExceptionInspection */ /** @noinspection PhpUnhandledExceptionInspection */
$id = bin2hex(random_bytes(5)); $id = bin2hex(random_bytes(5));
@ -255,7 +256,7 @@ function create_tables(Database $db)
width INTEGER NOT NULL, width INTEGER NOT NULL,
height INTEGER NOT NULL, height INTEGER NOT NULL,
posted TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, posted TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
locked BOOLEAN NOT NULL DEFAULT FALSE, locked SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N,
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT
"); ");
$db->execute("CREATE INDEX images_owner_id_idx ON images(owner_id)", []); $db->execute("CREATE INDEX images_owner_id_idx ON images(owner_id)", []);
@ -281,19 +282,13 @@ function create_tables(Database $db)
$db->execute("CREATE INDEX images_tags_tag_id_idx ON image_tags(tag_id)", []); $db->execute("CREATE INDEX images_tags_tag_id_idx ON image_tags(tag_id)", []);
$db->execute("INSERT INTO config(name, value) VALUES('db_version', 11)"); $db->execute("INSERT INTO config(name, value) VALUES('db_version', 11)");
// mysql auto-commits when creating a table, so the transaction
// is closed; other databases need to commit
if ($db->is_transaction_open()) {
$db->commit(); $db->commit();
}
} catch (PDOException $e) { } catch (PDOException $e) {
throw new InstallerException( throw new InstallerException(
"PDO Error:", "PDO Error:",
"<p>An error occurred while trying to create the database tables necessary for Shimmie.</p> "<p>An error occurred while trying to create the database tables necessary for Shimmie.</p>
<p>Please check and ensure that the database configuration options are all correct.</p> <p>Please check and ensure that the database configuration options are all correct.</p>
<p>{$e->getMessage()}</p> <p>{$e->getMessage()}</p>",
",
3 3
); );
} }

View File

@ -3,6 +3,9 @@
* Things which should be in the core API * * Things which should be in the core API *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ \* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
require_once "filetypes.php";
/** /**
* Return the unique elements of an array, case insensitively * Return the unique elements of an array, case insensitively
*/ */
@ -124,16 +127,10 @@ function list_files(string $base, string $_sub_dir=""): array
$files = []; $files = [];
$dir = opendir("$base/$_sub_dir"); $dir = opendir("$base/$_sub_dir");
if ($dir===false) {
throw new SCoreException("Unable to open directory $base/$_sub_dir");
}
try {
while ($f = readdir($dir)) { while ($f = readdir($dir)) {
$files[] = $f; $files[] = $f;
} }
} finally {
closedir($dir); closedir($dir);
}
sort($files); sort($files);
foreach ($files as $filename) { foreach ($files as $filename) {
@ -190,8 +187,8 @@ function stream_file(string $file, int $start, int $end): void
} }
} }
# http://www.php.net/manual/en/function.http-parse-headers.php#112917 if (!function_exists('http_parse_headers')) { #http://www.php.net/manual/en/function.http-parse-headers.php#112917
if (!function_exists('http_parse_headers')) {
/** /**
* #return string[] * #return string[]
*/ */
@ -222,7 +219,7 @@ if (!function_exists('http_parse_headers')) {
* HTTP Headers can sometimes be lowercase which will cause issues. * HTTP Headers can sometimes be lowercase which will cause issues.
* In cases like these, we need to make sure to check for them if the camelcase version does not exist. * In cases like these, we need to make sure to check for them if the camelcase version does not exist.
*/ */
function find_header(array $headers, string $name): ?string function findHeader(array $headers, string $name): ?string
{ {
if (!is_array($headers)) { if (!is_array($headers)) {
return null; return null;
@ -260,7 +257,7 @@ if (!function_exists('mb_strlen')) {
} }
/** @noinspection PhpUnhandledExceptionInspection */ /** @noinspection PhpUnhandledExceptionInspection */
function get_subclasses_of(string $parent) function getSubclassesOf(string $parent)
{ {
$result = []; $result = [];
foreach (get_declared_classes() as $class) { foreach (get_declared_classes() as $class) {
@ -341,26 +338,17 @@ function unparse_url($parsed_url)
return "$scheme$user$pass$host$port$path$query$fragment"; return "$scheme$user$pass$host$port$path$query$fragment";
} }
# finally in the core library starting from php8 function startsWith(string $haystack, string $needle): bool
if (!function_exists('str_starts_with')) {
function str_starts_with(string $haystack, string $needle): bool
{ {
return \strncmp($haystack, $needle, \strlen($needle)) === 0; $length = strlen($needle);
} return (substr($haystack, 0, $length) === $needle);
} }
if (!function_exists('str_ends_with')) { function endsWith(string $haystack, string $needle): bool
function str_ends_with(string $haystack, string $needle): bool
{ {
return $needle === '' || $needle === \substr($haystack, - \strlen($needle)); $length = strlen($needle);
} $start = $length * -1; //negative
} return (substr($haystack, $start) === $needle);
if (!function_exists('str_contains')) {
function str_contains(string $haystack, string $needle): bool
{
return '' === $needle || false !== strpos($haystack, $needle);
}
} }
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
@ -487,6 +475,25 @@ function clamp(?int $val, ?int $min=null, ?int $max=null): int
return $val; return $val;
} }
function xml_tag(string $name, array $attrs=[], array $children=[]): string
{
$xml = "<$name ";
foreach ($attrs as $k => $v) {
$xv = str_replace('&#039;', '&apos;', htmlspecialchars((string)$v, ENT_QUOTES));
$xml .= "$k=\"$xv\" ";
}
if (count($children) > 0) {
$xml .= ">\n";
foreach ($children as $child) {
$xml .= xml_tag($child);
}
$xml .= "</$name>\n";
} else {
$xml .= "/>\n";
}
return $xml;
}
/** /**
* Original PHP code by Chirp Internet: www.chirp.com.au * Original PHP code by Chirp Internet: www.chirp.com.au
* Please acknowledge use of this code by including this header. * Please acknowledge use of this code by including this header.
@ -556,41 +563,17 @@ function to_shorthand_int(int $int): string
return (string)$int; return (string)$int;
} }
} }
abstract class TIME_UNITS
{ const TIME_UNITS = ["s"=>60,"m"=>60,"h"=>24,"d"=>365,"y"=>PHP_INT_MAX];
public const MILLISECONDS = "ms"; function format_milliseconds(int $input): string
public const SECONDS = "s";
public const MINUTES = "m";
public const HOURS = "h";
public const DAYS = "d";
public const YEARS = "y";
public const CONVERSION = [
self::MILLISECONDS=>1000,
self::SECONDS=>60,
self::MINUTES=>60,
self::HOURS=>24,
self::DAYS=>365,
self::YEARS=>PHP_INT_MAX
];
}
function format_milliseconds(int $input, string $min_unit = TIME_UNITS::SECONDS): string
{ {
$output = ""; $output = "";
$remainder = $input; $remainder = floor($input / 1000);
$found = false; foreach (TIME_UNITS as $unit=>$conversion) {
foreach (TIME_UNITS::CONVERSION as $unit=>$conversion) {
$count = $remainder % $conversion; $count = $remainder % $conversion;
$remainder = floor($remainder / $conversion); $remainder = floor($remainder / $conversion);
if ($found||$unit==$min_unit) {
$found = true;
} else {
continue;
}
if ($count==0&&$remainder<1) { if ($count==0&&$remainder<1) {
break; break;
} }
@ -599,32 +582,6 @@ function format_milliseconds(int $input, string $min_unit = TIME_UNITS::SECONDS)
return trim($output); return trim($output);
} }
function parse_to_milliseconds(string $input): int
{
$output = 0;
$current_multiplier = 1;
if (preg_match('/^([0-9]+)$/i', $input, $match)) {
// If just a number, then we treat it as milliseconds
$length = $match[0];
if (is_numeric($length)) {
$length = floatval($length);
$output += $length;
}
} else {
foreach (TIME_UNITS::CONVERSION as $unit=>$conversion) {
if (preg_match('/([0-9]+)'.$unit.'/i', $input, $match)) {
$length = $match[1];
if (is_numeric($length)) {
$length = floatval($length);
$output += $length * $current_multiplier;
}
}
$current_multiplier *= $conversion;
}
}
return intval($output);
}
/** /**
* Turn a date into a time, a date, an "X minutes ago...", etc * Turn a date into a time, a date, an "X minutes ago...", etc

View File

@ -4,6 +4,39 @@
* be included right at the very start of index.php and tests/bootstrap.php * be included right at the very start of index.php and tests/bootstrap.php
*/ */
$min_php = "7.3";
if (version_compare(phpversion(), $min_php, ">=") === false) {
print "
Shimmie does not support versions of PHP lower than $min_php
(PHP reports that it is version ".phpversion().").
If your web host is running an older version, they are dangerously out of
date and you should plan on moving elsewhere.
";
exit;
}
# ini_set('zend.assertions', '1'); // generate assertions
ini_set('assert.exception', '1'); // throw exceptions when failed
set_error_handler(function ($errNo, $errStr) {
// Should we turn ALL notices into errors? PHP allows a lot of
// terrible things to happen by default...
if (strpos($errStr, 'Use of undefined constant ') === 0) {
throw new Exception("PHP Error#$errNo: $errStr");
} else {
return false;
}
});
ob_start();
if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') {
if (isset($_SERVER['REMOTE_ADDR'])) {
die("CLI with remote addr? Confused, not taking the risk.");
}
$_SERVER['REMOTE_ADDR'] = "0.0.0.0";
$_SERVER['HTTP_HOST'] = "<cli command>";
}
function die_nicely($title, $body, $code=0) function die_nicely($title, $body, $code=0)
{ {
print("<!DOCTYPE html> print("<!DOCTYPE html>
@ -28,33 +61,3 @@ function die_nicely($title, $body, $code=0)
} }
exit($code); exit($code);
} }
$min_php = "7.3";
if (version_compare(phpversion(), $min_php, ">=") === false) {
die_nicely("Not Supported", "
Shimmie does not support versions of PHP lower than $min_php
(PHP reports that it is version ".phpversion().").
", 1);
}
# ini_set('zend.assertions', '1'); // generate assertions
ini_set('assert.exception', '1'); // throw exceptions when failed
set_error_handler(function ($errNo, $errStr) {
// Should we turn ALL notices into errors? PHP allows a lot of
// terrible things to happen by default...
if (str_starts_with($errStr, 'Use of undefined constant ')) {
throw new Exception("PHP Error#$errNo: $errStr");
} else {
return false;
}
});
ob_start();
if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') {
if (isset($_SERVER['REMOTE_ADDR'])) {
die("CLI with remote addr? Confused, not taking the risk.");
}
$_SERVER['REMOTE_ADDR'] = "0.0.0.0";
$_SERVER['HTTP_HOST'] = "cli-command";
}

View File

@ -35,7 +35,7 @@ function _set_event_listeners(): void
global $_shm_event_listeners; global $_shm_event_listeners;
$_shm_event_listeners = []; $_shm_event_listeners = [];
foreach (get_subclasses_of("Extension") as $class) { foreach (getSubclassesOf("Extension") as $class) {
/** @var Extension $extension */ /** @var Extension $extension */
$extension = new $class(); $extension = new $class();
@ -61,7 +61,7 @@ function _dump_event_listeners(array $event_listeners, string $path): void
{ {
$p = "<"."?php\n"; $p = "<"."?php\n";
foreach (get_subclasses_of("Extension") as $class) { foreach (getSubclassesOf("Extension") as $class) {
$p .= "\$$class = new $class(); "; $p .= "\$$class = new $class(); ";
} }

View File

@ -9,13 +9,13 @@ class PolyfillsTest extends TestCase
public function test_html_escape() public function test_html_escape()
{ {
$this->assertEquals( $this->assertEquals(
"Foo &amp; &lt;main&gt;", html_escape("Foo & <waffles>"),
html_escape("Foo & <main>") "Foo &amp; &lt;waffles&gt;"
); );
$this->assertEquals( $this->assertEquals(
"Foo & <main>", html_unescape("Foo &amp; &lt;waffles&gt;"),
html_unescape("Foo &amp; &lt;main&gt;") "Foo & <waffles>"
); );
$x = "Foo &amp; &lt;waffles&gt;"; $x = "Foo &amp; &lt;waffles&gt;";
@ -24,17 +24,17 @@ class PolyfillsTest extends TestCase
public function test_int_escape() public function test_int_escape()
{ {
$this->assertEquals(0, int_escape("")); $this->assertEquals(int_escape(""), 0);
$this->assertEquals(1, int_escape("1")); $this->assertEquals(int_escape("1"), 1);
$this->assertEquals(-1, int_escape("-1")); $this->assertEquals(int_escape("-1"), -1);
$this->assertEquals(-1, int_escape("-1.5")); $this->assertEquals(int_escape("-1.5"), -1);
$this->assertEquals(0, int_escape(null)); $this->assertEquals(int_escape(null), 0);
} }
public function test_url_escape() public function test_url_escape()
{ {
$this->assertEquals("%5E%5Co%2F%5E", url_escape("^\o/^")); $this->assertEquals(url_escape("^\o/^"), "%5E%5Co%2F%5E");
$this->assertEquals("", url_escape(null)); $this->assertEquals(url_escape(null), "");
} }
public function test_bool_escape() public function test_bool_escape()
@ -69,33 +69,41 @@ class PolyfillsTest extends TestCase
public function test_clamp() public function test_clamp()
{ {
$this->assertEquals(5, clamp(0, 5, 10)); $this->assertEquals(clamp(0, 5, 10), 5);
$this->assertEquals(5, clamp(5, 5, 10)); $this->assertEquals(clamp(5, 5, 10), 5);
$this->assertEquals(7, clamp(7, 5, 10)); $this->assertEquals(clamp(7, 5, 10), 7);
$this->assertEquals(10, clamp(10, 5, 10)); $this->assertEquals(clamp(10, 5, 10), 10);
$this->assertEquals(10, clamp(15, 5, 10)); $this->assertEquals(clamp(15, 5, 10), 10);
}
public function test_xml_tag()
{
$this->assertEquals(
"<test foo=\"bar\" >\n<cake />\n</test>\n",
xml_tag("test", ["foo"=>"bar"], ["cake"])
);
} }
public function test_truncate() public function test_truncate()
{ {
$this->assertEquals("test words", truncate("test words", 10)); $this->assertEquals(truncate("test words", 10), "test words");
$this->assertEquals("test...", truncate("test...", 9)); $this->assertEquals(truncate("test...", 9), "test...");
$this->assertEquals("test...", truncate("test...", 6)); $this->assertEquals(truncate("test...", 6), "test...");
$this->assertEquals("te...", truncate("te...", 2)); $this->assertEquals(truncate("te...", 2), "te...");
} }
public function test_to_shorthand_int() public function test_to_shorthand_int()
{ {
$this->assertEquals("1.1GB", to_shorthand_int(1231231231)); $this->assertEquals(to_shorthand_int(1231231231), "1.1GB");
$this->assertEquals("2", to_shorthand_int(2)); $this->assertEquals(to_shorthand_int(2), "2");
} }
public function test_parse_shorthand_int() public function test_parse_shorthand_int()
{ {
$this->assertEquals(-1, parse_shorthand_int("foo")); $this->assertEquals(parse_shorthand_int("foo"), -1);
$this->assertEquals(33554432, parse_shorthand_int("32M")); $this->assertEquals(parse_shorthand_int("32M"), 33554432);
$this->assertEquals(44441, parse_shorthand_int("43.4KB")); $this->assertEquals(parse_shorthand_int("43.4KB"), 44441);
$this->assertEquals(1231231231, parse_shorthand_int("1231231231")); $this->assertEquals(parse_shorthand_int("1231231231"), 1231231231);
} }
public function test_format_milliseconds() public function test_format_milliseconds()
@ -105,13 +113,6 @@ class PolyfillsTest extends TestCase
$this->assertEquals("1y 213d 16h 53m 20s", format_milliseconds(50000000000)); $this->assertEquals("1y 213d 16h 53m 20s", format_milliseconds(50000000000));
} }
public function test_parse_to_milliseconds()
{
$this->assertEquals(10, parse_to_milliseconds("10"));
$this->assertEquals(5000, parse_to_milliseconds("5s"));
$this->assertEquals(50000000000, parse_to_milliseconds("1y 213d 16h 53m 20s"));
}
public function test_autodate() public function test_autodate()
{ {
$this->assertEquals( $this->assertEquals(

View File

@ -43,13 +43,13 @@ class UrlsTest extends TestCase
{ {
// relative to shimmie install // relative to shimmie install
$this->assertEquals( $this->assertEquals(
"http://cli-command/test/foo", "http://<cli command>/test/foo",
make_http("foo") make_http("foo")
); );
// relative to web server // relative to web server
$this->assertEquals( $this->assertEquals(
"http://cli-command/foo", "http://<cli command>/foo",
make_http("/foo") make_http("/foo")
); );

View File

@ -79,7 +79,7 @@ function modify_url(string $url, array $changes): string
*/ */
function make_http(string $link): string function make_http(string $link): string
{ {
if (str_contains($link, "://")) { if (strpos($link, "://") > 0) {
return $link; return $link;
} }
@ -105,7 +105,7 @@ function referer_or(string $dest, ?array $blacklist=null): string
} }
if ($blacklist) { if ($blacklist) {
foreach ($blacklist as $b) { foreach ($blacklist as $b) {
if (str_contains($_SERVER['HTTP_REFERER'], $b)) { if (strstr($_SERVER['HTTP_REFERER'], $b)) {
return $dest; return $dest;
} }
} }

View File

@ -118,7 +118,7 @@ class User
$my_user = User::by_name($name); $my_user = User::by_name($name);
// If user tried to log in as "foo bar" and failed, try "foo_bar" // If user tried to log in as "foo bar" and failed, try "foo_bar"
if (!$my_user && str_contains($name, " ")) { if (!$my_user && strpos($name, " ") !== false) {
$my_user = User::by_name(str_replace(" ", "_", $name)); $my_user = User::by_name(str_replace(" ", "_", $name));
} }
@ -163,7 +163,7 @@ class User
public function set_class(string $class): void public function set_class(string $class): void
{ {
global $database; global $database;
$database->execute("UPDATE users SET class=:class WHERE id=:id", ["class"=>$class, "id"=>$this->id]); $database->Execute("UPDATE users SET class=:class WHERE id=:id", ["class"=>$class, "id"=>$this->id]);
log_info("core-user", 'Set class for '.$this->name.' to '.$class); log_info("core-user", 'Set class for '.$this->name.' to '.$class);
} }
@ -175,7 +175,7 @@ class User
} }
$old_name = $this->name; $old_name = $this->name;
$this->name = $name; $this->name = $name;
$database->execute("UPDATE users SET name=:name WHERE id=:id", ["name"=>$this->name, "id"=>$this->id]); $database->Execute("UPDATE users SET name=:name WHERE id=:id", ["name"=>$this->name, "id"=>$this->id]);
log_info("core-user", "Changed username for {$old_name} to {$this->name}"); log_info("core-user", "Changed username for {$old_name} to {$this->name}");
} }
@ -185,7 +185,7 @@ class User
$hash = password_hash($password, PASSWORD_BCRYPT); $hash = password_hash($password, PASSWORD_BCRYPT);
if (is_string($hash)) { if (is_string($hash)) {
$this->passhash = $hash; $this->passhash = $hash;
$database->execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash"=>$this->passhash, "id"=>$this->id]); $database->Execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash"=>$this->passhash, "id"=>$this->id]);
log_info("core-user", 'Set password for '.$this->name); log_info("core-user", 'Set password for '.$this->name);
} else { } else {
throw new SCoreException("Failed to hash password"); throw new SCoreException("Failed to hash password");
@ -195,7 +195,7 @@ class User
public function set_email(string $address): void public function set_email(string $address): void
{ {
global $database; global $database;
$database->execute("UPDATE users SET email=:email WHERE id=:id", ["email"=>$address, "id"=>$this->id]); $database->Execute("UPDATE users SET email=:email WHERE id=:id", ["email"=>$address, "id"=>$this->id]);
log_info("core-user", 'Set email for '.$this->name); log_info("core-user", 'Set email for '.$this->name);
} }

View File

@ -20,6 +20,13 @@ use function MicroHTML\TD;
const DATA_DIR = "data"; const DATA_DIR = "data";
function mtimefile(string $file): string
{
$data_href = get_base_href();
$mtime = filemtime($file);
return "$data_href/$file?$mtime";
}
function get_theme(): string function get_theme(): string
{ {
global $config; global $config;
@ -39,18 +46,18 @@ function contact_link(): ?string
} }
if ( if (
str_starts_with($text, "http:") || startsWith($text, "http:") ||
str_starts_with($text, "https:") || startsWith($text, "https:") ||
str_starts_with($text, "mailto:") startsWith($text, "mailto:")
) { ) {
return $text; return $text;
} }
if (str_contains($text, "@")) { if (strpos($text, "@")) {
return "mailto:$text"; return "mailto:$text";
} }
if (str_contains($text, "/")) { if (strpos($text, "/")) {
return "http://$text"; return "http://$text";
} }
@ -252,11 +259,11 @@ function load_balance_url(string $tmpl, string $hash, int $n=0): string
return $tmpl; return $tmpl;
} }
function fetch_url(string $url, string $mfile): ?array function transload(string $url, string $mfile): ?array
{ {
global $config; global $config;
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "curl" && function_exists("curl_init")) { if ($config->get_string("transload_engine") === "curl" && function_exists("curl_init")) {
$ch = curl_init($url); $ch = curl_init($url);
$fp = fopen($mfile, "w"); $fp = fopen($mfile, "w");
@ -269,7 +276,8 @@ function fetch_url(string $url, string $mfile): ?array
$response = curl_exec($ch); $response = curl_exec($ch);
if ($response === false) { if ($response === false) {
return null; log_warning("core-util", "Failed to transload $url");
throw new SCoreException("Failed to fetch $url");
} }
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
@ -283,7 +291,7 @@ function fetch_url(string $url, string $mfile): ?array
return $headers; return $headers;
} }
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "wget") { if ($config->get_string("transload_engine") === "wget") {
$s_url = escapeshellarg($url); $s_url = escapeshellarg($url);
$s_mfile = escapeshellarg($mfile); $s_mfile = escapeshellarg($mfile);
system("wget --no-check-certificate $s_url --output-document=$s_mfile"); system("wget --no-check-certificate $s_url --output-document=$s_mfile");
@ -291,14 +299,14 @@ function fetch_url(string $url, string $mfile): ?array
return file_exists($mfile) ? ["ok"=>"true"] : null; return file_exists($mfile) ? ["ok"=>"true"] : null;
} }
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "fopen") { if ($config->get_string("transload_engine") === "fopen") {
$fp_in = @fopen($url, "r"); $fp_in = @fopen($url, "r");
$fp_out = fopen($mfile, "w"); $fp_out = fopen($mfile, "w");
if (!$fp_in || !$fp_out) { if (!$fp_in || !$fp_out) {
return null; return null;
} }
$length = 0; $length = 0;
while (!feof($fp_in) && $length <= $config->get_int(UploadConfig::SIZE)) { while (!feof($fp_in) && $length <= $config->get_int('upload_size')) {
$data = fread($fp_in, 8192); $data = fread($fp_in, 8192);
$length += strlen($data); $length += strlen($data);
fwrite($fp_out, $data); fwrite($fp_out, $data);
@ -340,7 +348,7 @@ function path_to_tags(string $path): string
// which is for inheriting to tags on the subfolder // which is for inheriting to tags on the subfolder
$category_to_inherit = $tag; $category_to_inherit = $tag;
} else { } else {
if ($category!="" && !str_contains($tag, ":")) { if ($category!=""&&strpos($tag, ":") === false) {
// This indicates that category inheritance is active, // This indicates that category inheritance is active,
// and we've encountered a tag that does not specify a category. // and we've encountered a tag that does not specify a category.
// So we attach the inherited category to the tag. // So we attach the inherited category to the tag.

View File

@ -15,9 +15,9 @@ class AdminPageInfo extends ExtensionInfo
<p>Lowercase all tags: <p>Lowercase all tags:
<br>Set all tags to lowercase for consistency <br>Set all tags to lowercase for consistency
<p>Recount tag use: <p>Recount tag use:
<br>If the counts of posts per tag get messed up somehow, this will reset them, and remove any unused tags <br>If the counts of images per tag get messed up somehow, this will reset them, and remove any unused tags
<p>Database dump: <p>Database dump:
<br>Download the contents of the database in plain text format, useful for backups. <br>Download the contents of the database in plain text format, useful for backups.
<p>Post dump: <p>Image dump:
<br>Download all the posts as a .zip file (Requires ZipArchive)"; <br>Download all the images as a .zip file (Requires ZipArchive)";
} }

View File

@ -37,7 +37,7 @@ class AdminPage extends Extension
public function onPageRequest(PageRequestEvent $event) public function onPageRequest(PageRequestEvent $event)
{ {
global $database, $page, $user; global $page, $user;
if ($event->page_matches("admin")) { if ($event->page_matches("admin")) {
if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) { if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) {
@ -52,7 +52,6 @@ class AdminPage extends Extension
if ($user->check_auth_token()) { if ($user->check_auth_token()) {
log_info("admin", "Util: $action"); log_info("admin", "Util: $action");
set_time_limit(0); set_time_limit(0);
$database->set_timeout(300000);
send_event($aae); send_event($aae);
} }
@ -81,10 +80,8 @@ class AdminPage extends Extension
} }
if ($event->cmd == "get-page") { if ($event->cmd == "get-page") {
global $page; global $page;
$_SERVER['REQUEST_URI'] = $event->args[0];
if (isset($event->args[1])) { if (isset($event->args[1])) {
parse_str($event->args[1], $_GET); parse_str($event->args[1], $_GET);
$_SERVER['REQUEST_URI'] .= "?" . $event->args[1];
} }
send_event(new PageRequestEvent($event->args[0])); send_event(new PageRequestEvent($event->args[0]));
$page->display(); $page->display();
@ -106,7 +103,7 @@ class AdminPage extends Extension
$uid = $event->args[0]; $uid = $event->args[0];
$image = Image::by_id_or_hash($uid); $image = Image::by_id_or_hash($uid);
if ($image) { if ($image) {
send_event(new ThumbnailGenerationEvent($image->hash, $image->get_mime(), true)); send_event(new ThumbnailGenerationEvent($image->hash, $image->ext, true));
} else { } else {
print("No post with ID '$uid'\n"); print("No post with ID '$uid'\n");
} }
@ -183,14 +180,14 @@ class AdminPage extends Extension
private function recount_tag_use() private function recount_tag_use()
{ {
global $database; global $database;
$database->execute(" $database->Execute("
UPDATE tags UPDATE tags
SET count = COALESCE( SET count = COALESCE(
(SELECT COUNT(image_id) FROM image_tags WHERE tag_id=tags.id GROUP BY tag_id), (SELECT COUNT(image_id) FROM image_tags WHERE tag_id=tags.id GROUP BY tag_id),
0 0
) )
"); ");
$database->execute("DELETE FROM tags WHERE count=0"); $database->Execute("DELETE FROM tags WHERE count=0");
log_warning("admin", "Re-counted tags", "Re-counted tags"); log_warning("admin", "Re-counted tags", "Re-counted tags");
return true; return true;
} }

View File

@ -28,14 +28,14 @@ class AdminPageTest extends ShimmiePHPUnitTestCase
// Validate problem // Validate problem
$page = $this->get_page("post/view/$image_id_1"); $page = $this->get_page("post/view/$image_id_1");
$this->assertEquals("Post $image_id_1: TeStCase$ts", $page->title); $this->assertEquals("Image $image_id_1: TeStCase$ts", $page->title);
// Fix // Fix
send_event(new AdminActionEvent('lowercase_all_tags')); send_event(new AdminActionEvent('lowercase_all_tags'));
// Validate fix // Validate fix
$this->get_page("post/view/$image_id_1"); $this->get_page("post/view/$image_id_1");
$this->assert_title("Post $image_id_1: testcase$ts"); $this->assert_title("Image $image_id_1: testcase$ts");
// Change // Change
$_POST["tag"] = "TestCase$ts"; $_POST["tag"] = "TestCase$ts";
@ -43,7 +43,7 @@ class AdminPageTest extends ShimmiePHPUnitTestCase
// Validate change // Validate change
$this->get_page("post/view/$image_id_1"); $this->get_page("post/view/$image_id_1");
$this->assert_title("Post $image_id_1: TestCase$ts"); $this->assert_title("Image $image_id_1: TestCase$ts");
} }
# FIXME: make sure the admin tools actually work # FIXME: make sure the admin tools actually work

View File

@ -96,7 +96,7 @@ class AliasEditor extends Extension
$this->theme->display_aliases($t->table($t->query()), $t->paginator()); $this->theme->display_aliases($t->table($t->query()), $t->paginator());
} elseif ($event->get_arg(0) == "export") { } elseif ($event->get_arg(0) == "export") {
$page->set_mode(PageMode::DATA); $page->set_mode(PageMode::DATA);
$page->set_mime(MimeType::CSV); $page->set_type(MIME_TYPE_CSV);
$page->set_filename("aliases.csv"); $page->set_filename("aliases.csv");
$page->set_data($this->get_alias_csv($database)); $page->set_data($this->get_alias_csv($database));
} elseif ($event->get_arg(0) == "import") { } elseif ($event->get_arg(0) == "import") {

View File

@ -36,7 +36,7 @@ class AliasEditorTest extends ShimmiePHPUnitTestCase
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1"); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
$this->get_page("post/view/$image_id"); # check that the tag has been replaced $this->get_page("post/view/$image_id"); # check that the tag has been replaced
$this->assert_title("Post $image_id: test2"); $this->assert_title("Image $image_id: test2");
$this->get_page("post/list/test1/1"); # searching for an alias should find the master tag $this->get_page("post/list/test1/1"); # searching for an alias should find the master tag
$this->assert_response(302); $this->assert_response(302);
$this->get_page("post/list/test2/1"); # check that searching for the main tag still works $this->get_page("post/list/test2/1"); # check that searching for the main tag still works
@ -67,13 +67,13 @@ class AliasEditorTest extends ShimmiePHPUnitTestCase
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "onetag"); $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "onetag");
$this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases $this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases
$this->assert_title("multi tag"); $this->assert_title("multi tag");
$this->assert_no_text("No Posts Found"); $this->assert_no_text("No Images Found");
$this->get_page("post/list/multi/1"); $this->get_page("post/list/multi/1");
$this->assert_title("multi"); $this->assert_title("multi");
$this->assert_no_text("No Posts Found"); $this->assert_no_text("No Images Found");
$this->get_page("post/list/multi tag/1"); $this->get_page("post/list/multi tag/1");
$this->assert_title("multi tag"); $this->assert_title("multi tag");
$this->assert_no_text("No Posts Found"); $this->assert_no_text("No Images Found");
$this->delete_image($image_id_1); $this->delete_image($image_id_1);
$this->delete_image($image_id_2); $this->delete_image($image_id_2);

View File

@ -33,7 +33,7 @@ class Approval extends Extension
$image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null; $image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
} }
if (empty($image_id)) { if (empty($image_id)) {
throw new SCoreException("Can not approve post: No valid Post ID given."); throw new SCoreException("Can not approve image: No valid Image ID given.");
} }
self::approve_image($image_id); self::approve_image($image_id);
@ -48,7 +48,7 @@ class Approval extends Extension
$image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null; $image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
} }
if (empty($image_id)) { if (empty($image_id)) {
throw new SCoreException("Can not disapprove image: No valid Post ID given."); throw new SCoreException("Can not disapprove image: No valid Image ID given.");
} }
self::disapprove_image($image_id); self::disapprove_image($image_id);
@ -99,9 +99,9 @@ class Approval extends Extension
public function onDisplayingImage(DisplayingImageEvent $event) public function onDisplayingImage(DisplayingImageEvent $event)
{ {
global $page; global $user, $page, $config;
if (!$this->check_permissions(($event->image))) { if ($config->get_bool(ApprovalConfig::IMAGES) && $event->image->approved===false && !$user->can(Permissions::APPROVE_IMAGE)) {
$page->set_mode(PageMode::REDIRECT); $page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/list")); $page->set_redirect(make_link("post/list"));
} }
@ -121,13 +121,13 @@ class Approval extends Extension
const SEARCH_REGEXP = "/^approved:(yes|no)/"; const SEARCH_REGEXP = "/^approved:(yes|no)/";
public function onSearchTermParse(SearchTermParseEvent $event) public function onSearchTermParse(SearchTermParseEvent $event)
{ {
global $user, $config; global $user, $database, $config;
if ($config->get_bool(ApprovalConfig::IMAGES)) { if ($config->get_bool(ApprovalConfig::IMAGES)) {
$matches = []; $matches = [];
if (is_null($event->term) && $this->no_approval_query($event->context)) { if (is_null($event->term) && $this->no_approval_query($event->context)) {
$event->add_querylet(new Querylet("approved = :true", ["true"=>true])); $event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
} }
if (is_null($event->term)) { if (is_null($event->term)) {
@ -135,9 +135,9 @@ class Approval extends Extension
} }
if (preg_match(self::SEARCH_REGEXP, strtolower($event->term), $matches)) { if (preg_match(self::SEARCH_REGEXP, strtolower($event->term), $matches)) {
if ($user->can(Permissions::APPROVE_IMAGE) && $matches[1] == "no") { if ($user->can(Permissions::APPROVE_IMAGE) && $matches[1] == "no") {
$event->add_querylet(new Querylet("approved != :true", ["true"=>true])); $event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_N ")));
} else { } else {
$event->add_querylet(new Querylet("approved = :true", ["true"=>true])); $event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
} }
} }
} }
@ -187,26 +187,6 @@ class Approval extends Extension
); );
} }
private function check_permissions(Image $image): bool
{
global $user, $config;
if ($config->get_bool(ApprovalConfig::IMAGES) && $image->approved===false && !$user->can(Permissions::APPROVE_IMAGE)) {
return false;
}
return true;
}
public function onImageDownloading(ImageDownloadingEvent $event)
{
/**
* Deny images upon insufficient permissions.
**/
if (!$this->check_permissions($event->image)) {
throw new SCoreException("Access denied");
}
}
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
{ {
global $user, $config; global $user, $config;
@ -261,15 +241,15 @@ class Approval extends Extension
global $database; global $database;
if ($this->get_version(ApprovalConfig::VERSION) < 1) { if ($this->get_version(ApprovalConfig::VERSION) < 1) {
$database->execute("ALTER TABLE images ADD COLUMN approved BOOLEAN NOT NULL DEFAULT FALSE"); $database->execute($database->scoreql_to_sql(
$database->execute("ALTER TABLE images ADD COLUMN approved_by_id INTEGER NULL"); "ALTER TABLE images ADD COLUMN approved SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N"
$database->execute("CREATE INDEX images_approved_idx ON images(approved)"); ));
$this->set_version(ApprovalConfig::VERSION, 2); $database->execute(
} "ALTER TABLE images ADD COLUMN approved_by_id INTEGER NULL"
);
if ($this->get_version(ApprovalConfig::VERSION) < 2) { $database->execute("CREATE INDEX images_approved_idx ON images(approved)");
$database->standardise_boolean("images", "approved"); $this->set_version(ApprovalConfig::VERSION, 1);
$this->set_version(ApprovalConfig::VERSION, 2);
} }
} }
} }

View File

@ -27,14 +27,14 @@ class ApprovalTheme extends Themelet
public function get_help_html() public function get_help_html()
{ {
return '<p>Search for posts that are approved/not approved.</p> return '<p>Search for images that are approved/not approved.</p>
<div class="command_example"> <div class="command_example">
<pre>approved:yes</pre> <pre>approved:yes</pre>
<p>Returns posts that have been approved.</p> <p>Returns images that have been approved.</p>
</div> </div>
<div class="command_example"> <div class="command_example">
<pre>approved:no</pre> <pre>approved:no</pre>
<p>Returns posts that have not been approved.</p> <p>Returns images that have not been approved.</p>
</div> </div>
'; ';
} }
@ -42,7 +42,7 @@ class ApprovalTheme extends Themelet
public function display_admin_block(SetupBuildingEvent $event) public function display_admin_block(SetupBuildingEvent $event)
{ {
$sb = new SetupBlock("Approval"); $sb = new SetupBlock("Approval");
$sb->add_bool_option(ApprovalConfig::IMAGES, "Posts: "); $sb->add_bool_option(ApprovalConfig::IMAGES, "Images: ");
$event->panel->add_block($sb); $event->panel->add_block($sb);
} }
@ -52,9 +52,9 @@ class ApprovalTheme extends Themelet
$html = (string)SHM_SIMPLE_FORM( $html = (string)SHM_SIMPLE_FORM(
"admin/approval", "admin/approval",
BUTTON(["name"=>'approval_action', "value"=>'approve_all'], "Approve All Posts"), BUTTON(["name"=>'approval_action', "value"=>'approve_all'], "Approve All Images"),
BR(), BR(),
BUTTON(["name"=>'approval_action', "value"=>'disapprove_all'], "Disapprove All Posts"), BUTTON(["name"=>'approval_action', "value"=>'disapprove_all'], "Disapprove All Images"),
); );
$page->add_block(new Block("Approval", $html)); $page->add_block(new Block("Approval", $html));
} }

View File

@ -553,7 +553,7 @@ class Artists extends Extension
$urlsAsString = $inputs["urls"]; $urlsAsString = $inputs["urls"];
$urlsIDsAsString = $inputs["urlsIDs"]; $urlsIDsAsString = $inputs["urlsIDs"];
if (str_contains($name, " ")) { if (strpos($name, " ")) {
return; return;
} }
@ -695,7 +695,7 @@ class Artists extends Extension
]); ]);
$name = $inputs["name"]; $name = $inputs["name"];
if (str_contains($name, " ")) { if (strpos($name, " ")) {
return -1; return -1;
} }

View File

@ -408,7 +408,7 @@ class ArtistsTheme extends Themelet
'</span>'; '</span>';
} }
$page->add_block(new Block("Artist Posts", $artist_images, "main", 20)); $page->add_block(new Block("Artist Images", $artist_images, "main", 20));
} }
private function render_aliases(array $aliases, bool $userIsLogged, bool $userIsAdmin): string private function render_aliases(array $aliases, bool $userIsLogged, bool $userIsAdmin): string
@ -548,10 +548,10 @@ class ArtistsTheme extends Themelet
public function get_help_html() public function get_help_html()
{ {
return '<p>Search for posts with a particular artist.</p> return '<p>Search for images with a particular artist.</p>
<div class="command_example"> <div class="command_example">
<pre>artist=leonardo</pre> <pre>artist=leonardo</pre>
<p>Returns posts with the artist "leonardo".</p> <p>Returns images with the artist "leonardo".</p>
</div> </div>
'; ';
} }

View File

@ -102,7 +102,7 @@ class AutoTagger extends Extension
$this->theme->display_auto_tagtable($t->table($t->query()), $t->paginator()); $this->theme->display_auto_tagtable($t->table($t->query()), $t->paginator());
} elseif ($event->get_arg(0) == "export") { } elseif ($event->get_arg(0) == "export") {
$page->set_mode(PageMode::DATA); $page->set_mode(PageMode::DATA);
$page->set_mime(MimeType::CSV); $page->set_type(MIME_TYPE_CSV);
$page->set_filename("auto_tag.csv"); $page->set_filename("auto_tag.csv");
$page->set_data($this->get_auto_tag_csv($database)); $page->set_data($this->get_auto_tag_csv($database));
} elseif ($event->get_arg(0) == "import") { } elseif ($event->get_arg(0) == "import") {
@ -314,10 +314,14 @@ class AutoTagger extends Extension
$tags_mixed = array_merge($tags_mixed, $new_tags); $tags_mixed = array_merge($tags_mixed, $new_tags);
} }
return array_intersect_key( $results = array_intersect_key(
$tags_mixed, $tags_mixed,
array_unique(array_map('strtolower', $tags_mixed)) array_unique(array_map('strtolower', $tags_mixed))
); );
return $results;
} }
/** /**

View File

@ -37,14 +37,14 @@ class AutoTaggerTest extends ShimmiePHPUnitTestCase
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1"); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
$this->get_page("post/view/$image_id"); # check that the tag has been replaced $this->get_page("post/view/$image_id"); # check that the tag has been replaced
$this->assert_title("Post $image_id: test1 test2"); $this->assert_title("Image $image_id: test1 test2");
$this->delete_image($image_id); $this->delete_image($image_id);
send_event(new AddAutoTagEvent("test2", "test3")); send_event(new AddAutoTagEvent("test2", "test3"));
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1"); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
$this->get_page("post/view/$image_id"); # check that the tag has been replaced $this->get_page("post/view/$image_id"); # check that the tag has been replaced
$this->assert_title("Post $image_id: test1 test2 test3"); $this->assert_title("Image $image_id: test1 test2 test3");
$this->delete_image($image_id); $this->delete_image($image_id);
send_event(new DeleteAutoTagEvent("test1")); send_event(new DeleteAutoTagEvent("test1"));

View File

@ -12,59 +12,37 @@ class AutoComplete extends Extension
public function onPageRequest(PageRequestEvent $event) public function onPageRequest(PageRequestEvent $event)
{ {
global $page; global $cache, $page, $database;
if ($event->page_matches("api/internal/autocomplete")) { if ($event->page_matches("api/internal/autocomplete")) {
$limit = $_GET["limit"] ?? 0; if (!isset($_GET["s"])) {
$s = $_GET["s"] ?? null; return;
}
$res = $this->complete($s, $limit);
$page->set_mode(PageMode::DATA); $page->set_mode(PageMode::DATA);
$page->set_mime(MimeType::JSON); $page->set_type(MIME_TYPE_JSON);
$page->set_data(json_encode($res));
}
$this->theme->build_autocomplete($page); $s = strtolower($_GET["s"]);
}
public function onApiRequest(ApiRequestEvent $event)
{
if ($event->method == "autocomplete") {
$event->result = $this->complete(
$event->params->search ?? "",
$event->params->limit ?? 0,
);
}
}
private function complete(string $search, int $limit): array
{
global $cache, $database;
if (!$search) {
return [];
}
$search = strtolower($search);
if ( if (
$search == '' || $s == '' ||
$search[0] == '_' || $s[0] == '_' ||
$search[0] == '%' || $s[0] == '%' ||
strlen($search) > 32 strlen($s) > 32
) { ) {
return []; $page->set_data("{}");
return;
} }
$cache_key = "autocomplete-$search"; //$limit = 0;
$cache_key = "autocomplete-$s";
$limitSQL = ""; $limitSQL = "";
$search = str_replace('_', '\_', $search); $s = str_replace('_', '\_', $s);
$search = str_replace('%', '\%', $search); $s = str_replace('%', '\%', $s);
$SQLarr = ["search"=>"$search%"]; #, "cat_search"=>"%:$search%"]; $SQLarr = ["search"=>"$s%"]; #, "cat_search"=>"%:$s%"];
if ($limit !== 0) { if (isset($_GET["limit"]) && $_GET["limit"] !== 0) {
$limitSQL = "LIMIT :limit"; $limitSQL = "LIMIT :limit";
$SQLarr['limit'] = $limit; $SQLarr['limit'] = $_GET["limit"];
$cache_key .= "-" . $limit; $cache_key .= "-" . $_GET["limit"];
} }
$res = $cache->get($cache_key); $res = $cache->get($cache_key);
@ -77,13 +55,15 @@ class AutoComplete extends Extension
-- OR LOWER(tag) LIKE LOWER(:cat_search) -- OR LOWER(tag) LIKE LOWER(:cat_search)
AND count > 0 AND count > 0
ORDER BY count DESC ORDER BY count DESC
$limitSQL $limitSQL",
",
$SQLarr $SQLarr
); );
$cache->set($cache_key, $res, 600); $cache->set($cache_key, $res, 600);
} }
return $res; $page->set_data(json_encode($res));
}
$this->theme->build_autocomplete($page);
} }
} }

View File

@ -87,7 +87,7 @@ xanax
} }
} else { } else {
// other words are literal // other words are literal
if (str_contains($comment, $word)) { if (strpos($comment, $word) !== false) {
throw $ex; throw $ex;
} }
} }

View File

@ -8,7 +8,7 @@ class BanWordsTest extends ShimmiePHPUnitTestCase
send_event(new CommentPostingEvent($image_id, $user, $words)); send_event(new CommentPostingEvent($image_id, $user, $words));
$this->fail("Exception not thrown"); $this->fail("Exception not thrown");
} catch (CommentPostingException $e) { } catch (CommentPostingException $e) {
$this->assertEquals("Comment contains banned terms", $e->getMessage()); $this->assertEquals($e->getMessage(), "Comment contains banned terms");
} }
} }

View File

@ -27,6 +27,6 @@ class BBCodeInfo extends ExtensionInfo
<li>[[wiki article|with some text]] <li>[[wiki article|with some text]]
<li>[quote]text[/quote] <li>[quote]text[/quote]
<li>[quote=Username]text[/quote] <li>[quote=Username]text[/quote]
<li>&gt;&gt;123 (link to post #123) <li>&gt;&gt;123 (link to image #123)
</ul>"; </ul>";
} }

View File

@ -4,60 +4,60 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
public function testBasics() public function testBasics()
{ {
$this->assertEquals( $this->assertEquals(
"<b>bold</b><i>italic</i>", $this->filter("[b]bold[/b][i]italic[/i]"),
$this->filter("[b]bold[/b][i]italic[/i]") "<b>bold</b><i>italic</i>"
); );
} }
public function testStacking() public function testStacking()
{ {
$this->assertEquals( $this->assertEquals(
"<b>B</b><i>I</i><b>B</b>", $this->filter("[b]B[/b][i]I[/i][b]B[/b]"),
$this->filter("[b]B[/b][i]I[/i][b]B[/b]") "<b>B</b><i>I</i><b>B</b>"
); );
$this->assertEquals( $this->assertEquals(
"<b>bold<i>bolditalic</i>bold</b>", $this->filter("[b]bold[i]bolditalic[/i]bold[/b]"),
$this->filter("[b]bold[i]bolditalic[/i]bold[/b]") "<b>bold<i>bolditalic</i>bold</b>"
); );
} }
public function testFailure() public function testFailure()
{ {
$this->assertEquals( $this->assertEquals(
"[b]bold[i]italic", $this->filter("[b]bold[i]italic"),
$this->filter("[b]bold[i]italic") "[b]bold[i]italic"
); );
} }
public function testCode() public function testCode()
{ {
$this->assertEquals( $this->assertEquals(
"<pre>[b]bold[/b]</pre>", $this->filter("[code][b]bold[/b][/code]"),
$this->filter("[code][b]bold[/b][/code]") "<pre>[b]bold[/b]</pre>"
); );
} }
public function testNestedList() public function testNestedList()
{ {
$this->assertEquals( $this->assertEquals(
"<ul><li>a<ul><li>a<li>b</ul><li>b</ul>", $this->filter("[list][*]a[list][*]a[*]b[/list][*]b[/list]"),
$this->filter("[list][*]a[list][*]a[*]b[/list][*]b[/list]") "<ul><li>a<ul><li>a<li>b</ul><li>b</ul>"
); );
$this->assertEquals( $this->assertEquals(
"<ul><li>a<ol><li>a<li>b</ol><li>b</ul>", $this->filter("[ul][*]a[ol][*]a[*]b[/ol][*]b[/ul]"),
$this->filter("[ul][*]a[ol][*]a[*]b[/ol][*]b[/ul]") "<ul><li>a<ol><li>a<li>b</ol><li>b</ul>"
); );
} }
public function testSpoiler() public function testSpoiler()
{ {
$this->assertEquals( $this->assertEquals(
"<span style=\"background-color:#000; color:#000;\">ShishNet</span>", $this->filter("[spoiler]ShishNet[/spoiler]"),
$this->filter("[spoiler]ShishNet[/spoiler]") "<span style=\"background-color:#000; color:#000;\">ShishNet</span>"
); );
$this->assertEquals( $this->assertEquals(
"FuvfuArg", $this->strip("[spoiler]ShishNet[/spoiler]"),
$this->strip("[spoiler]ShishNet[/spoiler]") "FuvfuArg"
); );
#$this->assertEquals( #$this->assertEquals(
# $this->filter("[spoiler]ShishNet"), # $this->filter("[spoiler]ShishNet"),
@ -67,32 +67,32 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
public function testURL() public function testURL()
{ {
$this->assertEquals( $this->assertEquals(
"<a href=\"https://shishnet.org\">https://shishnet.org</a>", $this->filter("[url]https://shishnet.org[/url]"),
$this->filter("[url]https://shishnet.org[/url]") "<a href=\"https://shishnet.org\">https://shishnet.org</a>"
); );
$this->assertEquals( $this->assertEquals(
"<a href=\"https://shishnet.org\">ShishNet</a>", $this->filter("[url=https://shishnet.org]ShishNet[/url]"),
$this->filter("[url=https://shishnet.org]ShishNet[/url]") "<a href=\"https://shishnet.org\">ShishNet</a>"
); );
$this->assertEquals( $this->assertEquals(
"[url=javascript:alert(\"owned\")]click to fail[/url]", $this->filter("[url=javascript:alert(\"owned\")]click to fail[/url]"),
$this->filter("[url=javascript:alert(\"owned\")]click to fail[/url]") "[url=javascript:alert(\"owned\")]click to fail[/url]"
); );
} }
public function testEmailURL() public function testEmailURL()
{ {
$this->assertEquals( $this->assertEquals(
"<a href=\"mailto:spam@shishnet.org\">spam@shishnet.org</a>", $this->filter("[email]spam@shishnet.org[/email]"),
$this->filter("[email]spam@shishnet.org[/email]") "<a href=\"mailto:spam@shishnet.org\">spam@shishnet.org</a>"
); );
} }
public function testAnchor() public function testAnchor()
{ {
$this->assertEquals( $this->assertEquals(
'<span class="anchor">Rules <a class="alink" href="#bb-rules" name="bb-rules" title="link to this anchor"> ¶ </a></span>', $this->filter("[anchor=rules]Rules[/anchor]"),
$this->filter("[anchor=rules]Rules[/anchor]") '<span class="anchor">Rules <a class="alink" href="#bb-rules" name="bb-rules" title="link to this anchor"> ¶ </a></span>'
); );
} }

View File

@ -15,26 +15,31 @@ class Blotter extends Extension
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{ {
global $database; global $config;
$version = $config->get_int("blotter_version", 0);
if ($this->get_version("blotter_version") < 1) { /**
* If this version is less than "1", it's time to install.
*
* REMINDER: If I change the database tables, I must change up version by 1.
*/
if ($version < 1) {
/**
* Installer
*/
global $database, $config;
$database->create_table("blotter", " $database->create_table("blotter", "
id SCORE_AIPK, id SCORE_AIPK,
entry_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, entry_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
entry_text TEXT NOT NULL, entry_text TEXT NOT NULL,
important BOOLEAN NOT NULL DEFAULT FALSE important SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N
"); ");
// Insert sample data: // Insert sample data:
$database->execute( $database->execute(
"INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)", "INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)",
["text"=>"Installed the blotter extension!", "important"=>true] ["text"=>"Installed the blotter extension!", "important"=>"Y"]
); );
log_info("blotter", "Installed tables for blotter extension."); log_info("blotter", "Installed tables for blotter extension.");
$this->set_version("blotter_version", 2); $config->set_int("blotter_version", 1);
}
if ($this->get_version("blotter_version") < 2) {
$database->standardise_boolean("blotter", "important");
$this->set_version("blotter_version", 2);
} }
} }
@ -93,7 +98,11 @@ class Blotter extends Extension
if ($entry_text == "") { if ($entry_text == "") {
die("No entry message!"); die("No entry message!");
} }
$important = isset($_POST['important']); if (isset($_POST['important'])) {
$important = 'Y';
} else {
$important = 'N';
}
// Now insert into db: // Now insert into db:
$database->execute( $database->execute(
"INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)", "INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)",
@ -115,7 +124,7 @@ class Blotter extends Extension
if (!isset($id)) { if (!isset($id)) {
die("No ID!"); die("No ID!");
} }
$database->execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]); $database->Execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]);
log_info("blotter", "Removed Entry #$id"); log_info("blotter", "Removed Entry #$id");
$page->set_mode(PageMode::REDIRECT); $page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("blotter/editor")); $page->set_redirect(make_link("blotter/editor"));

View File

@ -42,7 +42,7 @@ class BrowserSearch extends Extension
// And now to send it to the browser // And now to send it to the browser
$page->set_mode(PageMode::DATA); $page->set_mode(PageMode::DATA);
$page->set_mime(MimeType::XML); $page->set_type(MIME_TYPE_XML);
$page->set_data($xml); $page->set_data($xml);
} elseif ($event->page_matches("browser_search")) { } elseif ($event->page_matches("browser_search")) {
$suggestions = $config->get_string("search_suggestions_results_order"); $suggestions = $config->get_string("search_suggestions_results_order");

View File

@ -9,5 +9,5 @@ class BulkActionsInfo extends ExtensionInfo
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"]; public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public $license = self::LICENSE_WTFPL; public $license = self::LICENSE_WTFPL;
public $description = "Provides query and selection-based bulk action support"; public $description = "Provides query and selection-based bulk action support";
public $documentation = "Provides bulk action section in list view. Allows performing actions against a set of posts based on query or manual selection. Based on Mass Tagger by Christian Walde <walde.christian@googlemail.com>, contributions by Shish and Agasa."; public $documentation = "Provides bulk action section in list view. Allows performing actions against a set of images based on query or manual selection. Based on Mass Tagger by Christian Walde <walde.christian@googlemail.com>, contributions by Shish and Agasa.";
} }

View File

@ -193,14 +193,14 @@ class BulkActions extends Extension
if (is_iterable($items)) { if (is_iterable($items)) {
send_event($bae); send_event($bae);
} }
} catch (BulkActionException $e) {
log_error(BulkActionsInfo::KEY, $e->getMessage(), $e->getMessage());
}
if ($bae->redirect) { if ($bae->redirect) {
$page->set_mode(PageMode::REDIRECT); $page->set_mode(PageMode::REDIRECT);
$page->set_redirect(referer_or(make_link())); $page->set_redirect(referer_or(make_link()));
} }
} catch (BulkActionException $e) {
log_error(BulkActionsInfo::KEY, $e->getMessage(), $e->getMessage());
}
} }
} }
@ -255,7 +255,7 @@ class BulkActions extends Extension
$pos_tag_array = []; $pos_tag_array = [];
$neg_tag_array = []; $neg_tag_array = [];
foreach ($tags as $new_tag) { foreach ($tags as $new_tag) {
if (str_starts_with($new_tag, '-')) { if (strpos($new_tag, '-') === 0) {
$neg_tag_array[] = substr($new_tag, 1); $neg_tag_array[] = substr($new_tag, 1);
} else { } else {
$pos_tag_array[] = $new_tag; $pos_tag_array[] = $new_tag;

View File

@ -8,7 +8,7 @@ class BulkActionsTheme extends Themelet
<input id='bulk_selector_activate' type='button' onclick='activate_bulk_selector();' value='Activate (M)anual Select' accesskey='m'/> <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;'> <div id='bulk_selector_controls' style='display: none;'>
<input id='bulk_selector_deactivate' type='button' onclick='deactivate_bulk_selector();' value='Deactivate (M)anual Select' accesskey='m'/> <input id='bulk_selector_deactivate' type='button' onclick='deactivate_bulk_selector();' value='Deactivate (M)anual Select' accesskey='m'/>
Click on posts to mark them. Click on images to mark them.
<br /> <br />
<table><tr><td> <table><tr><td>
<input id='bulk_selector_select_all' type='button' <input id='bulk_selector_select_all' type='button'

View File

@ -6,7 +6,7 @@ class BulkAddTest extends ShimmiePHPUnitTestCase
{ {
send_event(new UserLoginEvent(User::by_name(self::$admin_name))); send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
$bae = send_event(new BulkAddEvent('asdf')); $bae = send_event(new BulkAddEvent('asdf'));
$this->assertContainsEquals( $this->assertContains(
"Error, asdf is not a readable directory", "Error, asdf is not a readable directory",
$bae->results, $bae->results,
implode("\n", $bae->results) implode("\n", $bae->results)

View File

@ -9,16 +9,16 @@ class BulkAddCSVInfo extends ExtensionInfo
public $url = self::SHIMMIE_URL; public $url = self::SHIMMIE_URL;
public $authors = ["velocity37"=>"velocity37@gmail.com"]; public $authors = ["velocity37"=>"velocity37@gmail.com"];
public $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;
public $description = "Bulk add server-side posts with metadata from CSV file"; public $description = "Bulk add server-side images with metadata from CSV file";
public $documentation = public $documentation =
"Modification of \"Bulk Add\" by Shish.<br><br> "Modification of \"Bulk Add\" by Shish.<br><br>
Adds posts from a CSV with the five following values: <br> Adds images from a CSV with the five following values: <br>
\"/path/to/image.jpg\",\"spaced tags\",\"source\",\"rating s/q/e\",\"/path/thumbnail.jpg\" <br> \"/path/to/image.jpg\",\"spaced tags\",\"source\",\"rating s/q/e\",\"/path/thumbnail.jpg\" <br>
<b>e.g.</b> \"/tmp/cat.png\",\"shish oekaki\",\"shimmie.shishnet.org\",\"s\",\"tmp/custom.jpg\" <br><br> <b>e.g.</b> \"/tmp/cat.png\",\"shish oekaki\",\"shimmie.shishnet.org\",\"s\",\"tmp/custom.jpg\" <br><br>
Any value but the first may be omitted, but there must be five values per line.<br> Any value but the first may be omitted, but there must be five values per line.<br>
<b>e.g.</b> \"/why/not/try/bulk_add.jpg\",\"\",\"\",\"\",\"\"<br><br> <b>e.g.</b> \"/why/not/try/bulk_add.jpg\",\"\",\"\",\"\",\"\"<br><br>
Post thumbnails will be displayed at the AR of the full post. Thumbnails that are Image thumbnails will be displayed at the AR of the full image. Thumbnails that are
normally static (e.g. SWF) will be displayed at the board's max thumbnail size<br><br> normally static (e.g. SWF) will be displayed at the board's max thumbnail size<br><br>
Useful for importing tagged posts without having to do database manipulation.<br> Useful for importing tagged images without having to do database manipulation.<br>
<p><b>Note:</b> requires \"Admin Controls\" and optionally \"Post Ratings\" to be enabled<br><br>"; <p><b>Note:</b> requires \"Admin Controls\" and optionally \"Image Ratings\" to be enabled<br><br>";
} }

View File

@ -9,8 +9,8 @@ class BulkAddCSVTheme extends Themelet
*/ */
public function display_upload_results(Page $page) public function display_upload_results(Page $page)
{ {
$page->set_title("Adding posts from csv"); $page->set_title("Adding images from csv");
$page->set_heading("Adding posts from csv"); $page->set_heading("Adding images from csv");
$page->add_block(new NavBlock()); $page->add_block(new NavBlock());
foreach ($this->messages as $block) { foreach ($this->messages as $block) {
$page->add_block($block); $page->add_block($block);
@ -26,8 +26,8 @@ class BulkAddCSVTheme extends Themelet
{ {
global $page; global $page;
$html = " $html = "
Add posts from a csv. Posts will be tagged and have their Add images from a csv. Images will be tagged and have their
source and rating set (if \"Post Ratings\" is enabled) source and rating set (if \"Image Ratings\" is enabled)
<br>Specify the absolute or relative path to a local .csv file. Check <a href=\"" . make_link("ext_doc/bulk_add_csv") . "\">here</a> for the expected format. <br>Specify the absolute or relative path to a local .csv file. Check <a href=\"" . make_link("ext_doc/bulk_add_csv") . "\">here</a> for the expected format.
<p>".make_form(make_link("bulk_add_csv"))." <p>".make_form(make_link("bulk_add_csv"))."

View File

@ -21,7 +21,7 @@ class BulkDownload extends Extension
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
{ {
global $user; global $user, $config;
if ($user->can(Permissions::BULK_DOWNLOAD)) { if ($user->can(Permissions::BULK_DOWNLOAD)) {
$event->add_action(BulkDownload::DOWNLOAD_ACTION_NAME, "Download ZIP"); $event->add_action(BulkDownload::DOWNLOAD_ACTION_NAME, "Download ZIP");

View File

@ -7,7 +7,6 @@ class BulkExportEvent extends Event
public function __construct(Image $image) public function __construct(Image $image)
{ {
parent::__construct();
$this->image = $image; $this->image = $image;
} }
} }
@ -20,7 +19,6 @@ class BulkImportEvent extends Event
public function __construct(Image $image, $fields) public function __construct(Image $image, $fields)
{ {
parent::__construct();
$this->image = $image; $this->image = $image;
$this->fields = $fields; $this->fields = $fields;
} }

View File

@ -5,24 +5,30 @@ class BulkImportExport extends DataHandlerExtension
{ {
const EXPORT_ACTION_NAME = "bulk_export"; const EXPORT_ACTION_NAME = "bulk_export";
const EXPORT_INFO_FILE_NAME = "export.json"; const EXPORT_INFO_FILE_NAME = "export.json";
protected $SUPPORTED_MIME = [MimeType::ZIP]; protected $SUPPORTED_MIME = [MIME_TYPE_ZIP];
public function onDataUpload(DataUploadEvent $event) public function onDataUpload(DataUploadEvent $event)
{ {
global $user, $database; global $user, $database;
if ($this->supported_mime($event->mime) && if ($this->supported_ext($event->type) &&
$user->can(Permissions::BULK_IMPORT)) { $user->can(Permissions::BULK_IMPORT)) {
$zip = new ZipArchive; $zip = new ZipArchive;
if ($zip->open($event->tmpname) === true) { if ($zip->open($event->tmpname) === true) {
$json_data = $this->get_export_data($zip); $info = $zip->getStream(self::EXPORT_INFO_FILE_NAME);
$json_data = [];
if (empty($json_data)) { if ($info !== false) {
return; try {
$json_string = stream_get_contents($info);
$json_data = json_decode($json_string);
} finally {
fclose($info);
}
} else {
throw new SCoreException("Could not get " . self::EXPORT_INFO_FILE_NAME . " from archive");
} }
$total = 0; $total = 0;
$skipped = 0; $skipped = 0;
$failed = 0; $failed = 0;
@ -36,7 +42,7 @@ class BulkImportExport extends DataHandlerExtension
$image = Image::by_hash($item->hash); $image = Image::by_hash($item->hash);
if ($image!=null) { if ($image!=null) {
$skipped++; $skipped++;
log_info(BulkImportExportInfo::KEY, "Post $item->hash already present, skipping"); log_info(BulkImportExportInfo::KEY, "Image $item->hash already present, skipping");
$database->commit(); $database->commit();
continue; continue;
} }
@ -100,7 +106,7 @@ class BulkImportExport extends DataHandlerExtension
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
{ {
global $user; global $user, $config;
if ($user->can(Permissions::BULK_EXPORT)) { if ($user->can(Permissions::BULK_EXPORT)) {
$event->add_action(self::EXPORT_ACTION_NAME, "Export"); $event->add_action(self::EXPORT_ACTION_NAME, "Export");
@ -163,20 +169,4 @@ class BulkImportExport extends DataHandlerExtension
{ {
return false; return false;
} }
private function get_export_data(ZipArchive $zip): ?array
{
$info = $zip->getStream(self::EXPORT_INFO_FILE_NAME);
if ($info !== false) {
try {
$json_string = stream_get_contents($info);
$json_data = json_decode($json_string);
} finally {
fclose($info);
}
return $json_data;
} else {
return null;
}
}
} }

View File

@ -5,7 +5,7 @@ class CommentListInfo extends ExtensionInfo
public const KEY = "comment"; public const KEY = "comment";
public $key = self::KEY; public $key = self::KEY;
public $name = "Post Comments"; public $name = "Image Comments";
public $url = self::SHIMMIE_URL; public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR; public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;

View File

@ -158,15 +158,15 @@ class CommentList extends Extension
} }
if ($this->get_version("ext_comments_version") == 1) { if ($this->get_version("ext_comments_version") == 1) {
$database->execute("CREATE INDEX comments_owner_ip ON comments(owner_ip)"); $database->Execute("CREATE INDEX comments_owner_ip ON comments(owner_ip)");
$database->execute("CREATE INDEX comments_posted ON comments(posted)"); $database->Execute("CREATE INDEX comments_posted ON comments(posted)");
$this->set_version("ext_comments_version", 2); $this->set_version("ext_comments_version", 2);
} }
if ($this->get_version("ext_comments_version") == 2) { if ($this->get_version("ext_comments_version") == 2) {
$this->set_version("ext_comments_version", 3); $this->set_version("ext_comments_version", 3);
$database->execute("ALTER TABLE comments ADD FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE"); $database->Execute("ALTER TABLE comments ADD FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE");
$database->execute("ALTER TABLE comments ADD FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT"); $database->Execute("ALTER TABLE comments ADD FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT");
} }
// FIXME: add foreign keys, bump to v3 // FIXME: add foreign keys, bump to v3
@ -266,7 +266,7 @@ class CommentList extends Extension
$total_pages = $cache->get("comment_pages"); $total_pages = $cache->get("comment_pages");
if (empty($total_pages)) { if (empty($total_pages)) {
$total_pages = (int)ceil($database->get_one(" $total_pages = (int)($database->get_one("
SELECT COUNT(c1) SELECT COUNT(c1)
FROM (SELECT COUNT(image_id) AS c1 FROM comments $where GROUP BY image_id) AS s1 FROM (SELECT COUNT(image_id) AS c1 FROM comments $where GROUP BY image_id) AS s1
") / 10); ") / 10);
@ -278,7 +278,7 @@ class CommentList extends Extension
$threads_per_page = 10; $threads_per_page = 10;
$start = $threads_per_page * $current_page; $start = $threads_per_page * $current_page;
$result = $database->execute(" $result = $database->Execute("
SELECT image_id,MAX(posted) AS latest SELECT image_id,MAX(posted) AS latest
FROM comments FROM comments
$where $where
@ -370,7 +370,7 @@ class CommentList extends Extension
public function onCommentDeletion(CommentDeletionEvent $event) public function onCommentDeletion(CommentDeletionEvent $event)
{ {
global $database; global $database;
$database->execute(" $database->Execute("
DELETE FROM comments DELETE FROM comments
WHERE id=:comment_id WHERE id=:comment_id
", ["comment_id"=>$event->comment_id]); ", ["comment_id"=>$event->comment_id]);
@ -595,7 +595,7 @@ class CommentList extends Extension
if ($user->is_anonymous()) { if ($user->is_anonymous()) {
$page->add_cookie("nocache", "Anonymous Commenter", time()+60*60*24, "/"); $page->add_cookie("nocache", "Anonymous Commenter", time()+60*60*24, "/");
} }
$database->execute( $database->Execute(
"INSERT INTO comments(image_id, owner_id, owner_ip, posted, comment) ". "INSERT INTO comments(image_id, owner_id, owner_ip, posted, comment) ".
"VALUES(:image_id, :user_id, :remote_addr, now(), :comment)", "VALUES(:image_id, :user_id, :remote_addr, now(), :comment)",
["image_id"=>$image_id, "user_id"=>$user->id, "remote_addr"=>$_SERVER['REMOTE_ADDR'], "comment"=>$comment] ["image_id"=>$image_id, "user_id"=>$user->id, "remote_addr"=>$_SERVER['REMOTE_ADDR'], "comment"=>$comment]
@ -604,7 +604,7 @@ class CommentList extends Extension
$snippet = substr($comment, 0, 100); $snippet = substr($comment, 0, 100);
$snippet = str_replace("\n", " ", $snippet); $snippet = str_replace("\n", " ", $snippet);
$snippet = str_replace("\r", " ", $snippet); $snippet = str_replace("\r", " ", $snippet);
log_info("comment", "Comment #$cid added to >>$image_id: $snippet"); log_info("comment", "Comment #$cid added to Image #$image_id: $snippet");
} }
private function comment_checks(int $image_id, User $user, string $comment) private function comment_checks(int $image_id, User $user, string $comment)

View File

@ -289,23 +289,23 @@ class CommentListTheme extends Themelet
public function get_help_html() public function get_help_html()
{ {
return '<p>Search for posts containing a certain number of comments, or comments by a particular individual.</p> return '<p>Search for images containing a certain number of comments, or comments by a particular individual.</p>
<div class="command_example"> <div class="command_example">
<pre>comments=1</pre> <pre>comments=1</pre>
<p>Returns posts with exactly 1 comment.</p> <p>Returns images with exactly 1 comment.</p>
</div> </div>
<div class="command_example"> <div class="command_example">
<pre>comments>0</pre> <pre>comments>0</pre>
<p>Returns posts with 1 or more comments. </p> <p>Returns images with 1 or more comments. </p>
</div> </div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p> <p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
<div class="command_example"> <div class="command_example">
<pre>commented_by:username</pre> <pre>commented_by:username</pre>
<p>Returns posts that have been commented on by "username". </p> <p>Returns images that have been commented on by "username". </p>
</div> </div>
<div class="command_example"> <div class="command_example">
<pre>commented_by_userno:123</pre> <pre>commented_by_userno:123</pre>
<p>Returns posts that have been commented on by user 123. </p> <p>Returns images that have been commented on by user 123. </p>
</div> </div>
'; ';
} }

View File

@ -419,10 +419,10 @@ class CronUploader extends Extension
if ($corrupt) { if ($corrupt) {
// Move to corrupt dir // Move to corrupt dir
$newDir = join_path($this->get_failed_dir(), $output_subdir, $relativeDir); $newDir = join_path($this->get_failed_dir(), $output_subdir, $relativeDir);
$info = "ERROR: Post was not uploaded. "; $info = "ERROR: Image was not uploaded. ";
} else { } else {
$newDir = join_path($this->get_uploaded_dir(), $output_subdir, $relativeDir); $newDir = join_path($this->get_uploaded_dir(), $output_subdir, $relativeDir);
$info = "Post successfully uploaded. "; $info = "Image successfully uploaded. ";
} }
$newDir = str_replace(DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, $newDir); $newDir = str_replace(DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, $newDir);
@ -434,7 +434,7 @@ class CronUploader extends Extension
// move file to correct dir // move file to correct dir
rename($path, $newFile); rename($path, $newFile);
$this->log_message(SCORE_LOG_INFO, $info . "Post \"$filename\" moved from queue to \"$newDir\"."); $this->log_message(SCORE_LOG_INFO, $info . "Image \"$filename\" moved from queue to \"$newDir\".");
} }
/** /**
@ -462,14 +462,11 @@ class CronUploader extends Extension
// Generate info message // Generate info message
if ($event->image_id == -1) { if ($event->image_id == -1) {
if (array_key_exists("mime", $event->metadata)) {
throw new UploadException("File type not recognised (".$event->metadata["mime"]."). Filename: {$filename}");
}
throw new UploadException("File type not recognised. Filename: {$filename}"); throw new UploadException("File type not recognised. Filename: {$filename}");
} elseif ($event->merged === true) { } elseif ($event->merged === true) {
$infomsg = "Post merged. ID: {$event->image_id} - Filename: {$filename}"; $infomsg = "Image merged. ID: {$event->image_id} - Filename: {$filename}";
} else { } else {
$infomsg = "Post uploaded. ID: {$event->image_id} - Filename: {$filename}"; $infomsg = "Image uploaded. ID: {$event->image_id} - Filename: {$filename}";
} }
$this->log_message(SCORE_LOG_INFO, $infomsg); $this->log_message(SCORE_LOG_INFO, $infomsg);
@ -511,7 +508,7 @@ class CronUploader extends Extension
$base = $this->get_queue_dir(); $base = $this->get_queue_dir();
if (!is_dir($base)) { if (!is_dir($base)) {
$this->log_message(SCORE_LOG_WARNING, "Post Queue Directory could not be found at \"$base\"."); $this->log_message(SCORE_LOG_WARNING, "Image Queue Directory could not be found at \"$base\".");
return; return;
} }
@ -548,7 +545,7 @@ class CronUploader extends Extension
global $page; global $page;
$page->set_mode(PageMode::MANUAL); $page->set_mode(PageMode::MANUAL);
$page->set_mime(MimeType::TEXT); $page->set_type(MIME_TYPE_TEXT);
$page->send_headers(); $page->send_headers();
} }
} }

View File

@ -54,7 +54,7 @@ class CronUploaderTheme extends Themelet
<li>Create a cron job or something else that can open a url on specified times. <li>Create a cron job or something else that can open a url on specified times.
<br/>cron is a service that runs commands over and over again on a a schedule. You can set up cron (or any similar tool) to run the command above to trigger the import on whatever schedule you desire. <br/>cron is a service that runs commands over and over again on a a schedule. You can set up cron (or any similar tool) to run the command above to trigger the import on whatever schedule you desire.
<br />If you're not sure how to do this, you can give the command to your web host and you can ask them to create the cron job for you. <br />If you're not sure how to do this, you can give the command to your web host and you can ask them to create the cron job for you.
<br />When you create the cron job, you choose when to upload new posts.</li> <br />When you create the cron job, you choose when to upload new images.</li>
</ol>"; </ol>";

View File

@ -55,7 +55,7 @@ class CustomHtmlHeaders extends Extension
$sitename_in_title = $config->get_string("sitename_in_title"); $sitename_in_title = $config->get_string("sitename_in_title");
// sitename is already in title (can occur on index & other pages) // sitename is already in title (can occur on index & other pages)
if (str_contains($page->title, $site_title)) { if (strstr($page->title, $site_title)) {
return; return;
} }

View File

@ -1,25 +1,5 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
use \MicroHTML\HTMLElement;
function TAGS(...$args)
{
return new HTMLElement("tags", $args);
}
function TAG(...$args)
{
return new HTMLElement("tag", $args);
}
function POSTS(...$args)
{
return new HTMLElement("posts", $args);
}
function POST(...$args)
{
return new HTMLElement("post", $args);
}
class DanbooruApi extends Extension class DanbooruApi extends Extension
{ {
public function onPageRequest(PageRequestEvent $event) public function onPageRequest(PageRequestEvent $event)
@ -30,14 +10,14 @@ class DanbooruApi extends Extension
if ($event->page_matches("api/danbooru/add_post") || $event->page_matches("api/danbooru/post/create.xml")) { if ($event->page_matches("api/danbooru/add_post") || $event->page_matches("api/danbooru/post/create.xml")) {
// No XML data is returned from this function // No XML data is returned from this function
$page->set_mime(MimeType::TEXT); $page->set_type(MIME_TYPE_TEXT);
$this->api_add_post(); $this->api_add_post();
} elseif ($event->page_matches("api/danbooru/find_posts") || $event->page_matches("api/danbooru/post/index.xml")) { } elseif ($event->page_matches("api/danbooru/find_posts") || $event->page_matches("api/danbooru/post/index.xml")) {
$page->set_mime(MimeType::XML_APPLICATION); $page->set_type(MIME_TYPE_XML_APPLICATION);
$page->set_data((string)$this->api_find_posts()); $page->set_data($this->api_find_posts());
} elseif ($event->page_matches("api/danbooru/find_tags")) { } elseif ($event->page_matches("api/danbooru/find_tags")) {
$page->set_mime(MimeType::XML_APPLICATION); $page->set_type(MIME_TYPE_XML_APPLICATION);
$page->set_data((string)$this->api_find_tags()); $page->set_data($this->api_find_tags());
} }
// Hackery for danbooruup 0.3.2 providing the wrong view url. This simply redirects to the proper // Hackery for danbooruup 0.3.2 providing the wrong view url. This simply redirects to the proper
@ -57,7 +37,7 @@ class DanbooruApi extends Extension
* Authenticates a user based on the contents of the login and password parameters * Authenticates a user based on the contents of the login and password parameters
* or makes them anonymous. Does not set any cookies or anything permanent. * or makes them anonymous. Does not set any cookies or anything permanent.
*/ */
private function authenticate_user(): void private function authenticate_user()
{ {
global $config, $user; global $config, $user;
@ -86,7 +66,7 @@ class DanbooruApi extends Extension
* - tags: any typical tag query. See Tag#parse_query for details. * - tags: any typical tag query. See Tag#parse_query for details.
* - after_id: limit results to tags with an id number after after_id. Useful if you only want to refresh * - after_id: limit results to tags with an id number after after_id. Useful if you only want to refresh
*/ */
private function api_find_tags(): HTMLElement private function api_find_tags(): string
{ {
global $database; global $database;
$results = []; $results = [];
@ -130,15 +110,16 @@ class DanbooruApi extends Extension
} }
// Tag results collected, build XML output // Tag results collected, build XML output
$xml = TAGS(); $xml = "<tags>\n";
foreach ($results as $tag) { foreach ($results as $tag) {
$xml->appendChild(TAG([ $xml .= xml_tag("tag", [
"type" => "0", "type" => "0",
"counts" => $tag[0], "counts" => $tag[0],
"name" => $tag[1], "name" => $tag[1],
"id" => $tag[2], "id" => $tag[2],
])); ]);
} }
$xml .= "</tags>";
return $xml; return $xml;
} }
@ -156,7 +137,7 @@ class DanbooruApi extends Extension
* *
* #return string * #return string
*/ */
private function api_find_posts(): HTMLElement private function api_find_posts()
{ {
$results = []; $results = [];
@ -194,7 +175,7 @@ class DanbooruApi extends Extension
// Now we have the array $results filled with Image objects // Now we have the array $results filled with Image objects
// Let's display them // Let's display them
$xml = POSTS(["count"=>$count, "offset"=>$start]); $xml = "<posts count=\"{$count}\" offset=\"{$start}\">\n";
foreach ($results as $img) { foreach ($results as $img) {
// Sanity check to see if $img is really an image object // Sanity check to see if $img is really an image object
// If it isn't (e.g. someone requested an invalid md5 or id), break out of the this // If it isn't (e.g. someone requested an invalid md5 or id), break out of the this
@ -204,7 +185,7 @@ class DanbooruApi extends Extension
$taglist = $img->get_tag_list(); $taglist = $img->get_tag_list();
$owner = $img->get_owner(); $owner = $img->get_owner();
$previewsize = get_thumbnail_size($img->width, $img->height); $previewsize = get_thumbnail_size($img->width, $img->height);
$xml->appendChild(TAG([ $xml .= xml_tag("post", [
"id" => $img->id, "id" => $img->id,
"md5" => $img->hash, "md5" => $img->hash,
"file_name" => $img->filename, "file_name" => $img->filename,
@ -221,8 +202,9 @@ class DanbooruApi extends Extension
"source" => $img->source, "source" => $img->source,
"score" => 0, "score" => 0,
"author" => $owner->name "author" => $owner->name
])); ]);
} }
$xml .= "</posts>";
return $xml; return $xml;
} }
@ -253,7 +235,7 @@ class DanbooruApi extends Extension
* Get: * Get:
* - Redirected to the newly uploaded post. * - Redirected to the newly uploaded post.
*/ */
private function api_add_post(): void private function api_add_post()
{ {
global $user, $page; global $user, $page;
$danboorup_kludge = 1; // danboorup for firefox makes broken links out of location: /path $danboorup_kludge = 1; // danboorup for firefox makes broken links out of location: /path
@ -289,8 +271,8 @@ class DanbooruApi extends Extension
} }
} elseif (isset($_REQUEST['source']) || isset($_REQUEST['post']['source'])) { // A url was provided } elseif (isset($_REQUEST['source']) || isset($_REQUEST['post']['source'])) { // A url was provided
$source = isset($_REQUEST['source']) ? $_REQUEST['source'] : $_REQUEST['post']['source']; $source = isset($_REQUEST['source']) ? $_REQUEST['source'] : $_REQUEST['post']['source'];
$file = tempnam(sys_get_temp_dir(), "shimmie_transload"); $file = tempnam("/tmp", "shimmie_transload");
$ok = fetch_url($source, $file); $ok = transload($source, $file);
if (!$ok) { if (!$ok) {
$page->set_code(409); $page->set_code(409);
$page->add_http_header("X-Danbooru-Errors: fopen read error"); $page->add_http_header("X-Danbooru-Errors: fopen read error");

View File

@ -8,12 +8,12 @@ class DanbooruApiTest extends ShimmiePHPUnitTestCase
$image_id = $this->post_image("tests/bedroom_workshop.jpg", "data"); $image_id = $this->post_image("tests/bedroom_workshop.jpg", "data");
$this->get_page("api/danbooru/find_posts"); $this->get_page("api/danbooru/find_posts");
$this->get_page("api/danbooru/find_posts", ["id"=>$image_id]); $this->get_page("api/danbooru/find_posts?id=$image_id");
$this->get_page("api/danbooru/find_posts", ["md5"=>"17fc89f372ed3636e28bd25cc7f3bac1"]); $this->get_page("api/danbooru/find_posts?md5=17fc89f372ed3636e28bd25cc7f3bac1");
$this->get_page("api/danbooru/find_tags"); $this->get_page("api/danbooru/find_tags");
$this->get_page("api/danbooru/find_tags", ["id"=>1]); $this->get_page("api/danbooru/find_tags?id=1");
$this->get_page("api/danbooru/find_tags", ["name"=>"data"]); $this->get_page("api/danbooru/find_tags?name=data");
$page = $this->get_page("api/danbooru/post/show/$image_id"); $page = $this->get_page("api/danbooru/post/show/$image_id");
$this->assertEquals(302, $page->code); $this->assertEquals(302, $page->code);

View File

@ -1,17 +0,0 @@
<?php
class ImageDownloadingEvent extends Event
{
public $image;
public $mime;
public $path;
public $file_modified = false;
public function __construct(Image $image, String $path, string $mime)
{
parent::__construct();
$this->image = $image;
$this->path = $path;
$this->mime = $mime;
}
}

View File

@ -1,14 +0,0 @@
<?php
class DownloadInfo extends ExtensionInfo
{
public const KEY = "download";
public $key = self::KEY;
public $name = "Download";
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public $license = self::LICENSE_WTFPL;
public $description = "System-wide download functions";
public $core = true;
public $visibility = self::VISIBLE_HIDDEN;
}

View File

@ -1,26 +0,0 @@
<?php
require_once "events.php";
class Download extends Extension
{
public function get_priority(): int
{
// Set near the end to give everything else a chance to process
return 99;
}
public function onImageDownloading(ImageDownloadingEvent $event)
{
global $page;
$page->set_mime($event->mime);
$page->set_mode(PageMode::FILE);
$page->set_file($event->path, $event->file_modified);
$event->stop_processing = true;
}
}

View File

@ -1,13 +0,0 @@
<?php declare(strict_types=1);
class EokmInfo extends ExtensionInfo
{
public const KEY = "eokm";
public $key = self::KEY;
public $name = "EOKM Filter";
public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2;
public $description = "Check uploads against the EOKM blocklist";
}

View File

@ -1,50 +0,0 @@
<?php declare(strict_types=1);
class Eokm extends Extension
{
public function get_priority(): int
{
return 40;
} // early, to veto ImageUploadEvent
public function onImageAddition(ImageAdditionEvent $event)
{
global $config;
$username = $config->get_string("eokm_username");
$password = $config->get_string("eokm_password");
if ($username && $password) {
$ch = curl_init("https://api.eokmhashdb.nl/v1/check/md5");
// curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/xml', $additionalHeaders));
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_USERPWD, $username . ":" . $password);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $event->image->hash);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$return = curl_exec($ch);
curl_close($ch);
if ($return == "false") {
// all ok
} elseif ($return == "true") {
log_warning("eokm", "User tried to upload banned image {$event->image->hash}");
throw new UploadException("Post banned");
} else {
log_warning("eokm", "Unexpected return from EOKM: $return");
}
}
}
public function onSetupBuilding(SetupBuildingEvent $event)
{
$sb = new SetupBlock("EOKM Filter");
$sb->start_table();
$sb->add_text_option("eokm_username", "Username", true);
$sb->add_text_option("eokm_password", "Password", true);
$sb->end_table();
$event->panel->add_block($sb);
}
}

View File

@ -1,27 +0,0 @@
<?php declare(strict_types=1);
class EokmTest extends ShimmiePHPUnitTestCase
{
public function testPass()
{
// no EOKM login details set, so be a no-op
$this->log_in_as_user();
$this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot");
$this->assert_no_text("Image too large");
$this->assert_no_text("Image too small");
$this->assert_no_text("ratio");
}
/*
public function testFail()
{
$this->log_in_as_user();
try {
$this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot");
$this->assertTrue(false, "Invalid-size image was allowed");
} catch (UploadException $e) {
$this->assertEquals("Image too small", $e->getMessage());
}
}
*/
}

View File

@ -36,10 +36,10 @@ class ET extends Extension
public function onCommand(CommandEvent $event) public function onCommand(CommandEvent $event)
{ {
if ($event->cmd == "help") { if ($event->cmd == "help") {
print "\tinfo\n"; print "\tshimmie-info\n";
print "\t\tList a bunch of info\n\n"; print "\t\tList a bunch of info\n\n";
} }
if ($event->cmd == "info") { if ($event->cmd == "shimmie-info") {
print($this->to_yaml($this->get_info())); print($this->to_yaml($this->get_info()));
} }
} }
@ -76,7 +76,7 @@ class ET extends Extension
"extensions" => [ "extensions" => [
"core" => $core_exts, "core" => $core_exts,
"extra" => $extra_exts, "extra" => $extra_exts,
"handled_mimes" => DataHandlerExtension::get_all_supported_mimes(), "handled_extensions" => DataHandlerExtension::get_all_supported_exts(),
], ],
"stats" => [ "stats" => [
'images' => (int)$database->get_one("SELECT COUNT(*) FROM images"), 'images' => (int)$database->get_one("SELECT COUNT(*) FROM images"),
@ -94,7 +94,7 @@ class ET extends Extension
"width" => $config->get_int(ImageConfig::THUMB_WIDTH), "width" => $config->get_int(ImageConfig::THUMB_WIDTH),
"height" => $config->get_int(ImageConfig::THUMB_HEIGHT), "height" => $config->get_int(ImageConfig::THUMB_HEIGHT),
"scaling" => $config->get_int(ImageConfig::THUMB_SCALING), "scaling" => $config->get_int(ImageConfig::THUMB_SCALING),
"mime" => $config->get_string(ImageConfig::THUMB_MIME), "type" => $config->get_string(ImageConfig::THUMB_TYPE),
], ],
]; ];

View File

@ -35,16 +35,17 @@ class ETServer extends Extension
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{ {
global $database; global $config, $database;
// shortcut to latest // shortcut to latest
if ($this->get_version("et_server_version") < 1) { if ($config->get_int("et_server_version") < 1) {
$database->create_table("registration", " $database->create_table("registration", "
id SCORE_AIPK, id SCORE_AIPK,
responded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, responded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
data TEXT NOT NULL, data TEXT NOT NULL,
"); ");
$this->set_version("et_server_version", 1); $config->set_int("et_server_version", 1);
log_info("et_server", "extension installed");
} }
} }
} }

View File

@ -13,5 +13,5 @@ class FavoritesInfo extends ExtensionInfo
"Gives users a \"favorite this image\" button that they can press "Gives users a \"favorite this image\" button that they can press
<p>Favorites for a user can then be retrieved by searching for \"favorited_by=UserName\" <p>Favorites for a user can then be retrieved by searching for \"favorited_by=UserName\"
<p>Popular images can be searched for by eg. \"favorites>5\" <p>Popular images can be searched for by eg. \"favorites>5\"
<p>Favorite info can be added to a post's filename or tooltip using the \$favorites placeholder"; <p>Favorite info can be added to an image's filename or tooltip using the \$favorites placeholder";
} }

View File

@ -75,7 +75,7 @@ class Favorites extends Extension
$i_days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1; $i_days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1;
$h_favorites_rate = sprintf("%.1f", ($i_favorites_count / $i_days_old)); $h_favorites_rate = sprintf("%.1f", ($i_favorites_count / $i_days_old));
$favorites_link = make_link("post/list/favorited_by={$event->display_user->name}/1"); $favorites_link = make_link("post/list/favorited_by={$event->display_user->name}/1");
$event->add_stats("<a href='$favorites_link'>Posts favorited</a>: $i_favorites_count, $h_favorites_rate per day"); $event->add_stats("<a href='$favorites_link'>Images favorited</a>: $i_favorites_count, $h_favorites_rate per day");
} }
public function onImageInfoSet(ImageInfoSetEvent $event) public function onImageInfoSet(ImageInfoSetEvent $event)
@ -202,8 +202,8 @@ class Favorites extends Extension
global $database; global $database;
if ($this->get_version("ext_favorites_version") < 1) { if ($this->get_version("ext_favorites_version") < 1) {
$database->execute("ALTER TABLE images ADD COLUMN favorites INTEGER NOT NULL DEFAULT 0"); $database->Execute("ALTER TABLE images ADD COLUMN favorites INTEGER NOT NULL DEFAULT 0");
$database->execute("CREATE INDEX images__favorites ON images(favorites)"); $database->Execute("CREATE INDEX images__favorites ON images(favorites)");
$database->create_table("user_favorites", " $database->create_table("user_favorites", "
image_id INTEGER NOT NULL, image_id INTEGER NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
@ -218,12 +218,12 @@ class Favorites extends Extension
if ($this->get_version("ext_favorites_version") < 2) { if ($this->get_version("ext_favorites_version") < 2) {
log_info("favorites", "Cleaning user favourites"); log_info("favorites", "Cleaning user favourites");
$database->execute("DELETE FROM user_favorites WHERE user_id NOT IN (SELECT id FROM users)"); $database->Execute("DELETE FROM user_favorites WHERE user_id NOT IN (SELECT id FROM users)");
$database->execute("DELETE FROM user_favorites WHERE image_id NOT IN (SELECT id FROM images)"); $database->Execute("DELETE FROM user_favorites WHERE image_id NOT IN (SELECT id FROM images)");
log_info("favorites", "Adding foreign keys to user favourites"); log_info("favorites", "Adding foreign keys to user favourites");
$database->execute("ALTER TABLE user_favorites ADD FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;"); $database->Execute("ALTER TABLE user_favorites ADD FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;");
$database->execute("ALTER TABLE user_favorites ADD FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE;"); $database->Execute("ALTER TABLE user_favorites ADD FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE;");
$this->set_version("ext_favorites_version", 2); $this->set_version("ext_favorites_version", 2);
} }
} }
@ -233,18 +233,18 @@ class Favorites extends Extension
global $database; global $database;
if ($do_set) { if ($do_set) {
if (!$database->get_row("select 1 from user_favorites where image_id=:image_id and user_id=:user_id", ["image_id"=>$image_id, "user_id"=>$user_id])) { if (!$database->get_row("select 1 from user_favorites where image_id=:image_id and user_id=:user_id", ["image_id"=>$image_id, "user_id"=>$user_id])) {
$database->execute( $database->Execute(
"INSERT INTO user_favorites(image_id, user_id, created_at) VALUES(:image_id, :user_id, NOW())", "INSERT INTO user_favorites(image_id, user_id, created_at) VALUES(:image_id, :user_id, NOW())",
["image_id"=>$image_id, "user_id"=>$user_id] ["image_id"=>$image_id, "user_id"=>$user_id]
); );
} }
} else { } else {
$database->execute( $database->Execute(
"DELETE FROM user_favorites WHERE image_id = :image_id AND user_id = :user_id", "DELETE FROM user_favorites WHERE image_id = :image_id AND user_id = :user_id",
["image_id"=>$image_id, "user_id"=>$user_id] ["image_id"=>$image_id, "user_id"=>$user_id]
); );
} }
$database->execute( $database->Execute(
"UPDATE images SET favorites=(SELECT COUNT(*) FROM user_favorites WHERE image_id=:image_id) WHERE id=:user_id", "UPDATE images SET favorites=(SELECT COUNT(*) FROM user_favorites WHERE image_id=:image_id) WHERE id=:user_id",
["image_id"=>$image_id, "user_id"=>$user_id] ["image_id"=>$image_id, "user_id"=>$user_id]
); );

View File

@ -9,7 +9,7 @@ class FavoritesTest extends ShimmiePHPUnitTestCase
# No favourites # No favourites
$this->get_page("post/view/$image_id"); $this->get_page("post/view/$image_id");
$this->assert_title("Post $image_id: test"); $this->assert_title("Image $image_id: test");
$this->assert_no_text("Favorited By"); $this->assert_no_text("Favorited By");
# Add a favourite # Add a favourite
@ -17,7 +17,7 @@ class FavoritesTest extends ShimmiePHPUnitTestCase
# Favourite shown on page # Favourite shown on page
$this->get_page("post/view/$image_id"); $this->get_page("post/view/$image_id");
$this->assert_title("Post $image_id: test"); $this->assert_title("Image $image_id: test");
$this->assert_text("Favorited By"); $this->assert_text("Favorited By");
# Favourite shown on index # Favourite shown on index
@ -26,14 +26,14 @@ class FavoritesTest extends ShimmiePHPUnitTestCase
# Favourite shown on user page # Favourite shown on user page
$this->get_page("user/test"); $this->get_page("user/test");
$this->assert_text("Posts favorited</a>: 1"); $this->assert_text("Images favorited</a>: 1");
# Delete a favourite # Delete a favourite
send_event(new FavoriteSetEvent($image_id, $user, false)); send_event(new FavoriteSetEvent($image_id, $user, false));
# No favourites # No favourites
$this->get_page("post/view/$image_id"); $this->get_page("post/view/$image_id");
$this->assert_title("Post $image_id: test"); $this->assert_title("Image $image_id: test");
$this->assert_no_text("Favorited By"); $this->assert_no_text("Favorited By");
} }
} }

View File

@ -34,23 +34,23 @@ class FavoritesTheme extends Themelet
public function get_help_html() public function get_help_html()
{ {
return '<p>Search for posts that have been favorited a certain number of times, or favorited by a particular individual.</p> 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"> <div class="command_example">
<pre>favorites=1</pre> <pre>favorites=1</pre>
<p>Returns posts that have been favorited once.</p> <p>Returns images that have been favorited once.</p>
</div> </div>
<div class="command_example"> <div class="command_example">
<pre>favorites>0</pre> <pre>favorites>0</pre>
<p>Returns posts that have been favorited 1 or more times</p> <p>Returns images that have been favorited 1 or more times</p>
</div> </div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p> <p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
<div class="command_example"> <div class="command_example">
<pre>favorited_by:username</pre> <pre>favorited_by:username</pre>
<p>Returns posts that have been favorited by "username". </p> <p>Returns images that have been favorited by "username". </p>
</div> </div>
<div class="command_example"> <div class="command_example">
<pre>favorited_by_userno:123</pre> <pre>favorited_by_userno:123</pre>
<p>Returns posts that have been favorited by user 123. </p> <p>Returns images that have been favorited by user 123. </p>
</div> </div>
'; ';
} }

View File

@ -5,21 +5,21 @@ class FeaturedInfo extends ExtensionInfo
public const KEY = "featured"; public const KEY = "featured";
public $key = self::KEY; public $key = self::KEY;
public $name = "Featured Post"; public $name = "Featured Image";
public $url = self::SHIMMIE_URL; public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR; public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;
public $description = "Bring a specific image to the users' attentions"; public $description = "Bring a specific image to the users' attentions";
public $documentation = public $documentation =
"Once enabled, a new \"feature this\" button will appear next "Once enabled, a new \"feature this\" button will appear next
to the other post control buttons (delete, rotate, etc). to the other image control buttons (delete, rotate, etc).
Clicking it will set the image as the site's current feature, Clicking it will set the image as the site's current feature,
which will be shown in the side bar of the post list. which will be shown in the side bar of the post list.
<p><b>Viewing a featured post</b> <p><b>Viewing a featured image</b>
<br>Visit <code>/featured_image/view</code> <br>Visit <code>/featured_image/view</code>
<p><b>Downloading a featured post</b> <p><b>Downloading a featured image</b>
<br>Link to <code>/featured_image/download</code>. This will give <br>Link to <code>/featured_image/download</code>. This will give
the raw data for a post (no HTML). This is useful so that you the raw data for an image (no HTML). This is useful so that you
can set your desktop wallpaper to be the download URL, refreshed can set your desktop wallpaper to be the download URL, refreshed
every couple of hours."; every couple of hours.";
} }

View File

@ -20,7 +20,7 @@ class Featured extends Extension
$id = int_escape($_POST['image_id']); $id = int_escape($_POST['image_id']);
if ($id > 0) { if ($id > 0) {
$config->set_int("featured_id", $id); $config->set_int("featured_id", $id);
log_info("featured", "Featured post set to >>$id", "Featured post set"); log_info("featured", "Featured image set to $id", "Featured image set");
$page->set_mode(PageMode::REDIRECT); $page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/$id")); $page->set_redirect(make_link("post/view/$id"));
} }
@ -30,7 +30,7 @@ class Featured extends Extension
$image = Image::by_id($config->get_int("featured_id")); $image = Image::by_id($config->get_int("featured_id"));
if (!is_null($image)) { if (!is_null($image)) {
$page->set_mode(PageMode::DATA); $page->set_mode(PageMode::DATA);
$page->set_mime($image->get_mime()); $page->set_type($image->get_mime_type());
$page->set_data(file_get_contents($image->get_image_filename())); $page->set_data(file_get_contents($image->get_image_filename()));
} }
} }

View File

@ -18,7 +18,7 @@ class FeaturedTest extends ShimmiePHPUnitTestCase
$config->set_int("featured_id", $image_id); $config->set_int("featured_id", $image_id);
$this->get_page("post/list"); $this->get_page("post/list");
$this->assert_text("Featured Post"); $this->assert_text("Featured Image");
# FIXME: test changing from one feature to another # FIXME: test changing from one feature to another
@ -31,6 +31,6 @@ class FeaturedTest extends ShimmiePHPUnitTestCase
// after deletion, there should be no feature // after deletion, there should be no feature
$this->delete_image($image_id); $this->delete_image($image_id);
$this->get_page("post/list"); $this->get_page("post/list");
$this->assert_no_text("Featured Post"); $this->assert_no_text("Featured Image");
} }
} }

View File

@ -8,7 +8,7 @@ class FeaturedTheme extends Themelet
{ {
public function display_featured(Page $page, Image $image): void public function display_featured(Page $page, Image $image): void
{ {
$page->add_block(new Block("Featured Post", $this->build_featured_html($image), "left", 3)); $page->add_block(new Block("Featured Image", $this->build_featured_html($image), "left", 3));
} }
public function get_buttons_html(int $image_id): string public function get_buttons_html(int $image_id): string

View File

@ -2,7 +2,7 @@
class ForumInfo extends ExtensionInfo class ForumInfo extends ExtensionInfo
{ {
public const KEY = "forum"; public const KEY = "dorum";
public $key = self::KEY; public $key = self::KEY;
public $name = "Forum"; public $name = "Forum";

View File

@ -18,10 +18,10 @@ class Forum extends Extension
// shortcut to latest // shortcut to latest
if ($this->get_version("forum_version") < 1) { if ($config->get_int("forum_version") < 1) {
$database->create_table("forum_threads", " $database->create_table("forum_threads", "
id SCORE_AIPK, id SCORE_AIPK,
sticky BOOLEAN NOT NULL DEFAULT FALSE, sticky SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N,
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
@ -41,22 +41,19 @@ class Forum extends Extension
"); ");
$database->execute("CREATE INDEX forum_posts_date_idx ON forum_posts(date)", []); $database->execute("CREATE INDEX forum_posts_date_idx ON forum_posts(date)", []);
$config->set_int("forum_version", 2);
$config->set_int("forumTitleSubString", 25); $config->set_int("forumTitleSubString", 25);
$config->set_int("forumThreadsPerPage", 15); $config->set_int("forumThreadsPerPage", 15);
$config->set_int("forumPostsPerPage", 15); $config->set_int("forumPostsPerPage", 15);
$config->set_int("forumMaxCharsPerPost", 512); $config->set_int("forumMaxCharsPerPost", 512);
$this->set_version("forum_version", 3); log_info("forum", "extension installed");
} }
if ($this->get_version("forum_version") < 2) { if ($config->get_int("forum_version") < 2) {
$database->execute("ALTER TABLE forum_threads ADD FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE RESTRICT"); $database->execute("ALTER TABLE forum_threads ADD FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE RESTRICT");
$database->execute("ALTER TABLE forum_posts ADD FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE RESTRICT"); $database->execute("ALTER TABLE forum_posts ADD FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE RESTRICT");
$this->set_version("forum_version", 2); $config->set_int("forum_version", 2);
}
if ($this->get_version("forum_version") < 3) {
$database->standardise_boolean("forum_threads", "sticky");
$this->set_version("forum_version", 3);
} }
} }
@ -87,10 +84,6 @@ class Forum extends Extension
$event->add_stats("Forum posts: $posts_count, $posts_rate per day"); $event->add_stats("Forum posts: $posts_count, $posts_rate per day");
} }
public function onPageNavBuilding(PageNavBuildingEvent $event)
{
$event->add_nav_link("forum", new Link('forum/index'), "Forum");
}
public function onPageRequest(PageRequestEvent $event) public function onPageRequest(PageRequestEvent $event)
{ {
@ -309,7 +302,11 @@ class Forum extends Extension
private function save_new_thread(User $user) private function save_new_thread(User $user)
{ {
$title = html_escape($_POST["title"]); $title = html_escape($_POST["title"]);
$sticky = !empty($_POST["sticky"]); $sticky = !empty($_POST["sticky"]) ? html_escape($_POST["sticky"]) : "N";
if ($sticky == "") {
$sticky = "N";
}
global $database; global $database;
$database->execute( $database->execute(

View File

@ -203,7 +203,7 @@ class ForumTheme extends Themelet
$title = $thread["title"]; $title = $thread["title"];
} }
if (bool_escape($thread["sticky"])) { if ($thread["sticky"] == "Y") {
$sticky = "Sticky: "; $sticky = "Sticky: ";
} else { } else {
$sticky = ""; $sticky = "";

View File

@ -2,7 +2,7 @@
class ArchiveFileHandler extends DataHandlerExtension class ArchiveFileHandler extends DataHandlerExtension
{ {
protected $SUPPORTED_MIME = [MimeType::ZIP]; protected $SUPPORTED_MIME = [MIME_TYPE_ZIP];
public function onInitExt(InitExtEvent $event) public function onInitExt(InitExtEvent $event)
{ {
@ -21,7 +21,7 @@ class ArchiveFileHandler extends DataHandlerExtension
public function onDataUpload(DataUploadEvent $event) public function onDataUpload(DataUploadEvent $event)
{ {
if ($this->supported_mime($event->mime)) { if ($this->supported_ext($event->type)) {
global $config, $page; global $config, $page;
$tmp = sys_get_temp_dir(); $tmp = sys_get_temp_dir();
$tmpdir = "$tmp/shimmie-archive-{$event->hash}"; $tmpdir = "$tmp/shimmie-archive-{$event->hash}";
@ -29,19 +29,14 @@ class ArchiveFileHandler extends DataHandlerExtension
$cmd = str_replace('%f', $event->tmpname, $cmd); $cmd = str_replace('%f', $event->tmpname, $cmd);
$cmd = str_replace('%d', $tmpdir, $cmd); $cmd = str_replace('%d', $tmpdir, $cmd);
exec($cmd); exec($cmd);
if (file_exists($tmpdir)) {
try {
$results = add_dir($tmpdir); $results = add_dir($tmpdir);
if (count($results) > 0) { if (count($results) > 0) {
$page->flash("Adding files" . implode("\n", $results)); $page->flash("Adding files" . implode("\n", $results));
} }
} finally {
deltree($tmpdir); deltree($tmpdir);
}
$event->image_id = -2; // default -1 = upload wasn't handled $event->image_id = -2; // default -1 = upload wasn't handled
} }
} }
}
public function onDisplayingImage(DisplayingImageEvent $event) public function onDisplayingImage(DisplayingImageEvent $event)
{ {

View File

@ -2,7 +2,7 @@
class CBZFileHandler extends DataHandlerExtension class CBZFileHandler extends DataHandlerExtension
{ {
protected $SUPPORTED_MIME = [MimeType::COMIC_ZIP]; public $SUPPORTED_MIME = [MIME_TYPE_COMIC_ZIP];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{ {
@ -20,14 +20,14 @@ class CBZFileHandler extends DataHandlerExtension
unlink($tmp); unlink($tmp);
} }
protected function create_thumb(string $hash, string $mime): bool protected function create_thumb(string $hash, string $type): bool
{ {
$cover = $this->get_representative_image(warehouse_path(Image::IMAGE_DIR, $hash)); $cover = $this->get_representative_image(warehouse_path(Image::IMAGE_DIR, $hash));
create_scaled_image( create_scaled_image(
$cover, $cover,
warehouse_path(Image::THUMBNAIL_DIR, $hash), warehouse_path(Image::THUMBNAIL_DIR, $hash),
get_thumbnail_max_size_scaled(), get_thumbnail_max_size_scaled(),
MimeType::get_for_file($cover), get_extension(get_mime($cover)),
null null
); );
return true; return true;
@ -55,7 +55,7 @@ class CBZFileHandler extends DataHandlerExtension
sort($names); sort($names);
$cover = $names[0]; $cover = $names[0];
foreach ($names as $name) { foreach ($names as $name) {
if (str_contains(strtolower($name), "cover")) { if (strpos(strtolower($name), "cover") !== false) {
$cover = $name; $cover = $name;
break; break;
} }

View File

@ -2,7 +2,7 @@
class FlashFileHandler extends DataHandlerExtension class FlashFileHandler extends DataHandlerExtension
{ {
protected $SUPPORTED_MIME = [MimeType::FLASH]; protected $SUPPORTED_MIME = [MIME_TYPE_FLASH];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{ {

View File

@ -2,14 +2,14 @@
class IcoFileHandler extends DataHandlerExtension class IcoFileHandler extends DataHandlerExtension
{ {
protected $SUPPORTED_MIME = [MimeType::ICO, MimeType::ANI, MimeType::WIN_BITMAP, MimeType::ICO_OSX]; protected $SUPPORTED_MIME = [MIME_TYPE_ICO, MIME_TYPE_ANI];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{ {
$event->image->lossless = true; $event->image->lossless = true;
$event->image->video = false; $event->image->video = false;
$event->image->audio = false; $event->image->audio = false;
$event->image->image = ($event->mime!= MimeType::ANI); $event->image->image = ($event->ext!="ani");
$fp = fopen($event->file_name, "r"); $fp = fopen($event->file_name, "r");
try { try {
@ -25,10 +25,10 @@ class IcoFileHandler extends DataHandlerExtension
$event->image->height = $height == 0 ? 256 : $height; $event->image->height = $height == 0 ? 256 : $height;
} }
protected function create_thumb(string $hash, string $mime): bool protected function create_thumb(string $hash, string $type): bool
{ {
try { try {
create_image_thumb($hash, $mime, MediaEngine::IMAGICK); create_image_thumb($hash, $type, MediaEngine::IMAGICK);
return true; return true;
} catch (MediaException $e) { } catch (MediaException $e) {
log_warning("handle_ico", "Could not generate thumbnail. " . $e->getMessage()); log_warning("handle_ico", "Could not generate thumbnail. " . $e->getMessage());

View File

@ -1,11 +1,8 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
// TODO: Add support for generating an icon from embedded cover art
// TODO: MORE AUDIO FORMATS
class MP3FileHandler extends DataHandlerExtension class MP3FileHandler extends DataHandlerExtension
{ {
protected $SUPPORTED_MIME = [MimeType::MP3]; protected $SUPPORTED_MIME = [MIME_TYPE_MP3];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{ {
@ -26,6 +23,6 @@ class MP3FileHandler extends DataHandlerExtension
protected function check_contents(string $tmpname): bool protected function check_contents(string $tmpname): bool
{ {
return MimeType::get_for_file($tmpname) === MimeType::MP3; return get_mime($tmpname) === MIME_TYPE_MP3;
} }
} }

View File

@ -2,18 +2,26 @@
class PixelFileHandler extends DataHandlerExtension class PixelFileHandler extends DataHandlerExtension
{ {
protected $SUPPORTED_MIME = [MimeType::JPEG, MimeType::GIF, MimeType::PNG, MimeType::WEBP]; protected $SUPPORTED_MIME = [MIME_TYPE_JPEG, MIME_TYPE_GIF, MIME_TYPE_PNG, MIME_TYPE_WEBP];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{ {
$event->image->lossless = Media::is_lossless($event->file_name, $event->mime); if (in_array($event->ext, Media::LOSSLESS_FORMATS)) {
$event->image->lossless = true;
} elseif ($event->ext==EXTENSION_WEBP) {
$event->image->lossless = Media::is_lossless_webp($event->file_name);
}
if ($event->image->lossless==null) {
$event->image->lossless = false;
}
$event->image->audio = false; $event->image->audio = false;
switch ($event->mime) { switch ($event->ext) {
case MimeType::GIF: case EXTENSION_GIF:
$event->image->video = MimeType::is_animated_gif($event->file_name); $event->image->video = Media::is_animated_gif($event->file_name);
break; break;
case MimeType::WEBP: case EXTENSION_WEBP:
$event->image->video = MimeType::is_animated_webp($event->file_name); $event->image->video = Media::is_animated_webp($event->file_name);
break; break;
default: default:
$event->image->video = false; $event->image->video = false;

View File

@ -3,7 +3,7 @@ use enshrined\svgSanitize\Sanitizer;
class SVGFileHandler extends DataHandlerExtension class SVGFileHandler extends DataHandlerExtension
{ {
protected $SUPPORTED_MIME = [MimeType::SVG]; protected $SUPPORTED_MIME = [MIME_TYPE_SVG];
/** @var SVGFileHandlerTheme */ /** @var SVGFileHandlerTheme */
protected $theme; protected $theme;
@ -16,7 +16,7 @@ class SVGFileHandler extends DataHandlerExtension
$image = Image::by_id($id); $image = Image::by_id($id);
$hash = $image->hash; $hash = $image->hash;
$page->set_mime(MimeType::SVG); $page->set_type(MIME_TYPE_SVG);
$page->set_mode(PageMode::DATA); $page->set_mode(PageMode::DATA);
$sanitizer = new Sanitizer(); $sanitizer = new Sanitizer();
@ -67,7 +67,7 @@ class SVGFileHandler extends DataHandlerExtension
protected function check_contents(string $file): bool protected function check_contents(string $file): bool
{ {
if (MimeType::get_for_file($file)!==MimeType::SVG) { if (get_mime($file)!==MIME_TYPE_SVG) {
return false; return false;
} }

View File

@ -4,21 +4,20 @@ abstract class VideoFileHandlerConfig
{ {
public const PLAYBACK_AUTOPLAY = "video_playback_autoplay"; public const PLAYBACK_AUTOPLAY = "video_playback_autoplay";
public const PLAYBACK_LOOP = "video_playback_loop"; public const PLAYBACK_LOOP = "video_playback_loop";
public const PLAYBACK_MUTE = "video_playback_mute";
public const ENABLED_FORMATS = "video_enabled_formats"; public const ENABLED_FORMATS = "video_enabled_formats";
} }
class VideoFileHandler extends DataHandlerExtension class VideoFileHandler extends DataHandlerExtension
{ {
public const SUPPORTED_MIME = [ public const SUPPORTED_MIME = [
MimeType::ASF, MIME_TYPE_ASF,
MimeType::AVI, MIME_TYPE_AVI,
MimeType::FLASH_VIDEO, MIME_TYPE_FLASH_VIDEO,
MimeType::MKV, MIME_TYPE_MKV,
MimeType::MP4_VIDEO, MIME_TYPE_MP4_VIDEO,
MimeType::OGG_VIDEO, MIME_TYPE_OGG_VIDEO,
MimeType::QUICKTIME, MIME_TYPE_QUICKTIME,
MimeType::WEBM, MIME_TYPE_WEBM,
]; ];
protected $SUPPORTED_MIME = self::SUPPORTED_MIME; protected $SUPPORTED_MIME = self::SUPPORTED_MIME;
@ -28,18 +27,17 @@ class VideoFileHandler extends DataHandlerExtension
$config->set_default_bool(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY, true); $config->set_default_bool(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY, true);
$config->set_default_bool(VideoFileHandlerConfig::PLAYBACK_LOOP, true); $config->set_default_bool(VideoFileHandlerConfig::PLAYBACK_LOOP, true);
$config->set_default_bool(VideoFileHandlerConfig::PLAYBACK_MUTE, false);
$config->set_default_array( $config->set_default_array(
VideoFileHandlerConfig::ENABLED_FORMATS, VideoFileHandlerConfig::ENABLED_FORMATS,
[MimeType::FLASH_VIDEO, MimeType::MP4_VIDEO, MimeType::OGG_VIDEO, MimeType::WEBM] [MIME_TYPE_FLASH_VIDEO, MIME_TYPE_MP4_VIDEO, MIME_TYPE_OGG_VIDEO, MIME_TYPE_WEBM]
); );
} }
private function get_options(): array private function get_options(): array
{ {
$output = []; $output = [];
foreach ($this->SUPPORTED_MIME as $mime) { foreach ($this->SUPPORTED_MIME as $format) {
$output[MimeMap::get_name_for_mime($mime)] = $mime; $output[MIME_TYPE_MAP[$format][MIME_TYPE_MAP_NAME]] = $format;
} }
return $output; return $output;
} }
@ -47,12 +45,11 @@ class VideoFileHandler extends DataHandlerExtension
public function onSetupBuilding(SetupBuildingEvent $event) public function onSetupBuilding(SetupBuildingEvent $event)
{ {
$sb = new SetupBlock("Video Options"); $sb = new SetupBlock("Video Options");
$sb->start_table(); $sb->add_bool_option(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY, "Autoplay: ");
$sb->add_bool_option(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY, "Autoplay", true); $sb->add_label("<br>");
$sb->add_bool_option(VideoFileHandlerConfig::PLAYBACK_LOOP, "Loop", true); $sb->add_bool_option(VideoFileHandlerConfig::PLAYBACK_LOOP, "Loop: ");
$sb->add_bool_option(VideoFileHandlerConfig::PLAYBACK_MUTE, "Mute", true); $sb->add_label("<br>Enabled Formats:");
$sb->add_multichoice_option(VideoFileHandlerConfig::ENABLED_FORMATS, $this->get_options(), "Enabled Formats", true); $sb->add_multichoice_option(VideoFileHandlerConfig::ENABLED_FORMATS, $this->get_options());
$sb->end_table();
$event->panel->add_block($sb); $event->panel->add_block($sb);
} }
@ -67,7 +64,6 @@ class VideoFileHandler extends DataHandlerExtension
if (array_key_exists("streams", $data)) { if (array_key_exists("streams", $data)) {
$video = false; $video = false;
$audio = true; $audio = true;
$video_codec = null;
$streams = $data["streams"]; $streams = $data["streams"];
if (is_array($streams)) { if (is_array($streams)) {
foreach ($streams as $stream) { foreach ($streams as $stream) {
@ -80,7 +76,6 @@ class VideoFileHandler extends DataHandlerExtension
break; break;
case "video": case "video":
$video = true; $video = true;
$video_codec = $stream["codec_name"];
break; break;
} }
} }
@ -95,14 +90,7 @@ class VideoFileHandler extends DataHandlerExtension
} }
} }
$event->image->video = $video; $event->image->video = $video;
$event->image->video_codec = $video_codec;
$event->image->audio = $audio; $event->image->audio = $audio;
if ($event->image->get_mime()==MimeType::MKV &&
VideoContainers::is_video_codec_supported(VideoContainers::WEBM, $event->image->video_codec)) {
// WEBMs are MKVs with the VP9 or VP8 codec
// For browser-friendliness, we'll just change the mime type
$event->image->set_mime(MimeType::WEBM);
}
} }
} }
if (array_key_exists("format", $data)&& is_array($data["format"])) { if (array_key_exists("format", $data)&& is_array($data["format"])) {
@ -117,13 +105,17 @@ class VideoFileHandler extends DataHandlerExtension
} }
} }
protected function supported_mime(string $mime): bool protected function supported_ext(string $ext): bool
{ {
global $config; global $config;
$enabled_formats = $config->get_array(VideoFileHandlerConfig::ENABLED_FORMATS); $enabled_formats = $config->get_array(VideoFileHandlerConfig::ENABLED_FORMATS);
foreach ($enabled_formats as $format) {
return MimeType::matches_array($mime, $enabled_formats, true); if (in_array($ext, MIME_TYPE_MAP[$format][MIME_TYPE_MAP_EXT])) {
return true;
}
}
return false;
} }
protected function create_thumb(string $hash, string $type): bool protected function create_thumb(string $hash, string $type): bool
@ -136,13 +128,15 @@ class VideoFileHandler extends DataHandlerExtension
global $config; global $config;
if (file_exists($tmpname)) { if (file_exists($tmpname)) {
$mime = MimeType::get_for_file($tmpname); $mime = get_mime($tmpname);
$enabled_formats = $config->get_array(VideoFileHandlerConfig::ENABLED_FORMATS); $enabled_formats = $config->get_array(VideoFileHandlerConfig::ENABLED_FORMATS);
if (MimeType::matches_array($mime, $enabled_formats)) { foreach ($enabled_formats as $format) {
if (in_array($mime, MIME_TYPE_MAP[$format][MIME_TYPE_MAP_MIME])) {
return true; return true;
} }
} }
}
return false; return false;
} }
} }

View File

@ -7,11 +7,10 @@ class VideoFileHandlerTheme extends Themelet
global $config; global $config;
$ilink = $image->get_image_link(); $ilink = $image->get_image_link();
$thumb_url = make_http($image->get_thumb_link()); //used as fallback image $thumb_url = make_http($image->get_thumb_link()); //used as fallback image
$mime = strtolower($image->get_mime()); $ext = strtolower($image->get_ext());
$full_url = make_http($ilink); $full_url = make_http($ilink);
$autoplay = $config->get_bool(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY); $autoplay = $config->get_bool(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY);
$loop = $config->get_bool(VideoFileHandlerConfig::PLAYBACK_LOOP); $loop = $config->get_bool(VideoFileHandlerConfig::PLAYBACK_LOOP);
$mute = $config->get_bool(VideoFileHandlerConfig::PLAYBACK_MUTE);
$player = make_link('vendor/bower-asset/mediaelement/build/flashmediaelement.swf'); $player = make_link('vendor/bower-asset/mediaelement/build/flashmediaelement.swf');
$width="auto"; $width="auto";
@ -26,10 +25,11 @@ class VideoFileHandlerTheme extends Themelet
$html = "Video not playing? <a href='$ilink'>Click here</a> to download the file.<br/>"; $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 //Browser media format support: https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
$mime = get_mime_for_extension($ext);
if (MimeType::matches_array($mime, VideoFileHandler::SUPPORTED_MIME)) { if (in_array($mime, VideoFileHandler::SUPPORTED_MIME)) {
//FLV isn't supported by <video>, but it should always fallback to the flash-based method. //FLV isn't supported by <video>, but it should always fallback to the flash-based method.
if ($mime == MimeType::WEBM) { if ($mime == MIME_TYPE_WEBM) {
//Several browsers still lack WebM support sadly: https://caniuse.com/#feat=webm //Several browsers still lack WebM support sadly: https://caniuse.com/#feat=webm
$html .= "<!--[if IE]><p>To view webm files with IE, please <a href='https://tools.google.com/dlpage/webmmf/' target='_blank'>download this plugin</a>.</p><![endif]-->"; $html .= "<!--[if IE]><p>To view webm files with IE, please <a href='https://tools.google.com/dlpage/webmmf/' target='_blank'>download this plugin</a>.</p><![endif]-->";
} }
@ -50,17 +50,16 @@ class VideoFileHandlerTheme extends Themelet
<img alt='thumb' src=\"{$thumb_url}\" /> <img alt='thumb' src=\"{$thumb_url}\" />
</object>"; </object>";
if ($mime == MimeType::FLASH_VIDEO) { if ($mime == MIME_TYPE_FLASH_VIDEO) {
//FLV doesn't support <video>. //FLV doesn't support <video>.
$html .= $html_fallback; $html .= $html_fallback;
} else { } else {
$autoplay = ($autoplay ? ' autoplay' : ''); $autoplay = ($autoplay ? ' autoplay' : '');
$loop = ($loop ? ' loop' : ''); $loop = ($loop ? ' loop' : '');
$mute = ($mute ? ' muted' : '');
$html .= " $html .= "
<video controls class='shm-main-image' id='main_image' alt='main image' poster='$thumb_url' {$autoplay} {$loop} {$mute} <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%; object-fit: contain; background-color: black;'> style='height: $height; width: $width; max-width: 100%'>
<source src='{$ilink}' type='{$mime}'> <source src='{$ilink}' type='{$mime}'>
<!-- If browser doesn't support filetype, fallback to flash --> <!-- If browser doesn't support filetype, fallback to flash -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -9,7 +9,7 @@ class HomeInfo extends ExtensionInfo
public $authors =["Bzchan"=>"bzchan@animemahou.com"]; public $authors =["Bzchan"=>"bzchan@animemahou.com"];
public $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;
public $visibility = self::VISIBLE_ADMIN; public $visibility = self::VISIBLE_ADMIN;
public $description = "Displays a front page with logo, search box and post count"; public $description = "Displays a front page with logo, search box and image count";
public $documentation = public $documentation =
"Once enabled, the page will show up at the URL \"home\", so if you want "Once enabled, the page will show up at the URL \"home\", so if you want
this to be the front page of your site, you should go to \"Board Config\" this to be the front page of your site, you should go to \"Board Config\"

View File

@ -5,6 +5,22 @@ class Home extends Extension
/** @var HomeTheme */ /** @var HomeTheme */
protected $theme; protected $theme;
private $femDimensions = array(
array(23, 64),
array(14, 86),
array(31, 63),
array(37, 100),
array(24, 90)
);
private $femTags = array(
"Hatsune_Miku",
"Monika",
"Violet_Parr",
"Keith_Kogane",
"Rin_Kagamine",
);
public function onPageRequest(PageRequestEvent $event) public function onPageRequest(PageRequestEvent $event)
{ {
global $config, $page; global $config, $page;
@ -34,6 +50,27 @@ class Home extends Extension
$event->panel->add_block($sb); $event->panel->add_block($sb);
} }
private function addCountToBlankImage($charId, $digit)
{
$font = realpath('ext/home/vga.ttf');
$file = "ext/home/counters/femcounter/$charId.png";
$img = imagecreatefrompng($file);
$black = imagecolorallocate($img, 0, 0, 0);
$x = $this->femDimensions[$charId][0];
$y = $this->femDimensions[$charId][1];
imagettftext($img, 20, 0, $x, $y + 20, $black, $font, $digit);
imagetruecolortopalette($img, true, 16);
imagesavealpha($img, true);
imagecolortransparent($img, imagecolorat($img, 0, 0));
ob_start();
imagegif($img);
$image_data = ob_get_contents();
ob_end_clean();
$data = base64_encode($image_data);
imagedestroy($img);
return $data;
}
private function get_body() private function get_body()
{ {
@ -48,15 +85,24 @@ class Home extends Extension
$counter_dir = $config->get_string('home_counter', 'default'); $counter_dir = $config->get_string('home_counter', 'default');
$total = Image::count_images(); $total = Image::count_images();
$streak = Image::count_upload_streak();
$strtotal = "$total"; $strtotal = "$total";
$num_comma = number_format($total); $num_comma = number_format($total);
$streak_comma = number_format($streak);
$counter_text = ""; $counter_text = "";
$length = strlen($strtotal); $length = strlen($strtotal);
for ($n=0; $n<$length; $n++) { for ($n=0; $n<$length; $n++) {
$cur = $strtotal[$n]; $cur = $strtotal[$n];
if ($counter_dir === 'femcounter') {
$charId = $n % count($this->femTags);
$base64url = $this->addCountToBlankImage($charId, $cur);
$tag = $this->femTags[$charId];
$counter_text .= " <a href='$base_href/post/list/$tag/1'><img alt='$cur' title='$tag' src='data:image/gif;base64,$base64url' /></a> ";
} else {
$counter_text .= " <img alt='$cur' src='$base_href/ext/home/counters/$counter_dir/$cur.gif' /> "; $counter_text .= " <img alt='$cur' src='$base_href/ext/home/counters/$counter_dir/$cur.gif' /> ";
} }
}
// get the homelinks and process them // get the homelinks and process them
if (strlen($config->get_string('home_links', '')) > 0) { if (strlen($config->get_string('home_links', '')) > 0) {
@ -74,6 +120,6 @@ class Home extends Extension
$main_links = format_text($main_links); $main_links = format_text($main_links);
$main_text = $config->get_string('home_text', ''); $main_text = $config->get_string('home_text', '');
return $this->theme->build_body($sitename, $main_links, $main_text, $contact_link, $num_comma, $counter_text); return $this->theme->build_body($sitename, $main_links, $main_text, $contact_link, $num_comma, $counter_text, $streak_comma);
} }
} }

View File

@ -5,7 +5,7 @@ div#front-page div#links a {margin: 0 0.5em;}
div#front-page li {list-style-type: none; margin: 0;} div#front-page li {list-style-type: none; margin: 0;}
@media (max-width: 800px) { @media (max-width: 800px) {
div#front-page h1 {font-size: 3em; margin-top: 0.5em; margin-bottom: 0.5em;} div#front-page h1 {font-size: 3em; margin-top: 0.5em; margin-bottom: 0.5em;}
#counter {display: none;} /*#counter {display: none;}*/
} }
div#front-page > #search > form { margin: 0 auto; } div#front-page > #search > form { margin: 0 auto; }

BIN
ext/home/vga.ttf Normal file

Binary file not shown.

View File

@ -2,29 +2,21 @@
abstract class ImageConfig abstract class ImageConfig
{ {
const VERSION = 'ext_image_version';
const THUMB_ENGINE = 'thumb_engine'; const THUMB_ENGINE = 'thumb_engine';
const THUMB_WIDTH = 'thumb_width'; const THUMB_WIDTH = 'thumb_width';
const THUMB_HEIGHT = 'thumb_height'; const THUMB_HEIGHT = 'thumb_height';
const THUMB_SCALING = 'thumb_scaling'; const THUMB_SCALING = 'thumb_scaling';
const THUMB_QUALITY = 'thumb_quality'; const THUMB_QUALITY = 'thumb_quality';
const THUMB_MIME = 'thumb_mime'; const THUMB_TYPE = 'thumb_type';
const THUMB_FIT = 'thumb_fit'; const THUMB_FIT = 'thumb_fit';
const THUMB_ALPHA_COLOR ='thumb_alpha_color';
const SHOW_META = 'image_show_meta'; const SHOW_META = 'image_show_meta';
const ILINK = 'image_ilink'; const ILINK = 'image_ilink';
const TLINK = 'image_tlink'; const TLINK = 'image_tlink';
const TIP = 'image_tip'; const TIP = 'image_tip';
const INFO = 'image_info';
const EXPIRES = 'image_expires'; const EXPIRES = 'image_expires';
const UPLOAD_COLLISION_HANDLER = 'upload_collision_handler'; const UPLOAD_COLLISION_HANDLER = 'upload_collision_handler';
const COLLISION_MERGE = 'merge'; const COLLISION_MERGE = 'merge';
const COLLISION_ERROR = 'error'; const COLLISION_ERROR = 'error';
const ON_DELETE = 'image_on_delete';
const ON_DELETE_NEXT = 'next';
const ON_DELETE_LIST = 'list';
} }

View File

@ -5,7 +5,7 @@ class ImageIOInfo extends ExtensionInfo
public const KEY = "image"; public const KEY = "image";
public $key = self::KEY; public $key = self::KEY;
public $name = "Post Manager"; public $name = "Image Manager";
public $url = self::SHIMMIE_URL; public $url = self::SHIMMIE_URL;
public $authors = [self::SHISH_NAME=> self::SHISH_EMAIL, "jgen"=>"jgen.tech@gmail.com"]; public $authors = [self::SHISH_NAME=> self::SHISH_EMAIL, "jgen"=>"jgen.tech@gmail.com"];
public $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;

View File

@ -10,39 +10,30 @@ class ImageIO extends Extension
/** @var ImageIOTheme */ /** @var ImageIOTheme */
protected $theme; protected $theme;
const COLLISION_OPTIONS = [ const COLLISION_OPTIONS = ['Error'=>ImageConfig::COLLISION_ERROR, 'Merge'=>ImageConfig::COLLISION_MERGE];
'Error'=>ImageConfig::COLLISION_ERROR,
'Merge'=>ImageConfig::COLLISION_MERGE
];
const ON_DELETE_OPTIONS = [
'Return to post list'=>ImageConfig::ON_DELETE_LIST,
'Go to next post'=>ImageConfig::ON_DELETE_NEXT
];
const EXIF_READ_FUNCTION = "exif_read_data"; const EXIF_READ_FUNCTION = "exif_read_data";
const THUMBNAIL_ENGINES = [ const THUMBNAIL_ENGINES = [
'Built-in GD' => MediaEngine::GD, 'Built-in GD' => MediaEngine::GD,
'ImageMagick' => MediaEngine::IMAGICK 'ImageMagick' => MediaEngine::IMAGICK
]; ];
const THUMBNAIL_TYPES = [ const THUMBNAIL_TYPES = [
'JPEG' => MimeType::JPEG, 'JPEG' => EXTENSION_JPG,
'WEBP (Not IE/Safari compatible)' => MimeType::WEBP 'WEBP (Not IE/Safari compatible)' => EXTENSION_WEBP
]; ];
public function onInitExt(InitExtEvent $event) public function onInitExt(InitExtEvent $event)
{ {
global $config; global $config;
$config->set_default_string(ImageConfig::THUMB_ENGINE, MediaEngine::GD);
$config->set_default_int(ImageConfig::THUMB_WIDTH, 192); $config->set_default_int(ImageConfig::THUMB_WIDTH, 192);
$config->set_default_int(ImageConfig::THUMB_HEIGHT, 192); $config->set_default_int(ImageConfig::THUMB_HEIGHT, 192);
$config->set_default_int(ImageConfig::THUMB_SCALING, 100); $config->set_default_int(ImageConfig::THUMB_SCALING, 100);
$config->set_default_int(ImageConfig::THUMB_QUALITY, 75); $config->set_default_int(ImageConfig::THUMB_QUALITY, 75);
$config->set_default_string(ImageConfig::THUMB_MIME, MimeType::JPEG); $config->set_default_string(ImageConfig::THUMB_TYPE, EXTENSION_JPG);
$config->set_default_string(ImageConfig::THUMB_FIT, Media::RESIZE_TYPE_FIT); $config->set_default_string(ImageConfig::THUMB_FIT, Media::RESIZE_TYPE_FIT);
$config->set_default_string(ImageConfig::THUMB_ALPHA_COLOR, Media::DEFAULT_ALPHA_CONVERSION_COLOR);
if (function_exists(self::EXIF_READ_FUNCTION)) { if (function_exists(self::EXIF_READ_FUNCTION)) {
$config->set_default_bool(ImageConfig::SHOW_META, false); $config->set_default_bool(ImageConfig::SHOW_META, false);
@ -54,43 +45,18 @@ class ImageIO extends Extension
$config->set_default_int(ImageConfig::EXPIRES, (60*60*24*31)); // defaults to one month $config->set_default_int(ImageConfig::EXPIRES, (60*60*24*31)); // defaults to one month
} }
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{
global $config;
if ($this->get_version(ImageConfig::VERSION) < 1) {
switch ($config->get_string("thumb_type")) {
case FileExtension::WEBP:
$config->set_string(ImageConfig::THUMB_MIME, MimeType::WEBP);
break;
case FileExtension::JPEG:
$config->set_string(ImageConfig::THUMB_MIME, MimeType::JPEG);
break;
}
$config->set_string("thumb_type", null);
$this->set_version(ImageConfig::VERSION, 1);
}
}
public function onPageRequest(PageRequestEvent $event) public function onPageRequest(PageRequestEvent $event)
{ {
global $config;
if ($event->page_matches("image/delete")) { if ($event->page_matches("image/delete")) {
global $page, $user; global $page, $user;
if ($user->can(Permissions::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(int_escape($_POST['image_id'])); $image = Image::by_id(int_escape($_POST['image_id']));
if ($image) { if ($image) {
send_event(new ImageDeletionEvent($image)); send_event(new ImageDeletionEvent($image));
if ($config->get_string(ImageConfig::ON_DELETE)===ImageConfig::ON_DELETE_NEXT) {
redirect_to_next_image($image);
} else {
$page->set_mode(PageMode::REDIRECT); $page->set_mode(PageMode::REDIRECT);
$page->set_redirect(referer_or(make_link("post/list"), ['post/view'])); $page->set_redirect(referer_or(make_link("post/list"), ['post/view']));
} }
} }
}
} elseif ($event->page_matches("image/replace")) { } elseif ($event->page_matches("image/replace")) {
global $page, $user; global $page, $user;
if ($user->can(Permissions::REPLACE_IMAGE) && isset($_POST['image_id']) && $user->check_auth_token()) { if ($user->can(Permissions::REPLACE_IMAGE) && isset($_POST['image_id']) && $user->check_auth_token()) {
@ -100,7 +66,7 @@ class ImageIO extends Extension
$page->set_redirect(make_link('upload/replace/'.$image->id)); $page->set_redirect(make_link('upload/replace/'.$image->id));
} else { } else {
/* Invalid image ID */ /* Invalid image ID */
throw new ImageReplaceException("Post to replace does not exist."); throw new ImageReplaceException("Image to replace does not exist.");
} }
} }
} elseif ($event->page_matches("image")) { } elseif ($event->page_matches("image")) {
@ -158,7 +124,7 @@ class ImageIO extends Extension
$event->image = Image::by_id($existing->id); $event->image = Image::by_id($existing->id);
return; return;
} else { } else {
$error = "Post <a href='".make_link("post/view/{$existing->id}")."'>{$existing->id}</a> ". $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); "already has hash {$image->hash}:<p>".$this->theme->build_thumb_html($existing);
throw new ImageAdditionException($error); throw new ImageAdditionException($error);
} }
@ -167,7 +133,7 @@ class ImageIO extends Extension
// actually insert the info // actually insert the info
$image->save_to_db(); $image->save_to_db();
log_info("image", "Uploaded >>{$image->id} ({$image->hash})"); log_info("image", "Uploaded Image #{$image->id} ({$image->hash})");
# at this point in time, the image's tags haven't really been set, # at this point in time, the image's tags haven't really been set,
# and so, having $image->tag_array set to something is a lie (but # and so, having $image->tag_array set to something is a lie (but
@ -179,7 +145,7 @@ class ImageIO extends Extension
send_event(new TagSetEvent($image, $tags_to_set)); send_event(new TagSetEvent($image, $tags_to_set));
if ($image->source !== null) { if ($image->source !== null) {
log_info("core-image", "Source for >>{$image->id} set to: {$image->source}"); log_info("core-image", "Source for Image #{$image->id} set to: {$image->source}");
} }
} catch (ImageAdditionException $e) { } catch (ImageAdditionException $e) {
throw new UploadException($e->error); throw new UploadException($e->error);
@ -197,20 +163,16 @@ class ImageIO extends Extension
$id = $event->id; $id = $event->id;
$image = $event->image; $image = $event->image;
$image->set_mime(
MimeType::get_for_file($image->get_image_filename())
);
/* Check to make sure the image exists. */ /* Check to make sure the image exists. */
$existing = Image::by_id($id); $existing = Image::by_id($id);
if (is_null($existing)) { if (is_null($existing)) {
throw new ImageReplaceException("Post to replace does not exist!"); throw new ImageReplaceException("Image to replace does not exist!");
} }
$duplicate = Image::by_hash($image->hash); $duplicate = Image::by_hash($image->hash);
if (!is_null($duplicate) && $duplicate->id!=$id) { if (!is_null($duplicate) && $duplicate->id!=$id) {
$error = "Post <a href='" . make_link("post/view/{$duplicate->id}") . "'>{$duplicate->id}</a> " . $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); "already has hash {$image->hash}:<p>" . $this->theme->build_thumb_html($duplicate);
throw new ImageReplaceException($error); throw new ImageReplaceException($error);
} }
@ -234,9 +196,9 @@ class ImageIO extends Extension
$existing->remove_image_only(); // Actually delete the old image file from disk $existing->remove_image_only(); // Actually delete the old image file from disk
/* Generate new thumbnail */ /* Generate new thumbnail */
send_event(new ThumbnailGenerationEvent($image->hash, strtolower($image->get_mime()))); send_event(new ThumbnailGenerationEvent($image->hash, strtolower($image->ext)));
log_info("image", "Replaced >>{$id} with ({$image->hash})"); log_info("image", "Replaced Image #{$id} with ({$image->hash})");
} catch (ImageReplaceException $e) { } catch (ImageReplaceException $e) {
throw new UploadException($e->error); throw new UploadException($e->error);
} }
@ -249,23 +211,21 @@ class ImageIO extends Extension
$i_days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1; $i_days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1;
$h_image_rate = sprintf("%.1f", ($i_image_count / $i_days_old)); $h_image_rate = sprintf("%.1f", ($i_image_count / $i_days_old));
$images_link = make_link("post/list/user=$u_name/1"); $images_link = make_link("post/list/user=$u_name/1");
$event->add_stats("<a href='$images_link'>Posts uploaded</a>: $i_image_count, $h_image_rate per day"); $event->add_stats("<a href='$images_link'>Images uploaded</a>: $i_image_count, $h_image_rate per day");
} }
public function onSetupBuilding(SetupBuildingEvent $event) public function onSetupBuilding(SetupBuildingEvent $event)
{ {
global $config; global $config;
$sb = new SetupBlock("Post Options"); $sb = new SetupBlock("Image Options");
$sb->start_table(); $sb->start_table();
$sb->position = 30; $sb->position = 30;
// advanced only // advanced only
//$sb->add_text_option(ImageConfig::ILINK, "Image link: "); //$sb->add_text_option(ImageConfig::ILINK, "Image link: ");
//$sb->add_text_option(ImageConfig::TLINK, "<br>Thumbnail link: "); //$sb->add_text_option(ImageConfig::TLINK, "<br>Thumbnail link: ");
$sb->add_text_option(ImageConfig::TIP, "Post tooltip", true); $sb->add_text_option(ImageConfig::TIP, "Image tooltip", true);
$sb->add_text_option(ImageConfig::INFO, "Post info", true);
$sb->add_choice_option(ImageConfig::UPLOAD_COLLISION_HANDLER, self::COLLISION_OPTIONS, "Upload collision handler", true); $sb->add_choice_option(ImageConfig::UPLOAD_COLLISION_HANDLER, self::COLLISION_OPTIONS, "Upload collision handler", true);
$sb->add_choice_option(ImageConfig::ON_DELETE, self::ON_DELETE_OPTIONS, "On Delete", true);
if (function_exists(self::EXIF_READ_FUNCTION)) { if (function_exists(self::EXIF_READ_FUNCTION)) {
$sb->add_bool_option(ImageConfig::SHOW_META, "Show metadata", true); $sb->add_bool_option(ImageConfig::SHOW_META, "Show metadata", true);
} }
@ -275,7 +235,7 @@ class ImageIO extends Extension
$sb = new SetupBlock("Thumbnailing"); $sb = new SetupBlock("Thumbnailing");
$sb->start_table(); $sb->start_table();
$sb->add_choice_option(ImageConfig::THUMB_ENGINE, self::THUMBNAIL_ENGINES, "Engine", true); $sb->add_choice_option(ImageConfig::THUMB_ENGINE, self::THUMBNAIL_ENGINES, "Engine", true);
$sb->add_choice_option(ImageConfig::THUMB_MIME, self::THUMBNAIL_TYPES, "Filetype", true); $sb->add_choice_option(ImageConfig::THUMB_TYPE, self::THUMBNAIL_TYPES, "Filetype", true);
$sb->add_int_option(ImageConfig::THUMB_WIDTH, "Max Width", true); $sb->add_int_option(ImageConfig::THUMB_WIDTH, "Max Width", true);
$sb->add_int_option(ImageConfig::THUMB_HEIGHT, "Max Height", true); $sb->add_int_option(ImageConfig::THUMB_HEIGHT, "Max Height", true);
@ -289,9 +249,6 @@ class ImageIO extends Extension
$sb->add_int_option(ImageConfig::THUMB_QUALITY, "Quality", true); $sb->add_int_option(ImageConfig::THUMB_QUALITY, "Quality", true);
$sb->add_int_option(ImageConfig::THUMB_SCALING, "High-DPI Scale %", true); $sb->add_int_option(ImageConfig::THUMB_SCALING, "High-DPI Scale %", true);
if ($config->get_string(ImageConfig::THUMB_MIME)===MimeType::JPEG) {
$sb->add_color_option(ImageConfig::THUMB_ALPHA_COLOR, "Alpha Conversion Color", true);
}
$sb->end_table(); $sb->end_table();
@ -301,7 +258,7 @@ class ImageIO extends Extension
public function onParseLinkTemplate(ParseLinkTemplateEvent $event) public function onParseLinkTemplate(ParseLinkTemplateEvent $event)
{ {
$fname = $event->image->get_filename(); $fname = $event->image->get_filename();
$base_fname = str_contains($fname, '.') ? substr($fname, 0, strrpos($fname, '.')) : $fname; $base_fname = strpos($fname, '.') ? substr($fname, 0, strrpos($fname, '.')) : $fname;
$event->replace('$id', (string)$event->image->id); $event->replace('$id', (string)$event->image->id);
$event->replace('$hash_ab', substr($event->image->hash, 0, 2)); $event->replace('$hash_ab', substr($event->image->hash, 0, 2));
@ -309,31 +266,31 @@ class ImageIO extends Extension
$event->replace('$hash', $event->image->hash); $event->replace('$hash', $event->image->hash);
$event->replace('$filesize', to_shorthand_int($event->image->filesize)); $event->replace('$filesize', to_shorthand_int($event->image->filesize));
$event->replace('$filename', $base_fname); $event->replace('$filename', $base_fname);
$event->replace('$ext', $event->image->get_ext());
$event->replace('$date', autodate($event->image->posted, false)); $event->replace('$date', autodate($event->image->posted, false));
$event->replace("\\n", "\n");
} }
private function send_file(int $image_id, string $type) private function send_file(int $image_id, string $type)
{ {
global $config, $page; global $config;
$image = Image::by_id($image_id); $image = Image::by_id($image_id);
global $page;
if (!is_null($image)) { if (!is_null($image)) {
if ($type == "thumb") { if ($type == "thumb") {
$mime = $config->get_string(ImageConfig::THUMB_MIME); $ext = $config->get_string(ImageConfig::THUMB_TYPE);
$page->set_type(get_mime_for_extension($ext));
$file = $image->get_thumb_filename(); $file = $image->get_thumb_filename();
} else { } else {
$mime = $image->get_mime(); $page->set_type($image->get_mime_type());
$file = $image->get_image_filename(); $file = $image->get_image_filename();
} }
if (!file_exists($file)) { if (!file_exists($file)) {
http_response_code(404); http_response_code(404);
die(); die();
} }
$page->set_mime($mime);
if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])) { if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])) {
$if_modified_since = preg_replace('/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"]); $if_modified_since = preg_replace('/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"]);
@ -362,14 +319,12 @@ class ImageIO extends Extension
} }
$page->add_http_header('Expires: ' . $expires); $page->add_http_header('Expires: ' . $expires);
} }
send_event(new ImageDownloadingEvent($image, $file, $mime));
} else { } else {
$page->set_title("Not Found"); $page->set_title("Not Found");
$page->set_heading("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( $page->add_block(new Block(
"Post not in database", "Image not in database",
"The requested image was not found in the database" "The requested image was not found in the database"
)); ));
} }

View File

@ -5,7 +5,7 @@ class ImageBanInfo extends ExtensionInfo
public const KEY = "image_hash_ban"; public const KEY = "image_hash_ban";
public $key = self::KEY; public $key = self::KEY;
public $name = "Post Hash Ban"; public $name = "Image Hash Ban";
public $url = "http://atravelinggeek.com/"; public $url = "http://atravelinggeek.com/";
public $authors = ["ATravelingGeek"=>"atg@atravelinggeek.com"]; public $authors = ["ATravelingGeek"=>"atg@atravelinggeek.com"];
public $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;

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