Merge pull request #1 from shish/master

Updating to current version
This commit is contained in:
yls4 2023-03-26 17:09:08 -05:00 committed by GitHub
commit d6a0d0cb8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
491 changed files with 5273 additions and 2760 deletions

View File

@ -1,10 +1,3 @@
# In retrospect I'm less of a fan of tabs for indentation, because
# while they're better when they work, they're worse when they don't
# work, and so many people use terrible editors when they don't work
# that everything is inconsistent... but tabs are what Shimmie went
# with back in the 90's, so that's what we use now, and we deal with
# the pain of making sure everybody configures their editor properly
# top-most EditorConfig file
root = true

View File

@ -12,44 +12,63 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set Up Cache
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
vendor
key: php-cs-fixer-${{ hashFiles('composer.lock') }}
- name: Validate composer.json and composer.lock
run: composer validate
- name: Install PHP dependencies
run: composer update && composer install --prefer-dist --no-progress
run: composer install --prefer-dist --no-progress
- name: Set up PHP
uses: shivammathur/setup-php@master
with:
php-version: 7.4
php-version: 8.1
- name: Format
run: ./vendor/bin/php-cs-fixer fix && git diff --exit-code
- name: Check format
run: ./vendor/bin/php-cs-fixer fix --dry-run
static:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Set Up Cache
uses: actions/cache@v3
with:
path: |
vendor
key: phpstan-${{ hashFiles('composer.lock') }}
- name: Install PHP dependencies
run: composer install --prefer-dist --no-progress
- name: PHPStan
uses: php-actions/phpstan@v3
with:
configuration: tests/phpstan.neon
memory_limit: 1G
test:
name: PHP ${{ matrix.php }} / DB ${{ matrix.database }}
strategy:
fail-fast: false
matrix:
php: ['7.4', '8.0']
php: ['8.1', '8.2']
database: ['pgsql', 'mysql', 'sqlite']
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Set Up Cache
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
vendor
@ -106,7 +125,6 @@ jobs:
vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover
- name: Upload coverage
if: matrix.php == '7.4'
if: matrix.php == '8.1'
run: |
wget https://scrutinizer-ci.com/ocular.phar
php ocular.phar code-coverage:upload --format=php-clover data/coverage.clover
vendor/bin/ocular code-coverage:upload --format=php-clover data/coverage.clover

View File

