Compare commits

..

2 Commits

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

View File

@ -1,24 +0,0 @@
name: Publish
on:
workflow_run:
workflows: Tests
branches: master
types: completed
workflow_dispatch:
jobs:
publish:
name: Publish
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@master
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
with:
name: shish2k/shimmie2
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
cache: ${{ github.event_name != 'schedule' }}
buildoptions: "--build-arg RUN_TESTS=false"

View File

@ -1,4 +1,4 @@
name: Tests name: Test & Publish
on: on:
push: push:
@ -7,40 +7,13 @@ on:
- cron: '0 2 * * 0' # Weekly on Sundays at 02:00 - cron: '0 2 * * 0' # Weekly on Sundays at 02:00
jobs: jobs:
format:
name: Format
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set Up Cache
uses: actions/cache@v2
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
- name: Set up PHP
uses: shivammathur/setup-php@master
with:
php-version: 7.4
- name: Check format
run: ./vendor/bin/php-cs-fixer fix --dry-run
test: test:
name: PHP ${{ matrix.php }} / DB ${{ matrix.database }} name: PHP ${{ matrix.php }} / DB ${{ matrix.database }}
strategy: strategy:
max-parallel: 3
fail-fast: false fail-fast: false
matrix: matrix:
php: ['7.4', '8.0'] php: ['7.3']
database: ['pgsql', 'mysql', 'sqlite'] database: ['pgsql', 'mysql', 'sqlite']
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -48,13 +21,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set Up Cache
uses: actions/cache@v2
with:
path: |
vendor
key: vendor-${{ matrix.php }}-${{ hashFiles('composer.lock') }}
- name: Set up PHP - name: Set up PHP
uses: shivammathur/setup-php@master uses: shivammathur/setup-php@master
with: with:
@ -90,8 +56,11 @@ jobs:
run: composer validate run: composer validate
- name: Install PHP dependencies - name: Install PHP dependencies
run: composer update && composer install --prefer-dist --no-progress run: composer install --prefer-dist --no-progress --no-suggest
- name: Install shimmie
run: php index.php
- name: Run test suite - name: Run test suite
run: | run: |
if [[ "${{ matrix.database }}" == "pgsql" ]]; then if [[ "${{ matrix.database }}" == "pgsql" ]]; then
@ -104,9 +73,23 @@ jobs:
export TEST_DSN="sqlite:data/shimmie.sqlite" export TEST_DSN="sqlite:data/shimmie.sqlite"
fi fi
vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover
- name: Upload coverage - name: Upload coverage
if: matrix.php == '7.4'
run: | run: |
wget https://scrutinizer-ci.com/ocular.phar wget https://scrutinizer-ci.com/ocular.phar
php ocular.phar code-coverage:upload --format=php-clover data/coverage.clover php ocular.phar code-coverage:upload --format=php-clover data/coverage.clover
publish:
name: Publish
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push'
steps:
- uses: actions/checkout@master
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
with:
name: shish2k/shimmie2
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
cache: ${{ github.event_name != 'schedule' }}
buildoptions: "--build-arg RUN_TESTS=false"

View File

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

19
.php_cs.dist Normal file
View File

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

View File

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

View File