@ -1,19 +1,17 @@
<?php
$finder = PhpCsFixer\Finder::create()
$_phpcs_finder = PhpCsFixer\Finder::create()
->exclude('ext/amazon_s3/lib')
->exclude('vendor')
->exclude('data')
->in(__DIR__)
;
$config = new PhpCsFixer\Config();
return $config->setRules([
$_phpcs_config = new PhpCsFixer\Config();
return $_phpcs_config->setRules([
'@PSR12' => true,
//'strict_param' => true,
'array_syntax' => ['syntax' => 'short'],
])
->setFinder($finder)
;
?>
->setFinder($_phpcs_finder)
;

View File

@ -6,6 +6,7 @@ filter:
excluded_paths: [ext/*/lib/*,ext/tagger/script.js,tests/*]
build:
image: default-bionic
nodes:
analysis:
tests:

View File

@ -1,7 +1,10 @@
ARG PHP_VERSION=8.2
# "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)
FROM debian:stable AS app
RUN apt update && apt install -y composer php7.4-gd php7.4-dom php7.4-sqlite3 php-xdebug imagemagick
FROM debian:unstable AS app
RUN apt update && apt upgrade -y
RUN apt install -y composer php${PHP_VERSION}-gd php${PHP_VERSION}-xml php${PHP_VERSION}-sqlite3 php${PHP_VERSION}-xdebug imagemagick
COPY composer.json composer.lock /app/
WORKDIR /app
RUN composer install --no-dev
@ -10,8 +13,9 @@ COPY . /app/
# 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 --no-dev && composer install` doesn't install dev
FROM debian:stable AS tests
RUN apt update && apt install -y composer php7.4-gd php7.4-dom php7.4-sqlite3 php-xdebug imagemagick
FROM debian:unstable AS tests
RUN apt update && apt upgrade -y
RUN apt install -y composer php${PHP_VERSION}-gd php${PHP_VERSION}-xml php${PHP_VERSION}-sqlite3 php${PHP_VERSION}-xdebug imagemagick
COPY composer.json composer.lock /app/
WORKDIR /app
RUN composer install
@ -25,22 +29,24 @@ RUN [ $RUN_TESTS = false ] || (\
echo '=== Cleaning ===' && rm -rf data)
# Build su-exec so that our final image can be nicer
FROM debian:stable AS suexec
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; \
gcc -Wall /usr/local/bin/su-exec.c -o/usr/local/bin/su-exec; \
chown root:root /usr/local/bin/su-exec; \
chmod 0755 /usr/local/bin/su-exec;
FROM debian:unstable AS suexec
RUN apt update && apt upgrade -y
RUN apt 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; \
gcc -Wall /usr/local/bin/su-exec.c -o/usr/local/bin/su-exec; \
chown root:root /usr/local/bin/su-exec; \
chmod 0755 /usr/local/bin/su-exec;
# Actually run shimmie
FROM debian:stable
FROM debian:unstable
EXPOSE 8000
HEALTHCHECK --interval=1m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1
ENV UID=1000 \
GID=1000
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 \
imagemagick zip unzip && \
RUN apt update && apt upgrade -y && apt install -y \
php${PHP_VERSION}-cli php${PHP_VERSION}-gd php${PHP_VERSION}-zip php${PHP_VERSION}-xml php${PHP_VERSION}-mbstring \
php${PHP_VERSION}-pgsql php${PHP_VERSION}-mysql php${PHP_VERSION}-sqlite3 \
curl imagemagick ffmpeg zip unzip && \
rm -rf /var/lib/apt/lists/*
COPY --from=app /app /app
COPY --from=suexec /usr/local/bin/su-exec /usr/local/bin/su-exec

View File

@ -7,7 +7,7 @@
"config": {
"platform": {
"php": "7.4.0"
"php": "8.1.0"
}
},
@ -31,7 +31,7 @@
],
"require" : {
"php" : "^7.4 | ^8.0",
"php" : "^8.1",
"ext-pdo": "*",
"ext-json": "*",
"ext-fileinfo": "*",
@ -39,28 +39,33 @@
"flexihash/flexihash" : "^2.0",
"ifixit/php-akismet" : "^1.0",
"google/recaptcha" : "^1.1",
"dapphp/securimage" : "^3.6",
"shish/eventtracer-php" : "^2.0",
"shish/ffsphp" : "^1.0",
"shish/microcrud" : "^2.0",
"shish/microhtml" : "^2.0",
"shish/gqla" : "dev-main",
"enshrined/svg-sanitize" : "^0.15",
"bower-asset/jquery" : "^1.12",
"bower-asset/jquery-timeago" : "^1.5",
"bower-asset/js-cookie" : "^2.1"
"bower-asset/js-cookie" : "^2.1",
"psr/simple-cache" : "^1.0",
"sabre/cache" : "^2.0.1",
"naroga/redis-cache": "dev-master"
},
"require-dev" : {
"phpunit/phpunit" : "^9.0",
"friendsofphp/php-cs-fixer" : "^3.4"
"friendsofphp/php-cs-fixer" : "^3.12",
"scrutinizer/ocular": "dev-master",
"phpstan/phpstan": "1.10.x-dev"
},
"suggest": {
"ext-memcache": "memcache caching",
"ext-memcached": "memcached caching",
"ext-apc": "apc caching",
"ext-apcu": "apc caching",
"ext-redis": "redis caching",
"ext-dom": "some extensions",
"ext-curl": "some extensions",
"ext-ctype": "some extensions",
"ext-json": "some extensions",

2713
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,18 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
require_once "core/event.php";
abstract class PageMode
enum PageMode: string
{
public const REDIRECT = 'redirect';
public const DATA = 'data';
public const PAGE = 'page';
public const FILE = 'file';
public const MANUAL = 'manual';
case REDIRECT = 'redirect';
case DATA = 'data';
case PAGE = 'page';
case FILE = 'file';
case MANUAL = 'manual';
}
/**
@ -22,13 +25,13 @@ abstract class PageMode
*/
class BasePage
{
public string $mode = PageMode::PAGE;
public PageMode $mode = PageMode::PAGE;
private string $mime;
/**
* Set what this page should do; "page", "data", or "redirect".
*/
public function set_mode(string $mode): void
public function set_mode(PageMode $mode): void
{
$this->mode = $mode;
}
@ -227,7 +230,7 @@ class BasePage
public function send_headers(): void
{
if (!headers_sent()) {
header("HTTP/1.0 {$this->code} Shimmie");
header("HTTP/1.1 {$this->code} Shimmie");
header("Content-type: " . $this->mime);
header("X-Powered-By: Shimmie-" . VERSION);
@ -255,7 +258,7 @@ class BasePage
case PageMode::MANUAL:
break;
case PageMode::PAGE:
usort($this->blocks, "blockcmp");
usort($this->blocks, "Shimmie2\blockcmp");
$this->add_auto_html_headers();
$this->render();
break;
@ -270,6 +273,7 @@ class BasePage
if (!is_null($this->filename)) {
header('Content-Disposition: ' . $this->disposition . '; filename=' . $this->filename);
}
assert($this->file, "file should not be null with PageMode::FILE");
// https://gist.github.com/codler/3906826
$size = filesize($this->file); // File size
@ -468,8 +472,8 @@ class BasePage
}
$sub_links = $sub_links??[];
usort($nav_links, "sort_nav_links");
usort($sub_links, "sort_nav_links");
usort($nav_links, "Shimmie2\sort_nav_links");
usort($sub_links, "Shimmie2\sort_nav_links");
return [$nav_links, $sub_links];
}

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
/**
* Class BaseThemelet
*
@ -64,7 +66,7 @@ class BaseThemelet
}
$custom_classes = "";
if (class_exists("Relationships")) {
if (class_exists("Shimmie2\Relationships")) {
if (property_exists($image, 'parent_id') && $image->parent_id !== null) {
$custom_classes .= "shm-thumb-has_parent ";
}
@ -136,8 +138,8 @@ class BaseThemelet
$next_html = $at_end ? "Next" : $this->gen_page_link($base_url, $query, $next, "Next");
$last_html = $at_end ? "Last" : $this->gen_page_link($base_url, $query, $total_pages, "Last");
$start = $current_page-5 > 1 ? $current_page-5 : 1;
$end = $start+10 < $total_pages ? $start+10 : $total_pages;
$start = max($current_page - 5, 1);
$end = min($start + 10, $total_pages);
$pages = [];
foreach (range($start, $end) as $i) {

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
/**
* Class Block
*
@ -43,10 +45,10 @@ class Block
*/
public bool $is_content = true;
public function __construct(string $header=null, string $body=null, string $section="main", int $position=50, string $id=null)
public function __construct(string $header=null, string|\MicroHTML\HTMLElement $body=null, string $section="main", int $position=50, string $id=null)
{
$this->header = $header;
$this->body = $body;
$this->body = (string)$body;
$this->section = $section;
$this->position = $position;

View File

@ -1,200 +1,129 @@
<?php
declare(strict_types=1);
interface CacheEngine
namespace Shimmie2;
use Psr\SimpleCache\CacheInterface;
class EventTracingCache implements CacheInterface
{
public function get(string $key);
public function set(string $key, $val, int $time=0): void;
public function delete(string $key): void;
}
private CacheInterface $engine;
private \EventTracer $tracer;
private int $hits=0;
private int $misses=0;
class NoCache implements CacheEngine
{
public function get(string $key)
public function __construct(CacheInterface $engine, \EventTracer $tracer)
{
return false;
}
public function set(string $key, $val, int $time=0): void
{
}
public function delete(string $key): void
{
}
}
class MemcachedCache implements CacheEngine
{
public ?Memcached $memcache=null;
public function __construct(string $args)
{
$hp = explode(":", $args);
$this->memcache = new Memcached();
#$this->memcache->setOption(Memcached::OPT_COMPRESSION, False);
#$this->memcache->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP);
#$this->memcache->setOption(Memcached::OPT_PREFIX_KEY, phpversion());
$this->memcache->addServer($hp[0], (int)$hp[1]);
$this->engine = $engine;
$this->tracer = $tracer;
}
public function get(string $key)
public function get($key, $default=null)
{
$key = urlencode($key);
$val = $this->memcache->get($key);
$res = $this->memcache->getResultCode();
if ($res == Memcached::RES_SUCCESS) {
return $val;
} elseif ($res == Memcached::RES_NOTFOUND) {
return false;
} else {
error_log("Memcached error during get($key): $res");
return false;
if ($key === "__etc_cache_hits") {
return $this->hits;
}
}
public function set(string $key, $val, int $time=0): void
{
$key = urlencode($key);
$this->memcache->set($key, $val, $time);
$res = $this->memcache->getResultCode();
if ($res != Memcached::RES_SUCCESS) {
error_log("Memcached error during set($key): $res");
if ($key === "__etc_cache_misses") {
return $this->misses;
}
}
public function delete(string $key): void
{
$key = urlencode($key);
$this->memcache->delete($key);
$res = $this->memcache->getResultCode();
if ($res != Memcached::RES_SUCCESS && $res != Memcached::RES_NOTFOUND) {
error_log("Memcached error during delete($key): $res");
}
}
}
class APCCache implements CacheEngine
{
public function __construct(string $args)
{
// $args is not used, but is passed in when APC cache is created.
}
public function get(string $key)
{
return apc_fetch($key);
}
public function set(string $key, $val, int $time=0): void
{
apc_store($key, $val, $time);
}
public function delete(string $key): void
{
apc_delete($key);
}
}
class RedisCache implements CacheEngine
{
private Redis $redis;
public function __construct(string $args)
{
$this->redis = new Redis();
$hp = explode(":", $args);
$this->redis->pconnect($hp[0], (int)$hp[1]);
$this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
$this->redis->setOption(Redis::OPT_PREFIX, 'shm:');
}
public function get(string $key)
{
return $this->redis->get($key);
}
public function set(string $key, $val, int $time=0): void
{
if ($time > 0) {
$this->redis->setEx($key, $time, $val);
} else {
$this->redis->set($key, $val);
}
}
public function delete(string $key): void
{
$this->redis->del($key);
}
}
class Cache
{
public $engine;
public int $hits=0;
public int $misses=0;
public int $time=0;
public function __construct(?string $dsn)
{
$matches = [];
$c = null;
if ($dsn && preg_match("#(.*)://(.*)#", $dsn, $matches) && !isset($_GET['DISABLE_CACHE'])) {
if ($matches[1] == "memcached") {
$c = new MemcachedCache($matches[2]);
} elseif ($matches[1] == "apc") {
$c = new APCCache($matches[2]);
} elseif ($matches[1] == "redis") {
$c = new RedisCache($matches[2]);
}
} else {
$c = new NoCache();
}
$this->engine = $c;
}
public function get(string $key)
{
global $_tracer;
$_tracer->begin("Cache Query", ["key"=>$key]);
$val = $this->engine->get($key);
if ($val !== false) {
$sentinel = "__etc_sentinel";
$this->tracer->begin("Cache Get", ["key"=>$key]);
$val = $this->engine->get($key, $sentinel);
if ($val != $sentinel) {
$res = "hit";
$this->hits++;
} else {
$res = "miss";
$val = $default;
$this->misses++;
}
$_tracer->end(null, ["result"=>$res]);
$this->tracer->end(null, ["result"=>$res]);
return $val;
}
public function set(string $key, $val, int $time=0)
public function set($key, $value, $ttl = null)
{
global $_tracer;
$_tracer->begin("Cache Set", ["key"=>$key, "time"=>$time]);
$this->engine->set($key, $val, $time);
$_tracer->end();
$this->tracer->begin("Cache Set", ["key"=>$key, "ttl"=>$ttl]);
$val = $this->engine->set($key, $value, $ttl);
$this->tracer->end();
return $val;
}
public function delete(string $key)
public function delete($key)
{
global $_tracer;
$_tracer->begin("Cache Delete", ["key"=>$key]);
$this->engine->delete($key);
$_tracer->end();
$this->tracer->begin("Cache Delete", ["key"=>$key]);
$val = $this->engine->delete($key);
$this->tracer->end();
return $val;
}
public function get_hits(): int
public function clear()
{
return $this->hits;
$this->tracer->begin("Cache Clear");
$val = $this->engine->clear();
$this->tracer->end();
return $val;
}
public function get_misses(): int
public function getMultiple($keys, $default = null)
{
return $this->misses;
$this->tracer->begin("Cache Get Multiple", ["keys" => $keys]);
$val = $this->engine->getMultiple($keys, $default);
$this->tracer->end();
return $val;
}
public function setMultiple($values, $ttl = null)
{
$this->tracer->begin("Cache Set Multiple", ["keys" => array_keys($values)]);
$val = $this->engine->setMultiple($values, $ttl);
$this->tracer->end();
return $val;
}
public function deleteMultiple($keys)
{
$this->tracer->begin("Cache Delete Multiple", ["keys" => $keys]);
$val = $this->engine->deleteMultiple($keys);
$this->tracer->end();
return $val;
}
public function has($key)
{
$this->tracer->begin("Cache Has", ["key"=>$key]);
$val = $this->engine->has($key);
$this->tracer->end(null, ["exists"=>$val]);
return $val;
}
}
function loadCache(?string $dsn): CacheInterface
{
$matches = [];
$c = null;
if ($dsn && preg_match("#(.*)://(.*)#", $dsn, $matches) && !isset($_GET['DISABLE_CACHE'])) {
if ($matches[1] == "memcached" || $matches[1] == "memcache") {
$hp = explode(":", $matches[2]);
$memcache = new \Memcached();
$memcache->addServer($hp[0], (int)$hp[1]);
$c = new \Sabre\Cache\Memcached($memcache);
} elseif ($matches[1] == "apc") {
$c = new \Sabre\Cache\Apcu();
} elseif ($matches[1] == "redis") {
$hp = explode(":", $matches[2]);
$redis = new \Predis\Client([
'scheme' => 'tcp',
'host' => $hp[0],
'port' => (int)$hp[1]
], ['prefix' => 'shm:']);
$c = new \Naroga\RedisCache\Redis($redis);
}
} else {
$c = new \Sabre\Cache\Memory();
}
global $_tracer;
return new EventTracingCache($c, $_tracer);
}

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* CAPTCHA abstraction *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@ -22,10 +25,10 @@ function captcha_get_html(): string
$captcha = "
<div class=\"g-recaptcha\" data-sitekey=\"{$r_publickey}\"></div>
<script type=\"text/javascript\" src=\"https://www.google.com/recaptcha/api.js\"></script>";
} else {
} /*else {
session_start();
$captcha = Securimage::getCaptchaHtml(['securimage_path' => './vendor/dapphp/securimage/']);
}
$captcha = \Securimage::getCaptchaHtml(['securimage_path' => './vendor/dapphp/securimage/']);
}*/
}
return $captcha;
}
@ -48,14 +51,14 @@ function captcha_check(): bool
log_info("core", "Captcha failed (ReCaptcha): " . implode("", $resp->getErrorCodes()));
return false;
}
} else {
} /*else {
session_start();
$securimg = new Securimage();
$securimg = new \Securimage();
if ($securimg->check($_POST['captcha_code']) === false) {
log_info("core", "Captcha failed (Securimage)");
return false;
}
}
}*/
}
return true;

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
// 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
@ -14,7 +16,7 @@ class CommandBuilder
public function __construct(String $executable)
{
if (empty($executable)) {
throw new InvalidArgumentException("executable cannot be empty");
throw new \InvalidArgumentException("executable cannot be empty");
}
$this->executable = $executable;

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
/**
* Interface Config
*
@ -262,6 +264,7 @@ class DatabaseConfig extends BaseConfig
private string $table_name;
private ?string $sub_column;
private ?string $sub_value;
private string $cache_name;
public function __construct(
Database $database,
@ -275,14 +278,10 @@ class DatabaseConfig extends BaseConfig
$this->table_name = $table_name;
$this->sub_value = $sub_value;
$this->sub_column = $sub_column;
$this->cache_name = empty($sub_value) ? "config" : "config_{$sub_value}";
$cache_name = "config";
if (!empty($sub_value)) {
$cache_name .= "_".$sub_value;
}
$cached = $cache->get($cache_name);
if ($cached) {
$cached = $cache->get($this->cache_name);
if (!is_null($cached)) {
$this->values = $cached;
} else {
$this->values = [];
@ -298,7 +297,7 @@ class DatabaseConfig extends BaseConfig
foreach ($this->database->get_all($query, $args) as $row) {
$this->values[$row["name"]] = $row["value"];
}
$cache->set($cache_name, $this->values);
$cache->set($this->cache_name, $this->values);
}
}
@ -333,7 +332,7 @@ class DatabaseConfig extends BaseConfig
}
// rather than deleting and having some other request(s) do a thundering
// herd of race-conditioned updates, just save the updated version once here
$cache->set("config", $this->values);
$this->database->notify("config");
$cache->set($this->cache_name, $this->values);
$this->database->notify($this->cache_name);
}
}

View File

@ -1,13 +1,17 @@
<?php
declare(strict_types=1);
use FFSPHP\PDO;
abstract class DatabaseDriver
namespace Shimmie2;
use FFSPHP\PDO;
use FFSPHP\PDOStatement;
enum DatabaseDriverID: string
{
public const MYSQL = "mysql";
public const PGSQL = "pgsql";
public const SQLITE = "sqlite";
case MYSQL = "mysql";
case PGSQL = "pgsql";
case SQLITE = "sqlite";
}
/**
@ -32,6 +36,7 @@ class Database
* How many queries this DB object has run
*/
public int $query_count = 0;
public array $queries = [];
public function __construct(string $dsn)
{
@ -42,7 +47,7 @@ class Database
{
$this->db = new PDO($this->dsn);
$this->connect_engine();
$this->engine->init($this->db);
$this->get_engine()->init($this->db);
$this->begin_transaction();
}
@ -54,11 +59,11 @@ class Database
throw new SCoreException("Can't figure out database engine");
}
if ($db_proto === DatabaseDriver::MYSQL) {
if ($db_proto === DatabaseDriverID::MYSQL->value) {
$this->engine = new MySQL();
} elseif ($db_proto === DatabaseDriver::PGSQL) {
} elseif ($db_proto === DatabaseDriverID::PGSQL->value) {
$this->engine = new PostgreSQL();
} elseif ($db_proto === DatabaseDriver::SQLITE) {
} elseif ($db_proto === DatabaseDriverID::SQLITE->value) {
$this->engine = new SQLite();
} else {
die_nicely(
@ -98,47 +103,53 @@ class Database
}
}
public function scoreql_to_sql(string $input): string
private function get_engine(): DBEngine
{
if (is_null($this->engine)) {
$this->connect_engine();
}
return $this->engine->scoreql_to_sql($input);
return $this->engine;
}
public function get_driver_name(): string
public function scoreql_to_sql(string $input): string
{
if (is_null($this->engine)) {
$this->connect_engine();
}
return $this->engine->name;
return $this->get_engine()->scoreql_to_sql($input);
}
public function get_driver_id(): DatabaseDriverID
{
return $this->get_engine()->id;
}
public function get_version(): string
{
return $this->engine->get_version($this->db);
return $this->get_engine()->get_version($this->db);
}
private function count_time(string $method, float $start, string $query, ?array $args): void
{
global $_tracer, $tracer_enabled;
$dur = microtime(true) - $start;
$dur = ftime() - $start;
// trim whitespace
$query = preg_replace('/[\n\t ]/m', ' ', $query);
$query = preg_replace('/ +/m', ' ', $query);
$query = trim($query);
if ($tracer_enabled) {
$query = trim(preg_replace('/^[\t ]+/m', '', $query)); // trim leading whitespace
$_tracer->complete($start * 1000000, $dur * 1000000, "DB Query", ["query"=>$query, "args"=>$args, "method"=>$method]);
}
$this->queries[] = $query;
$this->query_count++;
$this->dbtime += $dur;
}
public function set_timeout(?int $time): void
{
$this->engine->set_timeout($this->db, $time);
$this->get_engine()->set_timeout($this->db, $time);
}
public function notify(string $channel, ?string $data=null): void
{
$this->engine->notify($this->db, $channel, $data);
$this->get_engine()->notify($this->db, $channel, $data);
}
public function execute(string $query, array $args = []): PDOStatement
@ -147,12 +158,17 @@ class Database
if (is_null($this->db)) {
$this->connect_db();
}
return $this->db->execute(
$ret = $this->db->execute(
"-- " . str_replace("%2F", "/", urlencode($_GET['q'] ?? '')). "\n" .
$query,
$args
);
} catch (PDOException $pdoe) {
if ($ret === false) {
throw new SCoreException("Query failed", $query);
}
/** @noinspection PhpIncompatibleReturnTypeInspection */
return $ret;
} catch (\PDOException $pdoe) {
throw new SCoreException($pdoe->getMessage(), $query);
}
}
@ -162,7 +178,7 @@ class Database
*/
public function get_all(string $query, array $args = []): array
{
$_start = microtime(true);
$_start = ftime();
$data = $this->execute($query, $args)->fetchAll();
$this->count_time("get_all", $_start, $query, $args);
return $data;
@ -173,7 +189,7 @@ class Database
*/
public function get_all_iterable(string $query, array $args = []): PDOStatement
{
$_start = microtime(true);
$_start = ftime();
$data = $this->execute($query, $args);
$this->count_time("get_all_iterable", $_start, $query, $args);
return $data;
@ -184,7 +200,7 @@ class Database
*/
public function get_row(string $query, array $args = []): ?array
{
$_start = microtime(true);
$_start = ftime();
$row = $this->execute($query, $args)->fetch();
$this->count_time("get_row", $_start, $query, $args);
return $row ? $row : null;
@ -195,7 +211,7 @@ class Database
*/
public function get_col(string $query, array $args = []): array
{
$_start = microtime(true);
$_start = ftime();
$res = $this->execute($query, $args)->fetchAll(PDO::FETCH_COLUMN);
$this->count_time("get_col", $_start, $query, $args);
return $res;
@ -204,9 +220,9 @@ class Database
/**
* Execute an SQL query and return the first column of each row as a single iterable object.
*/
public function get_col_iterable(string $query, array $args = []): Generator
public function get_col_iterable(string $query, array $args = []): \Generator
{
$_start = microtime(true);
$_start = ftime();
$stmt = $this->execute($query, $args);
$this->count_time("get_col_iterable", $_start, $query, $args);
foreach ($stmt as $row) {
@ -219,7 +235,7 @@ class Database
*/
public function get_pairs(string $query, array $args = []): array
{
$_start = microtime(true);
$_start = ftime();
$res = $this->execute($query, $args)->fetchAll(PDO::FETCH_KEY_PAIR);
$this->count_time("get_pairs", $_start, $query, $args);
return $res;
@ -229,9 +245,9 @@ class Database
/**
* 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
public function get_pairs_iterable(string $query, array $args = []): \Generator
{
$_start = microtime(true);
$_start = ftime();
$stmt = $this->execute($query, $args);
$this->count_time("get_pairs_iterable", $_start, $query, $args);
foreach ($stmt as $row) {
@ -244,7 +260,7 @@ class Database
*/
public function get_one(string $query, array $args = [])
{
$_start = microtime(true);
$_start = ftime();
$row = $this->execute($query, $args)->fetch();
$this->count_time("get_one", $_start, $query, $args);
return $row ? $row[0] : null;
@ -255,7 +271,7 @@ class Database
*/
public function exists(string $query, array $args = []): bool
{
$_start = microtime(true);
$_start = ftime();
$row = $this->execute($query, $args)->fetch();
$this->count_time("exists", $_start, $query, $args);
if ($row==null) {
@ -269,7 +285,7 @@ class Database
*/
public function get_last_insert_id(string $seq): int
{
if ($this->engine->name == DatabaseDriver::PGSQL) {
if ($this->get_engine()->id == DatabaseDriverID::PGSQL) {
$id = $this->db->lastInsertId($seq);
} else {
$id = $this->db->lastInsertId();
@ -287,7 +303,7 @@ class Database
$this->connect_engine();
}
$data = trim($data, ", \t\n\r\0\x0B"); // mysql doesn't like trailing commas
$this->execute($this->engine->create_table_sql($name, $data));
$this->execute($this->get_engine()->create_table_sql($name, $data));
}
/**
@ -301,32 +317,35 @@ class Database
$this->connect_db();
}
if ($this->engine->name === DatabaseDriver::MYSQL) {
if ($this->get_engine()->id === DatabaseDriverID::MYSQL) {
return count(
$this->get_all("SHOW TABLES")
);
} elseif ($this->engine->name === DatabaseDriver::PGSQL) {
} elseif ($this->get_engine()->id === DatabaseDriverID::PGSQL) {
return count(
$this->get_all("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
);
} elseif ($this->engine->name === DatabaseDriver::SQLITE) {
} elseif ($this->get_engine()->id === DatabaseDriverID::SQLITE) {
return count(
$this->get_all("SELECT name FROM sqlite_master WHERE type = 'table'")
);
} else {
throw new SCoreException("Can't count tables for database type {$this->engine->name}");
throw new SCoreException("Can't count tables for database type {$this->get_engine()->id}");
}
}
public function raw_db(): PDO
{
if (is_null($this->db)) {
$this->connect_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) {
$d = $this->get_driver_id();
if ($d == DatabaseDriverID::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
@ -335,16 +354,16 @@ class Database
$this->execute("ALTER TABLE $table MODIFY COLUMN $column BOOLEAN;");
$this->execute("UPDATE $table SET $column=0 WHERE $column=2;");
}
if ($d == DatabaseDriver::SQLITE) {
if ($d == DatabaseDriverID::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')");
if ($d == DatabaseDriverID::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");
$this->execute("ALTER TABLE $table RENAME COLUMN {$column}_b TO $column");
}
}
}

View File

@ -1,6 +1,11 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
use FFSPHP\PDO;
abstract class SCORE
{
public const AIPK = "SCORE_AIPK";
@ -9,7 +14,7 @@ abstract class SCORE
abstract class DBEngine
{
public ?string $name = null;
public DatabaseDriverID $id;
public function init(PDO $db)
{
@ -34,7 +39,7 @@ abstract class DBEngine
class MySQL extends DBEngine
{
public ?string $name = DatabaseDriver::MYSQL;
public DatabaseDriverID $id = DatabaseDriverID::MYSQL;
public function init(PDO $db)
{
@ -73,7 +78,7 @@ class MySQL extends DBEngine
class PostgreSQL extends DBEngine
{
public ?string $name = DatabaseDriver::PGSQL;
public DatabaseDriverID $id = DatabaseDriverID::PGSQL;
public function init(PDO $db)
{
@ -171,22 +176,22 @@ function _ln($n): float
class SQLite extends DBEngine
{
public ?string $name = DatabaseDriver::SQLITE;
public DatabaseDriverID $id = DatabaseDriverID::SQLITE;
public function init(PDO $db)
{
ini_set('sqlite.assoc_case', '0');
$db->exec("PRAGMA foreign_keys = ON;");
$db->sqliteCreateFunction('UNIX_TIMESTAMP', '_unix_timestamp', 1);
$db->sqliteCreateFunction('now', '_now', 0);
$db->sqliteCreateFunction('floor', '_floor', 1);
$db->sqliteCreateFunction('log', '_log');
$db->sqliteCreateFunction('isnull', '_isnull', 1);
$db->sqliteCreateFunction('md5', '_md5', 1);
$db->sqliteCreateFunction('concat', '_concat', 2);
$db->sqliteCreateFunction('lower', '_lower', 1);
$db->sqliteCreateFunction('rand', '_rand', 0);
$db->sqliteCreateFunction('ln', '_ln', 1);
$db->sqliteCreateFunction('UNIX_TIMESTAMP', 'Shimmie2\_unix_timestamp', 1);
$db->sqliteCreateFunction('now', 'Shimmie2\_now', 0);
$db->sqliteCreateFunction('floor', 'Shimmie2\_floor', 1);
$db->sqliteCreateFunction('log', 'Shimmie2\_log');
$db->sqliteCreateFunction('isnull', 'Shimmie2\_isnull', 1);
$db->sqliteCreateFunction('md5', 'Shimmie2\_md5', 1);
$db->sqliteCreateFunction('concat', 'Shimmie2\_concat', 2);
$db->sqliteCreateFunction('lower', 'Shimmie2\_lower', 1);
$db->sqliteCreateFunction('rand', 'Shimmie2\_rand', 0);
$db->sqliteCreateFunction('ln', 'Shimmie2\_ln', 1);
}
public function scoreql_to_sql(string $data): string

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
/**
* Generic parent class for all events.
*

View File

@ -2,10 +2,12 @@
declare(strict_types=1);
namespace Shimmie2;
/**
* A base exception to be caught by the upper levels.
*/
class SCoreException extends RuntimeException
class SCoreException extends \RuntimeException
{
public ?string $query;
public string $error;
@ -19,7 +21,7 @@ class SCoreException extends RuntimeException
}
}
class InstallerException extends RuntimeException
class InstallerException extends \RuntimeException
{
public string $title;
public string $body;

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
/**
* Class Extension
*
@ -17,7 +20,7 @@ abstract class Extension
{
public string $key;
protected ?Themelet $theme;
public ?ExtensionInfo $info;
public ExtensionInfo $info;
private static array $enabled_extensions = [];
@ -26,9 +29,6 @@ abstract class Extension
$class = $class ?? get_called_class();
$this->theme = $this->get_theme_object($class);
$this->info = ExtensionInfo::get_for_extension_class($class);
if ($this->info===null) {
throw new ScoreException("Info class not found for extension $class");
}
$this->key = $this->info->key;
}
@ -37,8 +37,9 @@ abstract class Extension
*/
private function get_theme_object(string $base): ?Themelet
{
$custom = 'Custom'.$base.'Theme';
$normal = $base.'Theme';
$base = str_replace("Shimmie2\\", "", $base);
$custom = "Shimmie2\Custom{$base}Theme";
$normal = "Shimmie2\\{$base}Theme";
if (class_exists($custom)) {
return new $custom();
@ -61,9 +62,11 @@ abstract class Extension
public static function determine_enabled_extensions(): void
{
self::$enabled_extensions = [];
$extras = defined("EXTRA_EXTS") ? explode(",", EXTRA_EXTS) : [];
foreach (array_merge(
ExtensionInfo::get_core_extensions(),
explode(",", EXTRA_EXTS)
$extras
) as $key) {
$ext = ExtensionInfo::get_by_key($key);
if ($ext===null || !$ext->is_supported()) {
@ -107,6 +110,13 @@ abstract class Extension
}
}
enum ExtensionVisibility
{
case DEFAULT;
case ADMIN;
case HIDDEN;
}
abstract class ExtensionInfo
{
// Every credit you get costs us RAM. It stops now.
@ -119,11 +129,6 @@ abstract class ExtensionInfo
public const LICENSE_MIT = "MIT";
public const LICENSE_WTFPL = "WTFPL";
public const VISIBLE_DEFAULT = "default";
public const VISIBLE_ADMIN = "admin";
public const VISIBLE_HIDDEN = "hidden";
private const VALID_VISIBILITY = [self::VISIBLE_DEFAULT, self::VISIBLE_ADMIN, self::VISIBLE_HIDDEN];
public string $key;
public bool $core = false;
@ -135,12 +140,12 @@ abstract class ExtensionInfo
public array $authors = [];
public array $dependencies = [];
public array $conflicts = [];
public string $visibility = self::VISIBLE_DEFAULT;
public ExtensionVisibility $visibility = ExtensionVisibility::DEFAULT;
public ?string $link = null;
public ?string $version = null;
public ?string $documentation = null;
/** @var string[] which DBs this ext supports (blank for 'all') */
/** @var DatabaseDriverID[] which DBs this ext supports (blank for 'all') */
public array $db_support = [];
private ?bool $supported = null;
private ?string $support_info = null;
@ -169,7 +174,6 @@ abstract class ExtensionInfo
{
assert(!empty($this->key), "key field is required");
assert(!empty($this->name), "name field is required for extension $this->key");
assert(empty($this->visibility) || in_array($this->visibility, self::VALID_VISIBILITY), "Invalid visibility for extension $this->key");
assert(is_array($this->db_support), "db_support has to be an array for extension $this->key");
assert(is_array($this->authors), "authors has to be an array for extension $this->key");
assert(is_array($this->dependencies), "dependencies has to be an array for extension $this->key");
@ -184,7 +188,7 @@ abstract class ExtensionInfo
{
global $database;
$this->support_info = "";
if (!empty($this->db_support) && !in_array($database->get_driver_name(), $this->db_support)) {
if (!empty($this->db_support) && !in_array($database->get_driver_id(), $this->db_support)) {
$this->support_info .= "Database not supported. ";
}
if (!empty($this->conflicts)) {
@ -223,23 +227,24 @@ abstract class ExtensionInfo
}
}
public static function get_for_extension_class(string $base): ?ExtensionInfo
public static function get_for_extension_class(string $base): ExtensionInfo
{
$normal = $base.'Info';
$normal = "{$base}Info";
if (array_key_exists($normal, self::$all_info_by_class)) {
return self::$all_info_by_class[$normal];
} else {
return null;
$infos = print_r(array_keys(self::$all_info_by_class), true);
throw new SCoreException("$normal not found in {$infos}");
}
}
public static function load_all_extension_info()
{
foreach (get_subclasses_of("ExtensionInfo") as $class) {
foreach (get_subclasses_of("Shimmie2\ExtensionInfo") as $class) {
$extension_info = new $class();
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");
}
self::$all_info_by_key[$extension_info->key] = $extension_info;
@ -308,43 +313,20 @@ abstract class DataHandlerExtension extends Extension
if (is_null($existing)) {
throw new UploadException("Post to replace does not exist!");
}
if ($existing->hash === $event->metadata['hash']) {
if ($existing->hash === $event->hash) {
throw new UploadException("The uploaded post is the same as the one to replace.");
}
// even more hax..
$event->metadata['tags'] = $existing->get_tag_list();
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->metadata['hash']), $event->metadata);
if (is_null($image)) {
throw new UploadException("Data handler failed to create post object from data");
}
if (empty($image->get_mime())) {
throw new UploadException("Unable to determine MIME for ". $event->tmpname);
}
try {
send_event(new MediaCheckPropertiesEvent($image));
} catch (MediaException $e) {
throw new UploadException("Unable to scan media properties: ".$e->getMessage());
}
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
send_event(new ImageReplaceEvent($event->replace_id, $image));
$_id = $event->replace_id;
assert(!is_null($_id));
$event->image_id = $_id;
} else {
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
if (is_null($image)) {
throw new UploadException("Data handler failed to create post object from data");
}
if (empty($image->get_mime())) {
throw new UploadException("Unable to determine MIME for ". $event->tmpname);
}
try {
send_event(new MediaCheckPropertiesEvent($image));
} catch (MediaException $e) {
throw new UploadException("Unable to scan media properties: ".$e->getMessage());
}
$iae = send_event(new ImageAdditionEvent($image));
$event->image_id = $iae->image->id;
$event->merged = $iae->merged;
@ -358,7 +340,7 @@ abstract class DataHandlerExtension extends Extension
// Locked Stuff.
if (!empty($event->metadata['locked'])) {
$locked = $event->metadata['locked'];
send_event(new LockSetEvent($image, !empty($locked)));
send_event(new LockSetEvent($image, $locked));
}
}
} elseif ($supported_mime && !$check_contents) {
@ -390,14 +372,14 @@ abstract class DataHandlerExtension extends Extension
{
global $page;
if ($this->supported_mime($event->image->get_mime())) {
/** @noinspection PhpPossiblePolymorphicInvocationInspection */
// @phpstan-ignore-next-line
$this->theme->display_image($page, $event->image);
}
}
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
if ($this->supported_mime($event->mime)) {
if ($this->supported_mime($event->image->get_mime())) {
$this->media_check_properties($event);
}
}
@ -406,19 +388,23 @@ abstract class DataHandlerExtension extends Extension
{
$image = new Image();
$image->filesize = $metadata['size'];
$image->hash = $metadata['hash'];
assert(is_readable($filename));
$image->filesize = filesize($filename);
$image->hash = md5_file($filename);
$image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename'];
if (array_key_exists("extension", $metadata)) {
$image->set_mime(MimeType::get_for_file($filename, $metadata["extension"]));
} else {
$image->set_mime(MimeType::get_for_file($filename));
}
$image->set_mime(MimeType::get_for_file($filename, get_file_ext($metadata["filename"]) ?? null));
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
$image->source = $metadata['source'];
if (empty($image->get_mime())) {
throw new UploadException("Unable to determine MIME for $filename");
}
try {
send_event(new MediaCheckPropertiesEvent($image));
} catch (MediaException $e) {
throw new UploadException("Unable to scan media properties $filename / $image->filename / $image->hash: ".$e->getMessage());
}
return $image;
}
@ -434,13 +420,13 @@ abstract class DataHandlerExtension extends Extension
public static function get_all_supported_mimes(): array
{
$arr = [];
foreach (get_subclasses_of("DataHandlerExtension") as $handler) {
foreach (get_subclasses_of("Shimmie2\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")) {
if (class_exists("Shimmie2\TranscodeImage")) {
$arr = array_merge($arr, TranscodeImage::get_enabled_mimes());
}

View File

@ -2,13 +2,14 @@
declare(strict_types=1);
namespace Shimmie2;
/**
* An image is being added to the database.
*/
class ImageAdditionEvent extends Event
{
public User $user;
public Image $image;
public bool $merged = false;
/**
@ -16,10 +17,10 @@ class ImageAdditionEvent extends Event
* information. Also calls TagSetEvent to set the tags for
* this new image.
*/
public function __construct(Image $image)
{
public function __construct(
public Image $image,
) {
parent::__construct();
$this->image = $image;
}
}
@ -32,20 +33,17 @@ class ImageAdditionException extends SCoreException
*/
class ImageDeletionEvent extends Event
{
public Image $image;
public bool $force = false;
/**
* Deletes an image.
*
* Used by things like tags and comments handlers to
* clean out related rows in their tables.
*/
public function __construct(Image $image, bool $force = false)
{
public function __construct(
public Image $image,
public bool $force = false,
) {
parent::__construct();
$this->image = $image;
$this->force = $force;
}
}
@ -54,9 +52,6 @@ class ImageDeletionEvent extends Event
*/
class ImageReplaceEvent extends Event
{
public int $id;
public Image $image;
/**
* Replaces an image.
*
@ -64,11 +59,11 @@ class ImageReplaceEvent extends Event
* file, leaving the tags and such unchanged. Also removes
* the old image file and thumbnail from the disk.
*/
public function __construct(int $id, Image $image)
{
public function __construct(
public int $id,
public Image $image
) {
parent::__construct();
$this->id = $id;
$this->image = $image;
}
}
@ -81,20 +76,17 @@ class ImageReplaceException extends SCoreException
*/
class ThumbnailGenerationEvent extends Event
{
public string $hash;
public string $mime;
public bool $force;
public bool $generated;
/**
* Request a thumbnail be made for an image object
*/
public function __construct(string $hash, string $mime, bool $force=false)
{
public function __construct(
public string $hash,
public string $mime,
public bool $force=false
) {
parent::__construct();
$this->hash = $hash;
$this->mime = $mime;
$this->force = $force;
$this->generated = false;
}
}

View File

@ -1,6 +1,13 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
use GQLA\Type;
use GQLA\Field;
use GQLA\Query;
/**
* Class Image
*
@ -10,17 +17,25 @@ declare(strict_types=1);
* image per se, but could be a video, sound file, or any
* other supported upload type.
*/
#[\AllowDynamicProperties]
#[Type(name: "Post")]
class Image
{
public const IMAGE_DIR = "images";
public const THUMBNAIL_DIR = "thumbs";
public ?int $id = null;
#[Field]
public int $height = 0;
#[Field]
public int $width = 0;
#[Field]
public string $hash;
#[Field]
public int $filesize;
#[Field]
public string $filename;
#[Field]
private string $ext;
private string $mime;
@ -28,8 +43,11 @@ class Image
public ?array $tag_array;
public int $owner_id;
public string $owner_ip;
#[Field]
public ?string $posted = null;
#[Field]
public ?string $source;
#[Field]
public bool $locked = false;
public ?bool $lossless = null;
public ?bool $video = null;
@ -70,10 +88,26 @@ class Image
}
}
public static function by_id(int $id): ?Image
#[Field(name: "post_id")]
public function graphql_oid(): int
{
return $this->id;
}
#[Field(name: "id")]
public function graphql_guid(): string
{
return "post:{$this->id}";
}
#[Query(name: "post")]
public static function by_id(int $post_id): ?Image
{
global $database;
$row = $database->get_row("SELECT * FROM images WHERE images.id=:id", ["id"=>$id]);
if ($post_id > 2**32) {
// for some reason bots query huge numbers and pollute the DB error logs...
return null;
}
$row = $database->get_row("SELECT * FROM images WHERE images.id=:id", ["id"=>$post_id]);
return ($row ? new Image($row) : null);
}
@ -132,12 +166,13 @@ class Image
/**
* Search for an array of images
*
* #param string[] $tags
* #return Image[]
* @param String[] $tags
* @return Image[]
*/
public static function find_images(int $start, ?int $limit = null, array $tags=[]): array
#[Query(name: "posts", type: "[Post!]!", args: ["tags" => "[string!]"])]
public static function find_images(?int $offset = 0, ?int $limit = null, array $tags=[]): array
{
$result = self::find_images_internal($start, $limit, $tags);
$result = self::find_images_internal($offset, $limit, $tags);
$images = [];
foreach ($result as $row) {
@ -149,7 +184,7 @@ class Image
/**
* Search for an array of images, returning a iterable object of Image
*/
public static function find_images_iterable(int $start = 0, ?int $limit = null, array $tags=[]): Generator
public static function find_images_iterable(int $start = 0, ?int $limit = null, array $tags=[]): \Generator
{
$result = self::find_images_internal($start, $limit, $tags);
foreach ($result as $row) {
@ -165,7 +200,7 @@ class Image
{
global $cache, $database;
$total = $cache->get("image-count");
if (!$total) {
if (is_null($total)) {
$total = (int)$database->get_one("SELECT COUNT(*) FROM images");
$cache->set("image-count", $total, 600);
}
@ -184,7 +219,7 @@ class Image
/**
* Count the number of image results for a given search
*
* #param string[] $tags
* @param String[] $tags
*/
public static function count_images(array $tags=[]): int
{
@ -207,7 +242,7 @@ class Image
// implode(tags) can be too long for memcache...
$cache_key = "image-count:" . md5(Tag::implode($tags));
$total = $cache->get($cache_key);
if (!$total) {
if (is_null($total)) {
if (Extension::is_enabled(RatingsInfo::KEY)) {
$tags[] = "rating:*";
}
@ -229,7 +264,7 @@ class Image
/**
* Count the number of pages for a given search
*
* #param string[] $tags
* @param String[] $tags
*/
public static function count_pages(array $tags=[]): int
{
@ -248,7 +283,6 @@ class Image
* Turn a bunch of strings into a bunch of TagCondition
* and ImgCondition objects
*/
/** @var $stpe SearchTermParseEvent */
$stpe = send_event(new SearchTermParseEvent($stpen++, null, $terms));
if ($stpe->order) {
$order = $stpe->order;
@ -268,7 +302,6 @@ class Image
continue;
}
/** @var $stpe SearchTermParseEvent */
$stpe = send_event(new SearchTermParseEvent($stpen++, $term, $terms));
if ($stpe->order) {
$order = $stpe->order;
@ -296,7 +329,7 @@ class Image
* Rather than simply $this_id + 1, one must take into account
* deleted images and search queries
*
* #param string[] $tags
* @param String[] $tags
*/
public function get_next(array $tags=[], bool $next=true): ?Image
{
@ -332,7 +365,7 @@ class Image
/**
* The reverse of get_next
*
* #param string[] $tags
* @param String[] $tags
*/
public function get_prev(array $tags=[]): ?Image
{
@ -342,6 +375,7 @@ class Image
/**
* Find the User who owns this Image
*/
#[Field(name: "owner")]
public function get_owner(): User
{
return User::by_id($this->owner_id);
@ -369,7 +403,7 @@ class Image
$cut_name = substr($this->filename, 0, 255);
if (is_null($this->posted) || $this->posted == "") {
$this->posted = date('c', time());
$this->posted = date('Y-m-d H:i:s', time());
}
if (is_null($this->id)) {
@ -440,8 +474,9 @@ class Image
/**
* Get this image's tags as an array.
*
* #return string[]
* @return String[]
*/
#[Field(name: "tags", type: "[string!]!")]
public function get_tag_array(): array
{
global $database;
@ -469,6 +504,7 @@ class Image
/**
* Get the URL for the full size image
*/
#[Field(name: "image_link")]
public function get_image_link(): string
{
return $this->get_link(ImageConfig::ILINK, '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext');
@ -477,16 +513,16 @@ class Image
/**
* Get the nicely formatted version of the file name
*/
#[Field(name: "nice_name")]
public function get_nice_image_name(): string
{
$plte = new ParseLinkTemplateEvent('$id - $tags.$ext', $this);
send_event($plte);
return $plte->text;
return send_event(new ParseLinkTemplateEvent('$id - $tags.$ext', $this))->text;
}
/**
* Get the URL for the thumbnail
*/
#[Field(name: "thumb_link")]
public function get_thumb_link(): string
{
global $config;
@ -521,24 +557,22 @@ class Image
* Get the tooltip for this image, formatted according to the
* configured template.
*/
#[Field(name: "tooltip")]
public function get_tooltip(): string
{
global $config;
$plte = new ParseLinkTemplateEvent($config->get_string(ImageConfig::TIP), $this);
send_event($plte);
return $plte->text;
return send_event(new ParseLinkTemplateEvent($config->get_string(ImageConfig::TIP), $this))->text;
}
/**
* Get the info for this image, formatted according to the
* configured template.
*/
#[Field(name: "info")]
public function get_info(): string
{
global $config;
$plte = new ParseLinkTemplateEvent($config->get_string(ImageConfig::INFO), $this);
send_event($plte);
return $plte->text;
return send_event(new ParseLinkTemplateEvent($config->get_string(ImageConfig::INFO), $this))->text;
}
@ -561,6 +595,7 @@ class Image
/**
* Get the original filename.
*/
#[Field(name: "filename")]
public function get_filename(): string
{
return $this->filename;
@ -569,6 +604,7 @@ class Image
/**
* Get the image's extension.
*/
#[Field(name: "ext")]
public function get_ext(): string
{
return $this->ext;
@ -577,6 +613,7 @@ class Image
/**
* Get the image's mime type.
*/
#[Field(name: "mime")]
public function get_mime(): ?string
{
if ($this->mime===MimeType::WEBP&&$this->lossless) {
@ -650,7 +687,7 @@ class Image
public function delete_tags_from_image(): void
{
global $database;
if ($database->get_driver_name() == DatabaseDriver::MYSQL) {
if ($database->get_driver_id() == DatabaseDriverID::MYSQL) {
//mysql < 5.6 has terrible subquery optimization, using EXISTS / JOIN fixes this
$database->execute(
"
@ -743,7 +780,7 @@ class Image
VALUES(:iid, :tid)
", ["iid"=>$this->id, "tid"=>$id]);
array_push($written_tags, $id);
$written_tags[] = $id;
}
$database->execute(
"
@ -796,14 +833,14 @@ class Image
{
global $database;
$sq = "SELECT id FROM tags WHERE LOWER(tag) LIKE LOWER(:tag)";
if ($database->get_driver_name() === DatabaseDriver::SQLITE) {
if ($database->get_driver_id() === DatabaseDriverID::SQLITE) {
$sq .= "ESCAPE '\\'";
}
return $database->get_col($sq, ["tag" => Tag::sqlify($tag)]);
}
/**
* #param string[] $terms
* @param String[] $terms
*/
private static function build_search_querylet(
array $terms,

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Misc functions *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@ -20,8 +23,6 @@ function add_dir(string $base): array
$filename = basename($full_path);
$tags = path_to_tags($short_path);
if ($tags[0] == "\\")
$tags = "";
$result = "$short_path (".str_replace(" ", ", ", $tags).")... ";
try {
add_image($full_path, $filename, $tags);
@ -38,24 +39,18 @@ function add_dir(string $base): array
/**
* Sends a DataUploadEvent for a file.
*/
function add_image(string $tmpname, string $filename, string $tags): int
function add_image(string $tmpname, string $filename, string $tags, ?string $source=null): DataUploadEvent
{
assert(file_exists($tmpname));
return send_event(new DataUploadEvent($tmpname, [
'filename' => pathinfo($filename, PATHINFO_BASENAME),
'tags' => Tag::explode($tags),
'source' => $source,
]));
}
$pathinfo = pathinfo($filename);
$metadata = [];
$metadata['filename'] = $pathinfo['basename'];
if (array_key_exists('extension', $pathinfo)) {
$metadata['extension'] = $pathinfo['extension'];
}
$metadata['tags'] = Tag::explode($tags);
$metadata['source'] = null;
$due = new DataUploadEvent($tmpname, $metadata);
send_event($due);
return $due->image_id;
function get_file_ext(string $filename): ?string
{
return pathinfo($filename)['extension'] ?? null;
}
/**

View File

@ -1,15 +1,15 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class Querylet
{
public string $sql;
public array $variables;
public function __construct(string $sql, array $variables=[])
{
$this->sql = $sql;
$this->variables = $variables;
public function __construct(
public string $sql,
public array $variables=[],
) {
}
public function append(Querylet $querylet): void
@ -31,24 +31,18 @@ class Querylet
class TagCondition
{
public string $tag;
public bool $positive;
public function __construct(string $tag, bool $positive)
{
$this->tag = $tag;
$this->positive = $positive;
public function __construct(
public string $tag,
public bool $positive,
) {
}
}
class ImgCondition
{
public Querylet $qlet;
public bool $positive;
public function __construct(Querylet $qlet, bool $positive)
{
$this->qlet = $qlet;
$this->positive = $positive;
public function __construct(
public Querylet $qlet,
public bool $positive,
) {
}
}

View File

@ -1,6 +1,85 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
use GQLA\Type;
use GQLA\Field;
use GQLA\Query;
#[Type(name: "TagUsage")]
class TagUsage
{
#[Field]
public string $tag;
#[Field]
public int $uses;
public function __construct(string $tag, int $uses)
{
$this->tag = $tag;
$this->uses = $uses;
}
/**
* @return TagUsage[]
*/
#[Query(name: "tags", type: '[TagUsage!]!')]
public static function tags(string $search, int $limit=10): array
{
global $cache, $database;
if (!$search) {
return [];
}
$search = strtolower($search);
if (
$search == '' ||
$search[0] == '_' ||
$search[0] == '%' ||
strlen($search) > 32
) {
return [];
}
$cache_key = "tagusage-$search";
$limitSQL = "";
$search = str_replace('_', '\_', $search);
$search = str_replace('%', '\%', $search);
$SQLarr = ["search"=>"$search%"]; #, "cat_search"=>"%:$search%"];
if ($limit !== 0) {
$limitSQL = "LIMIT :limit";
$SQLarr['limit'] = $limit;
$cache_key .= "-" . $limit;
}
$res = $cache->get($cache_key);
if (is_null($res)) {
$res = $database->get_pairs(
"
SELECT tag, count
FROM tags
WHERE LOWER(tag) LIKE LOWER(:search)
-- OR LOWER(tag) LIKE LOWER(:cat_search)
AND count > 0
ORDER BY count DESC
$limitSQL
",
$SQLarr
);
$cache->set($cache_key, $res, 600);
}
$counts = [];
foreach ($res as $k => $v) {
$counts[] = new TagUsage($k, $v);
}
return $counts;
}
}
/**
* Class Tag
*
@ -90,7 +169,7 @@ class Tag
public static function sanitize(string $tag): string
{
$tag = preg_replace("/\s/", "", $tag); # whitespace
$tag = preg_replace('/\x20[\x0e\x0f]/', '', $tag); # unicode RTL
$tag = preg_replace('/\x20[\x0e\x0f]/', '', $tag); # unicode RTL
$tag = preg_replace("/\.+/", ".", $tag); # strings of dots?
$tag = preg_replace("/^(\.+[\/\\\\])+/", "", $tag); # trailing slashes?
$tag = trim($tag, ", \t\n\r\0\x0B");
@ -100,7 +179,7 @@ class Tag
} // hard-code one bad case...
if (mb_strlen($tag, 'UTF-8') > 255) {
throw new ScoreException("The tag below is longer than 255 characters, please use a shorter tag.\n$tag\n");
throw new SCoreException("The tag below is longer than 255 characters, please use a shorter tag.\n$tag\n");
}
return $tag;
}
@ -113,15 +192,10 @@ class Tag
$tags1 = array_map("strtolower", $tags1);
$tags2 = array_map("strtolower", $tags2);
natcasesort($tags1);
natcasesort($tags2);
sort($tags1);
sort($tags2);
for ($i = 0; $i < count($tags1); $i++) {
if ($tags1[$i]!==$tags2[$i]) {
return false;
}
}
return true;
return $tags1 == $tags2;
}
public static function get_diff_tags(array $source, array $remove): array
@ -144,7 +218,7 @@ class Tag
foreach ($tags as $tag) {
try {
$tag = Tag::sanitize($tag);
} catch (Exception $e) {
} catch (\Exception $e) {
$page->flash($e->getMessage());
continue;
}
@ -159,7 +233,7 @@ class Tag
public static function sqlify(string $term): string
{
global $database;
if ($database->get_driver_name() === DatabaseDriver::SQLITE) {
if ($database->get_driver_id() === DatabaseDriverID::SQLITE) {
$term = str_replace('\\', '\\\\', $term);
}
$term = str_replace('_', '\_', $term);

View File

@ -1,4 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
/**
* Shimmie Installer
*
@ -29,7 +34,7 @@ function install()
// Pull in necessary files
require_once "vendor/autoload.php";
global $_tracer;
$_tracer = new EventTracer();
$_tracer = new \EventTracer();
require_once "core/exceptions.php";
require_once "core/cacheengine.php";
@ -49,7 +54,7 @@ function get_dsn()
{
if (getenv("INSTALL_DSN")) {
$dsn = getenv("INSTALL_DSN");
} elseif (@$_POST["database_type"] == DatabaseDriver::SQLITE) {
} elseif (@$_POST["database_type"] == DatabaseDriverID::SQLITE->value) {
/** @noinspection PhpUnhandledExceptionInspection */
$id = bin2hex(random_bytes(5));
$dsn = "sqlite:data/shimmie.{$id}.sqlite";
@ -97,11 +102,11 @@ function ask_questions()
";
}
$drivers = PDO::getAvailableDrivers();
$drivers = \PDO::getAvailableDrivers();
if (
!in_array(DatabaseDriver::MYSQL, $drivers) &&
!in_array(DatabaseDriver::PGSQL, $drivers) &&
!in_array(DatabaseDriver::SQLITE, $drivers)
!in_array(DatabaseDriverID::MYSQL->value, $drivers) &&
!in_array(DatabaseDriverID::PGSQL->value, $drivers) &&
!in_array(DatabaseDriverID::SQLITE->value, $drivers)
) {
$errors[] = "
No database connection library could be found; shimmie needs
@ -109,9 +114,9 @@ function ask_questions()
";
}
$db_m = in_array(DatabaseDriver::MYSQL, $drivers) ? '<option value="'. DatabaseDriver::MYSQL .'">MySQL</option>' : "";
$db_p = in_array(DatabaseDriver::PGSQL, $drivers) ? '<option value="'. DatabaseDriver::PGSQL .'">PostgreSQL</option>' : "";
$db_s = in_array(DatabaseDriver::SQLITE, $drivers) ? '<option value="'. DatabaseDriver::SQLITE .'">SQLite</option>' : "";
$db_m = in_array(DatabaseDriverID::MYSQL->value, $drivers) ? '<option value="'. DatabaseDriverID::MYSQL->value .'">MySQL</option>' : "";
$db_p = in_array(DatabaseDriverID::PGSQL->value, $drivers) ? '<option value="'. DatabaseDriverID::PGSQL->value .'">PostgreSQL</option>' : "";
$db_s = in_array(DatabaseDriverID::SQLITE->value, $drivers) ? '<option value="'. DatabaseDriverID::SQLITE->value .'">SQLite</option>' : "";
$warn_msg = $warnings ? "<h3>Warnings</h3>".implode("\n<p>", $warnings) : "";
$err_msg = $errors ? "<h3>Errors</h3>".implode("\n<p>", $errors) : "";
@ -287,7 +292,7 @@ function create_tables(Database $db)
if ($db->is_transaction_open()) {
$db->commit();
}
} catch (PDOException $e) {
} catch (\PDOException $e) {
throw new InstallerException(
"PDO Error:",
"<p>An error occurred while trying to create the database tables necessary for Shimmie.</p>

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Logging convenience *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

View File

@ -2,9 +2,14 @@
declare(strict_types=1);
namespace Shimmie2;
use GQLA\Enum;
// action_object_attribute
// action = create / view / edit / delete
// object = image / user / tag / setting
#[Enum(name: "Permission")]
abstract class Permissions
{
public const CHANGE_SETTING = "change_setting"; # modify web-level settings, eg the config table
@ -67,9 +72,13 @@ abstract class Permissions
public const SEND_PM = "send_pm";
public const READ_PM = "read_pm";
public const VIEW_OTHER_PMS = "view_other_pms";
public const EDIT_FEATURE = "edit_feature";
public const CREATE_VOTE = "create_vote";
public const BULK_EDIT_VOTE = "bulk_edit_vote";
public const EDIT_OTHER_VOTE = "edit_other_vote";
public const VIEW_SYSINTO = "view_sysinfo";
public const HELLBANNED = "hellbanned";

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Things which should be in the core API *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@ -36,7 +39,7 @@ function ip_in_range(string $IP, string $CIDR): bool
list($net, $mask) = explode("/", $CIDR);
$ip_net = ip2long($net);
$ip_mask = ~((1 << (32 - $mask)) - 1);
$ip_mask = ~((1 << (32 - (int)$mask)) - 1);
$ip_ip = ip2long($IP);
@ -267,7 +270,7 @@ function get_subclasses_of(string $parent): array
{
$result = [];
foreach (get_declared_classes() as $class) {
$rclass = new ReflectionClass($class);
$rclass = new \ReflectionClass($class);
if (!$rclass->isAbstract() && is_subclass_of($class, $parent)) {
$result[] = $class;
}
@ -333,38 +336,17 @@ function get_base_href(): string
function unparse_url(array $parsed_url): string
{
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$host = $parsed_url['host'] ?? '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$user = $parsed_url['user'] ?? '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? "$pass@" : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$path = $parsed_url['path'] ?? '';
$query = !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = !empty($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
return "$scheme$user$pass$host$port$path$query$fragment";
}
# finally in the core library starting from php8
if (!function_exists('str_starts_with')) {
function str_starts_with(string $haystack, string $needle): bool
{
return strncmp($haystack, $needle, strlen($needle)) === 0;
}
}
if (!function_exists('str_ends_with')) {
function str_ends_with(string $haystack, string $needle): bool
{
return $needle === '' || $needle === substr($haystack, - strlen($needle));
}
}
if (!function_exists('str_contains')) {
function str_contains(string $haystack, string $needle): bool
{
return '' === $needle || false !== strpos($haystack, $needle);
}
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Input / Output Sanitising *
@ -517,19 +499,24 @@ function truncate(string $string, int $limit, string $break=" ", string $pad="..
function parse_shorthand_int(string $limit): int
{
if (preg_match('/^([\d\.]+)([tgmk])?b?$/i', (string)$limit, $m)) {
$value = $m[1];
$value = (float)$m[1];
if (isset($m[2])) {
switch (strtolower($m[2])) {
/** @noinspection PhpMissingBreakStatementInspection */
case 't': $value *= 1024; // fall through
/** @noinspection PhpMissingBreakStatementInspection */
// no break
case 'g': $value *= 1024; // fall through
/** @noinspection PhpMissingBreakStatementInspection */
// no break
case 'm': $value *= 1024; // fall through
// no break
case 'k': $value *= 1024; break;
case 't':
$value *= 1024; // fall through
/** @noinspection PhpMissingBreakStatementInspection */
// no break
case 'g':
$value *= 1024; // fall through
/** @noinspection PhpMissingBreakStatementInspection */
// no break
case 'm':
$value *= 1024; // fall through
// no break
case 'k':
$value *= 1024;
break;
default: $value = -1;
}
}
@ -787,7 +774,7 @@ function join_path(string ...$paths): string
/**
* Perform callback on each item returned by an iterator.
*/
function iterator_map(callable $callback, iterator $iter): Generator
function iterator_map(callable $callback, \iterator $iter): \Generator
{
foreach ($iter as $i) {
yield call_user_func($callback, $i);
@ -797,7 +784,7 @@ function iterator_map(callable $callback, iterator $iter): Generator
/**
* Perform callback on each item returned by an iterator and combine the result into an array.
*/
function iterator_map_to_array(callable $callback, iterator $iter): array
function iterator_map_to_array(callable $callback, \iterator $iter): array
{
return iterator_to_array(iterator_map($callback, $iter));
}
@ -806,7 +793,7 @@ function stringer($s): string
{
if (is_array($s)) {
if (isset($s[0])) {
return "[" . implode(", ", array_map("stringer", $s)) . "]";
return "[" . implode(", ", array_map("Shimmie2\stringer", $s)) . "]";
} else {
$pairs = [];
foreach ($s as $k=>$v) {
@ -815,8 +802,20 @@ function stringer($s): string
return "[" . implode(", ", $pairs) . "]";
}
}
if (is_null($s)) {
return "null";
}
if (is_string($s)) {
return "\"$s\""; // FIXME: handle escaping quotes
}
return (string)$s;
if (is_numeric($s)) {
return "$s";
}
if (is_bool($s)) {
return $s ? "true" : "false";
}
if (method_exists($s, "__toString")) {
return $s->__toString();
}
return "<Unstringable>";
}

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
/*
* A small number of PHP-sanity things (eg don't silently ignore errors) to
* be included right at the very start of index.php and tests/bootstrap.php
@ -31,7 +34,7 @@ function die_nicely($title, $body, $code=0)
exit($code);
}
$min_php = "7.3";
$min_php = "8.1";
if (version_compare(phpversion(), $min_php, ">=") === false) {
die_nicely("Not Supported", "
Shimmie does not support versions of PHP lower than $min_php
@ -45,7 +48,7 @@ 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");
throw new \Exception("PHP Error#$errNo: $errStr");
} else {
return false;
}

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Event API *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@ -37,7 +40,7 @@ function _set_event_listeners(): void
global $_shm_event_listeners;
$_shm_event_listeners = [];
foreach (get_subclasses_of("Extension") as $class) {
foreach (get_subclasses_of("Shimmie2\Extension") as $class) {
/** @var Extension $extension */
$extension = new $class();
@ -59,19 +62,25 @@ function _set_event_listeners(): void
}
}
function _namespaced_class_name(string $class): string
{
return str_replace("Shimmie2\\", "", $class);
}
function _dump_event_listeners(array $event_listeners, string $path): void
{
$p = "<"."?php\n";
$p = "<"."?php\nnamespace Shimmie2;\n";
foreach (get_subclasses_of("Extension") as $class) {
$p .= "\$$class = new $class(); ";
foreach (get_subclasses_of("Shimmie2\Extension") as $class) {
$scn = _namespaced_class_name($class);
$p .= "\$$scn = new $scn(); ";
}
$p .= "\$_shm_event_listeners = array(\n";
foreach ($event_listeners as $event => $listeners) {
$p .= "\t'$event' => array(\n";
foreach ($listeners as $id => $listener) {
$p .= "\t\t$id => \$".get_class($listener).",\n";
$p .= "\t\t$id => \$"._namespaced_class_name(get_class($listener)).",\n";
}
$p .= "\t),\n";
}
@ -87,16 +96,21 @@ $_shm_event_count = 0;
/**
* Send an event to all registered Extensions.
*
* @template T of Event
* @param T $event
* @return T
*/
function send_event(Event $event): Event
{
global $tracer_enabled;
global $_shm_event_listeners, $_shm_event_count, $_tracer;
if (!isset($_shm_event_listeners[get_class($event)])) {
$event_name = _namespaced_class_name(get_class($event));
if (!isset($_shm_event_listeners[$event_name])) {
return $event;
}
$method_name = "on".str_replace("Event", "", get_class($event));
$method_name = "on".str_replace("Event", "", $event_name);
// send_event() is performance sensitive, and with the number
// of times tracer gets called the time starts to add up
@ -104,7 +118,7 @@ function send_event(Event $event): Event
$_tracer->begin(get_class($event));
}
// SHIT: https://bugs.php.net/bug.php?id=35106
$my_event_listeners = $_shm_event_listeners[get_class($event)];
$my_event_listeners = $_shm_event_listeners[$event_name];
ksort($my_event_listeners);
foreach ($my_event_listeners as $listener) {

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
/**
* For any values that aren't defined in data/config/*.php,
* Shimmie will set the values to their defaults
@ -28,7 +31,7 @@ _d("DEBUG", false); // boolean print various debugging details
_d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes
_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
_d("VERSION", "2.9.1$_g"); // string shimmie version
_d("VERSION", "2.10.0-alpha$_g"); // string shimmie version
_d("TIMEZONE", null); // string timezone
_d("EXTRA_EXTS", ""); // string optional extra extensions
_d("BASE_HREF", null); // string force a specific base URL (default is auto-detect)

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
use PHPUnit\Framework\TestCase;
require_once "core/basepage.php";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
use PHPUnit\Framework\TestCase;
require_once "core/block.php";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
use PHPUnit\Framework\TestCase;
class TestInit extends TestCase

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
use PHPUnit\Framework\TestCase;
require_once "core/polyfills.php";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
use PHPUnit\Framework\TestCase;
require_once "core/imageboard/tag.php";
@ -21,4 +23,13 @@ class TagTest extends TestCase
$this->assertEquals("foo^q", Tag::caret("foo?"));
$this->assertEquals("a^^b^sc^bd^qe^af", Tag::caret("a^b/c\\d?e&f"));
}
public function test_compare()
{
$this->assertFalse(Tag::compare(["foo"], ["bar"]));
$this->assertFalse(Tag::compare(["foo"], ["foo", "bar"]));
$this->assertTrue(Tag::compare([], []));
$this->assertTrue(Tag::compare(["foo"], ["FoO"]));
$this->assertTrue(Tag::compare(["foo", "bar"], ["bar", "FoO"]));
}
}

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
use PHPUnit\Framework\TestCase;
require_once "core/urls.php";

View File

@ -2,12 +2,49 @@
declare(strict_types=1);
namespace Shimmie2;
use PHPUnit\Framework\TestCase;
require_once "core/util.php";
class UtilTest extends TestCase
{
public function test_get_theme()
{
$this->assertEquals("default", get_theme());
}
public function test_get_memory_limit()
{
get_memory_limit();
$this->assertTrue(true);
}
public function test_check_gd_version()
{
check_gd_version();
$this->assertTrue(true);
}
public function test_check_im_version()
{
check_im_version();
$this->assertTrue(true);
}
public function test_human_filesize()
{
$this->assertEquals("123.00B", human_filesize(123));
$this->assertEquals("123B", human_filesize(123, 0));
$this->assertEquals("120.56KB", human_filesize(123456));
}
public function test_generate_key()
{
$this->assertEquals(20, strlen(generate_key()));
}
public function test_warehouse_path()
{
$hash = "7ac19c10d6859415";
@ -85,4 +122,44 @@ class UtilTest extends TestCase
load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash, 1)
);
}
public function test_path_to_tags()
{
$this->assertEquals(
"",
path_to_tags("nope.jpg")
);
$this->assertEquals(
"",
path_to_tags("\\")
);
$this->assertEquals(
"",
path_to_tags("/")
);
$this->assertEquals(
"",
path_to_tags("C:\\")
);
$this->assertEquals(
"test tag",
path_to_tags("123 - test tag.jpg")
);
$this->assertEquals(
"foo bar",
path_to_tags("/foo/bar/baz.jpg")
);
$this->assertEquals(
"cake pie foo bar",
path_to_tags("/foo/bar/123 - cake pie.jpg")
);
$this->assertEquals(
"bacon lemon",
path_to_tags("\\bacon\\lemon\\baz.jpg")
);
$this->assertEquals(
"category:tag",
path_to_tags("/category:/tag/baz.jpg")
);
}
}

View File

@ -2,6 +2,10 @@
declare(strict_types=1);
namespace Shimmie2;
use PhpParser\Node\Expr\Cast\Double;
class Link
{
public ?string $page;

View File

@ -2,6 +2,12 @@
declare(strict_types=1);
namespace Shimmie2;
use GQLA\Type;
use GQLA\Field;
use GQLA\Query;
function _new_user(array $row): User
{
return new User($row);
@ -15,13 +21,17 @@ function _new_user(array $row): User
*
* The currently logged in user will always be accessible via the global variable $user.
*/
#[Type(name: "User")]
class User
{
public int $id;
#[Field]
public string $name;
public ?string $email;
#[Field]
public string $join_date;
public ?string $passhash;
#[Field]
public UserClass $class;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
@ -56,12 +66,31 @@ class User
}
}
#[Query]
public static function me(): User
{
global $user;
return $user;
}
#[Field(name: "user_id")]
public function graphql_oid(): int
{
return $this->id;
}
#[Field(name: "id")]
public function graphql_guid(): string
{
return "user:{$this->id}";
}
public static function by_session(string $name, string $session): ?User
{
global $cache, $config, $database;
$row = $cache->get("user-session:$name-$session");
if (!$row) {
if ($database->get_driver_name() === DatabaseDriver::MYSQL) {
if (is_null($row)) {
if ($database->get_driver_id() === DatabaseDriverID::MYSQL) {
$query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess";
} else {
$query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess";
@ -77,7 +106,7 @@ class User
global $cache, $database;
if ($id === 1) {
$cached = $cache->get('user-id:'.$id);
if ($cached) {
if (!is_null($cached)) {
return new User($cached);
}
}
@ -88,6 +117,7 @@ class User
return is_null($row) ? null : new User($row);
}
#[Query(name: "user")]
public static function by_name(string $name): ?User
{
global $database;
@ -163,7 +193,7 @@ class User
{
global $database;
if (User::by_name($name)) {
throw new ScoreException("Desired username is already in use");
throw new SCoreException("Desired username is already in use");
}
$old_name = $this->name;
$this->name = $name;
@ -196,6 +226,16 @@ class User
* a local file, a remote file, a gravatar, a something else, etc.
*/
public function get_avatar_html(): string
{
$url = $this->get_avatar_url();
if (!empty($url)) {
return "<img alt='avatar' class=\"avatar gravatar\" src=\"$url\">";
}
return "";
}
#[Field(name: "avatar_url")]
public function get_avatar_url(): ?string
{
// FIXME: configurable
global $config;
@ -206,10 +246,10 @@ class User
$d = urlencode($config->get_string("avatar_gravatar_default"));
$r = $config->get_string("avatar_gravatar_rating");
$cb = date("Y-m-d");
return "<img alt='avatar' class=\"avatar gravatar\" src=\"https://www.gravatar.com/avatar/$hash.jpg?s=$s&d=$d&r=$r&cacheBreak=$cb\">";
return "https://www.gravatar.com/avatar/$hash.jpg?s=$s&d=$d&r=$r&cacheBreak=$cb";
}
}
return "";
return null;
}
/**

View File

@ -1,6 +1,13 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
use GQLA\Type;
use GQLA\Field;
use GQLA\Query;
/**
* @global UserClass[] $_shm_user_classes
*/
@ -10,8 +17,10 @@ $_shm_user_classes = [];
/**
* Class UserClass
*/
#[Type(name: "UserClass")]
class UserClass
{
#[Field]
public ?string $name = null;
public ?UserClass $parent = null;
public array $abilities = [];
@ -30,6 +39,19 @@ class UserClass
$_shm_user_classes[$name] = $this;
}
#[Field(type: "[Permission!]!")]
public function permissions(): array
{
global $_all_false;
$perms = [];
foreach ((new \ReflectionClass('\Shimmie2\Permissions'))->getConstants() as $k => $v) {
if ($this->can($v)) {
$perms[] = $v;
}
}
return $perms;
}
/**
* Determine if this class of user can perform an action or has ability.
*
@ -58,7 +80,7 @@ class UserClass
}
$_all_false = [];
foreach ((new ReflectionClass('Permissions'))->getConstants() as $k => $v) {
foreach ((new \ReflectionClass('\Shimmie2\Permissions'))->getConstants() as $k => $v) {
$_all_false[$v] = false;
}
new UserClass("base", null, $_all_false);
@ -86,6 +108,7 @@ new UserClass("user", "base", [
Permissions::CREATE_IMAGE_REPORT => true,
Permissions::EDIT_IMAGE_RATING => true,
Permissions::EDIT_FAVOURITES => true,
Permissions::CREATE_VOTE => true,
Permissions::SEND_PM => true,
Permissions::READ_PM => true,
Permissions::SET_PRIVATE_IMAGE => true,
@ -161,6 +184,7 @@ new UserClass("admin", "base", [
Permissions::EDIT_FEATURE => true,
Permissions::BULK_EDIT_VOTE => true,
Permissions::EDIT_OTHER_VOTE => true,
Permissions::CREATE_VOTE => true,
Permissions::VIEW_SYSINTO => true,
Permissions::HELLBANNED => false,

View File

@ -1,7 +1,11 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
use MicroHTML\HTMLElement;
use function MicroHTML\emptyHTML;
use function MicroHTML\rawHTML;
use function MicroHTML\FORM;
@ -203,8 +207,8 @@ function get_session_ip(Config $config): string
*/
function format_text(string $string): string
{
$tfe = send_event(new TextFormattingEvent($string));
return $tfe->formatted;
$event = send_event(new TextFormattingEvent($string));
return $event->formatted;
}
/**
@ -258,7 +262,7 @@ function load_balance_url(string $tmpl, string $hash, int $n=0): string
if (isset($flexihashes[$opts])) {
$flexihash = $flexihashes[$opts];
} else {
$flexihash = new Flexihash\Flexihash();
$flexihash = new \Flexihash\Flexihash();
foreach (explode(",", $opts) as $opt) {
$parts = explode("=", $opt);
$parts_count = count($parts);
@ -354,10 +358,13 @@ function path_to_tags(string $path): string
$tags = explode(" ", $matches[1]);
}
$path = dirname($path);
$path = str_replace("\\", "/", $path);
$path = str_replace(";", ":", $path);
$path = str_replace("__", " ", $path);
$path = dirname($path);
if ($path == "\\" || $path == "/" || $path == ".") {
$path = "";
}
$category = "";
foreach (explode("/", $path) as $dir) {
@ -390,18 +397,6 @@ function path_to_tags(string $path): string
return implode(" ", $tags);
}
function join_url(string $base, string ...$paths): string
{
$output = $base;
foreach ($paths as $path) {
$output = rtrim($output, "/");
$path = ltrim($path, "/");
$output .= "/".$path;
}
return $output;
}
function get_dir_contents(string $dir): array
{
assert(!empty($dir));
@ -486,18 +481,18 @@ function scan_dir(string $path): array
$bytestotal = 0;
$nbfiles = 0;
$ite = new RecursiveDirectoryIterator(
$ite = new \RecursiveDirectoryIterator(
$path,
FilesystemIterator::KEY_AS_PATHNAME |
FilesystemIterator::CURRENT_AS_FILEINFO |
FilesystemIterator::SKIP_DOTS
\FilesystemIterator::KEY_AS_PATHNAME |
\FilesystemIterator::CURRENT_AS_FILEINFO |
\FilesystemIterator::SKIP_DOTS
);
foreach (new RecursiveIteratorIterator($ite) as $filename => $cur) {
foreach (new \RecursiveIteratorIterator($ite) as $filename => $cur) {
try {
$filesize = $cur->getSize();
$bytestotal += $filesize;
$nbfiles++;
} catch (RuntimeException $e) {
} catch (\RuntimeException $e) {
// This usually just means that the file got eaten by the import
continue;
}
@ -509,14 +504,20 @@ function scan_dir(string $path): array
}
/**
* because microtime() returns string|float, and we only ever want float
*/
function ftime(): float
{
return (float)microtime(true);
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Debugging functions *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
// SHIT by default this returns the time as a string. And it's not even a
// string representation of a number, it's two numbers separated by a space.
// What the fuck were the PHP developers smoking.
$_shm_load_start = microtime(true);
$_shm_load_start = ftime();
/**
* Collects some debug information (execution time, memory usage, queries, etc)
@ -524,28 +525,39 @@ $_shm_load_start = microtime(true);
*/
function get_debug_info(): string
{
global $cache, $config, $_shm_event_count, $database, $_shm_load_start;
$d = get_debug_info_arr();
$i_mem = sprintf("%5.2f", ((memory_get_peak_usage(true)+512)/1024)/1024);
$debug = "<br>Took {$d['time']} seconds (db:{$d['dbtime']}) and {$d['mem_mb']}MB of RAM";
$debug .= "; Used {$d['files']} files and {$d['query_count']} queries";
$debug .= "; Sent {$d['event_count']} events";
$debug .= "; {$d['cache_hits']} cache hits and {$d['cache_misses']} misses";
$debug .= "; Shimmie version {$d['version']}";
return $debug;
}
function get_debug_info_arr(): array
{
global $cache, $config, $_shm_event_count, $database, $_shm_load_start;
if ($config->get_string("commit_hash", "unknown") == "unknown") {
$commit = "";
} else {
$commit = " (".$config->get_string("commit_hash").")";
}
$time = sprintf("%.2f", microtime(true) - $_shm_load_start);
$dbtime = sprintf("%.2f", $database->dbtime);
$i_files = count(get_included_files());
$hits = $cache->get_hits();
$miss = $cache->get_misses();
$debug = "<br>Took $time seconds (db:$dbtime) and {$i_mem}MB of RAM";
$debug .= "; Used $i_files files and {$database->query_count} queries";
$debug .= "; Sent $_shm_event_count events";
$debug .= "; $hits cache hits and $miss misses";
$debug .= "; Shimmie version ". VERSION . $commit;
return $debug;
return [
"time" => round(ftime() - $_shm_load_start, 2),
"dbtime" => round($database->dbtime, 2),
"mem_mb" => round(((memory_get_peak_usage(true)+512)/1024)/1024, 2),
"files" => count(get_included_files()),
"query_count" => $database->query_count,
// "query_log" => $database->queries,
"event_count" => $_shm_event_count,
"cache_hits" => $cache->get("__etc_cache_hits"),
"cache_misses" => $cache->get("__etc_cache_misses"),
"version" => VERSION . $commit,
];
}
@ -553,10 +565,6 @@ function get_debug_info(): string
* Request initialisation stuff *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/** @privatesection
* @noinspection PhpIncludeInspection
*/
function require_all(array $files): void
{
foreach ($files as $filename) {
@ -575,7 +583,9 @@ function _load_core_files()
function _load_theme_files()
{
require_all(_get_themelet_files(get_theme()));
$theme = get_theme();
$files = _get_themelet_files($theme);
require_all($files);
}
function _set_up_shimmie_environment(): void
@ -617,13 +627,13 @@ function _get_themelet_files(string $_theme): array
/**
* Used to display fatal errors to the web user.
*/
function _fatal_error(Exception $e): void
function _fatal_error(\Exception $e): void
{
$version = VERSION;
$message = $e->getMessage();
$phpver = phpversion();
$query = is_subclass_of($e, "SCoreException") ? $e->query : null;
$code = is_subclass_of($e, "SCoreException") ? $e->http_code : 500;
$query = is_subclass_of($e, "Shimmie2\SCoreException") ? $e->query : null;
$code = is_subclass_of($e, "Shimmie2\SCoreException") ? $e->http_code : 500;
//$hash = exec("git rev-parse HEAD");
//$h_hash = $hash ? "<p><b>Hash:</b> $hash" : "";
@ -635,7 +645,7 @@ function _fatal_error(Exception $e): void
foreach ($t as $n => $f) {
$c = $f['class'] ?? '';
$t = $f['type'] ?? '';
$a = implode(", ", array_map("stringer", $f['args']));
$a = implode(", ", array_map("Shimmie2\stringer", $f['args']));
print("$n: {$f['file']}({$f['line']}): {$c}{$t}{$f['function']}({$a})\n");
}
@ -674,12 +684,18 @@ function _get_user(): User
{
global $config, $page;
$my_user = null;
if ($page->get_cookie("user") && $page->get_cookie("session")) {
$tmp_user = User::by_session($page->get_cookie("user"), $page->get_cookie("session"));
if (!is_null($tmp_user)) {
$my_user = $tmp_user;
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
$parts = explode(" ", $_SERVER['HTTP_AUTHORIZATION'], 2);
if (count($parts) == 2 && $parts[0] == "Bearer") {
$parts = explode(":", $parts[1], 2);
if (count($parts) == 2) {
$my_user = User::by_session($parts[0], $parts[1]);
}
}
}
if ($page->get_cookie("user") && $page->get_cookie("session")) {
$my_user = User::by_session($page->get_cookie("user"), $page->get_cookie("session"));
}
if (is_null($my_user)) {
$my_user = User::by_id($config->get_int("anon_id", 0));
}

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class AdminPageInfo extends ExtensionInfo
{
public const KEY = "admin";
@ -13,5 +15,5 @@ class AdminPageInfo extends ExtensionInfo
public string $license = self::LICENSE_GPLV2;
public string $description = "Provides a base for various small admin functions";
public bool $core = true;
public string $visibility = self::VISIBLE_HIDDEN;
public ExtensionVisibility $visibility = ExtensionVisibility::HIDDEN;
}

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
/**
* Sent when the admin page is ready to be added to
*/
@ -115,7 +117,7 @@ class AdminPage extends Extension
$key = $event->args[1];
switch ($cmd) {
case "get":
var_dump($cache->get($key));
var_export($cache->get($key));
break;
case "set":
$cache->set($key, $event->args[2], 60);

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class AdminPageTest extends ShimmiePHPUnitTestCase
{
public function testAuth()

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class AdminPageTheme extends Themelet
{
/*

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class AliasEditorInfo extends ExtensionInfo
{
public const KEY = "alias_editor";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
use MicroCRUD\ActionColumn;
use MicroCRUD\TextColumn;
use MicroCRUD\Table;
@ -104,7 +106,7 @@ class AliasEditor extends Extension
if (count($_FILES) > 0) {
$tmp = $_FILES['alias_file']['tmp_name'];
$contents = file_get_contents($tmp);
$this->add_alias_csv($database, $contents);
$this->add_alias_csv($contents);
log_info("alias_editor", "Imported aliases from file", "Imported aliases"); # FIXME: how many?
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("alias/list"));
@ -177,7 +179,7 @@ class AliasEditor extends Extension
return $csv;
}
private function add_alias_csv(Database $database, string $csv): int
private function add_alias_csv(string $csv): int
{
$csv = str_replace("\r", "\n", $csv);
$i = 0;

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class AliasEditorTest extends ShimmiePHPUnitTestCase
{
public function testAliasList()

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class AliasEditorTheme extends Themelet
{
/**

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class ApprovalInfo extends ExtensionInfo
{
public const KEY = "approval";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
abstract class ApprovalConfig
{
public const VERSION = "ext_approval_version";

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
use function MicroHTML\BR;
use function MicroHTML\BUTTON;
use function MicroHTML\INPUT;

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class ArtistsInfo extends ExtensionInfo
{
public const KEY = "artists";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class AuthorSetEvent extends Event
{
public Image $image;
@ -163,256 +165,256 @@ class Artists extends Extension
switch ($event->get_arg(0)) {
//*************ARTIST SECTION**************
case "list":
{
$this->get_listing($page, $event);
$this->theme->sidebar_options("neutral");
break;
}
{
$this->get_listing($event);
$this->theme->sidebar_options("neutral");
break;
}
case "new":
{
if (!$user->is_anonymous()) {
$this->theme->new_artist_composer();
} else {
$this->theme->display_error(401, "Error", "You must be registered and logged in to create a new artist.");
}
break;
}
case "new_artist":
{
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/new"));
break;
}
case "create":
{
if (!$user->is_anonymous()) {
$newArtistID = $this->add_artist();
if ($newArtistID == -1) {
$this->theme->display_error(400, "Error", "Error when entering artist data.");
{
if (!$user->is_anonymous()) {
$this->theme->new_artist_composer();
} else {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$newArtistID));
$this->theme->display_error(401, "Error", "You must be registered and logged in to create a new artist.");
}
} else {
$this->theme->display_error(401, "Error", "You must be registered and logged in to create a new artist.");
break;
}
case "new_artist":
{
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/new"));
break;
}
case "create":
{
if (!$user->is_anonymous()) {
$newArtistID = $this->add_artist();
if ($newArtistID == -1) {
$this->theme->display_error(400, "Error", "Error when entering artist data.");
} else {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$newArtistID));
}
} else {
$this->theme->display_error(401, "Error", "You must be registered and logged in to create a new artist.");
}
break;
}
break;
}
case "view":
{
$artistID = int_escape($event->get_arg(1));
$artist = $this->get_artist($artistID);
$aliases = $this->get_alias($artist['id']);
$members = $this->get_members($artist['id']);
$urls = $this->get_urls($artist['id']);
{
$artistID = int_escape($event->get_arg(1));
$artist = $this->get_artist($artistID);
$aliases = $this->get_alias($artist['id']);
$members = $this->get_members($artist['id']);
$urls = $this->get_urls($artist['id']);
$userIsLogged = !$user->is_anonymous();
$userIsAdmin = $user->can(Permissions::ARTISTS_ADMIN);
$userIsLogged = !$user->is_anonymous();
$userIsAdmin = $user->can(Permissions::ARTISTS_ADMIN);
$images = Image::find_images(0, 4, Tag::explode($artist['name']));
$images = Image::find_images(0, 4, Tag::explode($artist['name']));
$this->theme->show_artist($artist, $aliases, $members, $urls, $images, $userIsLogged, $userIsAdmin);
/*
if ($userIsLogged) {
$this->theme->show_new_alias_composer($artistID);
$this->theme->show_new_member_composer($artistID);
$this->theme->show_new_url_composer($artistID);
$this->theme->show_artist($artist, $aliases, $members, $urls, $images, $userIsLogged, $userIsAdmin);
/*
if ($userIsLogged) {
$this->theme->show_new_alias_composer($artistID);
$this->theme->show_new_member_composer($artistID);
$this->theme->show_new_url_composer($artistID);
}
*/
$this->theme->sidebar_options("editor", $artistID, $userIsAdmin);
break;
}
*/
$this->theme->sidebar_options("editor", $artistID, $userIsAdmin);
break;
}
case "edit":
{
$artistID = int_escape($event->get_arg(1));
$artist = $this->get_artist($artistID);
$aliases = $this->get_alias($artistID);
$members = $this->get_members($artistID);
$urls = $this->get_urls($artistID);
{
$artistID = int_escape($event->get_arg(1));
$artist = $this->get_artist($artistID);
$aliases = $this->get_alias($artistID);
$members = $this->get_members($artistID);
$urls = $this->get_urls($artistID);
if (!$user->is_anonymous()) {
$this->theme->show_artist_editor($artist, $aliases, $members, $urls);
if (!$user->is_anonymous()) {
$this->theme->show_artist_editor($artist, $aliases, $members, $urls);
$userIsAdmin = $user->can(Permissions::ARTISTS_ADMIN);
$this->theme->sidebar_options("editor", $artistID, $userIsAdmin);
} else {
$this->theme->display_error(401, "Error", "You must be registered and logged in to edit an artist.");
$userIsAdmin = $user->can(Permissions::ARTISTS_ADMIN);
$this->theme->sidebar_options("editor", $artistID, $userIsAdmin);
} else {
$this->theme->display_error(401, "Error", "You must be registered and logged in to edit an artist.");
}
break;
}
break;
}
case "edit_artist":
{
$artistID = $_POST['artist_id'];
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/edit/".$artistID));
break;
}
{
$artistID = $_POST['artist_id'];
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/edit/".$artistID));
break;
}
case "edited":
{
$artistID = int_escape($_POST['id']);
$this->update_artist();
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
{
$artistID = int_escape($_POST['id']);
$this->update_artist();
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "nuke_artist":
{
$artistID = $_POST['artist_id'];
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/nuke/".$artistID));
break;
}
{
$artistID = $_POST['artist_id'];
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/nuke/".$artistID));
break;
}
case "nuke":
{
$artistID = int_escape($event->get_arg(1));
$this->delete_artist($artistID); // this will delete the artist, its alias, its urls and its members
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/list"));
break;
}
{
$artistID = int_escape($event->get_arg(1));
$this->delete_artist($artistID); // this will delete the artist, its alias, its urls and its members
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/list"));
break;
}
case "add_alias":
{
$artistID = $_POST['artist_id'];
$this->theme->show_new_alias_composer($artistID);
break;
}
{
$artistID = $_POST['artist_id'];
$this->theme->show_new_alias_composer($artistID);
break;
}
case "add_member":
{
$artistID = $_POST['artist_id'];
$this->theme->show_new_member_composer($artistID);
break;
}
{
$artistID = $_POST['artist_id'];
$this->theme->show_new_member_composer($artistID);
break;
}
case "add_url":
{
$artistID = $_POST['artist_id'];
$this->theme->show_new_url_composer($artistID);
break;
}
//***********ALIAS SECTION ***********************
{
$artistID = $_POST['artist_id'];
$this->theme->show_new_url_composer($artistID);
break;
}
//***********ALIAS SECTION ***********************
case "alias":
{
switch ($event->get_arg(1)) {
case "add":
{
$artistID = $_POST['artistID'];
$this->add_alias();
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "delete":
{
$aliasID = int_escape($event->get_arg(2));
$artistID = $this->get_artistID_by_aliasID($aliasID);
$this->delete_alias($aliasID);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "edit":
{
$aliasID = int_escape($event->get_arg(2));
$alias = $this->get_alias_by_id($aliasID);
$this->theme->show_alias_editor($alias);
break;
}
case "edited":
{
$this->update_alias();
$aliasID = int_escape($_POST['aliasID']);
$artistID = $this->get_artistID_by_aliasID($aliasID);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
{
switch ($event->get_arg(1)) {
case "add":
{
$artistID = $_POST['artistID'];
$this->add_alias();
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "delete":
{
$aliasID = int_escape($event->get_arg(2));
$artistID = $this->get_artistID_by_aliasID($aliasID);
$this->delete_alias($aliasID);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "edit":
{
$aliasID = int_escape($event->get_arg(2));
$alias = $this->get_alias_by_id($aliasID);
$this->theme->show_alias_editor($alias);
break;
}
case "edited":
{
$this->update_alias();
$aliasID = int_escape($_POST['aliasID']);
$artistID = $this->get_artistID_by_aliasID($aliasID);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
}
break; // case: alias
}
break; // case: alias
}
//**************** URLS SECTION **********************
//**************** URLS SECTION **********************
case "url":
{
switch ($event->get_arg(1)) {
case "add":
{
$artistID = $_POST['artistID'];
$this->add_urls();
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "delete":
{
$urlID = int_escape($event->get_arg(2));
$artistID = $this->get_artistID_by_urlID($urlID);
$this->delete_url($urlID);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "edit":
{
$urlID = int_escape($event->get_arg(2));
$url = $this->get_url_by_id($urlID);
$this->theme->show_url_editor($url);
break;
}
case "edited":
{
$this->update_url();
$urlID = int_escape($_POST['urlID']);
$artistID = $this->get_artistID_by_urlID($urlID);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
{
switch ($event->get_arg(1)) {
case "add":
{
$artistID = $_POST['artistID'];
$this->add_urls();
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "delete":
{
$urlID = int_escape($event->get_arg(2));
$artistID = $this->get_artistID_by_urlID($urlID);
$this->delete_url($urlID);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "edit":
{
$urlID = int_escape($event->get_arg(2));
$url = $this->get_url_by_id($urlID);
$this->theme->show_url_editor($url);
break;
}
case "edited":
{
$this->update_url();
$urlID = int_escape($_POST['urlID']);
$artistID = $this->get_artistID_by_urlID($urlID);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
}
break; // case: urls
}
break; // case: urls
}
//******************* MEMBERS SECTION *********************
//******************* MEMBERS SECTION *********************
case "member":
{
switch ($event->get_arg(1)) {
case "add":
{
$artistID = $_POST['artistID'];
$this->add_members();
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "delete":
{
$memberID = int_escape($event->get_arg(2));
$artistID = $this->get_artistID_by_memberID($memberID);
$this->delete_member($memberID);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "edit":
{
$memberID = int_escape($event->get_arg(2));
$member = $this->get_member_by_id($memberID);
$this->theme->show_member_editor($member);
break;
}
case "edited":
{
$this->update_member();
$memberID = int_escape($_POST['memberID']);
$artistID = $this->get_artistID_by_memberID($memberID);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
{
switch ($event->get_arg(1)) {
case "add":
{
$artistID = $_POST['artistID'];
$this->add_members();
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "delete":
{
$memberID = int_escape($event->get_arg(2));
$artistID = $this->get_artistID_by_memberID($memberID);
$this->delete_member($memberID);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
case "edit":
{
$memberID = int_escape($event->get_arg(2));
$member = $this->get_member_by_id($memberID);
$this->theme->show_member_editor($member);
break;
}
case "edited":
{
$this->update_member();
$memberID = int_escape($_POST['memberID']);
$artistID = $this->get_artistID_by_memberID($memberID);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("artist/view/".$artistID));
break;
}
}
break; //case: members
}
break; //case: members
}
}
}
}
@ -845,7 +847,7 @@ class Artists extends Extension
/*
* HERE WE GET THE LIST OF ALL ARTIST WITH PAGINATION
*/
private function get_listing(Page $page, PageRequestEvent $event)
private function get_listing(PageRequestEvent $event)
{
global $config, $database;
@ -1048,7 +1050,8 @@ class Artists extends Extension
ORDER BY alias ASC
", ['artist_id'=>$artistID]);
for ($i = 0 ; $i < count($result) ; $i++) {
$rc = count($result);
for ($i = 0 ; $i < $rc ; $i++) {
$result[$i]["alias_name"] = stripslashes($result[$i]["alias_name"]);
}
return $result;

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class ArtistsTest extends ShimmiePHPUnitTestCase
{
public function testSearch()

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class ArtistsTheme extends Themelet
{
public function get_author_editor_html(string $author): string
@ -436,7 +439,8 @@ class ArtistsTheme extends Themelet
$html .= "</tr>";
if (count($aliases) > 1) {
for ($i = 1; $i < count($aliases); $i++) {
$ac = count($aliases);
for ($i = 1; $i < $ac; $i++) {
$aliasViewLink = str_replace("_", " ", $aliases[$i]['alias_name']); // no link anymore
$aliasEditLink = "<a href='" . make_link("artist/alias/edit/" . $aliases[$i]['alias_id']) . "'>Edit</a>";
$aliasDeleteLink = "<a href='" . make_link("artist/alias/delete/" . $aliases[$i]['alias_id']) . "'>Delete</a>";
@ -479,7 +483,8 @@ class ArtistsTheme extends Themelet
$html .= "</tr>";
if (count($members) > 1) {
for ($i = 1; $i < count($members); $i++) {
$mc = count($members);
for ($i = 1; $i < $mc; $i++) {
$memberViewLink = str_replace("_", " ", $members[$i]['name']); // no link anymore
$memberEditLink = "<a href='" . make_link("artist/member/edit/" . $members[$i]['id']) . "'>Edit</a>";
$memberDeleteLink = "<a href='" . make_link("artist/member/delete/" . $members[$i]['id']) . "'>Delete</a>";
@ -524,7 +529,8 @@ class ArtistsTheme extends Themelet
$html .= "</tr>";
if (count($urls) > 1) {
for ($i = 1; $i < count($urls); $i++) {
$uc = count($urls);
for ($i = 1; $i < $uc; $i++) {
$urlViewLink = "<a href='" . str_replace("_", " ", $urls[$i]['url']) . "' target='_blank'>" . str_replace("_", " ", $urls[$i]['url']) . "</a>";
$urlEditLink = "<a href='" . make_link("artist/url/edit/" . $urls[$i]['id']) . "'>Edit</a>";
$urlDeleteLink = "<a href='" . make_link("artist/url/delete/" . $urls[$i]['id']) . "'>Delete</a>";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
abstract class AutoTaggerConfig
{
public const VERSION = "ext_auto_tagger_ver";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class AutoTaggerInfo extends ExtensionInfo
{
public const KEY = "auto_tagger";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
require_once 'config.php';
use MicroCRUD\ActionColumn;
@ -110,7 +112,7 @@ class AutoTagger extends Extension
if (count($_FILES) > 0) {
$tmp = $_FILES['auto_tag_file']['tmp_name'];
$contents = file_get_contents($tmp);
$count = $this->add_auto_tag_csv($database, $contents);
$count = $this->add_auto_tag_csv($contents);
log_info(AutoTaggerInfo::KEY, "Imported $count auto-tag definitions from file from file", "Imported $count auto-tag definitions");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("auto_tag/list"));
@ -142,7 +144,7 @@ class AutoTagger extends Extension
additional_tags VARCHAR(2000) NOT NULL
");
if ($database->get_driver_name() == DatabaseDriver::PGSQL) {
if ($database->get_driver_id() == DatabaseDriverID::PGSQL) {
$database->execute('CREATE INDEX auto_tag_lower_tag_idx ON auto_tag ((lower(tag)))');
}
$this->set_version(AutoTaggerConfig::VERSION, 1);
@ -189,7 +191,7 @@ class AutoTagger extends Extension
return $csv;
}
private function add_auto_tag_csv(Database $database, string $csv): int
private function add_auto_tag_csv(string $csv): int
{
$csv = str_replace("\r", "\n", $csv);
$i = 0;
@ -210,23 +212,23 @@ class AutoTagger extends Extension
private function add_auto_tag(string $tag, string $additional_tags)
{
global $database;
$existing_tags = $database->get_one("SELECT additional_tags FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag"=>$tag]);
if (!is_null($existing_tags)) {
// Auto Tags already exist, so we will append new tags to the existing one
$tag = Tag::sanitize($tag);
$existing_tags = $database->get_one("SELECT additional_tags FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag"=>$tag]);
if (!is_null($existing_tags)) {
// Auto Tags already exist, so we will append new tags to the existing one
$tag = Tag::sanitize($tag);
$additional_tags = Tag::explode($additional_tags);
$existing_tags = Tag::explode($existing_tags);
foreach ($additional_tags as $t) {
if (!in_array(strtolower($t), $existing_tags)) {
array_push($existing_tags, strtolower($t));
}
}
$database->execute(
$existing_tags = Tag::explode($existing_tags);
foreach ($additional_tags as $t) {
if (!in_array(strtolower($t), $existing_tags)) {
$existing_tags[] = strtolower($t);
}
}
$database->execute(
"UPDATE auto_tag set additional_tags=:existing_tags where tag=:tag",
["tag"=>$tag, "existing_tags"=>Tag::implode($existing_tags)]
);
log_info(
log_info(
AutoTaggerInfo::KEY,
"Updated auto-tag for {$tag} -> {".implode(" ", $additional_tags)."}"
);
@ -244,39 +246,8 @@ class AutoTagger extends Extension
"Added auto-tag for {$tag} -> {".implode(" ", $additional_tags)."}"
);
}
// Now we apply it to existing items
$this->apply_new_auto_tag($tag);
}
private function update_auto_tag(string $tag, string $additional_tags): bool
{
global $database;
$result = $database->get_row("SELECT * FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag"=>$tag]);
if ($result===null) {
throw new AutoTaggerException("Auto-tag not set for $tag, can't update");
} else {
$additional_tags = Tag::explode($additional_tags);
$current_additional_tags = Tag::explode($result["additional_tags"]);
if (!Tag::compare($additional_tags, $current_additional_tags)) {
$database->execute(
"UPDATE auto_tag SET additional_tags = :additional_tags WHERE LOWER(tag)=LOWER(:tag)",
["tag"=>$tag, "additional_tags"=>Tag::implode($additional_tags)]
);
log_info(
AutoTaggerInfo::KEY,
"Updated auto-tag for {$tag} -> {".implode(" ", $additional_tags)."}",
"Updated Auto-Tag"
);
// Now we apply it to existing items
$this->apply_new_auto_tag($tag);
return true;
}
}
return false;
// Now we apply it to existing items
$this->apply_new_auto_tag($tag);
}
private function apply_new_auto_tag(string $tag)
@ -288,14 +259,11 @@ class AutoTagger extends Extension
foreach ($image_ids as $image_id) {
$image_id = (int) $image_id;
$image = Image::by_id($image_id);
$event = new TagSetEvent($image, $image->get_tag_array());
send_event($event);
send_event(new TagSetEvent($image, $image->get_tag_array()));
}
}
}
private function remove_auto_tag(String $tag)
{
global $database;

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class AutoTaggerTest extends ShimmiePHPUnitTestCase
{
public function testAutoTaggerList()

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class AutoTaggerTheme extends Themelet
{
/**

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class AutoCompleteInfo extends ExtensionInfo
{
public const KEY = "autocomplete";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class AutoComplete extends Extension
{
/** @var AutoCompleteTheme */
@ -48,7 +50,8 @@ class AutoComplete extends Extension
return [];
}
$cache_key = "autocomplete-$search";
# memcache keys can't contain spaces
$cache_key = "autocomplete:" . md5($search);
$limitSQL = "";
$search = str_replace('_', '\_', $search);
$search = str_replace('%', '\%', $search);
@ -60,7 +63,7 @@ class AutoComplete extends Extension
}
$res = $cache->get($cache_key);
if (!$res) {
if (is_null($res)) {
$res = $database->get_pairs(
"
SELECT tag, count

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class AutoCompleteTest extends ShimmiePHPUnitTestCase
{
public function testAuth()

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class AutoCompleteTheme extends Themelet
{
public function build_autocomplete(Page $page)

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BanWordsInfo extends ExtensionInfo
{
public const KEY = "ban_words";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BanWords extends Extension
{
public function onInitExt(InitExtEvent $event)

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class BanWordsTest extends ShimmiePHPUnitTestCase
{
public function check_blocked($image_id, $words)

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BBCodeInfo extends ExtensionInfo
{
public const KEY = "bbcode";

View File

@ -2,6 +2,7 @@
declare(strict_types=1);
namespace Shimmie2;
class BBCode extends FormatterExtension
{

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class BBCodeTest extends ShimmiePHPUnitTestCase
{
public function testBasics()

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BiographyInfo extends ExtensionInfo
{
public const KEY = "biography";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class Biography extends Extension
{
/** @var BiographyTheme */

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class BiographyTest extends ShimmiePHPUnitTestCase
{
public function testBio()

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
use function MicroHTML\TEXTAREA;
class BiographyTheme extends Themelet
@ -12,16 +15,12 @@ class BiographyTheme extends Themelet
public function display_composer(Page $page, string $bio)
{
global $user;
$post_url = make_link("biography");
$auth = $user->get_auth_html();
$html = SHM_SIMPLE_FORM(
$post_url,
make_link("biography"),
TEXTAREA(["style"=>"width: 100%", "rows"=>"6", "name"=>"biography"], $bio),
SHM_SUBMIT("Save")
);
$page->add_block(new Block("About Me", (string)$html, "main", 30));
$page->add_block(new Block("About Me", $html, "main", 30));
}
}

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BlocksInfo extends ExtensionInfo
{
public const KEY = "blocks";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class Blocks extends Extension
{
/** @var BlocksTheme */
@ -47,7 +49,7 @@ class Blocks extends Extension
global $cache, $database, $page, $user;
$blocks = $cache->get("blocks");
if ($blocks === false) {
if (is_null($blocks)) {
$blocks = $database->get_all("SELECT * FROM blocks");
$cache->set("blocks", $blocks, 600);
}

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class BlocksTest extends ShimmiePHPUnitTestCase
{
public function testBlocks()

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
use function MicroHTML\TABLE;
use function MicroHTML\TR;
use function MicroHTML\TH;

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BlotterInfo extends ExtensionInfo
{
public const KEY = "blotter";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class Blotter extends Extension
{
/** @var BlotterTheme */
@ -113,9 +115,6 @@ class Blotter extends Extension
$this->theme->display_permission_denied();
} else {
$id = int_escape($_POST['id']);
if (!isset($id)) {
die("No ID!");
}
$database->execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]);
log_info("blotter", "Removed Entry #$id");
$page->set_mode(PageMode::REDIRECT);

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class BlotterTest extends ShimmiePHPUnitTestCase
{
public function testDenial()

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class BlotterTheme extends Themelet
{
public function display_editor($entries)

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BrowserSearchInfo extends ExtensionInfo
{
public const KEY = "browser_search";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BrowserSearch extends Extension
{
public function onInitExt(InitExtEvent $event)

View File

@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class BrowserSearchTest extends ShimmiePHPUnitTestCase
{
public function testBasic()

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BulkActionsInfo extends ExtensionInfo
{
public const KEY = "bulk_actions";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BulkActionException extends SCoreException
{
}
@ -39,10 +41,10 @@ class BulkActionBlockBuildingEvent extends Event
class BulkActionEvent extends Event
{
public string $action;
public Generator $items;
public \Generator $items;
public bool $redirect = true;
public function __construct(String $action, Generator $items)
public function __construct(String $action, \Generator $items)
{
parent::__construct();
$this->action = $action;
@ -173,7 +175,7 @@ class BulkActions extends Extension
if (empty($data)) {
throw new BulkActionException("No ids specified in bulk_selected_ids");
}
if (is_array($data) && !empty($data)) {
if (is_array($data)) {
$items = $this->yield_items($data);
}
} elseif (isset($_POST['bulk_query']) && $_POST['bulk_query'] != "") {
@ -201,7 +203,7 @@ class BulkActions extends Extension
}
}
private function yield_items(array $data): Generator
private function yield_items(array $data): \Generator
{
foreach ($data as $id) {
if (is_numeric($id)) {
@ -213,7 +215,7 @@ class BulkActions extends Extension
}
}
private function yield_search_results(string $query): Generator
private function yield_search_results(string $query): \Generator
{
$tags = Tag::explode($query);
return Image::find_images_iterable(0, null, $tags);
@ -231,7 +233,7 @@ class BulkActions extends Extension
$size = 0;
foreach ($posts as $post) {
try {
if (class_exists("ImageBan") && isset($_POST['bulk_ban_reason'])) {
if (class_exists("Shimmie2\ImageBan") && isset($_POST['bulk_ban_reason'])) {
$reason = $_POST['bulk_ban_reason'];
if ($reason) {
send_event(new AddImageHashBanEvent($post->hash, $reason));
@ -240,7 +242,7 @@ class BulkActions extends Extension
send_event(new ImageDeletionEvent($post));
$total++;
$size += $post->filesize;
} catch (Exception $e) {
} catch (\Exception $e) {
$page->flash("Error while removing {$post->id}: " . $e->getMessage());
}
}
@ -295,7 +297,7 @@ class BulkActions extends Extension
try {
send_event(new SourceSetEvent($image, $source));
$total++;
} catch (Exception $e) {
} catch (\Exception $e) {
$page->flash("Error while setting source for {$image->id}: " . $e->getMessage());
}
}

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BulkActionsTheme extends Themelet
{
public function display_selector(Page $page, array $actions, string $query)
@ -49,7 +51,7 @@ class BulkActionsTheme extends Themelet
public function render_ban_reason_input(): string
{
if (class_exists("ImageBan")) {
if (class_exists("Shimmie2\ImageBan")) {
return "<input type='text' name='bulk_ban_reason' placeholder='Ban reason (leave blank to not ban)' />";
} else {
return "";
@ -64,6 +66,6 @@ class BulkActionsTheme extends Themelet
public function render_source_input(): string
{
return "<input type='text' name='bulk_source' required='required' placeholder='Enter source here' />";
return "<input type='text' name='bulk_source' placeholder='Enter source here' />";
}
}

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BulkAddInfo extends ExtensionInfo
{
public const KEY = "bulk_add";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BulkAddEvent extends Event
{
public string $dir;

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BulkAddTest extends ShimmiePHPUnitTestCase
{
public function testInvalidDir()

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BulkAddTheme extends Themelet
{
private array $messages = [];

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BulkAddCSVInfo extends ExtensionInfo
{
public const KEY = "bulk_add_csv";

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BulkAddCSV extends Extension
{
/** @var BulkAddCSVTheme */
@ -48,21 +50,11 @@ class BulkAddCSV extends Extension
*/
private function add_image(string $tmpname, string $filename, string $tags, string $source, string $rating, string $thumbfile)
{
assert(file_exists($tmpname));
$pathinfo = pathinfo($filename);
$metadata = [];
$metadata['filename'] = $pathinfo['basename'];
if (array_key_exists('extension', $pathinfo)) {
$metadata['extension'] = $pathinfo['extension'];
}
$metadata['tags'] = Tag::explode($tags);
$metadata['source'] = $source;
$event = send_event(new DataUploadEvent($tmpname, $metadata));
$event = add_image($tmpname, $filename, $tags, $source);
if ($event->image_id == -1) {
throw new UploadException("File type not recognised");
} else {
if (class_exists("RatingSetEvent") && in_array($rating, ["s", "q", "e"])) {
if (class_exists("Shimmie2\RatingSetEvent") && in_array($rating, ["s", "q", "e"])) {
send_event(new RatingSetEvent(Image::by_id($event->image_id), $rating));
}
if (file_exists($thumbfile)) {
@ -103,14 +95,13 @@ class BulkAddCSV extends Extension
$source = $csvdata[2];
$rating = $csvdata[3];
$thumbfile = $csvdata[4];
$pathinfo = pathinfo($fullpath);
$shortpath = $pathinfo["basename"];
$shortpath = pathinfo($fullpath, PATHINFO_BASENAME);
$list .= "<br>".html_escape("$shortpath (".str_replace(" ", ", ", $tags).")... ");
if (file_exists($csvdata[0]) && is_file($csvdata[0])) {
try {
$this->add_image($fullpath, $pathinfo["basename"], $tags, $source, $rating, $thumbfile);
$this->add_image($fullpath, $shortpath, $tags, $source, $rating, $thumbfile);
$list .= "ok\n";
} catch (Exception $ex) {
} catch (\Exception $ex) {
$list .= "failed:<br>". $ex->getMessage();
}
} else {

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BulkAddCSVTheme extends Themelet
{
private array $messages = [];

View File

@ -2,6 +2,7 @@
declare(strict_types=1);
namespace Shimmie2;
class BulkDownloadInfo extends ExtensionInfo
{

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BulkDownloadConfig
{
public const SIZE_LIMIT = "bulk_download_size_limit";
@ -47,11 +49,11 @@ class BulkDownload extends Extension
($event->action == BulkDownload::DOWNLOAD_ACTION_NAME)) {
$download_filename = $user->name . '-' . date('YmdHis') . '.zip';
$zip_filename = tempnam(sys_get_temp_dir(), "shimmie_bulk_download");
$zip = new ZipArchive();
$zip = new \ZipArchive();
$size_total = 0;
$max_size = $config->get_int(BulkDownloadConfig::SIZE_LIMIT);
if ($zip->open($zip_filename, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE) === true) {
if ($zip->open($zip_filename, \ZIPARCHIVE::CREATE | \ZIPARCHIVE::OVERWRITE) === true) {
foreach ($event->items as $image) {
$img_loc = warehouse_path(Image::IMAGE_DIR, $image->hash, false);
$size_total += filesize($img_loc);

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Shimmie2;
class BulkExportEvent extends Event
{
public Image $image;

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