@ -5,17 +5,7 @@
"license" : "GPL-2.0-or-later", "license" : "GPL-2.0-or-later",
"minimum-stability" : "dev", "minimum-stability" : "dev",
"config": {
"platform": {
"php": "7.4.0"
}
},
"repositories" : [ "repositories" : [
{
"type": "composer",
"url": "https://asset-packagist.org"
},
{ {
"type" : "package", "type" : "package",
"package" : { "package" : {
@ -31,30 +21,30 @@
], ],
"require" : { "require" : {
"php" : "^7.4 | ^8.0", "php" : "^7.3",
"ext-pdo": "*", "ext-pdo": "*",
"ext-json": "*", "ext-json": "*",
"ext-fileinfo": "*", "ext-fileinfo": "*",
"flexihash/flexihash" : "^2.0", "flexihash/flexihash" : "^2.0.0",
"ifixit/php-akismet" : "^1.0", "ifixit/php-akismet" : "1.*",
"google/recaptcha" : "^1.1", "google/recaptcha" : "~1.1",
"dapphp/securimage" : "^3.6", "dapphp/securimage" : "3.6.*",
"shish/eventtracer-php" : "^2.0", "shish/eventtracer-php" : "^2.0.0",
"shish/ffsphp" : "^1.0", "shish/ffsphp" : "^1.0.0",
"shish/microcrud" : "^2.0", "shish/microcrud" : "^2.0.0",
"shish/microhtml" : "^2.0", "shish/microhtml" : "^2.0.0",
"enshrined/svg-sanitize" : "^0.15", "enshrined/svg-sanitize" : "0.13.*",
"bower-asset/jquery" : "^1.12", "bower-asset/jquery" : "1.12.*",
"bower-asset/jquery-timeago" : "^1.5", "bower-asset/jquery-timeago" : "1.5.*",
"bower-asset/js-cookie" : "^2.1" "bower-asset/mediaelement" : "2.21.*",
"bower-asset/js-cookie" : "2.1.*"
}, },
"require-dev" : { "require-dev" : {
"phpunit/phpunit" : "^9.0", },
"friendsofphp/php-cs-fixer" : "^3.4"
},
"suggest": { "suggest": {
"ext-memcache": "memcache caching", "ext-memcache": "memcache caching",
"ext-memcached": "memcached caching", "ext-memcached": "memcached caching",
@ -68,5 +58,10 @@
"ext-zlib": "anti-spam", "ext-zlib": "anti-spam",
"ext-xml": "some extensions", "ext-xml": "some extensions",
"ext-gd": "GD-based thumbnailing" "ext-gd": "GD-based thumbnailing"
} },"replace": {
"bower-asset/jquery": ">=1.11.0",
"bower-asset/inputmask": ">=3.2.0",
"bower-asset/punycode": ">=1.3.0",
"bower-asset/yii2-pjax": ">=2.0.0"
} }
}

4308
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,13 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
require_once "core/event.php"; require_once "core/event.php";
abstract class PageMode abstract class PageMode
{ {
public const REDIRECT = 'redirect'; const REDIRECT = 'redirect';
public const DATA = 'data'; const DATA = 'data';
public const PAGE = 'page'; const PAGE = 'page';
public const FILE = 'file'; const FILE = 'file';
public const MANUAL = 'manual'; const MANUAL = 'manual';
} }
/** /**
@ -22,8 +20,10 @@ abstract class PageMode
*/ */
class BasePage class BasePage
{ {
public string $mode = PageMode::PAGE; /** @var string */
private string $mime; public $mode = PageMode::PAGE;
/** @var string */
private $type = "text/html; charset=utf-8";
/** /**
* Set what this page should do; "page", "data", or "redirect". * Set what this page should do; "page", "data", or "redirect".
@ -36,14 +36,13 @@ class BasePage
/** /**
* Set the page's MIME type. * Set the page's MIME type.
*/ */
public function set_mime(string $mime): void public function set_type(string $type): void
{ {
$this->mime = $mime; $this->type = $type;
} }
public function __construct() public function __construct()
{ {
$this->mime = MimeType::add_parameters(MimeType::HTML, MimeType::CHARSET_UTF8);
if (@$_GET["flash"]) { if (@$_GET["flash"]) {
$this->flash[] = $_GET['flash']; $this->flash[] = $_GET['flash'];
unset($_GET["flash"]); unset($_GET["flash"]);
@ -52,11 +51,19 @@ class BasePage
// ============================================== // ==============================================
public string $data = ""; // public only for unit test /** @var string; public only for unit test */
private ?string $file = null; public $data = "";
private bool $file_delete = false;
private ?string $filename = null; /** @var string */
private ?string $disposition = null; private $file = null;
/** @var bool */
private $file_delete = false;
/** @var string */
private $filename = null;
private $disposition = null;
/** /**
* Set the raw data to be sent. * Set the raw data to be sent.
@ -83,7 +90,8 @@ class BasePage
// ============================================== // ==============================================
public string $redirect = ""; /** @var string */
public $redirect = "";
/** /**
* Set the URL to redirect to (remember to use make_link() if linking * Set the URL to redirect to (remember to use make_link() if linking
@ -96,25 +104,32 @@ class BasePage
// ============================================== // ==============================================
public int $code = 200; /** @var int */
public string $title = ""; public $code = 200;
public string $heading = "";
public string $subheading = ""; /** @var string */
public $title = "";
/** @var string */
public $heading = "";
/** @var string */
public $subheading = "";
/** @var string[] */ /** @var string[] */
public array $html_headers = []; public $html_headers = [];
/** @var string[] */ /** @var string[] */
public array $http_headers = []; public $http_headers = [];
/** @var string[][] */ /** @var string[][] */
public array $cookies = []; public $cookies = [];
/** @var Block[] */ /** @var Block[] */
public array $blocks = []; public $blocks = [];
/** @var string[] */ /** @var string[] */
public array $flash = []; public $flash = [];
/** /**
* Set the HTTP status code * Set the HTTP status code
@ -228,7 +243,7 @@ class BasePage
{ {
if (!headers_sent()) { if (!headers_sent()) {
header("HTTP/1.0 {$this->code} Shimmie"); header("HTTP/1.0 {$this->code} Shimmie");
header("Content-type: " . $this->mime); header("Content-type: " . $this->type);
header("X-Powered-By: Shimmie-" . VERSION); header("X-Powered-By: Shimmie-" . VERSION);
foreach ($this->http_headers as $head) { foreach ($this->http_headers as $head) {
@ -282,7 +297,7 @@ class BasePage
if (isset($_SERVER['HTTP_RANGE'])) { if (isset($_SERVER['HTTP_RANGE'])) {
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2); list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (str_contains($range, ',')) { if (strpos($range, ',') !== false) {
header('HTTP/1.1 416 Requested Range Not Satisfiable'); header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size"); header("Content-Range: bytes $start-$end/$size");
break; break;
@ -319,7 +334,7 @@ class BasePage
break; break;
case PageMode::REDIRECT: case PageMode::REDIRECT:
if ($this->flash) { if ($this->flash) {
$this->redirect .= str_contains($this->redirect, "?") ? "&" : "?"; $this->redirect .= (strpos($this->redirect, "?") === false) ? "?" : "&";
$this->redirect .= "flash=" . url_escape(implode("\n", $this->flash)); $this->redirect .= "flash=" . url_escape(implode("\n", $this->flash));
} }
header('Location: ' . $this->redirect); header('Location: ' . $this->redirect);
@ -339,6 +354,8 @@ class BasePage
* Why do this? Two reasons: * Why do this? Two reasons:
* 1. Reduces the number of files the user's browser needs to download. * 1. Reduces the number of files the user's browser needs to download.
* 2. Allows these cached files to be compressed/minified by the admin. * 2. Allows these cached files to be compressed/minified by the admin.
*
* TODO: This should really be configurable somehow...
*/ */
public function add_auto_html_headers(): void public function add_auto_html_headers(): void
{ {
@ -363,7 +380,7 @@ class BasePage
$css_latest = $config_latest; $css_latest = $config_latest;
$css_files = array_merge( $css_files = array_merge(
zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/style.css"), zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/style.css"),
zglob("themes/$theme_name/{" . implode(",", $this->get_theme_stylesheets()) . "}") zglob("themes/$theme_name/style.css")
); );
foreach ($css_files as $css) { foreach ($css_files as $css) {
$css_latest = max($css_latest, filemtime($css)); $css_latest = max($css_latest, filemtime($css));
@ -387,13 +404,12 @@ class BasePage
$js_latest = $config_latest; $js_latest = $config_latest;
$js_files = array_merge( $js_files = array_merge(
[ [
"vendor/bower-asset/jquery/dist/jquery.min.js",
"vendor/bower-asset/jquery-timeago/jquery.timeago.js", "vendor/bower-asset/jquery-timeago/jquery.timeago.js",
"vendor/bower-asset/js-cookie/src/js.cookie.js", "vendor/bower-asset/js-cookie/src/js.cookie.js",
"ext/static_files/modernizr-3.3.1.custom.js", "ext/static_files/modernizr-3.3.1.custom.js",
], ],
zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/script.js"), zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/script.js"),
zglob("themes/$theme_name/{" . implode(",", $this->get_theme_scripts()) . "}") zglob("themes/$theme_name/script.js")
); );
foreach ($js_files as $js) { foreach ($js_files as $js) {
$js_latest = max($js_latest, filemtime($js)); $js_latest = max($js_latest, filemtime($js));
@ -410,25 +426,7 @@ class BasePage
$this->add_html_header("<script defer src='$data_href/$js_cache_file' type='text/javascript'></script>", 44); $this->add_html_header("<script defer src='$data_href/$js_cache_file' type='text/javascript'></script>", 44);
} }
protected function get_nav_links()
/**
* @return array A list of stylesheets relative to the theme root.
*/
protected function get_theme_stylesheets(): array
{
return ["style.css"];
}
/**
* @return array A list of script files relative to the theme root.
*/
protected function get_theme_scripts(): array
{
return ["script.js"];
}
protected function get_nav_links(): array
{ {
$pnbe = send_event(new PageNavBuildingEvent()); $pnbe = send_event(new PageNavBuildingEvent());
@ -560,7 +558,7 @@ EOD;
$contact = empty($contact_link) ? "" : "<br><a href='$contact_link'>Contact</a>"; $contact = empty($contact_link) ? "" : "<br><a href='$contact_link'>Contact</a>";
return " return "
Media &copy; their respective owners, Images &copy; their respective owners,
<a href=\"https://code.shishnet.org/shimmie2/\">Shimmie</a> &copy; <a href=\"https://code.shishnet.org/shimmie2/\">Shimmie</a> &copy;
<a href=\"https://www.shishnet.org/\">Shish</a> &amp; <a href=\"https://www.shishnet.org/\">Shish</a> &amp;
<a href=\"https://github.com/shish/shimmie2/graphs/contributors\">The Team</a> <a href=\"https://github.com/shish/shimmie2/graphs/contributors\">The Team</a>
@ -574,7 +572,7 @@ EOD;
class PageNavBuildingEvent extends Event class PageNavBuildingEvent extends Event
{ {
public array $links = []; public $links = [];
public function add_nav_link(string $name, Link $link, string $desc, ?bool $active = null, int $order = 50) public function add_nav_link(string $name, Link $link, string $desc, ?bool $active = null, int $order = 50)
{ {
@ -584,9 +582,9 @@ class PageNavBuildingEvent extends Event
class PageSubNavBuildingEvent extends Event class PageSubNavBuildingEvent extends Event
{ {
public string $parent; public $parent;
public array $links = []; public $links = [];
public function __construct(string $parent) public function __construct(string $parent)
{ {
@ -602,11 +600,11 @@ class PageSubNavBuildingEvent extends Event
class NavLink class NavLink
{ {
public string $name; public $name;
public Link $link; public $link;
public string $description; public $description;
public int $order; public $order;
public bool $active = false; public $active = false;
public function __construct(String $name, Link $link, String $description, ?bool $active = null, int $order = 50) public function __construct(String $name, Link $link, String $description, ?bool $active = null, int $order = 50)
{ {
@ -663,7 +661,7 @@ class NavLink
} }
} }
function sort_nav_links(NavLink $a, NavLink $b): int function sort_nav_links(NavLink $a, NavLink $b)
{ {
return $a->order - $b->order; return $a->order - $b->order;
} }

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
/** /**
* Class BaseThemelet * Class BaseThemelet
@ -9,6 +7,7 @@ declare(strict_types=1);
*/ */
class BaseThemelet class BaseThemelet
{ {
/** /**
* Generic error message display * Generic error message display
*/ */
@ -54,9 +53,8 @@ class BaseThemelet
$h_tip = html_escape($image->get_tooltip()); $h_tip = html_escape($image->get_tooltip());
$h_tags = html_escape(strtolower($image->get_tag_list())); $h_tags = html_escape(strtolower($image->get_tag_list()));
// TODO: Set up a function for fetching what kind of files are currently thumbnailable $extArr = array_flip([EXTENSION_FLASH, EXTENSION_SVG, EXTENSION_MP3]); //List of thumbless filetypes
$mimeArr = array_flip([MimeType::MP3]); //List of thumbless filetypes if (!isset($extArr[$image->ext])) {
if (!isset($mimeArr[$image->get_mime()])) {
$tsize = get_thumbnail_size($image->width, $image->height); $tsize = get_thumbnail_size($image->width, $image->height);
} else { } else {
//Use max thumbnail size if using thumbless filetype //Use max thumbnail size if using thumbless filetype
@ -73,7 +71,7 @@ class BaseThemelet
} }
} }
return "<a href='$h_view_link' class='thumb shm-thumb shm-thumb-link {$custom_classes}' data-tags='$h_tags' data-height='$image->height' data-width='$image->width' data-mime='{$image->get_mime()}' data-post-id='$i_id'>". return "<a href='$h_view_link' class='thumb shm-thumb shm-thumb-link {$custom_classes}' data-tags='$h_tags' data-post-id='$i_id'>".
"<img id='thumb_$i_id' title='$h_tip' alt='$h_tip' height='{$tsize[1]}' width='{$tsize[0]}' src='$h_thumb_link'>". "<img id='thumb_$i_id' title='$h_tip' alt='$h_tip' height='{$tsize[1]}' width='{$tsize[0]}' src='$h_thumb_link'>".
"</a>\n"; "</a>\n";
} }
@ -125,7 +123,7 @@ class BaseThemelet
$at_end = ($current_page >= $total_pages); $at_end = ($current_page >= $total_pages);
$first_html = $at_start ? "First" : $this->gen_page_link($base_url, $query, 1, "First"); $first_html = $at_start ? "First" : $this->gen_page_link($base_url, $query, 1, "First");
$prev_html = $at_start ? "Prev" : $this->gen_page_link($base_url, $query, $prev, "Prev"); $prev_html = $at_start ? "Prev" : $this->gen_page_link($base_url, $query, $prev, "Prev");
$random_html = "-"; $random_html = "-";
if ($show_random) { if ($show_random) {
@ -133,8 +131,8 @@ class BaseThemelet
$random_html = $this->gen_page_link($base_url, $query, $rand, "Random"); $random_html = $this->gen_page_link($base_url, $query, $rand, "Random");
} }
$next_html = $at_end ? "Next" : $this->gen_page_link($base_url, $query, $next, "Next"); $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"); $last_html = $at_end ? "Last" : $this->gen_page_link($base_url, $query, $total_pages, "Last");
$start = $current_page-5 > 1 ? $current_page-5 : 1; $start = $current_page-5 > 1 ? $current_page-5 : 1;
$end = $start+10 < $total_pages ? $start+10 : $total_pages; $end = $start+10 < $total_pages ? $start+10 : $total_pages;

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
/** /**
* Class Block * Class Block
@ -11,37 +9,49 @@ class Block
{ {
/** /**
* The block's title. * The block's title.
*
* @var string
*/ */
public ?string $header; public $header;
/** /**
* The content of the block. * The content of the block.
*
* @var string
*/ */
public ?string $body; public $body;
/** /**
* Where the block should be placed. The default theme supports * Where the block should be placed. The default theme supports
* "main" and "left", other themes can add their own areas. * "main" and "left", other themes can add their own areas.
*
* @var string
*/ */
public string $section; public $section;
/** /**
* How far down the section the block should appear, higher * How far down the section the block should appear, higher
* numbers appear lower. The scale is 0-100 by convention, * numbers appear lower. The scale is 0-100 by convention,
* though any number will work. * though any number will work.
*
* @var int
*/ */
public int $position; public $position;
/** /**
* A unique ID for the block. * A unique ID for the block.
*
* @var string
*/ */
public string $id; public $id;
/** /**
* Should this block count as content for the sake of * Should this block count as content for the sake of
* the 404 handler * the 404 handler
*
* @var boolean
*/ */
public bool $is_content = true; public $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 $body=null, string $section="main", int $position=50, string $id=null)
{ {
@ -53,9 +63,7 @@ class Block
if (is_null($id)) { if (is_null($id)) {
$id = (empty($header) ? md5($body ?? '') : $header) . $section; $id = (empty($header) ? md5($body ?? '') : $header) . $section;
} }
$str_id = preg_replace('/[^\w-]/', '', str_replace(' ', '_', $id)); $this->id = preg_replace('/[^\w-]/', '', str_replace(' ', '_', $id));
assert(is_string($str_id));
$this->id = $str_id;
} }
/** /**

View File

@ -1,11 +1,9 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
interface CacheEngine interface CacheEngine
{ {
public function get(string $key); public function get(string $key);
public function set(string $key, $val, int $time=0): void; public function set(string $key, $val, int $time=0);
public function delete(string $key): void; public function delete(string $key);
} }
class NoCache implements CacheEngine class NoCache implements CacheEngine
@ -14,22 +12,23 @@ class NoCache implements CacheEngine
{ {
return false; return false;
} }
public function set(string $key, $val, int $time=0): void public function set(string $key, $val, int $time=0)
{ {
} }
public function delete(string $key): void public function delete(string $key)
{ {
} }
} }
class MemcachedCache implements CacheEngine class MemcachedCache implements CacheEngine
{ {
public ?Memcached $memcache=null; /** @var ?Memcached */
public $memcache=null;
public function __construct(string $args) public function __construct(string $args)
{ {
$hp = explode(":", $args); $hp = explode(":", $args);
$this->memcache = new Memcached(); $this->memcache = new Memcached;
#$this->memcache->setOption(Memcached::OPT_COMPRESSION, False); #$this->memcache->setOption(Memcached::OPT_COMPRESSION, False);
#$this->memcache->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP); #$this->memcache->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP);
#$this->memcache->setOption(Memcached::OPT_PREFIX_KEY, phpversion()); #$this->memcache->setOption(Memcached::OPT_PREFIX_KEY, phpversion());
@ -53,7 +52,7 @@ class MemcachedCache implements CacheEngine
} }
} }
public function set(string $key, $val, int $time=0): void public function set(string $key, $val, int $time=0)
{ {
$key = urlencode($key); $key = urlencode($key);
@ -64,7 +63,7 @@ class MemcachedCache implements CacheEngine
} }
} }
public function delete(string $key): void public function delete(string $key)
{ {
$key = urlencode($key); $key = urlencode($key);
@ -88,12 +87,12 @@ class APCCache implements CacheEngine
return apc_fetch($key); return apc_fetch($key);
} }
public function set(string $key, $val, int $time=0): void public function set(string $key, $val, int $time=0)
{ {
apc_store($key, $val, $time); apc_store($key, $val, $time);
} }
public function delete(string $key): void public function delete(string $key)
{ {
apc_delete($key); apc_delete($key);
} }
@ -101,7 +100,7 @@ class APCCache implements CacheEngine
class RedisCache implements CacheEngine class RedisCache implements CacheEngine
{ {
private Redis $redis; private $redis=null;
public function __construct(string $args) public function __construct(string $args)
{ {
@ -117,7 +116,7 @@ class RedisCache implements CacheEngine
return $this->redis->get($key); return $this->redis->get($key);
} }
public function set(string $key, $val, int $time=0): void public function set(string $key, $val, int $time=0)
{ {
if ($time > 0) { if ($time > 0) {
$this->redis->setEx($key, $time, $val); $this->redis->setEx($key, $time, $val);
@ -126,7 +125,7 @@ class RedisCache implements CacheEngine
} }
} }
public function delete(string $key): void public function delete(string $key)
{ {
$this->redis->del($key); $this->redis->del($key);
} }
@ -135,9 +134,9 @@ class RedisCache implements CacheEngine
class Cache class Cache
{ {
public $engine; public $engine;
public int $hits=0; public $hits=0;
public int $misses=0; public $misses=0;
public int $time=0; public $time=0;
public function __construct(?string $dsn) public function __construct(?string $dsn)
{ {

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* CAPTCHA abstraction * * CAPTCHA abstraction *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ \* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@ -11,7 +9,7 @@ function captcha_get_html(): string
{ {
global $config, $user; global $config, $user;
if (DEBUG && ip_in_range(get_real_ip(), "127.0.0.0/8")) { if (DEBUG && ip_in_range($_SERVER['REMOTE_ADDR'], "127.0.0.0/8")) {
return ""; return "";
} }
@ -34,7 +32,7 @@ function captcha_check(): bool
{ {
global $config, $user; global $config, $user;
if (DEBUG && ip_in_range(get_real_ip(), "127.0.0.0/8")) { if (DEBUG && ip_in_range($_SERVER['REMOTE_ADDR'], "127.0.0.0/8")) {
return true; return true;
} }
@ -42,7 +40,7 @@ function captcha_check(): bool
$r_privatekey = $config->get_string('api_recaptcha_privkey'); $r_privatekey = $config->get_string('api_recaptcha_privkey');
if (!empty($r_privatekey)) { if (!empty($r_privatekey)) {
$recaptcha = new ReCaptcha($r_privatekey); $recaptcha = new ReCaptcha($r_privatekey);
$resp = $recaptcha->verify($_POST['g-recaptcha-response'] ?? "", get_real_ip()); $resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']);
if (!$resp->isSuccess()) { if (!$resp->isSuccess()) {
log_info("core", "Captcha failed (ReCaptcha): " . implode("", $resp->getErrorCodes())); log_info("core", "Captcha failed (ReCaptcha): " . implode("", $resp->getErrorCodes()));

View File

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

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
/** /**
* Interface Config * Interface Config
@ -132,7 +130,7 @@ interface Config
*/ */
abstract class BaseConfig implements Config abstract class BaseConfig implements Config
{ {
public array $values = []; public $values = [];
public function set_int(string $name, ?int $value): void public function set_int(string $name, ?int $value): void
{ {
@ -258,10 +256,12 @@ abstract class BaseConfig implements Config
*/ */
class DatabaseConfig extends BaseConfig class DatabaseConfig extends BaseConfig
{ {
private Database $database; /** @var Database */
private string $table_name; private $database = null;
private ?string $sub_column;
private ?string $sub_value; private $table_name;
private $sub_column;
private $sub_value;
public function __construct( public function __construct(
Database $database, Database $database,
@ -323,10 +323,10 @@ class DatabaseConfig extends BaseConfig
$params[] = ":sub_value"; $params[] = ":sub_value";
} }
$this->database->execute($query, $args); $this->database->Execute($query, $args);
$args["value"] =$this->values[$name]; $args["value"] =$this->values[$name];
$this->database->execute( $this->database->Execute(
"INSERT INTO {$this->table_name} (".join(",", $cols).") VALUES (".join(",", $params).")", "INSERT INTO {$this->table_name} (".join(",", $cols).") VALUES (".join(",", $params).")",
$args $args
); );
@ -334,6 +334,5 @@ class DatabaseConfig extends BaseConfig
// rather than deleting and having some other request(s) do a thundering // rather than deleting and having some other request(s) do a thundering
// herd of race-conditioned updates, just save the updated version once here // herd of race-conditioned updates, just save the updated version once here
$cache->set("config", $this->values); $cache->set("config", $this->values);
$this->database->notify("config");
} }
} }

View File

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

View File

@ -1,23 +1,28 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
abstract class SCORE abstract class SCORE
{ {
public const AIPK = "SCORE_AIPK"; const AIPK = "SCORE_AIPK";
public const INET = "SCORE_INET"; const INET = "SCORE_INET";
const BOOL_Y = "SCORE_BOOL_Y";
const BOOL_N = "SCORE_BOOL_N";
const BOOL = "SCORE_BOOL";
} }
abstract class DBEngine abstract class DBEngine
{ {
public ?string $name = null; /** @var null|string */
public $name = null;
public $BOOL_Y = null;
public $BOOL_N = null;
public function init(PDO $db) public function init(PDO $db)
{ {
} }
public function scoreql_to_sql(string $data): string public function scoreql_to_sql(string $scoreql): string
{ {
return $data; return $scoreql;
} }
public function create_table_sql(string $name, string $data): string public function create_table_sql(string $name, string $data): string
@ -25,16 +30,18 @@ abstract class DBEngine
return 'CREATE TABLE '.$name.' ('.$data.')'; return 'CREATE TABLE '.$name.' ('.$data.')';
} }
abstract public function set_timeout(PDO $db, ?int $time); abstract public function set_timeout(PDO $db, int $time);
abstract public function get_version(PDO $db): string; abstract public function get_version(PDO $db): string;
abstract public function notify(PDO $db, string $channel, ?string $data=null): void;
} }
class MySQL extends DBEngine class MySQL extends DBEngine
{ {
public ?string $name = DatabaseDriver::MYSQL; /** @var string */
public $name = DatabaseDriver::MYSQL;
public $BOOL_Y = 'Y';
public $BOOL_N = 'N';
public function init(PDO $db) public function init(PDO $db)
{ {
@ -45,6 +52,9 @@ class MySQL extends DBEngine
{ {
$data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY auto_increment", $data); $data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY auto_increment", $data);
$data = str_replace(SCORE::INET, "VARCHAR(45)", $data); $data = str_replace(SCORE::INET, "VARCHAR(45)", $data);
$data = str_replace(SCORE::BOOL_Y, "'$this->BOOL_Y'", $data);
$data = str_replace(SCORE::BOOL_N, "'$this->BOOL_N'", $data);
$data = str_replace(SCORE::BOOL, "ENUM('Y', 'N')", $data);
return $data; return $data;
} }
@ -55,16 +65,12 @@ class MySQL extends DBEngine
return 'CREATE TABLE '.$name.' ('.$data.') '.$ctes; return 'CREATE TABLE '.$name.' ('.$data.') '.$ctes;
} }
public function set_timeout(PDO $db, ?int $time): void public function set_timeout(PDO $db, int $time): void
{ {
// These only apply to read-only queries, which appears to be the best we can to mysql-wise // These only apply to read-only queries, which appears to be the best we can to mysql-wise
// $db->exec("SET SESSION MAX_EXECUTION_TIME=".$time.";"); // $db->exec("SET SESSION MAX_EXECUTION_TIME=".$time.";");
} }
public function notify(PDO $db, string $channel, ?string $data=null): void
{
}
public function get_version(PDO $db): string public function get_version(PDO $db): string
{ {
return $db->query('select version()')->fetch()[0]; return $db->query('select version()')->fetch()[0];
@ -73,7 +79,11 @@ class MySQL extends DBEngine
class PostgreSQL extends DBEngine class PostgreSQL extends DBEngine
{ {
public ?string $name = DatabaseDriver::PGSQL; /** @var string */
public $name = DatabaseDriver::PGSQL;
public $BOOL_Y = "true";
public $BOOL_N = "false";
public function init(PDO $db) public function init(PDO $db)
{ {
@ -91,6 +101,9 @@ class PostgreSQL extends DBEngine
{ {
$data = str_replace(SCORE::AIPK, "INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY", $data); $data = str_replace(SCORE::AIPK, "INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY", $data);
$data = str_replace(SCORE::INET, "INET", $data); $data = str_replace(SCORE::INET, "INET", $data);
$data = str_replace(SCORE::BOOL_Y, "true", $data);
$data = str_replace(SCORE::BOOL_N, "false", $data);
$data = str_replace(SCORE::BOOL, "BOOL", $data);
return $data; return $data;
} }
@ -100,23 +113,11 @@ class PostgreSQL extends DBEngine
return "CREATE TABLE $name ($data)"; return "CREATE TABLE $name ($data)";
} }
public function set_timeout(PDO $db, ?int $time): void public function set_timeout(PDO $db, int $time): void
{ {
if (is_null($time)) {
$time = 0;
}
$db->exec("SET statement_timeout TO ".$time.";"); $db->exec("SET statement_timeout TO ".$time.";");
} }
public function notify(PDO $db, string $channel, ?string $data=null): void
{
if ($data) {
$db->exec("NOTIFY $channel, '$data';");
} else {
$db->exec("NOTIFY $channel;");
}
}
public function get_version(PDO $db): string public function get_version(PDO $db): string
{ {
return $db->query('select version()')->fetch()[0]; return $db->query('select version()')->fetch()[0];
@ -124,19 +125,19 @@ class PostgreSQL extends DBEngine
} }
// shimmie functions for export to sqlite // shimmie functions for export to sqlite
function _unix_timestamp($date): int function _unix_timestamp($date)
{ {
return strtotime($date); return strtotime($date);
} }
function _now(): string function _now()
{ {
return date("Y-m-d H:i:s"); return date("Y-m-d H:i:s");
} }
function _floor($a): float function _floor($a)
{ {
return floor($a); return floor($a);
} }
function _log($a, $b=null): float function _log($a, $b=null)
{ {
if (is_null($b)) { if (is_null($b)) {
return log($a); return log($a);
@ -144,34 +145,39 @@ function _log($a, $b=null): float
return log($a, $b); return log($a, $b);
} }
} }
function _isnull($a): bool function _isnull($a)
{ {
return is_null($a); return is_null($a);
} }
function _md5($a): string function _md5($a)
{ {
return md5($a); return md5($a);
} }
function _concat($a, $b): string function _concat($a, $b)
{ {
return $a . $b; return $a . $b;
} }
function _lower($a): string function _lower($a)
{ {
return strtolower($a); return strtolower($a);
} }
function _rand(): int function _rand()
{ {
return rand(); return rand();
} }
function _ln($n): float function _ln($n)
{ {
return log($n); return log($n);
} }
class SQLite extends DBEngine class SQLite extends DBEngine
{ {
public ?string $name = DatabaseDriver::SQLITE; /** @var string */
public $name = DatabaseDriver::SQLITE;
public $BOOL_Y = 'Y';
public $BOOL_N = 'N';
public function init(PDO $db) public function init(PDO $db)
{ {
@ -193,6 +199,9 @@ class SQLite extends DBEngine
{ {
$data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY", $data); $data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY", $data);
$data = str_replace(SCORE::INET, "VARCHAR(45)", $data); $data = str_replace(SCORE::INET, "VARCHAR(45)", $data);
$data = str_replace(SCORE::BOOL_Y, "'$this->BOOL_Y'", $data);
$data = str_replace(SCORE::BOOL_N, "'$this->BOOL_N'", $data);
$data = str_replace(SCORE::BOOL, "CHAR(1)", $data);
return $data; return $data;
} }
@ -215,15 +224,11 @@ class SQLite extends DBEngine
return "CREATE TABLE $name ($cols_redone); $extras"; return "CREATE TABLE $name ($cols_redone); $extras";
} }
public function set_timeout(PDO $db, ?int $time): void public function set_timeout(PDO $db, int $time): void
{ {
// There doesn't seem to be such a thing for SQLite, so it does nothing // There doesn't seem to be such a thing for SQLite, so it does nothing
} }
public function notify(PDO $db, string $channel, ?string $data=null): void
{
}
public function get_version(PDO $db): string public function get_version(PDO $db): string
{ {
return $db->query('select sqlite_version()')->fetch()[0]; return $db->query('select sqlite_version()')->fetch()[0];

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
/** /**
* Generic parent class for all events. * Generic parent class for all events.
* *
@ -8,13 +6,13 @@ declare(strict_types=1);
*/ */
abstract class Event abstract class Event
{ {
public bool $stop_processing = false; public $stop_processing = false;
public function __construct() public function __construct()
{ {
} }
public function __toString(): string public function __toString()
{ {
return var_export($this, true); return var_export($this, true);
} }
@ -44,11 +42,19 @@ class InitExtEvent extends Event
class PageRequestEvent extends Event class PageRequestEvent extends Event
{ {
/** /**
* @var string[] * @var array
*/ */
public $args; public $args;
public int $arg_count;
public int $part_count; /**
* @var int
*/
public $arg_count;
/**
* @var int
*/
public $part_count;
public function __construct(string $path) public function __construct(string $path)
{ {
@ -103,7 +109,7 @@ class PageRequestEvent extends Event
return $this->args[$offset]; return $this->args[$offset];
} else { } else {
$nm1 = $this->arg_count - 1; $nm1 = $this->arg_count - 1;
throw new UserErrorException("Requested an invalid page argument {$offset} / {$nm1}"); throw new SCoreException("Requested an invalid page argument {$offset} / {$nm1}");
} }
} }
@ -173,12 +179,15 @@ class PageRequestEvent extends Event
*/ */
class CommandEvent extends Event class CommandEvent extends Event
{ {
public string $cmd = "help"; /**
* @var string
*/
public $cmd = "help";
/** /**
* @var string[] * @var array
*/ */
public array $args = []; public $args = [];
/** /**
* #param string[] $args * #param string[] $args
@ -247,18 +256,24 @@ class TextFormattingEvent extends Event
{ {
/** /**
* For reference * For reference
*
* @var string
*/ */
public string $original; public $original;
/** /**
* with formatting applied * with formatting applied
*
* @var string
*/ */
public string $formatted; public $formatted;
/** /**
* with formatting removed * with formatting removed
*
* @var string
*/ */
public string $stripped; public $stripped;
public function __construct(string $text) public function __construct(string $text)
{ {
@ -281,30 +296,38 @@ class LogEvent extends Event
{ {
/** /**
* a category, normally the extension name * a category, normally the extension name
*
* @var string
*/ */
public string $section; public $section;
/** /**
* See python... * See python...
*
* @var int
*/ */
public int $priority = 0; public $priority = 0;
/** /**
* Free text to be logged * Free text to be logged
*
* @var string
*/ */
public string $message; public $message;
/** /**
* The time that the event was created * The time that the event was created
*
* @var int
*/ */
public int $time; public $time;
/** /**
* Extra data to be held separate * Extra data to be held separate
* *
* @var string[] * @var array
*/ */
public array $args; public $args;
public function __construct(string $section, int $priority, string $message) public function __construct(string $section, int $priority, string $message)
{ {

View File

@ -1,15 +1,17 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
/** /**
* Class SCoreException
*
* A base exception to be caught by the upper levels. * A base exception to be caught by the upper levels.
*/ */
class SCoreException extends RuntimeException class SCoreException extends RuntimeException
{ {
public ?string $query; /** @var string|null */
public string $error; public $query;
public int $http_code = 500;
/** @var string */
public $error;
public function __construct(string $msg, ?string $query=null) public function __construct(string $msg, ?string $query=null)
{ {
@ -21,71 +23,61 @@ class SCoreException extends RuntimeException
class InstallerException extends RuntimeException class InstallerException extends RuntimeException
{ {
public string $title; /** @var string */
public string $body; public $title;
public int $exit_code;
public function __construct(string $title, string $body, int $exit_code) /** @var string */
public $body;
/** @var int */
public $code;
public function __construct(string $title, string $body, int $code)
{ {
parent::__construct($body); parent::__construct($body);
$this->title = $title; $this->title = $title;
$this->body = $body; $this->body = $body;
$this->exit_code = $exit_code; $this->code = $code;
} }
} }
class UserErrorException extends SCoreException
{
public int $http_code = 400;
}
class ServerErrorException extends SCoreException
{
public int $http_code = 500;
}
/** /**
* Class PermissionDeniedException
*
* A fairly common, generic exception. * A fairly common, generic exception.
*/ */
class PermissionDeniedException extends UserErrorException class PermissionDeniedException extends SCoreException
{ {
public int $http_code = 403;
} }
/** /**
* Class ImageDoesNotExist
*
* This exception is used when an Image cannot be found by ID. * This exception is used when an Image cannot be found by ID.
*
* Example: Image::by_id(-1) returns null
*/ */
class ImageDoesNotExist extends UserErrorException class ImageDoesNotExist extends SCoreException
{ {
public int $http_code = 404;
}
/**
* This exception is used when a User cannot be found by some criteria.
*/
class UserDoesNotExist extends UserErrorException
{
public int $http_code = 404;
} }
/* /*
* For validate_input() * For validate_input()
*/ */
class InvalidInput extends UserErrorException class InvalidInput extends SCoreException
{ {
public int $http_code = 402;
} }
/* /*
* This is used by the image resizing code when there is not enough memory to perform a resize. * This is used by the image resizing code when there is not enough memory to perform a resize.
*/ */
class InsufficientMemoryException extends ServerErrorException class InsufficientMemoryException extends SCoreException
{ {
} }
/* /*
* This is used by the image resizing code when there is an error while resizing * This is used by the image resizing code when there is an error while resizing
*/ */
class ImageResizeException extends ServerErrorException class ImageResizeException extends SCoreException
{ {
} }

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
/** /**
* Class Extension * Class Extension
* *
@ -15,11 +13,16 @@ declare(strict_types=1);
*/ */
abstract class Extension abstract class Extension
{ {
public string $key; /** @var string */
protected ?Themelet $theme; public $key;
public ?ExtensionInfo $info;
private static array $enabled_extensions = []; /** @var Themelet */
protected $theme;
/** @var ExtensionInfo */
public $info;
private static $enabled_extensions = [];
public function __construct($class = null) public function __construct($class = null)
{ {
@ -58,7 +61,7 @@ abstract class Extension
return 50; return 50;
} }
public static function determine_enabled_extensions(): void public static function determine_enabled_extensions()
{ {
self::$enabled_extensions = []; self::$enabled_extensions = [];
foreach (array_merge( foreach (array_merge(
@ -119,31 +122,34 @@ abstract class ExtensionInfo
public const LICENSE_MIT = "MIT"; public const LICENSE_MIT = "MIT";
public const LICENSE_WTFPL = "WTFPL"; public const LICENSE_WTFPL = "WTFPL";
public const VISIBLE_DEFAULT = "default";
public const VISIBLE_ADMIN = "admin"; public const VISIBLE_ADMIN = "admin";
public const VISIBLE_HIDDEN = "hidden"; public const VISIBLE_HIDDEN = "hidden";
private const VALID_VISIBILITY = [self::VISIBLE_DEFAULT, self::VISIBLE_ADMIN, self::VISIBLE_HIDDEN]; private const VALID_VISIBILITY = [self::VISIBLE_ADMIN, self::VISIBLE_HIDDEN];
public string $key; public $key;
public bool $core = false; public $core = false;
public bool $beta = false;
public string $name; public $beta = false;
public string $license;
public string $description;
public array $authors = [];
public array $dependencies = [];
public array $conflicts = [];
public string $visibility = self::VISIBLE_DEFAULT;
public ?string $link = null;
public ?string $version = null;
public ?string $documentation = null;
/** @var string[] which DBs this ext supports (blank for 'all') */ public $name;
public array $db_support = []; public $authors = [];
private ?bool $supported = null; public $link;
private ?string $support_info = null; public $license;
public $version;
public $dependencies = [];
public $visibility;
public $description;
public $documentation;
/** @var array which DBs this ext supports (blank for 'all') */
public $db_support = [];
/** @var bool */
private $supported = null;
/** @var string */
private $support_info = null;
public function is_supported(): bool public function is_supported(): bool
{ {
@ -161,9 +167,9 @@ abstract class ExtensionInfo
return $this->support_info; return $this->support_info;
} }
private static array $all_info_by_key = []; private static $all_info_by_key = [];
private static array $all_info_by_class = []; private static $all_info_by_class = [];
private static array $core_extensions = []; private static $core_extensions = [];
protected function __construct() protected function __construct()
{ {
@ -187,13 +193,6 @@ abstract class ExtensionInfo
if (!empty($this->db_support) && !in_array($database->get_driver_name(), $this->db_support)) { if (!empty($this->db_support) && !in_array($database->get_driver_name(), $this->db_support)) {
$this->support_info .= "Database not supported. "; $this->support_info .= "Database not supported. ";
} }
if (!empty($this->conflicts)) {
$intersects = array_intersect($this->conflicts, Extension::get_enabled_extensions());
if (!empty($intersects)) {
$this->support_info .= "Conflicts with other extension(s): " . join(", ", $intersects);
}
}
// Additional checks here as needed // Additional checks here as needed
$this->supported = empty($this->support_info); $this->supported = empty($this->support_info);
@ -236,7 +235,7 @@ abstract class ExtensionInfo
public static function load_all_extension_info() public static function load_all_extension_info()
{ {
foreach (get_subclasses_of("ExtensionInfo") as $class) { foreach (getSubclassesOf("ExtensionInfo") as $class) {
$extension_info = new $class(); $extension_info = new $class();
if (array_key_exists($extension_info->key, self::$all_info_by_key)) { if (array_key_exists($extension_info->key, self::$all_info_by_key)) {
throw new ScoreException("Extension Info $class with key $extension_info->key has already been loaded"); throw new ScoreException("Extension Info $class with key $extension_info->key has already been loaded");
@ -276,7 +275,7 @@ abstract class FormatterExtension extends Extension
*/ */
abstract class DataHandlerExtension extends Extension abstract class DataHandlerExtension extends Extension
{ {
protected array $SUPPORTED_MIME = []; protected $SUPPORTED_MIME = [];
protected function move_upload_to_archive(DataUploadEvent $event) protected function move_upload_to_archive(DataUploadEvent $event)
{ {
@ -292,11 +291,11 @@ abstract class DataHandlerExtension extends Extension
public function onDataUpload(DataUploadEvent $event) public function onDataUpload(DataUploadEvent $event)
{ {
$supported_mime = $this->supported_mime($event->mime); $supported_ext = $this->supported_ext($event->type);
$check_contents = $this->check_contents($event->tmpname); $check_contents = $this->check_contents($event->tmpname);
if ($supported_mime && $check_contents) { if ($supported_ext && $check_contents) {
$this->move_upload_to_archive($event); $this->move_upload_to_archive($event);
send_event(new ThumbnailGenerationEvent($event->hash, $event->mime)); send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
/* Check if we are replacing an image */ /* Check if we are replacing an image */
if (!is_null($event->replace_id)) { if (!is_null($event->replace_id)) {
@ -306,20 +305,20 @@ abstract class DataHandlerExtension extends Extension
$existing = Image::by_id($event->replace_id); $existing = Image::by_id($event->replace_id);
if (is_null($existing)) { if (is_null($existing)) {
throw new UploadException("Post to replace does not exist!"); throw new UploadException("Image to replace does not exist!");
} }
if ($existing->hash === $event->metadata['hash']) { if ($existing->hash === $event->metadata['hash']) {
throw new UploadException("The uploaded post is the same as the one to replace."); throw new UploadException("The uploaded image is the same as the one to replace.");
} }
// even more hax.. // even more hax..
$event->metadata['tags'] = $existing->get_tag_list(); $event->metadata['tags'] = $existing->get_tag_list();
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->metadata['hash']), $event->metadata); $image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->metadata['hash']), $event->metadata);
if (is_null($image)) { if (is_null($image)) {
throw new UploadException("Data handler failed to create post object from data"); throw new UploadException("Data handler failed to create image object from data");
} }
if (empty($image->get_mime())) { if (empty($image->ext)) {
throw new UploadException("Unable to determine MIME for ". $event->tmpname); throw new UploadException("Unable to determine extension for ". $event->tmpname);
} }
try { try {
send_event(new MediaCheckPropertiesEvent($image)); send_event(new MediaCheckPropertiesEvent($image));
@ -328,16 +327,14 @@ abstract class DataHandlerExtension extends Extension
} }
send_event(new ImageReplaceEvent($event->replace_id, $image)); send_event(new ImageReplaceEvent($event->replace_id, $image));
$_id = $event->replace_id; $event->image_id = $event->replace_id;
assert(!is_null($_id));
$event->image_id = $_id;
} else { } else {
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata); $image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
if (is_null($image)) { if (is_null($image)) {
throw new UploadException("Data handler failed to create post object from data"); throw new UploadException("Data handler failed to create image object from data");
} }
if (empty($image->get_mime())) { if (empty($image->ext)) {
throw new UploadException("Unable to determine MIME for ". $event->tmpname); throw new UploadException("Unable to determine extension for ". $event->tmpname);
} }
try { try {
send_event(new MediaCheckPropertiesEvent($image)); send_event(new MediaCheckPropertiesEvent($image));
@ -361,7 +358,7 @@ abstract class DataHandlerExtension extends Extension
send_event(new LockSetEvent($image, !empty($locked))); send_event(new LockSetEvent($image, !empty($locked)));
} }
} }
} elseif ($supported_mime && !$check_contents) { } elseif ($supported_ext && !$check_contents) {
// We DO support this extension - but the file looks corrupt // We DO support this extension - but the file looks corrupt
throw new UploadException("Invalid or corrupted file"); throw new UploadException("Invalid or corrupted file");
} }
@ -370,15 +367,15 @@ abstract class DataHandlerExtension extends Extension
public function onThumbnailGeneration(ThumbnailGenerationEvent $event) public function onThumbnailGeneration(ThumbnailGenerationEvent $event)
{ {
$result = false; $result = false;
if ($this->supported_mime($event->mime)) { if ($this->supported_ext($event->type)) {
if ($event->force) { if ($event->force) {
$result = $this->create_thumb($event->hash, $event->mime); $result = $this->create_thumb($event->hash, $event->type);
} else { } else {
$outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash); $outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash);
if (file_exists($outname)) { if (file_exists($outname)) {
return; return;
} }
$result = $this->create_thumb($event->hash, $event->mime); $result = $this->create_thumb($event->hash, $event->type);
} }
} }
if ($result) { if ($result) {
@ -389,7 +386,7 @@ abstract class DataHandlerExtension extends Extension
public function onDisplayingImage(DisplayingImageEvent $event) public function onDisplayingImage(DisplayingImageEvent $event)
{ {
global $page; global $page;
if ($this->supported_mime($event->image->get_mime())) { if ($this->supported_ext($event->image->ext)) {
/** @noinspection PhpPossiblePolymorphicInvocationInspection */ /** @noinspection PhpPossiblePolymorphicInvocationInspection */
$this->theme->display_image($page, $event->image); $this->theme->display_image($page, $event->image);
} }
@ -397,23 +394,25 @@ abstract class DataHandlerExtension extends Extension
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event) public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{ {
if ($this->supported_mime($event->mime)) { if ($this->supported_ext($event->ext)) {
$this->media_check_properties($event); $this->media_check_properties($event);
} }
} }
protected function create_image_from_data(string $filename, array $metadata): Image protected function create_image_from_data(string $filename, array $metadata): Image
{ {
global $config;
$image = new Image(); $image = new Image();
$image->filesize = $metadata['size']; $image->filesize = $metadata['size'];
$image->hash = $metadata['hash']; $image->hash = $metadata['hash'];
$image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename']; $image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename'];
if ($config->get_bool("upload_use_mime")) {
if (array_key_exists("extension", $metadata)) { $image->ext = get_extension_for_file($filename);
$image->set_mime(MimeType::get_for_file($filename, $metadata["extension"])); }
} else { if (empty($image->ext)) {
$image->set_mime(MimeType::get_for_file($filename)); $image->ext = (($pos = strpos($metadata['extension'], '?')) !== false) ? substr($metadata['extension'], 0, $pos) : $metadata['extension'];
} }
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']); $image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
@ -424,35 +423,22 @@ abstract class DataHandlerExtension extends Extension
abstract protected function media_check_properties(MediaCheckPropertiesEvent $event): void; abstract protected function media_check_properties(MediaCheckPropertiesEvent $event): void;
abstract protected function check_contents(string $tmpname): bool; abstract protected function check_contents(string $tmpname): bool;
abstract protected function create_thumb(string $hash, string $mime): bool; abstract protected function create_thumb(string $hash, string $type): bool;
protected function supported_mime(string $mime): bool protected function supported_ext(string $ext): bool
{ {
return MimeType::matches_array($mime, $this->SUPPORTED_MIME); return in_array(get_mime_for_extension($ext), $this->SUPPORTED_MIME);
}
public static function get_all_supported_mimes(): array
{
$arr = [];
foreach (get_subclasses_of("DataHandlerExtension") as $handler) {
$handler = (new $handler());
$arr = array_merge($arr, $handler->SUPPORTED_MIME);
}
// Not sure how to handle this otherwise, don't want to set up a whole other event for this one class
if (class_exists("TranscodeImage")) {
$arr = array_merge($arr, TranscodeImage::get_enabled_mimes());
}
$arr = array_unique($arr);
return $arr;
} }
public static function get_all_supported_exts(): array public static function get_all_supported_exts(): array
{ {
$arr = []; $arr = [];
foreach (self::get_all_supported_mimes() as $mime) { foreach (getSubclassesOf("DataHandlerExtension") as $handler) {
$arr = array_merge($arr, FileExtension::get_all_for_mime($mime)); $handler = (new $handler());
foreach ($handler->SUPPORTED_MIME as $mime) {
$arr = array_merge($arr, get_all_extension_for_mime($mime));
}
} }
$arr = array_unique($arr); $arr = array_unique($arr);
return $arr; return $arr;

448
core/filetypes.php Normal file
View File

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

View File

@ -1,15 +1,17 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
/** /**
* An image is being added to the database. * An image is being added to the database.
*/ */
class ImageAdditionEvent extends Event class ImageAdditionEvent extends Event
{ {
public User $user; /** @var User */
public Image $image; public $user;
public bool $merged = false;
/** @var Image */
public $image;
public $merged = false;
/** /**
* Inserts a new image into the database with its associated * Inserts a new image into the database with its associated
@ -32,8 +34,11 @@ class ImageAdditionException extends SCoreException
*/ */
class ImageDeletionEvent extends Event class ImageDeletionEvent extends Event
{ {
public Image $image; /** @var Image */
public bool $force = false; public $image;
/** @var bool */
public $force = false;
/** /**
* Deletes an image. * Deletes an image.
@ -54,8 +59,10 @@ class ImageDeletionEvent extends Event
*/ */
class ImageReplaceEvent extends Event class ImageReplaceEvent extends Event
{ {
public int $id; /** @var int */
public Image $image; public $id;
/** @var Image */
public $image;
/** /**
* Replaces an image. * Replaces an image.
@ -81,19 +88,24 @@ class ImageReplaceException extends SCoreException
*/ */
class ThumbnailGenerationEvent extends Event class ThumbnailGenerationEvent extends Event
{ {
public string $hash; /** @var string */
public string $mime; public $hash;
public bool $force; /** @var string */
public bool $generated; public $type;
/** @var bool */
public $force;
/** @var bool */
public $generated;
/** /**
* Request a thumbnail be made for an image object * Request a thumbnail be made for an image object
*/ */
public function __construct(string $hash, string $mime, bool $force=false) public function __construct(string $hash, string $type, bool $force=false)
{ {
parent::__construct(); parent::__construct();
$this->hash = $hash; $this->hash = $hash;
$this->mime = $mime; $this->type = $type;
$this->force = $force; $this->force = $force;
$this->generated = false; $this->generated = false;
} }
@ -109,10 +121,14 @@ class ThumbnailGenerationEvent extends Event
*/ */
class ParseLinkTemplateEvent extends Event class ParseLinkTemplateEvent extends Event
{ {
public string $link; /** @var string */
public string $text; public $link;
public string $original; /** @var string */
public Image $image; public $text;
/** @var string */
public $original;
/** @var Image */
public $image;
public function __construct(string $link, Image $image) public function __construct(string $link, Image $image)
{ {

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
/** /**
* Class Image * Class Image
* *
@ -15,31 +13,64 @@ class Image
public const IMAGE_DIR = "images"; public const IMAGE_DIR = "images";
public const THUMBNAIL_DIR = "thumbs"; public const THUMBNAIL_DIR = "thumbs";
public ?int $id = null; public static $order_sql = null; // this feels ugly
public int $height = 0;
public int $width = 0;
public string $hash;
public int $filesize;
public string $filename;
private string $ext;
private string $mime;
/** @var ?string[] */ /** @var null|int */
public ?array $tag_array; public $id = null;
public int $owner_id;
public string $owner_ip;
public ?string $posted = null;
public ?string $source;
public bool $locked = false;
public ?bool $lossless = null;
public ?bool $video = null;
public ?string $video_codec = null;
public ?bool $image = null;
public ?bool $audio = null;
public ?int $length = null;
public static array $bool_props = ["locked", "lossless", "video", "audio", "image"]; /** @var int */
public static array $int_props = ["id", "owner_id", "height", "width", "filesize", "length"]; public $height;
/** @var int */
public $width;
/** @var string */
public $hash;
/** @var int */
public $filesize;
/** @var string */
public $filename;
/** @var string */
public $ext;
/** @var string[]|null */
public $tag_array;
/** @var int */
public $owner_id;
/** @var string */
public $owner_ip;
/** @var string */
public $posted;
/** @var string */
public $source;
/** @var boolean */
public $locked = false;
/** @var boolean */
public $lossless = null;
/** @var boolean */
public $video = null;
/** @var boolean */
public $image = null;
/** @var boolean */
public $audio = null;
/** @var int */
public $length = null;
public static $bool_props = ["locked", "lossless", "video", "audio"];
public static $int_props = ["id", "owner_id", "height", "width", "filesize", "length"];
/** /**
* One will very rarely construct an image directly, more common * One will very rarely construct an image directly, more common
@ -49,10 +80,6 @@ class Image
{ {
if (!is_null($row)) { if (!is_null($row)) {
foreach ($row as $name => $value) { foreach ($row as $name => $value) {
if (is_numeric($name)) {
continue;
}
// some databases use table.name rather than name // some databases use table.name rather than name
$name = str_replace("images.", "", $name); $name = str_replace("images.", "", $name);
@ -110,7 +137,7 @@ class Image
private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags=[]): iterable private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags=[]): iterable
{ {
global $database, $user; global $database, $user, $config;
if ($start < 0) { if ($start < 0) {
$start = 0; $start = 0;
@ -121,12 +148,17 @@ class Image
if (SPEED_HAX) { if (SPEED_HAX) {
if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > 3) { if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > 3) {
throw new PermissionDeniedException("Anonymous users may only search for up to 3 tags at a time"); throw new SCoreException("Anonymous users may only search for up to 3 tags at a time");
} }
} }
$querylet = Image::build_search_querylet($tags, $limit, $start); $order = (Image::$order_sql ?: "images.".$config->get_string(IndexConfig::ORDER));
return $database->get_all_iterable($querylet->sql, $querylet->variables); $querylet = Image::build_search_querylet($tags, $order, $limit, $start);
$result = $database->get_all_iterable($querylet->sql, $querylet->variables);
Image::$order_sql = null;
return $result;
} }
/** /**
@ -181,6 +213,27 @@ class Image
); );
} }
/**
* Counts consecutive days of image uploads
*/
public static function count_upload_streak(): int
{
$now = date_create();
$last_date = $now;
foreach (self::find_images_iterable() as $img) {
$next_date = date_create($img->posted);
if (date_diff($next_date, $last_date)->days > 0) {
break;
}
$last_date = $next_date;
}
if ($last_date === $now) {
return 0;
}
$diff_d = ($now->getTimestamp() - $last_date->getTimestamp()) / 86400;
return (int)ceil($diff_d);
}
/** /**
* Count the number of image results for a given search * Count the number of image results for a given search
* *
@ -191,11 +244,11 @@ class Image
global $cache, $database; global $cache, $database;
$tag_count = count($tags); $tag_count = count($tags);
if (SPEED_HAX && $tag_count === 0) { if ($tag_count === 0) {
// total number of images in the DB // total number of images in the DB
$total = self::count_total_images(); $total = self::count_total_images();
} elseif (SPEED_HAX && $tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) { } elseif ($tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) {
if (!str_starts_with($tags[0], "-")) { if (!startsWith($tags[0], "-")) {
// one tag - we can look that up directly // one tag - we can look that up directly
$total = self::count_tag($tags[0]); $total = self::count_tag($tags[0]);
} else { } else {
@ -241,19 +294,14 @@ class Image
{ {
$tag_conditions = []; $tag_conditions = [];
$img_conditions = []; $img_conditions = [];
$stpen = 0; // search term parse event number
$order = null;
/* /*
* Turn a bunch of strings into a bunch of TagCondition * Turn a bunch of strings into a bunch of TagCondition
* and ImgCondition objects * and ImgCondition objects
*/ */
/** @var $stpe SearchTermParseEvent */ $stpe = send_event(new SearchTermParseEvent(null, $terms));
$stpe = send_event(new SearchTermParseEvent($stpen++, null, $terms)); if ($stpe->is_querylet_set()) {
if ($stpe->order) { foreach ($stpe->get_querylets() as $querylet) {
$order = $stpe->order;
} elseif (!empty($stpe->querylets)) {
foreach ($stpe->querylets as $querylet) {
$img_conditions[] = new ImgCondition($querylet, true); $img_conditions[] = new ImgCondition($querylet, true);
} }
} }
@ -268,12 +316,9 @@ class Image
continue; continue;
} }
/** @var $stpe SearchTermParseEvent */ $stpe = send_event(new SearchTermParseEvent($term, $terms));
$stpe = send_event(new SearchTermParseEvent($stpen++, $term, $terms)); if ($stpe->is_querylet_set()) {
if ($stpe->order) { foreach ($stpe->get_querylets() as $querylet) {
$order = $stpe->order;
} elseif (!empty($stpe->querylets)) {
foreach ($stpe->querylets as $querylet) {
$img_conditions[] = new ImgCondition($querylet, $positive); $img_conditions[] = new ImgCondition($querylet, $positive);
} }
} else { } else {
@ -283,7 +328,7 @@ class Image
} }
} }
} }
return [$tag_conditions, $img_conditions, $order]; return [$tag_conditions, $img_conditions];
} }
/* /*
@ -320,9 +365,8 @@ class Image
'); ');
} else { } else {
$tags[] = 'id'. $gtlt . $this->id; $tags[] = 'id'. $gtlt . $this->id;
$tags[] = 'order:id_'. strtolower($dir);
$querylet = Image::build_search_querylet($tags); $querylet = Image::build_search_querylet($tags);
$querylet->append_sql(' LIMIT 1'); $querylet->append_sql(' ORDER BY images.id '.$dir.' LIMIT 1');
$row = $database->get_row($querylet->sql, $querylet->variables); $row = $database->get_row($querylet->sql, $querylet->variables);
} }
@ -359,7 +403,7 @@ class Image
SET owner_id=:owner_id SET owner_id=:owner_id
WHERE id=:id WHERE id=:id
", ["owner_id"=>$owner->id, "id"=>$this->id]); ", ["owner_id"=>$owner->id, "id"=>$this->id]);
log_info("core_image", "Owner for Post #{$this->id} set to {$owner->name}"); log_info("core_image", "Owner for Image #{$this->id} set to {$owner->name}");
} }
} }
@ -368,32 +412,27 @@ class Image
global $database, $user; global $database, $user;
$cut_name = substr($this->filename, 0, 255); $cut_name = substr($this->filename, 0, 255);
if (is_null($this->posted) || $this->posted == "") {
$this->posted = date('c', time());
}
if (is_null($this->id)) { if (is_null($this->id)) {
$database->execute( $database->execute(
"INSERT INTO images( "INSERT INTO images(
owner_id, owner_ip, owner_id, owner_ip,
filename, filesize, filename, filesize,
hash, mime, ext, hash, ext,
width, height, width, height,
posted, source posted, source
) )
VALUES ( VALUES (
:owner_id, :owner_ip, :owner_id, :owner_ip,
:filename, :filesize, :filename, :filesize,
:hash, :mime, :ext, :hash, :ext,
0, 0, 0, 0,
:posted, :source now(), :source
)", )",
[ [
"owner_id" => $user->id, "owner_ip" => get_real_ip(), "owner_id" => $user->id, "owner_ip" => $_SERVER['REMOTE_ADDR'],
"filename" => $cut_name, "filesize" => $this->filesize, "filename" => $cut_name, "filesize" => $this->filesize,
"hash" => $this->hash, "mime" => strtolower($this->mime), "hash" => $this->hash, "ext" => strtolower($this->ext),
"ext" => strtolower($this->ext), "source" => $this->source
"posted" => $this->posted, "source" => $this->source
] ]
); );
$this->id = $database->get_last_insert_id('images_id_seq'); $this->id = $database->get_last_insert_id('images_id_seq');
@ -401,16 +440,13 @@ class Image
$database->execute( $database->execute(
"UPDATE images SET ". "UPDATE images SET ".
"filename = :filename, filesize = :filesize, hash = :hash, ". "filename = :filename, filesize = :filesize, hash = :hash, ".
"mime = :mime, ext = :ext, width = 0, height = 0, ". "ext = :ext, width = 0, height = 0, source = :source ".
"posted = :posted, source = :source ".
"WHERE id = :id", "WHERE id = :id",
[ [
"filename" => $cut_name, "filename" => $cut_name,
"filesize" => $this->filesize, "filesize" => $this->filesize,
"hash" => $this->hash, "hash" => $this->hash,
"mime" => strtolower($this->mime),
"ext" => strtolower($this->ext), "ext" => strtolower($this->ext),
"posted" => $this->posted,
"source" => $this->source, "source" => $this->source,
"id" => $this->id, "id" => $this->id,
] ]
@ -420,18 +456,17 @@ class Image
$database->execute( $database->execute(
"UPDATE images SET ". "UPDATE images SET ".
"lossless = :lossless, ". "lossless = :lossless, ".
"video = :video, video_codec = :video_codec, audio = :audio,image = :image, ". "video = :video, audio = :audio,image = :image, ".
"height = :height, width = :width, ". "height = :height, width = :width, ".
"length = :length WHERE id = :id", "length = :length WHERE id = :id",
[ [
"id" => $this->id, "id" => $this->id,
"width" => $this->width ?? 0, "width" => $this->width ?? 0,
"height" => $this->height ?? 0, "height" => $this->height ?? 0,
"lossless" => $this->lossless, "lossless" => $database->scoresql_value_prepare($this->lossless),
"video" => $this->video, "video" => $database->scoresql_value_prepare($this->video),
"video_codec" => $this->video_codec, "image" => $database->scoresql_value_prepare($this->image),
"image" => $this->image, "audio" => $database->scoresql_value_prepare($this->audio),
"audio" => $this->audio,
"length" => $this->length "length" => $this->length
] ]
); );
@ -453,7 +488,6 @@ class Image
WHERE image_id=:id WHERE image_id=:id
ORDER BY tag ORDER BY tag
", ["id"=>$this->id]); ", ["id"=>$this->id]);
sort($this->tag_array);
} }
return $this->tag_array; return $this->tag_array;
} }
@ -490,8 +524,7 @@ class Image
public function get_thumb_link(): string public function get_thumb_link(): string
{ {
global $config; global $config;
$mime = $config->get_string(ImageConfig::THUMB_MIME); $ext = $config->get_string(ImageConfig::THUMB_TYPE);
$ext = FileExtension::get_for_mime($mime);
return $this->get_link(ImageConfig::TLINK, '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext); return $this->get_link(ImageConfig::TLINK, '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext);
} }
@ -505,7 +538,7 @@ class Image
$image_link = $config->get_string($template); $image_link = $config->get_string($template);
if (!empty($image_link)) { if (!empty($image_link)) {
if (!str_contains($image_link, "://") && !str_starts_with($image_link, "/")) { if (!(strpos($image_link, "://") > 0) && !startsWith($image_link, "/")) {
$image_link = make_link($image_link); $image_link = make_link($image_link);
} }
$chosen = $image_link; $chosen = $image_link;
@ -529,19 +562,6 @@ class Image
return $plte->text; return $plte->text;
} }
/**
* Get the info for this image, formatted according to the
* configured template.
*/
public function get_info(): string
{
global $config;
$plte = new ParseLinkTemplateEvent($config->get_string(ImageConfig::INFO), $this);
send_event($plte);
return $plte->text;
}
/** /**
* Figure out where the full size image is on disk. * Figure out where the full size image is on disk.
*/ */
@ -567,40 +587,21 @@ class Image
} }
/** /**
* Get the image's extension. * Get the image's mime type.
*/
public function get_mime_type(): string
{
return get_mime($this->get_image_filename(), $this->get_ext());
}
/**
* Get the image's filename extension
*/ */
public function get_ext(): string public function get_ext(): string
{ {
return $this->ext; return $this->ext;
} }
/**
* Get the image's mime type.
*/
public function get_mime(): ?string
{
if ($this->mime===MimeType::WEBP&&$this->lossless) {
return MimeType::WEBP_LOSSLESS;
}
$m = $this->mime;
if (is_null($m)) {
$m = MimeMap::get_for_extension($this->ext)[0];
}
return $m;
}
/**
* Set the image's mime type.
*/
public function set_mime($mime): void
{
$this->mime = $mime;
$ext = FileExtension::get_for_mime($this->get_mime());
assert($ext != null);
$this->ext = $ext;
}
/** /**
* Get the image's source URL * Get the image's source URL
*/ */
@ -621,7 +622,7 @@ class Image
} }
if ($new_source != $old_source) { if ($new_source != $old_source) {
$database->execute("UPDATE images SET source=:source WHERE id=:id", ["source"=>$new_source, "id"=>$this->id]); $database->execute("UPDATE images SET source=:source WHERE id=:id", ["source"=>$new_source, "id"=>$this->id]);
log_info("core_image", "Source for Post #{$this->id} set to: $new_source (was $old_source)"); log_info("core_image", "Source for Image #{$this->id} set to: $new_source (was $old_source)");
} }
} }
@ -633,12 +634,16 @@ class Image
return $this->locked; return $this->locked;
} }
public function set_locked(bool $locked): void public function set_locked(bool $tf): void
{ {
global $database; global $database;
if ($locked !== $this->locked) { $ln = $tf ? "Y" : "N";
$database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn"=>$locked, "id"=>$this->id]); $sln = $database->scoreql_to_sql('SCORE_BOOL_'.$ln);
log_info("core_image", "Setting Post #{$this->id} lock to: $locked"); $sln = str_replace("'", "", $sln);
$sln = str_replace('"', "", $sln);
if (bool_escape($sln) !== $this->locked) {
$database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn"=>$sln, "id"=>$this->id]);
log_info("core_image", "Setting Image #{$this->id} lock to: $ln");
} }
} }
@ -693,7 +698,7 @@ class Image
$page->flash("Can't set a tag longer than 255 characters"); $page->flash("Can't set a tag longer than 255 characters");
continue; continue;
} }
if (str_starts_with($tag, "-")) { if (startsWith($tag, "-")) {
$page->flash("Can't set a tag which starts with a minus"); $page->flash("Can't set a tag which starts with a minus");
continue; continue;
} }
@ -755,7 +760,7 @@ class Image
); );
} }
log_info("core_image", "Tags for Post #{$this->id} set to: ".Tag::implode($tags)); log_info("core_image", "Tags for Image #{$this->id} set to: ".Tag::implode($tags));
$cache->delete("image-{$this->id}-tags"); $cache->delete("image-{$this->id}-tags");
} }
} }
@ -768,7 +773,7 @@ class Image
global $database; global $database;
$this->delete_tags_from_image(); $this->delete_tags_from_image();
$database->execute("DELETE FROM images WHERE id=:id", ["id"=>$this->id]); $database->execute("DELETE FROM images WHERE id=:id", ["id"=>$this->id]);
log_info("core_image", 'Deleted Post #'.$this->id.' ('.$this->hash.')'); log_info("core_image", 'Deleted Image #'.$this->id.' ('.$this->hash.')');
unlink($this->get_image_filename()); unlink($this->get_image_filename());
unlink($this->get_thumb_filename()); unlink($this->get_thumb_filename());
@ -780,7 +785,7 @@ class Image
*/ */
public function remove_image_only(): void public function remove_image_only(): void
{ {
log_info("core_image", 'Removed Post File ('.$this->hash.')'); log_info("core_image", 'Removed Image File ('.$this->hash.')');
@unlink($this->get_image_filename()); @unlink($this->get_image_filename());
@unlink($this->get_thumb_filename()); @unlink($this->get_thumb_filename());
} }
@ -806,14 +811,12 @@ class Image
* #param string[] $terms * #param string[] $terms
*/ */
private static function build_search_querylet( private static function build_search_querylet(
array $terms, array $tags,
?string $order=null,
?int $limit=null, ?int $limit=null,
?int $offset=null ?int $offset=null
): Querylet { ): Querylet {
global $config; list($tag_conditions, $img_conditions) = self::terms_to_conditions($tags);
list($tag_conditions, $img_conditions, $order) = self::terms_to_conditions($terms);
$order = ($order ?: "images.".$config->get_string(IndexConfig::ORDER));
$positive_tag_count = 0; $positive_tag_count = 0;
$negative_tag_count = 0; $negative_tag_count = 0;
@ -891,14 +894,12 @@ class Image
$positive_tag_id_array = []; $positive_tag_id_array = [];
$positive_wildcard_id_array = []; $positive_wildcard_id_array = [];
$negative_tag_id_array = []; $negative_tag_id_array = [];
$all_nonexistent_negatives = true;
foreach ($tag_conditions as $tq) { foreach ($tag_conditions as $tq) {
$tag_ids = self::tag_or_wildcard_to_ids($tq->tag); $tag_ids = self::tag_or_wildcard_to_ids($tq->tag);
$tag_count = count($tag_ids); $tag_count = count($tag_ids);
if ($tq->positive) { if ($tq->positive) {
$all_nonexistent_negatives = false;
if ($tag_count== 0) { if ($tag_count== 0) {
# one of the positive tags had zero results, therefor there # one of the positive tags had zero results, therefor there
# can be no results; "where 1=0" should shortcut things # can be no results; "where 1=0" should shortcut things
@ -912,20 +913,14 @@ class Image
$positive_wildcard_id_array[] = $tag_ids; $positive_wildcard_id_array[] = $tag_ids;
} }
} else { } else {
if ($tag_count > 0) { // Unlike positive criteria, negative criteria are all handled in an OR fashion,
$all_nonexistent_negatives = false; // so we can just compile them all into a single sub-query.
// Unlike positive criteria, negative criteria are all handled in an OR fashion, $negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids);
// so we can just compile them all into a single sub-query.
$negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids);
}
} }
} }
assert($positive_tag_id_array || $positive_wildcard_id_array || $negative_tag_id_array || $all_nonexistent_negatives, @$_GET['q']); assert($positive_tag_id_array || $positive_wildcard_id_array || $negative_tag_id_array, @$_GET['q']);
if (!empty($positive_tag_id_array) || !empty($positive_wildcard_id_array)) {
if ($all_nonexistent_negatives) {
$query = new Querylet("SELECT images.* FROM images WHERE 1=1");
} elseif (!empty($positive_tag_id_array) || !empty($positive_wildcard_id_array)) {
$inner_joins = []; $inner_joins = [];
if (!empty($positive_tag_id_array)) { if (!empty($positive_tag_id_array)) {
foreach ($positive_tag_id_array as $tag) { foreach ($positive_tag_id_array as $tag) {

View File

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

View File

@ -1,10 +1,10 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class Querylet class Querylet
{ {
public string $sql; /** @var string */
public array $variables; public $sql;
/** @var array */
public $variables;
public function __construct(string $sql, array $variables=[]) public function __construct(string $sql, array $variables=[])
{ {
@ -31,8 +31,10 @@ class Querylet
class TagCondition class TagCondition
{ {
public string $tag; /** @var string */
public bool $positive; public $tag;
/** @var bool */
public $positive;
public function __construct(string $tag, bool $positive) public function __construct(string $tag, bool $positive)
{ {
@ -43,8 +45,10 @@ class TagCondition
class ImgCondition class ImgCondition
{ {
public Querylet $qlet; /** @var Querylet */
public bool $positive; public $qlet;
/** @var bool */
public $positive;
public function __construct(Querylet $qlet, bool $positive) public function __construct(Querylet $qlet, bool $positive)
{ {

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
/** /**
* Class Tag * Class Tag
* *

View File

@ -49,6 +49,7 @@ function get_dsn()
{ {
if (getenv("INSTALL_DSN")) { if (getenv("INSTALL_DSN")) {
$dsn = getenv("INSTALL_DSN"); $dsn = getenv("INSTALL_DSN");
;
} elseif (@$_POST["database_type"] == DatabaseDriver::SQLITE) { } elseif (@$_POST["database_type"] == DatabaseDriver::SQLITE) {
/** @noinspection PhpUnhandledExceptionInspection */ /** @noinspection PhpUnhandledExceptionInspection */
$id = bin2hex(random_bytes(5)); $id = bin2hex(random_bytes(5));
@ -68,7 +69,7 @@ function do_install($dsn)
create_tables(new Database($dsn)); create_tables(new Database($dsn));
write_config($dsn); write_config($dsn);
} catch (InstallerException $e) { } catch (InstallerException $e) {
die_nicely($e->title, $e->body, $e->exit_code); die_nicely($e->title, $e->body, $e->code);
} }
} }
@ -109,8 +110,8 @@ function ask_questions()
"; ";
} }
$db_m = in_array(DatabaseDriver::MYSQL, $drivers) ? '<option value="'. DatabaseDriver::MYSQL .'">MySQL</option>' : ""; $db_m = in_array(DatabaseDriver::MYSQL, $drivers) ? '<option value="'. DatabaseDriver::MYSQL .'">MySQL</option>' : "";
$db_p = in_array(DatabaseDriver::PGSQL, $drivers) ? '<option value="'. DatabaseDriver::PGSQL .'">PostgreSQL</option>' : ""; $db_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_s = in_array(DatabaseDriver::SQLITE, $drivers) ? '<option value="'. DatabaseDriver::SQLITE .'">SQLite</option>' : "";
$warn_msg = $warnings ? "<h3>Warnings</h3>".implode("\n<p>", $warnings) : ""; $warn_msg = $warnings ? "<h3>Warnings</h3>".implode("\n<p>", $warnings) : "";
@ -255,7 +256,7 @@ function create_tables(Database $db)
width INTEGER NOT NULL, width INTEGER NOT NULL,
height INTEGER NOT NULL, height INTEGER NOT NULL,
posted TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, posted TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
locked BOOLEAN NOT NULL DEFAULT FALSE, locked SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N,
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT
"); ");
$db->execute("CREATE INDEX images_owner_id_idx ON images(owner_id)", []); $db->execute("CREATE INDEX images_owner_id_idx ON images(owner_id)", []);
@ -281,19 +282,13 @@ function create_tables(Database $db)
$db->execute("CREATE INDEX images_tags_tag_id_idx ON image_tags(tag_id)", []); $db->execute("CREATE INDEX images_tags_tag_id_idx ON image_tags(tag_id)", []);
$db->execute("INSERT INTO config(name, value) VALUES('db_version', 11)"); $db->execute("INSERT INTO config(name, value) VALUES('db_version', 11)");
$db->commit();
// mysql auto-commits when creating a table, so the transaction
// is closed; other databases need to commit
if ($db->is_transaction_open()) {
$db->commit();
}
} catch (PDOException $e) { } catch (PDOException $e) {
throw new InstallerException( throw new InstallerException(
"PDO Error:", "PDO Error:",
"<p>An error occurred while trying to create the database tables necessary for Shimmie.</p> "<p>An error occurred while trying to create the database tables necessary for Shimmie.</p>
<p>Please check and ensure that the database configuration options are all correct.</p> <p>Please check and ensure that the database configuration options are all correct.</p>
<p>{$e->getMessage()}</p> <p>{$e->getMessage()}</p>",
",
3 3
); );
} }

View File

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

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
// action_object_attribute // action_object_attribute
// action = create / view / edit / delete // action = create / view / edit / delete
@ -9,9 +7,6 @@ abstract class Permissions
{ {
public const CHANGE_SETTING = "change_setting"; # modify web-level settings, eg the config table public const CHANGE_SETTING = "change_setting"; # modify web-level settings, eg the config table
public const OVERRIDE_CONFIG = "override_config"; # modify sys-level settings, eg shimmie.conf.php public const OVERRIDE_CONFIG = "override_config"; # modify sys-level settings, eg shimmie.conf.php
public const CHANGE_USER_SETTING = "change_user_setting"; # modify own user-level settings
public const CHANGE_OTHER_USER_SETTING = "change_other_user_setting"; # modify own user-level settings
public const BIG_SEARCH = "big_search"; # search for more than 3 tags at once (speed mode only) public const BIG_SEARCH = "big_search"; # search for more than 3 tags at once (speed mode only)
public const MANAGE_EXTENSION_LIST = "manage_extension_list"; public const MANAGE_EXTENSION_LIST = "manage_extension_list";
@ -105,7 +100,6 @@ abstract class Permissions
public const SET_PRIVATE_IMAGE = "set_private_image"; public const SET_PRIVATE_IMAGE = "set_private_image";
public const SET_OTHERS_PRIVATE_IMAGES = "set_others_private_images"; public const SET_OTHERS_PRIVATE_IMAGES = "set_others_private_images";
public const CRON_RUN = "cron_run";
public const BULK_IMPORT = "bulk_import"; public const BULK_IMPORT = "bulk_import";
public const BULK_EXPORT = "bulk_export"; public const BULK_EXPORT = "bulk_export";
public const BULK_DOWNLOAD = "bulk_download"; public const BULK_DOWNLOAD = "bulk_download";

View File

@ -1,10 +1,11 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Things which should be in the core API * * Things which should be in the core API *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ \* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
require_once "filetypes.php";
/** /**
* Return the unique elements of an array, case insensitively * Return the unique elements of an array, case insensitively
*/ */
@ -126,16 +127,10 @@ function list_files(string $base, string $_sub_dir=""): array
$files = []; $files = [];
$dir = opendir("$base/$_sub_dir"); $dir = opendir("$base/$_sub_dir");
if ($dir===false) { while ($f = readdir($dir)) {
throw new SCoreException("Unable to open directory $base/$_sub_dir"); $files[] = $f;
}
try {
while ($f = readdir($dir)) {
$files[] = $f;
}
} finally {
closedir($dir);
} }
closedir($dir);
sort($files); sort($files);
foreach ($files as $filename) { foreach ($files as $filename) {
@ -192,8 +187,8 @@ function stream_file(string $file, int $start, int $end): void
} }
} }
# http://www.php.net/manual/en/function.http-parse-headers.php#112917 if (!function_exists('http_parse_headers')) { #http://www.php.net/manual/en/function.http-parse-headers.php#112917
if (!function_exists('http_parse_headers')) {
/** /**
* #return string[] * #return string[]
*/ */
@ -224,7 +219,7 @@ if (!function_exists('http_parse_headers')) {
* HTTP Headers can sometimes be lowercase which will cause issues. * HTTP Headers can sometimes be lowercase which will cause issues.
* In cases like these, we need to make sure to check for them if the camelcase version does not exist. * In cases like these, we need to make sure to check for them if the camelcase version does not exist.
*/ */
function find_header(array $headers, string $name): ?string function findHeader(array $headers, string $name): ?string
{ {
if (!is_array($headers)) { if (!is_array($headers)) {
return null; return null;
@ -248,22 +243,21 @@ function find_header(array $headers, string $name): ?string
if (!function_exists('mb_strlen')) { if (!function_exists('mb_strlen')) {
// TODO: we should warn the admin that they are missing multibyte support // TODO: we should warn the admin that they are missing multibyte support
/** @noinspection PhpUnusedParameterInspection */ function mb_strlen($str, $encoding)
function mb_strlen($str, $encoding): int
{ {
return strlen($str); return strlen($str);
} }
function mb_internal_encoding($encoding): void function mb_internal_encoding($encoding)
{ {
} }
function mb_strtolower($str): string function mb_strtolower($str)
{ {
return strtolower($str); return strtolower($str);
} }
} }
/** @noinspection PhpUnhandledExceptionInspection */ /** @noinspection PhpUnhandledExceptionInspection */
function get_subclasses_of(string $parent): array function getSubclassesOf(string $parent)
{ {
$result = []; $result = [];
foreach (get_declared_classes() as $class) { foreach (get_declared_classes() as $class) {
@ -330,13 +324,13 @@ function get_base_href(): string
/** /**
* The opposite of the standard library's parse_url * The opposite of the standard library's parse_url
*/ */
function unparse_url(array $parsed_url): string function unparse_url($parsed_url)
{ {
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : ''; $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : ''; $host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : ''; $user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? "$pass@" : ''; $pass = ($user || $pass) ? "$pass@" : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; $path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$query = !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; $query = !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
@ -344,26 +338,17 @@ function unparse_url(array $parsed_url): string
return "$scheme$user$pass$host$port$path$query$fragment"; return "$scheme$user$pass$host$port$path$query$fragment";
} }
# finally in the core library starting from php8 function startsWith(string $haystack, string $needle): bool
if (!function_exists('str_starts_with')) { {
function str_starts_with(string $haystack, string $needle): bool $length = strlen($needle);
{ return (substr($haystack, 0, $length) === $needle);
return strncmp($haystack, $needle, strlen($needle)) === 0;
}
} }
if (!function_exists('str_ends_with')) { function endsWith(string $haystack, string $needle): bool
function str_ends_with(string $haystack, string $needle): bool {
{ $length = strlen($needle);
return $needle === '' || $needle === substr($haystack, - strlen($needle)); $start = $length * -1; //negative
} return (substr($haystack, $start) === $needle);
}
if (!function_exists('str_contains')) {
function str_contains(string $haystack, string $needle): bool
{
return '' === $needle || false !== strpos($haystack, $needle);
}
} }
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
@ -490,6 +475,25 @@ function clamp(?int $val, ?int $min=null, ?int $max=null): int
return $val; return $val;
} }
function xml_tag(string $name, array $attrs=[], array $children=[]): string
{
$xml = "<$name ";
foreach ($attrs as $k => $v) {
$xv = str_replace('&#039;', '&apos;', htmlspecialchars((string)$v, ENT_QUOTES));
$xml .= "$k=\"$xv\" ";
}
if (count($children) > 0) {
$xml .= ">\n";
foreach ($children as $child) {
$xml .= xml_tag($child);
}
$xml .= "</$name>\n";
} else {
$xml .= "/>\n";
}
return $xml;
}
/** /**
* Original PHP code by Chirp Internet: www.chirp.com.au * Original PHP code by Chirp Internet: www.chirp.com.au
* Please acknowledge use of this code by including this header. * Please acknowledge use of this code by including this header.
@ -528,6 +532,7 @@ function parse_shorthand_int(string $limit): int
/** @noinspection PhpMissingBreakStatementInspection */ /** @noinspection PhpMissingBreakStatementInspection */
// no break // no break
case 'm': $value *= 1024; // fall through case 'm': $value *= 1024; // fall through
/** @noinspection PhpMissingBreakStatementInspection */
// no break // no break
case 'k': $value *= 1024; break; case 'k': $value *= 1024; break;
default: $value = -1; default: $value = -1;
@ -558,41 +563,17 @@ function to_shorthand_int(int $int): string
return (string)$int; return (string)$int;
} }
} }
abstract class TIME_UNITS
{ const TIME_UNITS = ["s"=>60,"m"=>60,"h"=>24,"d"=>365,"y"=>PHP_INT_MAX];
public const MILLISECONDS = "ms"; function format_milliseconds(int $input): string
public const SECONDS = "s";
public const MINUTES = "m";
public const HOURS = "h";
public const DAYS = "d";
public const YEARS = "y";
public const CONVERSION = [
self::MILLISECONDS=>1000,
self::SECONDS=>60,
self::MINUTES=>60,
self::HOURS=>24,
self::DAYS=>365,
self::YEARS=>PHP_INT_MAX
];
}
function format_milliseconds(int $input, string $min_unit = TIME_UNITS::SECONDS): string
{ {
$output = ""; $output = "";
$remainder = $input; $remainder = floor($input / 1000);
$found = false; foreach (TIME_UNITS as $unit=>$conversion) {
foreach (TIME_UNITS::CONVERSION as $unit=>$conversion) {
$count = $remainder % $conversion; $count = $remainder % $conversion;
$remainder = floor($remainder / $conversion); $remainder = floor($remainder / $conversion);
if ($found||$unit==$min_unit) {
$found = true;
} else {
continue;
}
if ($count==0&&$remainder<1) { if ($count==0&&$remainder<1) {
break; break;
} }
@ -601,32 +582,6 @@ function format_milliseconds(int $input, string $min_unit = TIME_UNITS::SECONDS)
return trim($output); return trim($output);
} }
function parse_to_milliseconds(string $input): int
{
$output = 0;
$current_multiplier = 1;
if (preg_match('/^([0-9]+)$/i', $input, $match)) {
// If just a number, then we treat it as milliseconds
$length = $match[0];
if (is_numeric($length)) {
$length = floatval($length);
$output += $length;
}
} else {
foreach (TIME_UNITS::CONVERSION as $unit=>$conversion) {
if (preg_match('/([0-9]+)'.$unit.'/i', $input, $match)) {
$length = $match[1];
if (is_numeric($length)) {
$length = floatval($length);
$output += $length * $current_multiplier;
}
}
$current_multiplier *= $conversion;
}
}
return intval($output);
}
/** /**
* Turn a date into a time, a date, an "X minutes ago...", etc * Turn a date into a time, a date, an "X minutes ago...", etc
@ -802,7 +757,7 @@ function iterator_map_to_array(callable $callback, iterator $iter): array
return iterator_to_array(iterator_map($callback, $iter)); return iterator_to_array(iterator_map($callback, $iter));
} }
function stringer($s): string function stringer($s)
{ {
if (is_array($s)) { if (is_array($s)) {
if (isset($s[0])) { if (isset($s[0])) {

View File

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

View File

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

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
/** /**
* For any values that aren't defined in data/config/*.php, * For any values that aren't defined in data/config/*.php,
* Shimmie will set the values to their defaults * Shimmie will set the values to their defaults
@ -21,17 +19,16 @@ function _d(string $name, $value): void
} }
} }
$_g = file_exists(".git") ? '+' : ''; $_g = file_exists(".git") ? '+' : '';
_d("DATABASE_DSN", null); // string PDO database connection details _d("DATABASE_DSN", null); // string PDO database connection details
_d("DATABASE_TIMEOUT", 10000); // int Time to wait for each statement to complete _d("DATABASE_TIMEOUT", 10000);// int Time to wait for each statement to complete
_d("CACHE_DSN", null); // string cache connection details _d("CACHE_DSN", null); // string cache connection details
_d("DEBUG", false); // boolean print various debugging details _d("DEBUG", false); // boolean print various debugging details
_d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes _d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes
_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance _d("SPEED_HAX", false); // boolean do some questionable things in the name of performance
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse _d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
_d("VERSION", "2.9.1$_g"); // string shimmie version _d("VERSION", "2.8.4$_g"); // string shimmie version
_d("TIMEZONE", null); // string timezone _d("TIMEZONE", null); // string timezone
_d("EXTRA_EXTS", ""); // string optional extra extensions _d("EXTRA_EXTS", ""); // string optional extra extensions
_d("BASE_HREF", null); // string force a specific base URL (default is auto-detect) _d("BASE_HREF", null); // string force a specific base URL (default is auto-detect)
_d("TRACE_FILE", null); // string file to log performance data into _d("TRACE_FILE", null); // string file to log performance data into
_d("TRACE_THRESHOLD", 0.0); // float log pages which take more time than this many seconds _d("TRACE_THRESHOLD", 0.0); // float log pages which take more time than this many seconds
_d("REVERSE_PROXY_X_HEADERS", false); // boolean get request IPs from "X-Real-IP" and protocol from "X-Forwarded-Proto" HTTP headers

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;

View File

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

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -45,13 +43,13 @@ class UrlsTest extends TestCase
{ {
// relative to shimmie install // relative to shimmie install
$this->assertEquals( $this->assertEquals(
"http://cli-command/test/foo", "http://<cli command>/test/foo",
make_http("foo") make_http("foo")
); );
// relative to web server // relative to web server
$this->assertEquals( $this->assertEquals(
"http://cli-command/foo", "http://<cli command>/foo",
make_http("/foo") make_http("/foo")
); );

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;

View File

@ -1,11 +1,9 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class Link class Link
{ {
public ?string $page; public $page;
public ?string $query; public $query;
public function __construct(?string $page=null, ?string $query=null) public function __construct(?string $page=null, ?string $query=null)
{ {
@ -81,7 +79,7 @@ function modify_url(string $url, array $changes): string
*/ */
function make_http(string $link): string function make_http(string $link): string
{ {
if (str_contains($link, "://")) { if (strpos($link, "://") > 0) {
return $link; return $link;
} }
@ -107,7 +105,7 @@ function referer_or(string $dest, ?array $blacklist=null): string
} }
if ($blacklist) { if ($blacklist) {
foreach ($blacklist as $b) { foreach ($blacklist as $b) {
if (str_contains($_SERVER['HTTP_REFERER'], $b)) { if (strstr($_SERVER['HTTP_REFERER'], $b)) {
return $dest; return $dest;
} }
} }

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
function _new_user(array $row): User function _new_user(array $row): User
{ {
@ -17,12 +15,22 @@ function _new_user(array $row): User
*/ */
class User class User
{ {
public int $id; /** @var int */
public string $name; public $id;
public ?string $email;
public string $join_date; /** @var string */
public ?string $passhash; public $name;
public UserClass $class;
/** @var string */
public $email;
public $join_date;
/** @var string */
public $passhash;
/** @var UserClass */
public $class;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Initialisation * * Initialisation *
@ -99,7 +107,7 @@ class User
{ {
$u = User::by_name($name); $u = User::by_name($name);
if (is_null($u)) { if (is_null($u)) {
throw new UserDoesNotExist("Can't find any user named $name"); throw new ScoreException("Can't find any user named $name");
} else { } else {
return $u->id; return $u->id;
} }
@ -110,7 +118,7 @@ class User
$my_user = User::by_name($name); $my_user = User::by_name($name);
// If user tried to log in as "foo bar" and failed, try "foo_bar" // If user tried to log in as "foo bar" and failed, try "foo_bar"
if (!$my_user && str_contains($name, " ")) { if (!$my_user && strpos($name, " ") !== false) {
$my_user = User::by_name(str_replace(" ", "_", $name)); $my_user = User::by_name(str_replace(" ", "_", $name));
} }
@ -155,7 +163,7 @@ class User
public function set_class(string $class): void public function set_class(string $class): void
{ {
global $database; global $database;
$database->execute("UPDATE users SET class=:class WHERE id=:id", ["class"=>$class, "id"=>$this->id]); $database->Execute("UPDATE users SET class=:class WHERE id=:id", ["class"=>$class, "id"=>$this->id]);
log_info("core-user", 'Set class for '.$this->name.' to '.$class); log_info("core-user", 'Set class for '.$this->name.' to '.$class);
} }
@ -167,7 +175,7 @@ class User
} }
$old_name = $this->name; $old_name = $this->name;
$this->name = $name; $this->name = $name;
$database->execute("UPDATE users SET name=:name WHERE id=:id", ["name"=>$this->name, "id"=>$this->id]); $database->Execute("UPDATE users SET name=:name WHERE id=:id", ["name"=>$this->name, "id"=>$this->id]);
log_info("core-user", "Changed username for {$old_name} to {$this->name}"); log_info("core-user", "Changed username for {$old_name} to {$this->name}");
} }
@ -177,7 +185,7 @@ class User
$hash = password_hash($password, PASSWORD_BCRYPT); $hash = password_hash($password, PASSWORD_BCRYPT);
if (is_string($hash)) { if (is_string($hash)) {
$this->passhash = $hash; $this->passhash = $hash;
$database->execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash"=>$this->passhash, "id"=>$this->id]); $database->Execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash"=>$this->passhash, "id"=>$this->id]);
log_info("core-user", 'Set password for '.$this->name); log_info("core-user", 'Set password for '.$this->name);
} else { } else {
throw new SCoreException("Failed to hash password"); throw new SCoreException("Failed to hash password");
@ -187,7 +195,7 @@ class User
public function set_email(string $address): void public function set_email(string $address): void
{ {
global $database; global $database;
$database->execute("UPDATE users SET email=:email WHERE id=:id", ["email"=>$address, "id"=>$this->id]); $database->Execute("UPDATE users SET email=:email WHERE id=:id", ["email"=>$address, "id"=>$this->id]);
log_info("core-user", 'Set email for '.$this->name); log_info("core-user", 'Set email for '.$this->name);
} }

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
/** /**
* @global UserClass[] $_shm_user_classes * @global UserClass[] $_shm_user_classes
*/ */
@ -12,9 +10,21 @@ $_shm_user_classes = [];
*/ */
class UserClass class UserClass
{ {
public ?string $name = null;
public ?UserClass $parent = null; /**
public array $abilities = []; * @var ?string
*/
public $name = null;
/**
* @var ?UserClass
*/
public $parent = null;
/**
* @var array
*/
public $abilities = [];
public function __construct(string $name, string $parent = null, array $abilities = []) public function __construct(string $name, string $parent = null, array $abilities = [])
{ {
@ -90,7 +100,6 @@ new UserClass("user", "base", [
Permissions::READ_PM => true, Permissions::READ_PM => true,
Permissions::SET_PRIVATE_IMAGE => true, Permissions::SET_PRIVATE_IMAGE => true,
Permissions::BULK_DOWNLOAD => true, Permissions::BULK_DOWNLOAD => true,
Permissions::CHANGE_USER_SETTING => true
]); ]);
new UserClass("hellbanned", "user", [ new UserClass("hellbanned", "user", [
@ -99,8 +108,6 @@ new UserClass("hellbanned", "user", [
new UserClass("admin", "base", [ new UserClass("admin", "base", [
Permissions::CHANGE_SETTING => true, Permissions::CHANGE_SETTING => true,
Permissions::CHANGE_USER_SETTING => true,
Permissions::CHANGE_OTHER_USER_SETTING => true,
Permissions::OVERRIDE_CONFIG => true, Permissions::OVERRIDE_CONFIG => true,
Permissions::BIG_SEARCH => true, Permissions::BIG_SEARCH => true,
@ -193,8 +200,6 @@ new UserClass("admin", "base", [
Permissions::APPROVE_IMAGE => true, Permissions::APPROVE_IMAGE => true,
Permissions::APPROVE_COMMENT => true, Permissions::APPROVE_COMMENT => true,
Permissions::CRON_RUN =>true,
Permissions::BULK_IMPORT =>true, Permissions::BULK_IMPORT =>true,
Permissions::BULK_EXPORT =>true, Permissions::BULK_EXPORT =>true,
Permissions::BULK_DOWNLOAD => true, Permissions::BULK_DOWNLOAD => true,

View File

@ -1,7 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
use MicroHTML\HTMLElement;
use function MicroHTML\emptyHTML; use function MicroHTML\emptyHTML;
use function MicroHTML\rawHTML; use function MicroHTML\rawHTML;
use function MicroHTML\FORM; use function MicroHTML\FORM;
@ -23,6 +20,13 @@ use function MicroHTML\TD;
const DATA_DIR = "data"; const DATA_DIR = "data";
function mtimefile(string $file): string
{
$data_href = get_base_href();
$mtime = filemtime($file);
return "$data_href/$file?$mtime";
}
function get_theme(): string function get_theme(): string
{ {
global $config; global $config;
@ -42,18 +46,18 @@ function contact_link(): ?string
} }
if ( if (
str_starts_with($text, "http:") || startsWith($text, "http:") ||
str_starts_with($text, "https:") || startsWith($text, "https:") ||
str_starts_with($text, "mailto:") startsWith($text, "mailto:")
) { ) {
return $text; return $text;
} }
if (str_contains($text, "@")) { if (strpos($text, "@")) {
return "mailto:$text"; return "mailto:$text";
} }
if (str_contains($text, "/")) { if (strpos($text, "/")) {
return "http://$text"; return "http://$text";
} }
@ -65,10 +69,6 @@ function contact_link(): ?string
*/ */
function is_https_enabled(): bool function is_https_enabled(): bool
{ {
// check forwarded protocol
if (REVERSE_PROXY_X_HEADERS && !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
$_SERVER['HTTPS']='on';
}
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
} }
@ -160,29 +160,6 @@ function check_im_version(): int
return (empty($convert_check) ? 0 : 1); return (empty($convert_check) ? 0 : 1);
} }
/**
* Get request IP
*/
function get_remote_addr() {
return $_SERVER['REMOTE_ADDR'];
}
/**
* Get real IP if behind a reverse proxy
*/
function get_real_ip() {
$ip = get_remote_addr();
if (REVERSE_PROXY_X_HEADERS && isset($_SERVER['HTTP_X_REAL_IP'])) {
$ip = $_SERVER['HTTP_X_REAL_IP'];
if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$ip = "0.0.0.0";
}
}
return $ip;
}
/** /**
* Get the currently active IP, masked to make it not change when the last * Get the currently active IP, masked to make it not change when the last
* octet or two change, for use in session cookies and such * octet or two change, for use in session cookies and such
@ -190,7 +167,7 @@ function get_real_ip() {
function get_session_ip(Config $config): string function get_session_ip(Config $config): string
{ {
$mask = $config->get_string("session_hash_mask", "255.255.0.0"); $mask = $config->get_string("session_hash_mask", "255.255.0.0");
$addr = get_real_ip(); $addr = $_SERVER['REMOTE_ADDR'];
$addr = inet_ntop(inet_pton($addr) & inet_pton($mask)); $addr = inet_ntop(inet_pton($addr) & inet_pton($mask));
return $addr; return $addr;
} }
@ -282,11 +259,11 @@ function load_balance_url(string $tmpl, string $hash, int $n=0): string
return $tmpl; return $tmpl;
} }
function fetch_url(string $url, string $mfile): ?array function transload(string $url, string $mfile): ?array
{ {
global $config; global $config;
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "curl" && function_exists("curl_init")) { if ($config->get_string("transload_engine") === "curl" && function_exists("curl_init")) {
$ch = curl_init($url); $ch = curl_init($url);
$fp = fopen($mfile, "w"); $fp = fopen($mfile, "w");
@ -299,7 +276,8 @@ function fetch_url(string $url, string $mfile): ?array
$response = curl_exec($ch); $response = curl_exec($ch);
if ($response === false) { if ($response === false) {
return null; log_warning("core-util", "Failed to transload $url");
throw new SCoreException("Failed to fetch $url");
} }
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
@ -313,7 +291,7 @@ function fetch_url(string $url, string $mfile): ?array
return $headers; return $headers;
} }
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "wget") { if ($config->get_string("transload_engine") === "wget") {
$s_url = escapeshellarg($url); $s_url = escapeshellarg($url);
$s_mfile = escapeshellarg($mfile); $s_mfile = escapeshellarg($mfile);
system("wget --no-check-certificate $s_url --output-document=$s_mfile"); system("wget --no-check-certificate $s_url --output-document=$s_mfile");
@ -321,14 +299,14 @@ function fetch_url(string $url, string $mfile): ?array
return file_exists($mfile) ? ["ok"=>"true"] : null; return file_exists($mfile) ? ["ok"=>"true"] : null;
} }
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "fopen") { if ($config->get_string("transload_engine") === "fopen") {
$fp_in = @fopen($url, "r"); $fp_in = @fopen($url, "r");
$fp_out = fopen($mfile, "w"); $fp_out = fopen($mfile, "w");
if (!$fp_in || !$fp_out) { if (!$fp_in || !$fp_out) {
return null; return null;
} }
$length = 0; $length = 0;
while (!feof($fp_in) && $length <= $config->get_int(UploadConfig::SIZE)) { while (!feof($fp_in) && $length <= $config->get_int('upload_size')) {
$data = fread($fp_in, 8192); $data = fread($fp_in, 8192);
$length += strlen($data); $length += strlen($data);
fwrite($fp_out, $data); fwrite($fp_out, $data);
@ -370,7 +348,7 @@ function path_to_tags(string $path): string
// which is for inheriting to tags on the subfolder // which is for inheriting to tags on the subfolder
$category_to_inherit = $tag; $category_to_inherit = $tag;
} else { } else {
if ($category!="" && !str_contains($tag, ":")) { if ($category!=""&&strpos($tag, ":") === false) {
// This indicates that category inheritance is active, // This indicates that category inheritance is active,
// and we've encountered a tag that does not specify a category. // and we've encountered a tag that does not specify a category.
// So we attach the inherited category to the tag. // So we attach the inherited category to the tag.
@ -389,7 +367,7 @@ function path_to_tags(string $path): string
} }
function join_url(string $base, string ...$paths): string function join_url(string $base, string ...$paths)
{ {
$output = $base; $output = $base;
foreach ($paths as $path) { foreach ($paths as $path) {
@ -440,7 +418,7 @@ function remove_empty_dirs(string $dir): bool
} }
} }
if ($result===true) { if ($result===true) {
$result = rmdir($dir); $result = $result && rmdir($dir);
} }
return $result; return $result;
} }
@ -614,6 +592,7 @@ function _get_themelet_files(string $_theme): array
/** /**
* Used to display fatal errors to the web user. * Used to display fatal errors to the web user.
* @noinspection PhpPossiblePolymorphicInvocationInspection
*/ */
function _fatal_error(Exception $e): void function _fatal_error(Exception $e): void
{ {
@ -621,7 +600,6 @@ function _fatal_error(Exception $e): void
$message = $e->getMessage(); $message = $e->getMessage();
$phpver = phpversion(); $phpver = phpversion();
$query = is_subclass_of($e, "SCoreException") ? $e->query : null; $query = is_subclass_of($e, "SCoreException") ? $e->query : null;
$code = is_subclass_of($e, "SCoreException") ? $e->http_code : 500;
//$hash = exec("git rev-parse HEAD"); //$hash = exec("git rev-parse HEAD");
//$h_hash = $hash ? "<p><b>Hash:</b> $hash" : ""; //$h_hash = $hash ? "<p><b>Hash:</b> $hash" : "";
@ -646,10 +624,7 @@ function _fatal_error(Exception $e): void
print("Version: $version (on $phpver)\n"); print("Version: $version (on $phpver)\n");
} else { } else {
$q = $query ? "" : "<p><b>Query:</b> " . html_escape($query); $q = $query ? "" : "<p><b>Query:</b> " . html_escape($query);
if ($code >= 500) { header("HTTP/1.0 500 Internal Error");
error_log("Shimmie Error: $message (Query: $query)\n{$e->getTraceAsString()}");
}
header("HTTP/1.0 $code Error");
echo ' echo '
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
@ -688,7 +663,7 @@ function _get_user(): User
function _get_query(): string function _get_query(): string
{ {
return (@$_POST["q"] ?: @$_GET["q"]) ?: "/"; return (@$_POST["q"]?:@$_GET["q"])?:"/";
} }
@ -736,7 +711,7 @@ function make_form(string $target, string $method="POST", bool $multipart=false,
return '<form action="'.$target.'" method="'.$method.'" '.$extra.'>'.$extra_inputs; return '<form action="'.$target.'" method="'.$method.'" '.$extra.'>'.$extra_inputs;
} }
function SHM_FORM(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit=""): HTMLElement function SHM_FORM(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit="")
{ {
global $user; global $user;
@ -761,19 +736,19 @@ function SHM_FORM(string $target, string $method="POST", bool $multipart=false,
); );
} }
function SHM_SIMPLE_FORM($target, ...$children): HTMLElement function SHM_SIMPLE_FORM($target, ...$children)
{ {
$form = SHM_FORM($target); $form = SHM_FORM($target);
$form->appendChild(emptyHTML(...$children)); $form->appendChild(emptyHTML(...$children));
return $form; return $form;
} }
function SHM_SUBMIT(string $text): HTMLElement function SHM_SUBMIT(string $text)
{ {
return INPUT(["type"=>"submit", "value"=>$text]); return INPUT(["type"=>"submit", "value"=>$text]);
} }
function SHM_COMMAND_EXAMPLE(string $ex, string $desc): HTMLElement function SHM_COMMAND_EXAMPLE(string $ex, string $desc)
{ {
return DIV( return DIV(
["class"=>"command_example"], ["class"=>"command_example"],
@ -782,7 +757,7 @@ function SHM_COMMAND_EXAMPLE(string $ex, string $desc): HTMLElement
); );
} }
function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot): HTMLElement function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot)
{ {
if (is_string($foot)) { if (is_string($foot)) {
$foot = TFOOT(TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>$foot])))); $foot = TFOOT(TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>$foot]))));
@ -802,16 +777,16 @@ function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot)
} }
const BYTE_DENOMINATIONS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const BYTE_DENOMINATIONS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
function human_filesize(int $bytes, $decimals = 2): string function human_filesize(int $bytes, $decimals = 2)
{ {
$factor = floor((strlen(strval($bytes)) - 1) / 3); $factor = floor((strlen(strval($bytes)) - 1) / 3);
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @BYTE_DENOMINATIONS[$factor]; return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @BYTE_DENOMINATIONS[$factor];
} }
/** /*
* Generates a unique key for the website to prevent unauthorized access. * Generates a unique key for the website to prevent unauthorized access.
*/ */
function generate_key(int $length = 20): string function generate_key(int $length = 20)
{ {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = ''; $randomString = '';
@ -822,4 +797,3 @@ function generate_key(int $length = 20): string
return $randomString; return $randomString;
} }

View File

@ -1,17 +1,23 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class AdminPageInfo extends ExtensionInfo class AdminPageInfo extends ExtensionInfo
{ {
public const KEY = "admin"; public const KEY = "admin";
public string $key = self::KEY; public $key = self::KEY;
public string $name = "Admin Controls"; public $name = "Admin Controls";
public string $url = self::SHIMMIE_URL; public $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR; public $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;
public string $description = "Provides a base for various small admin functions"; public $description = "Various things to make admins' lives easier";
public bool $core = true; public $documentation =
public string $visibility = self::VISIBLE_HIDDEN; "Various moderate-level tools for admins; for advanced, obscure, and possibly dangerous tools see the shimmie2-utils script set
<p>Lowercase all tags:
<br>Set all tags to lowercase for consistency
<p>Recount tag use:
<br>If the counts of images per tag get messed up somehow, this will reset them, and remove any unused tags
<p>Database dump:
<br>Download the contents of the database in plain text format, useful for backups.
<p>Image dump:
<br>Download all the images as a .zip file (Requires ZipArchive)";
} }

View File

@ -1,5 +1,4 @@
<?php <?php /** @noinspection PhpUnusedPrivateMethodInspection */
declare(strict_types=1); declare(strict_types=1);
/** /**
@ -7,7 +6,8 @@ declare(strict_types=1);
*/ */
class AdminBuildingEvent extends Event class AdminBuildingEvent extends Event
{ {
public Page $page; /** @var Page */
public $page;
public function __construct(Page $page) public function __construct(Page $page)
{ {
@ -18,8 +18,10 @@ class AdminBuildingEvent extends Event
class AdminActionEvent extends Event class AdminActionEvent extends Event
{ {
public string $action; /** @var string */
public bool $redirect = true; public $action;
/** @var bool */
public $redirect = true;
public function __construct(string $action) public function __construct(string $action)
{ {
@ -31,11 +33,11 @@ class AdminActionEvent extends Event
class AdminPage extends Extension class AdminPage extends Extension
{ {
/** @var AdminPageTheme */ /** @var AdminPageTheme */
protected ?Themelet $theme; protected $theme;
public function onPageRequest(PageRequestEvent $event) public function onPageRequest(PageRequestEvent $event)
{ {
global $database, $page, $user; global $page, $user;
if ($event->page_matches("admin")) { if ($event->page_matches("admin")) {
if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) { if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) {
@ -50,7 +52,6 @@ class AdminPage extends Extension
if ($user->check_auth_token()) { if ($user->check_auth_token()) {
log_info("admin", "Util: $action"); log_info("admin", "Util: $action");
set_time_limit(0); set_time_limit(0);
$database->set_timeout(null);
send_event($aae); send_event($aae);
} }
@ -79,10 +80,8 @@ class AdminPage extends Extension
} }
if ($event->cmd == "get-page") { if ($event->cmd == "get-page") {
global $page; global $page;
$_SERVER['REQUEST_URI'] = $event->args[0];
if (isset($event->args[1])) { if (isset($event->args[1])) {
parse_str($event->args[1], $_GET); parse_str($event->args[1], $_GET);
$_SERVER['REQUEST_URI'] .= "?" . $event->args[1];
} }
send_event(new PageRequestEvent($event->args[0])); send_event(new PageRequestEvent($event->args[0]));
$page->display(); $page->display();
@ -104,7 +103,7 @@ class AdminPage extends Extension
$uid = $event->args[0]; $uid = $event->args[0];
$image = Image::by_id_or_hash($uid); $image = Image::by_id_or_hash($uid);
if ($image) { if ($image) {
send_event(new ThumbnailGenerationEvent($image->hash, $image->get_mime(), true)); send_event(new ThumbnailGenerationEvent($image->hash, $image->ext, true));
} else { } else {
print("No post with ID '$uid'\n"); print("No post with ID '$uid'\n");
} }
@ -130,6 +129,7 @@ class AdminPage extends Extension
public function onAdminBuilding(AdminBuildingEvent $event) public function onAdminBuilding(AdminBuildingEvent $event)
{ {
$this->theme->display_page(); $this->theme->display_page();
$this->theme->display_form();
} }
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
@ -149,4 +149,46 @@ class AdminPage extends Extension
$event->add_link("Board Admin", make_link("admin")); $event->add_link("Board Admin", make_link("admin"));
} }
} }
public function onAdminAction(AdminActionEvent $event)
{
$action = $event->action;
if (method_exists($this, $action)) {
$event->redirect = $this->$action();
}
}
private function set_tag_case()
{
global $database;
$database->execute(
"UPDATE tags SET tag=:tag1 WHERE LOWER(tag) = LOWER(:tag2)",
["tag1" => $_POST['tag'], "tag2" => $_POST['tag']]
);
log_info("admin", "Fixed the case of {$_POST['tag']}", "Fixed case");
return true;
}
private function lowercase_all_tags()
{
global $database;
$database->execute("UPDATE tags SET tag=lower(tag)");
log_warning("admin", "Set all tags to lowercase", "Set all tags to lowercase");
return true;
}
private function recount_tag_use()
{
global $database;
$database->Execute("
UPDATE tags
SET count = COALESCE(
(SELECT COUNT(image_id) FROM image_tags WHERE tag_id=tags.id GROUP BY tag_id),
0
)
");
$database->Execute("DELETE FROM tags WHERE count=0");
log_warning("admin", "Re-counted tags", "Re-counted tags");
return true;
}
} }

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class AdminPageTest extends ShimmiePHPUnitTestCase class AdminPageTest extends ShimmiePHPUnitTestCase
{ {
public function testAuth() public function testAuth()
@ -21,6 +19,59 @@ class AdminPageTest extends ShimmiePHPUnitTestCase
$this->assertEquals("Admin Tools", $page->title); $this->assertEquals("Admin Tools", $page->title);
} }
public function testLowercaseAndSetCase()
{
// Create a problem
$ts = time(); // we need a tag that hasn't been used before
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "TeStCase$ts");
// Validate problem
$page = $this->get_page("post/view/$image_id_1");
$this->assertEquals("Image $image_id_1: TeStCase$ts", $page->title);
// Fix
send_event(new AdminActionEvent('lowercase_all_tags'));
// Validate fix
$this->get_page("post/view/$image_id_1");
$this->assert_title("Image $image_id_1: testcase$ts");
// Change
$_POST["tag"] = "TestCase$ts";
send_event(new AdminActionEvent('set_tag_case'));
// Validate change
$this->get_page("post/view/$image_id_1");
$this->assert_title("Image $image_id_1: TestCase$ts");
}
# FIXME: make sure the admin tools actually work
public function testRecount()
{
global $database;
// Create a problem
$ts = time(); // we need a tag that hasn't been used before
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
$database->execute(
"INSERT INTO tags(tag, count) VALUES(:tag, :count)",
["tag"=>"tes$ts", "count"=>42]
);
// Fix
send_event(new AdminActionEvent('recount_tag_use'));
// Validate fix
$this->assertEquals(
0,
$database->get_one(
"SELECT count FROM tags WHERE tag = :tag",
["tag"=>"tes$ts"]
)
);
}
public function testCommands() public function testCommands()
{ {
send_event(new UserLoginEvent(User::by_name(self::$admin_name))); send_event(new UserLoginEvent(User::by_name(self::$admin_name)));

View File

@ -1,6 +1,5 @@
<?php <?php declare(strict_types=1);
use function MicroHTML\INPUT;
declare(strict_types=1);
class AdminPageTheme extends Themelet class AdminPageTheme extends Themelet
{ {
@ -15,4 +14,41 @@ class AdminPageTheme extends Themelet
$page->set_heading("Admin Tools"); $page->set_heading("Admin Tools");
$page->add_block(new NavBlock()); $page->add_block(new NavBlock());
} }
protected function button(string $name, string $action, bool $protected=false): string
{
$c_protected = $protected ? " protected" : "";
$html = make_form(make_link("admin/$action"), "POST", false, "admin$c_protected");
if ($protected) {
$html .= "<input type='submit' id='$action' value='$name' disabled='disabled'>";
$html .= "<input type='checkbox' onclick='$(\"#$action\").attr(\"disabled\", !$(this).is(\":checked\"))'>";
} else {
$html .= "<input type='submit' id='$action' value='$name'>";
}
$html .= "</form>\n";
return $html;
}
/*
* Show a form which links to admin_utils with POST[action] set to one of:
* 'lowercase all tags'
* 'recount tag use'
* etc
*/
public function display_form()
{
global $page;
$html = "";
$html .= $this->button("All tags to lowercase", "lowercase_all_tags", true);
$html .= $this->button("Recount tag use", "recount_tag_use", false);
$page->add_block(new Block("Misc Admin Tools", $html));
$html = (string)SHM_SIMPLE_FORM(
"admin/set_tag_case",
INPUT(["type"=>'text', "name"=>'tag', "placeholder"=>'Enter tag with correct case', "class"=>'autocomplete_tags', "autocomplete"=>'off']),
SHM_SUBMIT('Set Tag Case'),
);
$page->add_block(new Block("Set Tag Case", $html));
}
} }

View File

@ -1,17 +1,15 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class AliasEditorInfo extends ExtensionInfo class AliasEditorInfo extends ExtensionInfo
{ {
public const KEY = "alias_editor"; public const KEY = "alias_editor";
public string $key = self::KEY; public $key = self::KEY;
public string $name = "Alias Editor"; public $name = "Alias Editor";
public string $url = self::SHIMMIE_URL; public $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR; public $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;
public string $description = "Edit the alias list"; public $description = "Edit the alias list";
public ?string $documentation = 'The list is visible at <a href="$site/alias/list">/alias/list</a>; only site admins can edit it, other people can view and download it'; public $documentation = 'The list is visible at <a href="$site/alias/list">/alias/list</a>; only site admins can edit it, other people can view and download it';
public bool $core = true; public $core = true;
} }

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
use MicroCRUD\ActionColumn; use MicroCRUD\ActionColumn;
use MicroCRUD\TextColumn; use MicroCRUD\TextColumn;
@ -28,8 +26,10 @@ class AliasTable extends Table
class AddAliasEvent extends Event class AddAliasEvent extends Event
{ {
public string $oldtag; /** @var string */
public string $newtag; public $oldtag;
/** @var string */
public $newtag;
public function __construct(string $oldtag, string $newtag) public function __construct(string $oldtag, string $newtag)
{ {
@ -41,7 +41,7 @@ class AddAliasEvent extends Event
class DeleteAliasEvent extends Event class DeleteAliasEvent extends Event
{ {
public string $oldtag; public $oldtag;
public function __construct(string $oldtag) public function __construct(string $oldtag)
{ {
@ -57,7 +57,7 @@ class AddAliasException extends SCoreException
class AliasEditor extends Extension class AliasEditor extends Extension
{ {
/** @var AliasEditorTheme */ /** @var AliasEditorTheme */
protected ?Themelet $theme; protected $theme;
public function onPageRequest(PageRequestEvent $event) public function onPageRequest(PageRequestEvent $event)
{ {
@ -96,7 +96,7 @@ class AliasEditor extends Extension
$this->theme->display_aliases($t->table($t->query()), $t->paginator()); $this->theme->display_aliases($t->table($t->query()), $t->paginator());
} elseif ($event->get_arg(0) == "export") { } elseif ($event->get_arg(0) == "export") {
$page->set_mode(PageMode::DATA); $page->set_mode(PageMode::DATA);
$page->set_mime(MimeType::CSV); $page->set_type(MIME_TYPE_CSV);
$page->set_filename("aliases.csv"); $page->set_filename("aliases.csv");
$page->set_data($this->get_alias_csv($database)); $page->set_data($this->get_alias_csv($database));
} elseif ($event->get_arg(0) == "import") { } elseif ($event->get_arg(0) == "import") {

View File

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

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class AliasEditorTheme extends Themelet class AliasEditorTheme extends Themelet
{ {

View File

@ -1,14 +1,12 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class ApprovalInfo extends ExtensionInfo class ApprovalInfo extends ExtensionInfo
{ {
public const KEY = "approval"; public const KEY = "approval";
public string $key = self::KEY; public $key = self::KEY;
public string $name = "Approval"; public $name = "Approval";
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"]; public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public string $license = self::LICENSE_WTFPL; public $license = self::LICENSE_WTFPL;
public string $description = "Adds an approval step to the upload/import process."; public $description = "Adds an approval step to the upload/import process.";
} }

View File

@ -1,18 +1,16 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
abstract class ApprovalConfig abstract class ApprovalConfig
{ {
public const VERSION = "ext_approval_version"; const VERSION = "ext_approval_version";
public const IMAGES = "approve_images"; const IMAGES = "approve_images";
public const COMMENTS = "approve_comments"; const COMMENTS = "approve_comments";
} }
class Approval extends Extension class Approval extends Extension
{ {
/** @var ApprovalTheme */ /** @var ApprovalTheme */
protected ?Themelet $theme; protected $theme;
public function onInitExt(InitExtEvent $event) public function onInitExt(InitExtEvent $event)
{ {
@ -35,7 +33,7 @@ class Approval extends Extension
$image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null; $image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
} }
if (empty($image_id)) { if (empty($image_id)) {
throw new SCoreException("Can not approve post: No valid Post ID given."); throw new SCoreException("Can not approve image: No valid Image ID given.");
} }
self::approve_image($image_id); self::approve_image($image_id);
@ -50,7 +48,7 @@ class Approval extends Extension
$image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null; $image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
} }
if (empty($image_id)) { if (empty($image_id)) {
throw new SCoreException("Can not disapprove image: No valid Post ID given."); throw new SCoreException("Can not disapprove image: No valid Image ID given.");
} }
self::disapprove_image($image_id); self::disapprove_image($image_id);
@ -79,14 +77,14 @@ class Approval extends Extension
$approval_action = $_POST["approval_action"]; $approval_action = $_POST["approval_action"];
switch ($approval_action) { switch ($approval_action) {
case "approve_all": case "approve_all":
$database->set_timeout(null); // These updates can take a little bit $database->set_timeout(300000); // These updates can take a little bit
$database->execute( $database->execute(
"UPDATE images SET approved = :true, approved_by_id = :approved_by_id WHERE approved = :false", "UPDATE images SET approved = :true, approved_by_id = :approved_by_id WHERE approved = :false",
["approved_by_id"=>$user->id, "true"=>true, "false"=>false] ["approved_by_id"=>$user->id, "true"=>true, "false"=>false]
); );
break; break;
case "disapprove_all": case "disapprove_all":
$database->set_timeout(null); // These updates can take a little bit $database->set_timeout(300000); // These updates can take a little bit
$database->execute( $database->execute(
"UPDATE images SET approved = :false, approved_by_id = NULL WHERE approved = :true", "UPDATE images SET approved = :false, approved_by_id = NULL WHERE approved = :true",
["true"=>true, "false"=>false] ["true"=>true, "false"=>false]
@ -101,9 +99,9 @@ class Approval extends Extension
public function onDisplayingImage(DisplayingImageEvent $event) public function onDisplayingImage(DisplayingImageEvent $event)
{ {
global $page; global $user, $page, $config;
if (!$this->check_permissions(($event->image))) { if ($config->get_bool(ApprovalConfig::IMAGES) && $event->image->approved===false && !$user->can(Permissions::APPROVE_IMAGE)) {
$page->set_mode(PageMode::REDIRECT); $page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/list")); $page->set_redirect(make_link("post/list"));
} }
@ -120,16 +118,16 @@ class Approval extends Extension
} }
public const SEARCH_REGEXP = "/^approved:(yes|no)/"; const SEARCH_REGEXP = "/^approved:(yes|no)/";
public function onSearchTermParse(SearchTermParseEvent $event) public function onSearchTermParse(SearchTermParseEvent $event)
{ {
global $user, $config; global $user, $database, $config;
if ($config->get_bool(ApprovalConfig::IMAGES)) { if ($config->get_bool(ApprovalConfig::IMAGES)) {
$matches = []; $matches = [];
if (is_null($event->term) && $this->no_approval_query($event->context)) { if (is_null($event->term) && $this->no_approval_query($event->context)) {
$event->add_querylet(new Querylet("approved = :true", ["true"=>true])); $event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
} }
if (is_null($event->term)) { if (is_null($event->term)) {
@ -137,9 +135,9 @@ class Approval extends Extension
} }
if (preg_match(self::SEARCH_REGEXP, strtolower($event->term), $matches)) { if (preg_match(self::SEARCH_REGEXP, strtolower($event->term), $matches)) {
if ($user->can(Permissions::APPROVE_IMAGE) && $matches[1] == "no") { if ($user->can(Permissions::APPROVE_IMAGE) && $matches[1] == "no") {
$event->add_querylet(new Querylet("approved != :true", ["true"=>true])); $event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_N ")));
} else { } else {
$event->add_querylet(new Querylet("approved = :true", ["true"=>true])); $event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
} }
} }
} }
@ -189,26 +187,6 @@ class Approval extends Extension
); );
} }
private function check_permissions(Image $image): bool
{
global $user, $config;
if ($config->get_bool(ApprovalConfig::IMAGES) && $image->approved===false && !$user->can(Permissions::APPROVE_IMAGE)) {
return false;
}
return true;
}
public function onImageDownloading(ImageDownloadingEvent $event)
{
/**
* Deny images upon insufficient permissions.
**/
if (!$this->check_permissions($event->image)) {
throw new SCoreException("Access denied");
}
}
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
{ {
global $user, $config; global $user, $config;
@ -263,15 +241,15 @@ class Approval extends Extension
global $database; global $database;
if ($this->get_version(ApprovalConfig::VERSION) < 1) { if ($this->get_version(ApprovalConfig::VERSION) < 1) {
$database->execute("ALTER TABLE images ADD COLUMN approved BOOLEAN NOT NULL DEFAULT FALSE"); $database->execute($database->scoreql_to_sql(
$database->execute("ALTER TABLE images ADD COLUMN approved_by_id INTEGER NULL"); "ALTER TABLE images ADD COLUMN approved SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N"
$database->execute("CREATE INDEX images_approved_idx ON images(approved)"); ));
$this->set_version(ApprovalConfig::VERSION, 2); $database->execute(
} "ALTER TABLE images ADD COLUMN approved_by_id INTEGER NULL"
);
if ($this->get_version(ApprovalConfig::VERSION) < 2) { $database->execute("CREATE INDEX images_approved_idx ON images(approved)");
$database->standardise_boolean("images", "approved"); $this->set_version(ApprovalConfig::VERSION, 1);
$this->set_version(ApprovalConfig::VERSION, 2);
} }
} }
} }

View File

@ -1,13 +1,11 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
use function MicroHTML\BR; use function MicroHTML\BR;
use function MicroHTML\BUTTON; use function MicroHTML\BUTTON;
use function MicroHTML\INPUT; use function MicroHTML\INPUT;
class ApprovalTheme extends Themelet class ApprovalTheme extends Themelet
{ {
public function get_image_admin_html(Image $image): string public function get_image_admin_html(Image $image)
{ {
if ($image->approved===true) { if ($image->approved===true) {
$html = SHM_SIMPLE_FORM( $html = SHM_SIMPLE_FORM(
@ -26,24 +24,26 @@ class ApprovalTheme extends Themelet
return (string)$html; return (string)$html;
} }
public function get_help_html(): string
public function get_help_html()
{ {
return '<p>Search for posts that are approved/not approved.</p> return '<p>Search for images that are approved/not approved.</p>
<div class="command_example"> <div class="command_example">
<pre>approved:yes</pre> <pre>approved:yes</pre>
<p>Returns posts that have been approved.</p> <p>Returns images that have been approved.</p>
</div> </div>
<div class="command_example"> <div class="command_example">
<pre>approved:no</pre> <pre>approved:no</pre>
<p>Returns posts that have not been approved.</p> <p>Returns images that have not been approved.</p>
</div> </div>
'; ';
} }
public function display_admin_block(SetupBuildingEvent $event) public function display_admin_block(SetupBuildingEvent $event)
{ {
$sb = $event->panel->create_new_block("Approval"); $sb = new SetupBlock("Approval");
$sb->add_bool_option(ApprovalConfig::IMAGES, "Posts: "); $sb->add_bool_option(ApprovalConfig::IMAGES, "Images: ");
$event->panel->add_block($sb);
} }
public function display_admin_form() public function display_admin_form()
@ -52,9 +52,9 @@ class ApprovalTheme extends Themelet
$html = (string)SHM_SIMPLE_FORM( $html = (string)SHM_SIMPLE_FORM(
"admin/approval", "admin/approval",
BUTTON(["name"=>'approval_action', "value"=>'approve_all'], "Approve All Posts"), BUTTON(["name"=>'approval_action', "value"=>'approve_all'], "Approve All Images"),
BR(), BR(),
BUTTON(["name"=>'approval_action', "value"=>'disapprove_all'], "Disapprove All Posts"), BUTTON(["name"=>'approval_action', "value"=>'disapprove_all'], "Disapprove All Images"),
); );
$page->add_block(new Block("Approval", $html)); $page->add_block(new Block("Approval", $html));
} }

View File

@ -1,16 +1,14 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class ArtistsInfo extends ExtensionInfo class ArtistsInfo extends ExtensionInfo
{ {
public const KEY = "artists"; public const KEY = "artists";
public string $key = self::KEY; public $key = self::KEY;
public string $name = "Artists System"; public $name = "Artists System";
public string $url = self::SHIMMIE_URL; public $url = self::SHIMMIE_URL;
public array $authors = ["Sein Kraft"=>"mail@seinkraft.info","Alpha"=>"alpha@furries.com.ar"]; public $authors = ["Sein Kraft"=>"mail@seinkraft.info","Alpha"=>"alpha@furries.com.ar"];
public string $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;
public string $description = "Simple artists extension"; public $description = "Simple artists extension";
public bool $beta = true; public $beta = true;
} }

View File

@ -1,12 +1,13 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class AuthorSetEvent extends Event class AuthorSetEvent extends Event
{ {
public Image $image; /** @var Image */
public User $user; public $image;
public string $author; /** @var User */
public $user;
/** @var string */
public $author;
public function __construct(Image $image, User $user, string $author) public function __construct(Image $image, User $user, string $author)
{ {
@ -20,7 +21,7 @@ class AuthorSetEvent extends Event
class Artists extends Extension class Artists extends Extension
{ {
/** @var ArtistsTheme */ /** @var ArtistsTheme */
protected ?Themelet $theme; protected $theme;
public function onImageInfoSet(ImageInfoSetEvent $event) public function onImageInfoSet(ImageInfoSetEvent $event)
{ {
@ -552,7 +553,7 @@ class Artists extends Extension
$urlsAsString = $inputs["urls"]; $urlsAsString = $inputs["urls"];
$urlsIDsAsString = $inputs["urlsIDs"]; $urlsIDsAsString = $inputs["urlsIDs"];
if (str_contains($name, " ")) { if (strpos($name, " ")) {
return; return;
} }
@ -682,7 +683,7 @@ class Artists extends Extension
); );
} }
private function add_artist(): int private function add_artist()
{ {
global $user; global $user;
$inputs = validate_input([ $inputs = validate_input([
@ -694,7 +695,7 @@ class Artists extends Extension
]); ]);
$name = $inputs["name"]; $name = $inputs["name"];
if (str_contains($name, " ")) { if (strpos($name, " ")) {
return -1; return -1;
} }

View File

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

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class ArtistsTheme extends Themelet class ArtistsTheme extends Themelet
{ {
public function get_author_editor_html(string $author): string public function get_author_editor_html(string $author): string
@ -29,19 +27,19 @@ class ArtistsTheme extends Themelet
<input type='submit' name='edit' id='edit' value='New Artist'/> <input type='submit' name='edit' id='edit' value='New Artist'/>
</form>"; </form>";
} }
if ($mode == "editor") { if ($mode == "editor") {
$html = "<form method='post' action='".make_link("artist/new_artist")."'> $html = "<form method='post' action='".make_link("artist/new_artist")."'>
".$user->get_auth_html()." ".$user->get_auth_html()."
<input type='submit' name='edit' value='New Artist'/> <input type='submit' name='edit' value='New Artist'/>
</form> </form>
<form method='post' action='".make_link("artist/edit_artist")."'> <form method='post' action='".make_link("artist/edit_artist")."'>
".$user->get_auth_html()." ".$user->get_auth_html()."
<input type='submit' name='edit' value='Edit Artist'/> <input type='submit' name='edit' value='Edit Artist'/>
<input type='hidden' name='artist_id' value='".$artistID."'> <input type='hidden' name='artist_id' value='".$artistID."'>
</form>"; </form>";
if ($is_admin) { if ($is_admin) {
$html .= "<form method='post' action='".make_link("artist/nuke_artist")."'> $html .= "<form method='post' action='".make_link("artist/nuke_artist")."'>
".$user->get_auth_html()." ".$user->get_auth_html()."
@ -49,19 +47,19 @@ class ArtistsTheme extends Themelet
<input type='hidden' name='artist_id' value='".$artistID."'> <input type='hidden' name='artist_id' value='".$artistID."'>
</form>"; </form>";
} }
$html .= "<form method='post' action='".make_link("artist/add_alias")."'> $html .= "<form method='post' action='".make_link("artist/add_alias")."'>
".$user->get_auth_html()." ".$user->get_auth_html()."
<input type='submit' name='edit' value='Add Alias'/> <input type='submit' name='edit' value='Add Alias'/>
<input type='hidden' name='artist_id' value='".$artistID."'> <input type='hidden' name='artist_id' value='".$artistID."'>
</form> </form>
<form method='post' action='".make_link("artist/add_member")."'> <form method='post' action='".make_link("artist/add_member")."'>
".$user->get_auth_html()." ".$user->get_auth_html()."
<input type='submit' name='edit' value='Add Member'/> <input type='submit' name='edit' value='Add Member'/>
<input type='hidden' name='artist_id' value='".$artistID."'> <input type='hidden' name='artist_id' value='".$artistID."'>
</form> </form>
<form method='post' action='".make_link("artist/add_url")."'> <form method='post' action='".make_link("artist/add_url")."'>
".$user->get_auth_html()." ".$user->get_auth_html()."
<input type='submit' name='edit' value='Add Url'/> <input type='submit' name='edit' value='Add Url'/>
@ -133,7 +131,7 @@ class ArtistsTheme extends Themelet
global $page; global $page;
$page->add_block(new Block("Edit artist", $html, "main", 10)); $page->add_block(new Block("Edit artist", $html, "main", 10));
} }
public function new_artist_composer() public function new_artist_composer()
{ {
global $page, $user; global $page, $user;
@ -154,7 +152,7 @@ class ArtistsTheme extends Themelet
$page->set_heading("Artists"); $page->set_heading("Artists");
$page->add_block(new Block("Artists", $html, "main", 10)); $page->add_block(new Block("Artists", $html, "main", 10));
} }
public function list_artists($artists, $pageNumber, $totalPages) public function list_artists($artists, $pageNumber, $totalPages)
{ {
global $user, $page; global $user, $page;
@ -169,7 +167,7 @@ class ArtistsTheme extends Themelet
if (!$user->is_anonymous()) { if (!$user->is_anonymous()) {
$html .= "<th colspan='2'>Action</th>"; $html .= "<th colspan='2'>Action</th>";
} // space for edit link } // space for edit link
$html .= "</tr></thead>"; $html .= "</tr></thead>";
$deletionLinkActionArray = [ $deletionLinkActionArray = [
@ -246,7 +244,7 @@ class ArtistsTheme extends Themelet
<input type="hidden" name="artistID" value='.$artistID.' /></td></tr> <input type="hidden" name="artistID" value='.$artistID.' /></td></tr>
<tr><td colspan="2"><input type="submit" value="Submit" /></td></tr> <tr><td colspan="2"><input type="submit" value="Submit" /></td></tr>
</table> </table>
</form> </form>
'; ';
global $page; global $page;
@ -356,7 +354,7 @@ class ArtistsTheme extends Themelet
<tr> <tr>
<th></th> <th></th>
<th></th>"; <th></th>";
if ($userIsLogged) { if ($userIsLogged) {
$html .= "<th></th>"; $html .= "<th></th>";
} }
@ -404,13 +402,13 @@ class ArtistsTheme extends Themelet
$artist_images = ""; $artist_images = "";
foreach ($images as $image) { foreach ($images as $image) {
$thumb_html = $this->build_thumb_html($image); $thumb_html = $this->build_thumb_html($image);
$artist_images .= '<span class="thumb">'. $artist_images .= '<span class="thumb">'.
'<a href="$image_link">'.$thumb_html.'</a>'. '<a href="$image_link">'.$thumb_html.'</a>'.
'</span>'; '</span>';
} }
$page->add_block(new Block("Artist Posts", $artist_images, "main", 20)); $page->add_block(new Block("Artist Images", $artist_images, "main", 20));
} }
private function render_aliases(array $aliases, bool $userIsLogged, bool $userIsAdmin): string private function render_aliases(array $aliases, bool $userIsLogged, bool $userIsAdmin): string
@ -548,13 +546,13 @@ class ArtistsTheme extends Themelet
return $html; return $html;
} }
public function get_help_html(): string public function get_help_html()
{ {
return '<p>Search for posts with a particular artist.</p> return '<p>Search for images with a particular artist.</p>
<div class="command_example"> <div class="command_example">
<pre>artist=leonardo</pre> <pre>artist=leonardo</pre>
<p>Returns posts with the artist "leonardo".</p> <p>Returns images with the artist "leonardo".</p>
</div> </div>
'; ';
} }
} }

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
abstract class AutoTaggerConfig abstract class AutoTaggerConfig
{ {

View File

@ -1,14 +1,12 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class AutoTaggerInfo extends ExtensionInfo class AutoTaggerInfo extends ExtensionInfo
{ {
public const KEY = "auto_tagger"; public const KEY = "auto_tagger";
public string $key = self::KEY; public $key = self::KEY;
public string $name = "Auto-Tagger"; public $name = "Auto-Tagger";
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"]; public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public string $license = self::LICENSE_WTFPL; public $license = self::LICENSE_WTFPL;
public string $description = "Provides several automatic tagging functions"; public $description = "Provides several automatic tagging functions";
} }

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
require_once 'config.php'; require_once 'config.php';
@ -30,8 +28,10 @@ class AutoTaggerTable extends Table
class AddAutoTagEvent extends Event class AddAutoTagEvent extends Event
{ {
public string $tag; /** @var string */
public string $additional_tags; public $tag;
/** @var string */
public $additional_tags;
public function __construct(string $tag, string $additional_tags) public function __construct(string $tag, string $additional_tags)
{ {
@ -43,7 +43,7 @@ class AddAutoTagEvent extends Event
class DeleteAutoTagEvent extends Event class DeleteAutoTagEvent extends Event
{ {
public string $tag; public $tag;
public function __construct(string $tag) public function __construct(string $tag)
{ {
@ -63,7 +63,7 @@ class AddAutoTagException extends SCoreException
class AutoTagger extends Extension class AutoTagger extends Extension
{ {
/** @var AutoTaggerTheme */ /** @var AutoTaggerTheme */
protected ?Themelet $theme; protected $theme;
public function onPageRequest(PageRequestEvent $event) public function onPageRequest(PageRequestEvent $event)
{ {
@ -102,7 +102,7 @@ class AutoTagger extends Extension
$this->theme->display_auto_tagtable($t->table($t->query()), $t->paginator()); $this->theme->display_auto_tagtable($t->table($t->query()), $t->paginator());
} elseif ($event->get_arg(0) == "export") { } elseif ($event->get_arg(0) == "export") {
$page->set_mode(PageMode::DATA); $page->set_mode(PageMode::DATA);
$page->set_mime(MimeType::CSV); $page->set_type(MIME_TYPE_CSV);
$page->set_filename("auto_tag.csv"); $page->set_filename("auto_tag.csv");
$page->set_data($this->get_auto_tag_csv($database)); $page->set_data($this->get_auto_tag_csv($database));
} elseif ($event->get_arg(0) == "import") { } elseif ($event->get_arg(0) == "import") {
@ -269,7 +269,6 @@ class AutoTagger extends Extension
if (!empty($tag_id)) { if (!empty($tag_id)) {
$image_ids = $database->get_col_iterable("SELECT image_id FROM image_tags WHERE tag_id = :tag_id", ["tag_id"=>$tag_id]); $image_ids = $database->get_col_iterable("SELECT image_id FROM image_tags WHERE tag_id = :tag_id", ["tag_id"=>$tag_id]);
foreach ($image_ids as $image_id) { foreach ($image_ids as $image_id) {
$image_id = (int) $image_id;
$image = Image::by_id($image_id); $image = Image::by_id($image_id);
$event = new TagSetEvent($image, $image->get_tag_array()); $event = new TagSetEvent($image, $image->get_tag_array());
send_event($event); send_event($event);
@ -315,10 +314,14 @@ class AutoTagger extends Extension
$tags_mixed = array_merge($tags_mixed, $new_tags); $tags_mixed = array_merge($tags_mixed, $new_tags);
} }
return array_intersect_key( $results = array_intersect_key(
$tags_mixed, $tags_mixed,
array_unique(array_map('strtolower', $tags_mixed)) array_unique(array_map('strtolower', $tags_mixed))
); );
return $results;
} }
/** /**

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class AutoTaggerTest extends ShimmiePHPUnitTestCase class AutoTaggerTest extends ShimmiePHPUnitTestCase
{ {
public function testAutoTaggerList() public function testAutoTaggerList()
@ -39,14 +37,14 @@ class AutoTaggerTest extends ShimmiePHPUnitTestCase
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1"); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
$this->get_page("post/view/$image_id"); # check that the tag has been replaced $this->get_page("post/view/$image_id"); # check that the tag has been replaced
$this->assert_title("Post $image_id: test1 test2"); $this->assert_title("Image $image_id: test1 test2");
$this->delete_image($image_id); $this->delete_image($image_id);
send_event(new AddAutoTagEvent("test2", "test3")); send_event(new AddAutoTagEvent("test2", "test3"));
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1"); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
$this->get_page("post/view/$image_id"); # check that the tag has been replaced $this->get_page("post/view/$image_id"); # check that the tag has been replaced
$this->assert_title("Post $image_id: test1 test2 test3"); $this->assert_title("Image $image_id: test1 test2 test3");
$this->delete_image($image_id); $this->delete_image($image_id);
send_event(new DeleteAutoTagEvent("test1")); send_event(new DeleteAutoTagEvent("test1"));

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class AutoTaggerTheme extends Themelet class AutoTaggerTheme extends Themelet
{ {

View File

@ -1,13 +1,11 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class AutoCompleteInfo extends ExtensionInfo class AutoCompleteInfo extends ExtensionInfo
{ {
public const KEY = "autocomplete"; public const KEY = "autocomplete";
public string $key = self::KEY; public $key = self::KEY;
public string $name = "Autocomplete"; public $name = "Autocomplete";
public array $authors = ["Daku"=>"admin@codeanimu.net"]; public $authors = ["Daku"=>"admin@codeanimu.net"];
public string $description = "Adds autocomplete to search & tagging."; public $description = "Adds autocomplete to search & tagging.";
} }

View File

@ -1,11 +1,9 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class AutoComplete extends Extension class AutoComplete extends Extension
{ {
/** @var AutoCompleteTheme */ /** @var AutoCompleteTheme */
protected ?Themelet $theme; protected $theme;
public function get_priority(): int public function get_priority(): int
{ {
@ -14,68 +12,58 @@ class AutoComplete extends Extension
public function onPageRequest(PageRequestEvent $event) public function onPageRequest(PageRequestEvent $event)
{ {
global $page; global $cache, $page, $database;
if ($event->page_matches("api/internal/autocomplete")) { if ($event->page_matches("api/internal/autocomplete")) {
$limit = (int)($_GET["limit"] ?? 0); if (!isset($_GET["s"])) {
$s = $_GET["s"] ?? ""; return;
}
$res = $this->complete($s, $limit);
$page->set_mode(PageMode::DATA); $page->set_mode(PageMode::DATA);
$page->set_mime(MimeType::JSON); $page->set_type(MIME_TYPE_JSON);
$s = strtolower($_GET["s"]);
if (
$s == '' ||
$s[0] == '_' ||
$s[0] == '%' ||
strlen($s) > 32
) {
$page->set_data("{}");
return;
}
//$limit = 0;
$cache_key = "autocomplete-$s";
$limitSQL = "";
$s = str_replace('_', '\_', $s);
$s = str_replace('%', '\%', $s);
$SQLarr = ["search"=>"$s%"]; #, "cat_search"=>"%:$s%"];
if (isset($_GET["limit"]) && $_GET["limit"] !== 0) {
$limitSQL = "LIMIT :limit";
$SQLarr['limit'] = $_GET["limit"];
$cache_key .= "-" . $_GET["limit"];
}
$res = $cache->get($cache_key);
if (!$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);
}
$page->set_data(json_encode($res)); $page->set_data(json_encode($res));
} }
$this->theme->build_autocomplete($page); $this->theme->build_autocomplete($page);
} }
private function complete(string $search, int $limit): array
{
global $cache, $database;
if (!$search) {
return [];
}
$search = strtolower($search);
if (
$search == '' ||
$search[0] == '_' ||
$search[0] == '%' ||
strlen($search) > 32
) {
return [];
}
$cache_key = "autocomplete-$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 (!$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);
}
return $res;
}
} }

View File

@ -1,5 +1,5 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
var metatags = ['order:id', 'order:width', 'order:height', 'order:filesize', 'order:filename', 'order:favorites']; var metatags = ['order:id', 'order:width', 'order:height', 'order:filesize', 'order:filename'];
$('[name="search"]').tagit({ $('[name="search"]').tagit({
singleFieldDelimiter: ' ', singleFieldDelimiter: ' ',
@ -63,12 +63,12 @@ document.addEventListener('DOMContentLoaded', () => {
var keyCode = e.keyCode || e.which; var keyCode = e.keyCode || e.which;
//Stop tags containing space. //Stop tags containing space.
if(keyCode === 32) { if(keyCode == 32) {
e.preventDefault(); e.preventDefault();
$('.autocomplete_tags').tagit('createTag', $(this).val()); $('.autocomplete_tags').tagit('createTag', $(this).val());
$(this).autocomplete('close'); $(this).autocomplete('close');
} else if (keyCode === 9) { } else if (keyCode == 9) {
e.preventDefault(); e.preventDefault();
var tag = $('.tagit-autocomplete[style*=\"display: block\"] > li:focus, .tagit-autocomplete[style*=\"display: block\"] > li:first').first(); var tag = $('.tagit-autocomplete[style*=\"display: block\"] > li:focus, .tagit-autocomplete[style*=\"display: block\"] > li:first').first();

View File

@ -1,5 +1,4 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
class AutoCompleteTest extends ShimmiePHPUnitTestCase class AutoCompleteTest extends ShimmiePHPUnitTestCase

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class AutoCompleteTheme extends Themelet class AutoCompleteTheme extends Themelet
{ {

View File

@ -1,18 +1,16 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BanWordsInfo extends ExtensionInfo class BanWordsInfo extends ExtensionInfo
{ {
public const KEY = "ban_words"; public const KEY = "ban_words";
public string $key = self::KEY; public $key = self::KEY;
public string $name = "Comment Word Ban"; public $name = "Comment Word Ban";
public string $url = self::SHIMMIE_URL; public $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR; public $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;
public string $description = "For stopping spam and other comment abuse"; public $description = "For stopping spam and other comment abuse";
public ?string $documentation = public $documentation =
"Allows an administrator to ban certain words "Allows an administrator to ban certain words
from comments. This can be a very simple but effective way from comments. This can be a very simple but effective way
of stopping spam; just add \"viagra\", \"porn\", etc to the of stopping spam; just add \"viagra\", \"porn\", etc to the

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BanWords extends Extension class BanWords extends Extension
{ {
@ -57,7 +55,7 @@ xanax
public function onSetupBuilding(SetupBuildingEvent $event) public function onSetupBuilding(SetupBuildingEvent $event)
{ {
$sb = $event->panel->create_new_block("Banned Phrases"); $sb = new SetupBlock("Banned Phrases");
$sb->add_label("One per line, lines that start with slashes are treated as regex<br/>"); $sb->add_label("One per line, lines that start with slashes are treated as regex<br/>");
$sb->add_longtext_option("banned_words"); $sb->add_longtext_option("banned_words");
$failed = []; $failed = [];
@ -71,6 +69,7 @@ xanax
if ($failed) { if ($failed) {
$sb->add_label("Failed regexes: ".join(", ", $failed)); $sb->add_label("Failed regexes: ".join(", ", $failed));
} }
$event->panel->add_block($sb);
} }
/** /**
@ -88,7 +87,7 @@ xanax
} }
} else { } else {
// other words are literal // other words are literal
if (str_contains($comment, $word)) { if (strpos($comment, $word) !== false) {
throw $ex; throw $ex;
} }
} }

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BanWordsTest extends ShimmiePHPUnitTestCase class BanWordsTest extends ShimmiePHPUnitTestCase
{ {
public function check_blocked($image_id, $words) public function check_blocked($image_id, $words)
@ -10,7 +8,7 @@ class BanWordsTest extends ShimmiePHPUnitTestCase
send_event(new CommentPostingEvent($image_id, $user, $words)); send_event(new CommentPostingEvent($image_id, $user, $words));
$this->fail("Exception not thrown"); $this->fail("Exception not thrown");
} catch (CommentPostingException $e) { } catch (CommentPostingException $e) {
$this->assertEquals("Comment contains banned terms", $e->getMessage()); $this->assertEquals($e->getMessage(), "Comment contains banned terms");
} }
} }

View File

@ -1,57 +1,32 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BBCodeInfo extends ExtensionInfo class BBCodeInfo extends ExtensionInfo
{ {
public const KEY = "bbcode"; public const KEY = "bbcode";
public string $key = self::KEY; public $key = self::KEY;
public string $name = "BBCode"; public $name = "BBCode";
public string $url = self::SHIMMIE_URL; public $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR; public $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;
public bool $core = true; public $core = true;
public string $description = "Turns BBCode into HTML"; public $description = "Turns BBCode into HTML";
public ?string $documentation = public $documentation =
" Basic formatting tags: " Supported tags:
<ul> <ul>
<li>[img]url[/img]
<li>[url]<a href=\"{self::SHIMMIE_URL}\">https://code.shishnet.org/</a>[/url]
<li>[email]<a href=\"mailto:{self::SHISH_EMAIL}\">webmaster@shishnet.org</a>[/email]
<li>[b]<b>bold</b>[/b] <li>[b]<b>bold</b>[/b]
<li>[i]<i>italic</i>[/i] <li>[i]<i>italic</i>[/i]
<li>[u]<u>underline</u>[/u] <li>[u]<u>underline</u>[/u]
<li>[s]<s>strikethrough</s>[/s] <li>[s]<s>strikethrough</s>[/s]
<li>[sup]<sup>superscript</sup>[/sup] <li>[sup]<sup>superscript</sup>[/sup]
<li>[sub]<sub>subscript</sub>[/sub] <li>[sub]<sub>subscript</sub>[/sub]
<li>[h1]Heading 1[/h1]
<li>[h2]Heading 2[/h2]
<li>[h3]Heading 3[/h3]
<li>[h4]Heading 4[/h4]
<li>[align=left|center|right]Aligned Text[/align]
</ul>
<br>
Link tags:
<ul>
<li>[img]url[/img]
<li>[url]<a href=\"{self::SHIMMIE_URL}\">https://code.shishnet.org/</a>[/url]
<li>[url=<a href=\"{self::SHIMMIE_URL}\">https://code.shishnet.org/</a>]some text[/url]
<li>[url]site://ext_doc/bbcode[/url]
<li>[url=site://ext_doc/bbcode]Link to BBCode docs[/url]
<li>[email]<a href=\"mailto:{self::SHISH_EMAIL}\">webmaster@shishnet.org</a>[/email]
<li>[[wiki article]] <li>[[wiki article]]
<li>[[wiki article|with some text]] <li>[[wiki article|with some text]]
<li>&gt;&gt;123 (link to post #123) <li>[quote]text[/quote]
<li>[anchor=target]Scroll to #bb-target[/anchor] <li>[quote=Username]text[/quote]
</ul> <li>&gt;&gt;123 (link to image #123)
<br>
More format Tags:
<ul>
<li>[list]Unordered list[/list]
<li>[ul]Unordered list[/ul]
<li>[ol]Ordered list[/ol]
<li>[li]List Item[/li]
<li>[code]<pre>print(\"Hello World!\");</pre>[/code]
<li>[spoiler]<span style=\"background-color:#000; color:#000;\">Voldemort is bad</span>[/spoiler]
<li>[quote]<blockquote><small>To be or not to be...</small></blockquote>[/quote]
<li>[quote=Shakespeare]<blockquote><em>Shakespeare said:</em><br><small>... That is the question</small></blockquote>[/quote]
</ul>"; </ul>";
} }

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BBCode extends FormatterExtension class BBCode extends FormatterExtension

View File

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

View File

@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
class BiographyInfo extends ExtensionInfo
{
public const KEY = "biography";
public string $key = self::KEY;
public string $name = "User Bios";
public string $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2;
public string $description = "Allow users to write a bit about themselves";
}

View File

@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
class Biography extends Extension
{
/** @var BiographyTheme */
protected ?Themelet $theme;
public function onUserPageBuilding(UserPageBuildingEvent $event)
{
global $page, $user;
$duser = $event->display_user;
$duser_config = UserConfig::get_for_user($event->display_user->id);
$bio = $duser_config->get_string("biography", "");
if ($user->id == $duser->id) {
$this->theme->display_composer($page, $bio);
} else {
$this->theme->display_biography($page, $bio);
}
}
public function onPageRequest(PageRequestEvent $event)
{
global $page, $user, $user_config;
if ($event->page_matches("biography")) {
if ($user->check_auth_token()) {
$user_config->set_string("biography", $_POST['biography']);
$page->flash("Bio Updated");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(referer_or(make_link()));
}
}
}
}

View File

@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
class BiographyTest extends ShimmiePHPUnitTestCase
{
public function testBio()
{
$this->log_in_as_user();
$this->post_page("biography", ["biography"=>"My bio goes here"]);
$this->get_page("user/" . self::$user_name);
$this->assert_text("My bio goes here");
$this->log_in_as_admin();
$this->get_page("user/" . self::$user_name);
$this->assert_text("My bio goes here");
$this->get_page("user/" . self::$admin_name);
$this->assert_no_text("My bio goes here");
}
}

View File

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
use function MicroHTML\TEXTAREA;
class BiographyTheme extends Themelet
{
public function display_biography(Page $page, string $bio)
{
$page->add_block(new Block("About Me", format_text($bio), "main", 30, "about-me"));
}
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,
TEXTAREA(["style"=>"width: 100%", "rows"=>"6", "name"=>"biography"], $bio),
SHM_SUBMIT("Save")
);
$page->add_block(new Block("About Me", (string)$html, "main", 30));
}
}

View File

@ -1,15 +1,13 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BlocksInfo extends ExtensionInfo class BlocksInfo extends ExtensionInfo
{ {
public const KEY = "blocks"; public const KEY = "blocks";
public string $key = self::KEY; public $key = self::KEY;
public string $name = "Generic Blocks"; public $name = "Generic Blocks";
public string $url = self::SHIMMIE_URL; public $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR; public $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;
public string $description = "Add HTML to some space (News, Ads, etc)"; public $description = "Add HTML to some space (News, Ads, etc)";
} }

View File

@ -1,11 +1,9 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class Blocks extends Extension class Blocks extends Extension
{ {
/** @var BlocksTheme */ /** @var BlocksTheme */
protected ?Themelet $theme; protected $theme;
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{ {

View File

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

View File

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

View File

@ -1,17 +1,15 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BlotterInfo extends ExtensionInfo class BlotterInfo extends ExtensionInfo
{ {
public const KEY = "blotter"; public const KEY = "blotter";
public string $key = self::KEY; public $key = self::KEY;
public string $name = "Blotter"; public $name = "Blotter";
public string $url = "http://seemslegit.com/"; public $url = "http://seemslegit.com/";
public array $authors = ["Zach Hall"=>"zach@sosguy.net"]; public $authors = ["Zach Hall"=>"zach@sosguy.net"];
public string $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;
public string $description = "Displays brief updates about whatever you want on every page. public $description = "Displays brief updates about whatever you want on every page.
Colors and positioning can be configured to match your site's design. Colors and positioning can be configured to match your site's design.
Development TODO at https://github.com/zshall/shimmie2/issues"; Development TODO at https://github.com/zshall/shimmie2/issues";

View File

@ -1,11 +1,9 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class Blotter extends Extension class Blotter extends Extension
{ {
/** @var BlotterTheme */ /** @var BlotterTheme */
protected ?Themelet $theme; protected $theme;
public function onInitExt(InitExtEvent $event) public function onInitExt(InitExtEvent $event)
{ {
@ -17,35 +15,41 @@ class Blotter extends Extension
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{ {
global $database; global $config;
$version = $config->get_int("blotter_version", 0);
if ($this->get_version("blotter_version") < 1) { /**
* If this version is less than "1", it's time to install.
*
* REMINDER: If I change the database tables, I must change up version by 1.
*/
if ($version < 1) {
/**
* Installer
*/
global $database, $config;
$database->create_table("blotter", " $database->create_table("blotter", "
id SCORE_AIPK, id SCORE_AIPK,
entry_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, entry_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
entry_text TEXT NOT NULL, entry_text TEXT NOT NULL,
important BOOLEAN NOT NULL DEFAULT FALSE important SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N
"); ");
// Insert sample data: // Insert sample data:
$database->execute( $database->execute(
"INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)", "INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)",
["text"=>"Installed the blotter extension!", "important"=>true] ["text"=>"Installed the blotter extension!", "important"=>"Y"]
); );
log_info("blotter", "Installed tables for blotter extension."); log_info("blotter", "Installed tables for blotter extension.");
$this->set_version("blotter_version", 2); $config->set_int("blotter_version", 1);
}
if ($this->get_version("blotter_version") < 2) {
$database->standardise_boolean("blotter", "important");
$this->set_version("blotter_version", 2);
} }
} }
public function onSetupBuilding(SetupBuildingEvent $event) public function onSetupBuilding(SetupBuildingEvent $event)
{ {
$sb = $event->panel->create_new_block("Blotter"); $sb = new SetupBlock("Blotter");
$sb->add_int_option("blotter_recent", "<br />Number of recent entries to display: "); $sb->add_int_option("blotter_recent", "<br />Number of recent entries to display: ");
$sb->add_text_option("blotter_color", "<br />Color of important updates: (ABCDEF format) "); $sb->add_text_option("blotter_color", "<br />Color of important updates: (ABCDEF format) ");
$sb->add_choice_option("blotter_position", ["Top of page" => "subheading", "In navigation bar" => "left"], "<br>Position: "); $sb->add_choice_option("blotter_position", ["Top of page" => "subheading", "In navigation bar" => "left"], "<br>Position: ");
$event->panel->add_block($sb);
} }
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
@ -94,7 +98,11 @@ class Blotter extends Extension
if ($entry_text == "") { if ($entry_text == "") {
die("No entry message!"); die("No entry message!");
} }
$important = isset($_POST['important']); if (isset($_POST['important'])) {
$important = 'Y';
} else {
$important = 'N';
}
// Now insert into db: // Now insert into db:
$database->execute( $database->execute(
"INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)", "INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)",
@ -116,7 +124,7 @@ class Blotter extends Extension
if (!isset($id)) { if (!isset($id)) {
die("No ID!"); die("No ID!");
} }
$database->execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]); $database->Execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]);
log_info("blotter", "Removed Entry #$id"); log_info("blotter", "Removed Entry #$id");
$page->set_mode(PageMode::REDIRECT); $page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("blotter/editor")); $page->set_redirect(make_link("blotter/editor"));

View File

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

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BlotterTheme extends Themelet class BlotterTheme extends Themelet
{ {
public function display_editor($entries) public function display_editor($entries)
@ -22,7 +20,7 @@ class BlotterTheme extends Themelet
$page->add_block(new Block("Blotter Entries", $html, "main", 10)); $page->add_block(new Block("Blotter Entries", $html, "main", 10));
} }
public function display_blotter(array $entries): void public function display_blotter($entries)
{ {
global $page, $config; global $page, $config;
$html = $this->get_html_for_blotter($entries); $html = $this->get_html_for_blotter($entries);
@ -30,7 +28,7 @@ class BlotterTheme extends Themelet
$page->add_block(new Block(null, $html, $position, 20)); $page->add_block(new Block(null, $html, $position, 20));
} }
private function get_html_for_blotter_editor(array $entries): string private function get_html_for_blotter_editor($entries)
{ {
global $user; global $user;
@ -101,7 +99,7 @@ class BlotterTheme extends Themelet
return $html; return $html;
} }
private function get_html_for_blotter_page(array $entries): string private function get_html_for_blotter_page($entries)
{ {
/** /**
* This one displays a list of all blotter entries. * This one displays a list of all blotter entries.
@ -132,7 +130,7 @@ class BlotterTheme extends Themelet
return $html; return $html;
} }
private function get_html_for_blotter(array $entries): string private function get_html_for_blotter($entries)
{ {
global $config; global $config;
$i_color = $config->get_string("blotter_color", "#FF0000"); $i_color = $config->get_string("blotter_color", "#FF0000");

View File

@ -1,19 +1,17 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BrowserSearchInfo extends ExtensionInfo class BrowserSearchInfo extends ExtensionInfo
{ {
public const KEY = "browser_search"; public const KEY = "browser_search";
public string $key = self::KEY; public $key = self::KEY;
public string $name = "Browser Search"; public $name = "Browser Search";
public string $url = "http://atravelinggeek.com/"; public $url = "http://atravelinggeek.com/";
public array $authors = ["ATravelingGeek"=>"atg@atravelinggeek.com"]; public $authors = ["ATravelingGeek"=>"atg@atravelinggeek.com"];
public string $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;
public ?string $version = "0.1c, October 26, 2007"; public $version = "0.1c, October 26, 2007";
public string $description = "Allows the user to add a browser 'plugin' to search the site with real-time suggestions"; public $description = "Allows the user to add a browser 'plugin' to search the site with real-time suggestions";
public ?string $documentation = public $documentation =
"Once installed, users with an opensearch compatible browser should see their search box light up with whatever \"click here to add a search engine\" notification they have "Once installed, users with an opensearch compatible browser should see their search box light up with whatever \"click here to add a search engine\" notification they have
Some code (and lots of help) by Artanis (Erik Youngren <artanis.00@gmail.com>) from the 'tagger' extension - Used with permission"; Some code (and lots of help) by Artanis (Erik Youngren <artanis.00@gmail.com>) from the 'tagger' extension - Used with permission";

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BrowserSearch extends Extension class BrowserSearch extends Extension
{ {
@ -44,7 +42,7 @@ class BrowserSearch extends Extension
// And now to send it to the browser // And now to send it to the browser
$page->set_mode(PageMode::DATA); $page->set_mode(PageMode::DATA);
$page->set_mime(MimeType::XML); $page->set_type(MIME_TYPE_XML);
$page->set_data($xml); $page->set_data($xml);
} elseif ($event->page_matches("browser_search")) { } elseif ($event->page_matches("browser_search")) {
$suggestions = $config->get_string("search_suggestions_results_order"); $suggestions = $config->get_string("search_suggestions_results_order");
@ -80,7 +78,8 @@ class BrowserSearch extends Extension
$sort_by['Tag Count'] = 't'; $sort_by['Tag Count'] = 't';
$sort_by['Disabled'] = 'n'; $sort_by['Disabled'] = 'n';
$sb = $event->panel->create_new_block("Browser Search"); $sb = new SetupBlock("Browser Search");
$sb->add_choice_option("search_suggestions_results_order", $sort_by, "Sort the suggestions by:"); $sb->add_choice_option("search_suggestions_results_order", $sort_by, "Sort the suggestions by:");
$event->panel->add_block($sb);
} }
} }

View File

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

View File

@ -1,15 +1,13 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BulkActionsInfo extends ExtensionInfo class BulkActionsInfo extends ExtensionInfo
{ {
public const KEY = "bulk_actions"; public const KEY = "bulk_actions";
public string $key = self::KEY; public $key = self::KEY;
public string $name = "Bulk Actions"; public $name = "Bulk Actions";
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"]; public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public string $license = self::LICENSE_WTFPL; public $license = self::LICENSE_WTFPL;
public string $description = "Provides query and selection-based bulk action support"; public $description = "Provides query and selection-based bulk action support";
public ?string $documentation = "Provides bulk action section in list view. Allows performing actions against a set of posts based on query or manual selection. Based on Mass Tagger by Christian Walde <walde.christian@googlemail.com>, contributions by Shish and Agasa."; public $documentation = "Provides bulk action section in list view. Allows performing actions against a set of images based on query or manual selection. Based on Mass Tagger by Christian Walde <walde.christian@googlemail.com>, contributions by Shish and Agasa.";
} }

View File

@ -1,14 +1,14 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BulkActionException extends SCoreException class BulkActionException extends SCoreException
{ {
} }
class BulkActionBlockBuildingEvent extends Event class BulkActionBlockBuildingEvent extends Event
{ {
public array $actions = []; /** @var array */
public array $search_terms = []; public $actions = [];
public $search_terms = [];
public function add_action(String $action, string $button_text, string $access_key = null, String $confirmation_message = "", String $block = "", int $position = 40) public function add_action(String $action, string $button_text, string $access_key = null, String $confirmation_message = "", String $block = "", int $position = 40)
{ {
@ -38,9 +38,12 @@ class BulkActionBlockBuildingEvent extends Event
class BulkActionEvent extends Event class BulkActionEvent extends Event
{ {
public string $action; /** @var string */
public Generator $items; public $action;
public bool $redirect = true; /** @var array */
public $items;
/** @var bool */
public $redirect = true;
public function __construct(String $action, Generator $items) public function __construct(String $action, Generator $items)
{ {
@ -53,7 +56,7 @@ class BulkActionEvent extends Event
class BulkActions extends Extension class BulkActions extends Extension
{ {
/** @var BulkActionsTheme */ /** @var BulkActionsTheme */
protected ?Themelet $theme; protected $theme;
public function onPostListBuilding(PostListBuildingEvent $event) public function onPostListBuilding(PostListBuildingEvent $event)
{ {
@ -124,8 +127,8 @@ class BulkActions extends Extension
switch ($event->action) { switch ($event->action) {
case "bulk_delete": case "bulk_delete":
if ($user->can(Permissions::DELETE_IMAGE)) { if ($user->can(Permissions::DELETE_IMAGE)) {
$i = $this->delete_posts($event->items); $i = $this->delete_items($event->items);
$page->flash("Deleted $i[0] items, totaling ".human_filesize($i[1])); $page->flash("Deleted $i items");
} }
break; break;
case "bulk_tag": case "bulk_tag":
@ -190,14 +193,14 @@ class BulkActions extends Extension
if (is_iterable($items)) { if (is_iterable($items)) {
send_event($bae); send_event($bae);
} }
if ($bae->redirect) {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(referer_or(make_link()));
}
} catch (BulkActionException $e) { } catch (BulkActionException $e) {
log_error(BulkActionsInfo::KEY, $e->getMessage(), $e->getMessage()); log_error(BulkActionsInfo::KEY, $e->getMessage(), $e->getMessage());
} }
if ($bae->redirect) {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(referer_or(make_link()));
}
} }
} }
@ -224,27 +227,25 @@ class BulkActions extends Extension
return $a["position"] - $b["position"]; return $a["position"] - $b["position"];
} }
private function delete_posts(iterable $posts): array private function delete_items(iterable $items): int
{ {
global $page; global $page;
$total = 0; $total = 0;
$size = 0; foreach ($items as $image) {
foreach ($posts as $post) {
try { try {
if (class_exists("ImageBan") && isset($_POST['bulk_ban_reason'])) { if (class_exists("ImageBan") && isset($_POST['bulk_ban_reason'])) {
$reason = $_POST['bulk_ban_reason']; $reason = $_POST['bulk_ban_reason'];
if ($reason) { if ($reason) {
send_event(new AddImageHashBanEvent($post->hash, $reason)); send_event(new AddImageHashBanEvent($image->hash, $reason));
} }
} }
send_event(new ImageDeletionEvent($post)); send_event(new ImageDeletionEvent($image));
$total++; $total++;
$size += $post->filesize;
} catch (Exception $e) { } catch (Exception $e) {
$page->flash("Error while removing {$post->id}: " . $e->getMessage()); $page->flash("Error while removing {$image->id}: " . $e->getMessage());
} }
} }
return [$total, $size]; return $total;
} }
private function tag_items(iterable $items, string $tags, bool $replace): int private function tag_items(iterable $items, string $tags, bool $replace): int
@ -254,7 +255,7 @@ class BulkActions extends Extension
$pos_tag_array = []; $pos_tag_array = [];
$neg_tag_array = []; $neg_tag_array = [];
foreach ($tags as $new_tag) { foreach ($tags as $new_tag) {
if (str_starts_with($new_tag, '-')) { if (strpos($new_tag, '-') === 0) {
$neg_tag_array[] = substr($new_tag, 1); $neg_tag_array[] = substr($new_tag, 1);
} else { } else {
$pos_tag_array[] = $new_tag; $pos_tag_array[] = $new_tag;

View File

@ -8,13 +8,13 @@ function validate_selections(form, confirmationMessage) {
var queryOnly = false; var queryOnly = false;
if(bulk_selector_active) { if(bulk_selector_active) {
var data = get_selected_items(); var data = get_selected_items();
if(data.length===0) { if(data.length==0) {
return false; return false;
} }
} else { } else {
var query = $(form).find('input[name="bulk_query"]').val(); var query = $(form).find('input[name="bulk_query"]').val();
if (query == null || query === "") { if (query == null || query == "") {
return false; return false;
} else { } else {
queryOnly = true; queryOnly = true;
@ -22,7 +22,7 @@ function validate_selections(form, confirmationMessage) {
} }
if(confirmationMessage!=null&&confirmationMessage!=="") { if(confirmationMessage!=null&&confirmationMessage!="") {
return confirm(confirmationMessage); return confirm(confirmationMessage);
} else if(queryOnly) { } else if(queryOnly) {
var action = $(form).find('input[name="submit_button"]').val(); var action = $(form).find('input[name="submit_button"]').val();
@ -59,7 +59,7 @@ function deactivate_bulk_selector() {
function get_selected_items() { function get_selected_items() {
var data = $('#bulk_selected_ids').val(); var data = $('#bulk_selected_ids').val();
if(data===""||data==null) { if(data==""||data==null) {
data = []; data = [];
} else { } else {
data = JSON.parse(data); data = JSON.parse(data);
@ -97,11 +97,11 @@ function toggle_selection( id ) {
var data = get_selected_items(); var data = get_selected_items();
if(data.includes(id)) { if(data.includes(id)) {
data.splice(data.indexOf(id),1); data.splice(data.indexOf(id),1);
set_selected_items(data); set_selected_items(data);
return false; return false;
} else { } else {
data.push(id); data.push(id);
set_selected_items(data); set_selected_items(data);
return true; return true;
} }
} }
@ -116,7 +116,7 @@ function select_all() {
items.push(id); items.push(id);
} }
); );
set_selected_items(items); set_selected_items(items);
} }
function select_invert() { function select_invert() {
@ -131,11 +131,11 @@ function select_invert() {
} }
} }
); );
set_selected_items(items); set_selected_items(items);
} }
function select_none() { function select_none() {
set_selected_items([]); set_selected_items([]);
} }
function select_range(start, end) { function select_range(start, end) {
@ -145,7 +145,7 @@ function select_range(start, end) {
function ( index, block ) { function ( index, block ) {
block = $(block); block = $(block);
var id = block.data("post-id"); var id = block.data("post-id");
if(id===start) if(id==start)
selecting = true; selecting = true;
if(selecting) { if(selecting) {
@ -153,7 +153,7 @@ function select_range(start, end) {
data.push(id); data.push(id);
} }
if(id===end) { if(id==end) {
selecting = false; selecting = false;
} }
} }
@ -163,14 +163,14 @@ function select_range(start, end) {
var last_clicked_item; var last_clicked_item;
function add_selector_button($block) { function add_selector_button($block) {
var c = function(e) { var c = function(e) {
if(!bulk_selector_active) if(!bulk_selector_active)
return true; return true;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
var id = $block.data("post-id"); var id = $block.data("post-id");
if(e.shiftKey) { if(e.shiftKey) {
if(last_clicked_item<id) { if(last_clicked_item<id) {
@ -182,7 +182,7 @@ function add_selector_button($block) {
last_clicked_item = id; last_clicked_item = id;
toggle_selection(id); toggle_selection(id);
} }
return false; return false;
}; };
$block.find("A").click(c); $block.find("A").click(c);

View File

@ -1,6 +1,4 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BulkActionsTheme extends Themelet class BulkActionsTheme extends Themelet
{ {
@ -10,7 +8,7 @@ class BulkActionsTheme extends Themelet
<input id='bulk_selector_activate' type='button' onclick='activate_bulk_selector();' value='Activate (M)anual Select' accesskey='m'/> <input id='bulk_selector_activate' type='button' onclick='activate_bulk_selector();' value='Activate (M)anual Select' accesskey='m'/>
<div id='bulk_selector_controls' style='display: none;'> <div id='bulk_selector_controls' style='display: none;'>
<input id='bulk_selector_deactivate' type='button' onclick='deactivate_bulk_selector();' value='Deactivate (M)anual Select' accesskey='m'/> <input id='bulk_selector_deactivate' type='button' onclick='deactivate_bulk_selector();' value='Deactivate (M)anual Select' accesskey='m'/>
Click on posts to mark them. Click on images to mark them.
<br /> <br />
<table><tr><td> <table><tr><td>
<input id='bulk_selector_select_all' type='button' <input id='bulk_selector_select_all' type='button'
@ -47,7 +45,7 @@ class BulkActionsTheme extends Themelet
$page->add_block($block); $page->add_block($block);
} }
public function render_ban_reason_input(): string public function render_ban_reason_input()
{ {
if (class_exists("ImageBan")) { if (class_exists("ImageBan")) {
return "<input type='text' name='bulk_ban_reason' placeholder='Ban reason (leave blank to not ban)' />"; return "<input type='text' name='bulk_ban_reason' placeholder='Ban reason (leave blank to not ban)' />";
@ -56,13 +54,13 @@ class BulkActionsTheme extends Themelet
} }
} }
public function render_tag_input(): string public function render_tag_input()
{ {
return "<label><input type='checkbox' style='width:13px;' name='bulk_tags_replace' value='true'/>Replace tags</label>" . return "<label><input type='checkbox' style='width:13px;' name='bulk_tags_replace' value='true'/>Replace tags</label>" .
"<input type='text' name='bulk_tags' required='required' placeholder='Enter tags here' />"; "<input type='text' name='bulk_tags' required='required' placeholder='Enter tags here' />";
} }
public function render_source_input(): string public function render_source_input()
{ {
return "<input type='text' name='bulk_source' required='required' placeholder='Enter source here' />"; return "<input type='text' name='bulk_source' required='required' placeholder='Enter source here' />";
} }

View File

@ -1,18 +1,16 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BulkAddInfo extends ExtensionInfo class BulkAddInfo extends ExtensionInfo
{ {
public const KEY = "bulk_add"; public const KEY = "bulk_add";
public string $key = self::KEY; public $key = self::KEY;
public string $name = "Bulk Add"; public $name = "Bulk Add";
public string $url = self::SHIMMIE_URL; public $url = self::SHIMMIE_URL;
public array $authors = self::SHISH_AUTHOR; public $authors = self::SHISH_AUTHOR;
public string $license = self::LICENSE_GPLV2; public $license = self::LICENSE_GPLV2;
public string $description = "Bulk add server-side images"; public $description = "Bulk add server-side images";
public ?string $documentation = public $documentation =
"Upload the images into a new directory via ftp or similar, go to "Upload the images into a new directory via ftp or similar, go to
shimmie's admin page and put that directory in the bulk add box. shimmie's admin page and put that directory in the bulk add box.
If there are subdirectories, they get used as tags (eg if you If there are subdirectories, they get used as tags (eg if you

View File

@ -1,11 +1,9 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BulkAddEvent extends Event class BulkAddEvent extends Event
{ {
public string $dir; public $dir;
public array $results; public $results;
public function __construct(string $dir) public function __construct(string $dir)
{ {
@ -18,7 +16,7 @@ class BulkAddEvent extends Event
class BulkAdd extends Extension class BulkAdd extends Extension
{ {
/** @var BulkAddTheme */ /** @var BulkAddTheme */
protected ?Themelet $theme; protected $theme;
public function onPageRequest(PageRequestEvent $event) public function onPageRequest(PageRequestEvent $event)
{ {

View File

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

View File

@ -1,10 +1,8 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BulkAddTheme extends Themelet class BulkAddTheme extends Themelet
{ {
private array $messages = []; private $messages = [];
/* /*
* Show a standard page for results to be put into * Show a standard page for results to be put into

View File

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

View File

@ -1,11 +1,9 @@
<?php <?php declare(strict_types=1);
declare(strict_types=1);
class BulkAddCSV extends Extension class BulkAddCSV extends Extension
{ {
/** @var BulkAddCSVTheme */ /** @var BulkAddCSVTheme */
protected ?Themelet $theme; protected $theme;
public function onPageRequest(PageRequestEvent $event) public function onPageRequest(PageRequestEvent $event)
{ {

View File

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

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