Merge and integrate upstream fixes

This commit is contained in:
MichaelYick 2023-09-10 09:16:26 -05:00
parent f2ee60fac5
commit 1257d87e51
157 changed files with 1684 additions and 2388 deletions

26
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Master to Main
on:
workflow_run:
workflows: Tests
branches: master
types: completed
workflow_dispatch:
jobs:
merge-master-to-main:
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set Git config
run: |
git config --local user.email "actions@github.com"
git config --local user.name "Github Actions"
- name: Merge master back to dev
run: |
git fetch --unshallow
git checkout main
git pull
git merge --ff origin/master -m "Auto-merge master to main"
git push

View File

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

View File

@ -1,20 +0,0 @@
imports:
- javascript
- php
filter:
excluded_paths: [ext/*/lib/*,ext/tagger/script.js,tests/*]
build:
image: default-bionic
nodes:
analysis:
tests:
before:
- mkdir -p data/config
- cp tests/defines.php data/config/shimmie.conf.php
override:
- php-scrutinizer-run
tools:
external_code_coverage: true

View File

@ -1,10 +1,20 @@
ARG PHP_VERSION=8.2
# Install base packages which all stages (build, test, run) need
FROM debian:bookworm AS base
RUN apt update && apt upgrade -y && apt install -y \
php${PHP_VERSION}-cli php${PHP_VERSION}-gd php${PHP_VERSION}-zip php${PHP_VERSION}-xml php${PHP_VERSION}-mbstring \
php${PHP_VERSION}-pgsql php${PHP_VERSION}-mysql php${PHP_VERSION}-sqlite3 \
gosu curl imagemagick ffmpeg zip unzip && \
rm -rf /var/lib/apt/lists/*
# Composer has 100MB of dependencies, and we only need that during build and test
FROM base AS composer
RUN apt update && apt upgrade -y && apt install -y composer php${PHP_VERSION}-xdebug && rm -rf /var/lib/apt/lists/*
# "Build" shimmie (composer install - done in its own stage so that we don't
# need to include all the composer fluff in the final image)
FROM debian:unstable AS app
RUN apt update && apt upgrade -y
RUN apt install -y composer php${PHP_VERSION}-gd php${PHP_VERSION}-xml php${PHP_VERSION}-sqlite3 php${PHP_VERSION}-xdebug imagemagick
FROM composer AS app
COPY composer.json composer.lock /app/
WORKDIR /app
RUN composer install --no-dev
@ -13,9 +23,7 @@ COPY . /app/
# Tests in their own image. Really we should inherit from app and then
# `composer install` phpunit on top of that; but for some reason
# `composer install --no-dev && composer install` doesn't install dev
FROM debian:unstable AS tests
RUN apt update && apt upgrade -y
RUN apt install -y composer php${PHP_VERSION}-gd php${PHP_VERSION}-xml php${PHP_VERSION}-sqlite3 php${PHP_VERSION}-xdebug imagemagick
FROM composer AS tests
COPY composer.json composer.lock /app/
WORKDIR /app
RUN composer install
@ -25,31 +33,17 @@ RUN [ $RUN_TESTS = false ] || (\
echo '=== Installing ===' && mkdir -p data/config && INSTALL_DSN="sqlite:data/shimmie.sqlite" php index.php && \
echo '=== Smoke Test ===' && php index.php get-page /post/list && \
echo '=== Unit Tests ===' && ./vendor/bin/phpunit --configuration tests/phpunit.xml && \
echo '=== Coverage ===' && ./vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-text && \
echo '=== Coverage ===' && XDEBUG_MODE=coverage ./vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-text && \
echo '=== Cleaning ===' && rm -rf data)
# Build su-exec so that our final image can be nicer
FROM debian:unstable AS suexec
RUN apt update && apt upgrade -y
RUN apt install -y --no-install-recommends gcc libc-dev curl
RUN curl -k -o /usr/local/bin/su-exec.c https://raw.githubusercontent.com/ncopa/su-exec/master/su-exec.c; \
gcc -Wall /usr/local/bin/su-exec.c -o/usr/local/bin/su-exec; \
chown root:root /usr/local/bin/su-exec; \
chmod 0755 /usr/local/bin/su-exec;
# Actually run shimmie
FROM debian:unstable
FROM base
EXPOSE 8000
HEALTHCHECK --interval=1m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1
ENV UID=1000 \
GID=1000
RUN apt update && apt upgrade -y && apt install -y \
php${PHP_VERSION}-cli php${PHP_VERSION}-gd php${PHP_VERSION}-zip php${PHP_VERSION}-xml php${PHP_VERSION}-mbstring \
php${PHP_VERSION}-pgsql php${PHP_VERSION}-mysql php${PHP_VERSION}-sqlite3 \
curl imagemagick ffmpeg zip unzip && \
rm -rf /var/lib/apt/lists/*
GID=1000 \
UPLOAD_MAX_FILESIZE=50M
COPY --from=app /app /app
COPY --from=suexec /usr/local/bin/su-exec /usr/local/bin/su-exec
WORKDIR /app
CMD ["/bin/sh", "/app/tests/docker-init.sh"]

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Shimmie2;
use MicroHTML\HTMLElement;
require_once "core/event.php";
enum PageMode: string
@ -80,6 +82,12 @@ class BasePage
*/
public function set_filename(string $filename, string $disposition = "attachment"): void
{
$max_len = 250;
if(strlen($filename) > $max_len) {
// remove extension, truncate filename, apply extension
$ext = pathinfo($filename, PATHINFO_EXTENSION);
$filename = substr($filename, 0, $max_len - strlen($ext) - 1) . '.' . $ext;
}
$this->filename = $filename;
$this->disposition = $disposition;
}
@ -568,7 +576,7 @@ EOD;
<a href=\"https://code.shishnet.org/shimmie2/\">Shimmie</a> &copy;
<a href=\"https://www.shishnet.org/\">Shish</a> &amp;
<a href=\"https://github.com/shish/shimmie2/graphs/contributors\">The Team</a>
2007-2020,
2007-2023,
based on the Danbooru concept.
$debug
$contact
@ -598,7 +606,7 @@ class PageSubNavBuildingEvent extends Event
$this->parent= $parent;
}
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|HTMLElement $desc, ?bool $active = null, int $order = 50)
{
$this->links[] = new NavLink($name, $link, $desc, $active, $order);
}
@ -608,11 +616,11 @@ class NavLink
{
public string $name;
public Link $link;
public string $description;
public string|HTMLElement $description;
public int $order;
public bool $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|HTMLElement $description, ?bool $active = null, int $order = 50)
{
global $config;

View File

@ -4,6 +4,10 @@ declare(strict_types=1);
namespace Shimmie2;
use MicroHTML\HTMLElement;
use function MicroHTML\{A,B,BR,IMG,OPTION,SELECT,emptyHTML};
/**
* Class BaseThemelet
*
@ -46,15 +50,15 @@ class BaseThemelet
* Generic thumbnail code; returns HTML rather than adding
* a block since thumbs tend to go inside blocks...
*/
public function build_thumb_html(Image $image): string
public function build_thumb_html(Image $image): HTMLElement
{
global $config;
$i_id = (int) $image->id;
$h_view_link = make_link('post/view/'.$i_id);
$h_thumb_link = $image->get_thumb_link();
$h_tip = html_escape($image->get_tooltip());
$h_tags = html_escape(strtolower($image->get_tag_list()));
$id = $image->id;
$view_link = make_link('post/view/'.$id);
$thumb_link = $image->get_thumb_link();
$tip = $image->get_tooltip();
$tags = strtolower($image->get_tag_list());
// TODO: Set up a function for fetching what kind of files are currently thumbnailable
$mimeArr = array_flip([MimeType::MP3]); //List of thumbless filetypes
@ -75,9 +79,27 @@ 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'>".
"<img id='thumb_$i_id' title='$h_tip' alt='$h_tip' height='{$tsize[1]}' width='{$tsize[0]}' src='$h_thumb_link'>".
"</a>\n";
return A(
[
"href"=>$view_link,
"class"=>"thumb shm-thumb shm-thumb-link $custom_classes",
"data-tags"=>$tags,
"data-height"=>$image->height,
"data-width"=>$image->width,
"data-mime"=>$image->get_mime(),
"data-post-id"=>$id,
],
IMG(
[
"id"=>"thumb_$id",
"title"=>$tip,
"alt"=>$tip,
"height"=>$tsize[1],
"width"=>$tsize[0],
"src"=>$thumb_link,
]
)
);
}
public function display_paginator(Page $page, string $base, ?string $query, int $page_number, int $total_pages, bool $show_random = false)
@ -99,26 +121,34 @@ class BaseThemelet
$page->add_html_header("<link rel='last' href='".make_http(make_link($base.'/'.$total_pages, $query))."'>");
}
private function gen_page_link(string $base_url, ?string $query, int $page, string $name): string
private function gen_page_link(string $base_url, ?string $query, int $page, string $name): HTMLElement
{
$link = make_link($base_url.'/'.$page, $query);
return '<a href="'.$link.'">'.$name.'</a>';
return A(["href"=>make_link($base_url.'/'.$page, $query)], $name);
}
private function gen_page_link_block(string $base_url, ?string $query, int $page, int $current_page, string $name): string
private function gen_page_link_block(string $base_url, ?string $query, int $page, int $current_page, string $name): HTMLElement
{
$paginator = "";
$paginator = $this->gen_page_link($base_url, $query, $page, $name);
if ($page == $current_page) {
$paginator .= "<b>";
}
$paginator .= $this->gen_page_link($base_url, $query, $page, $name);
if ($page == $current_page) {
$paginator .= "</b>";
$paginator = B($paginator);
}
return $paginator;
}
private function build_paginator(int $current_page, int $total_pages, string $base_url, ?string $query, bool $show_random): string
protected function implode(string|HTMLElement $glue, array $pieces): HTMLElement
{
$out = emptyHTML();
$n = 0;
foreach ($pieces as $piece) {
if ($n++ > 0) {
$out->appendChild($glue);
}
$out->appendChild($piece);
}
return $out;
}
private function build_paginator(int $current_page, int $total_pages, string $base_url, ?string $query, bool $show_random): HTMLElement
{
$next = $current_page + 1;
$prev = $current_page - 1;
@ -145,9 +175,20 @@ class BaseThemelet
foreach (range($start, $end) as $i) {
$pages[] = $this->gen_page_link_block($base_url, $query, $i, $current_page, (string)$i);
}
$pages_html = implode(" | ", $pages);
$pages_html = $this->implode(" | ", $pages);
return $first_html.' | '.$prev_html.' | '.$random_html.' | '.$next_html.' | '.$last_html
.'<br>&lt;&lt; '.$pages_html.' &gt;&gt;';
return emptyHTML(
$this->implode(" | ", [
$first_html,
$prev_html,
$random_html,
$next_html,
$last_html,
]),
BR(),
'<< ',
$pages_html,
' >>'
);
}
}

View File

@ -53,7 +53,7 @@ class Block
$this->position = $position;
if (is_null($id)) {
$id = (empty($header) ? md5($body ?? '') : $header) . $section;
$id = (empty($header) ? md5($this->body ?? '') : $header) . $section;
}
$str_id = preg_replace('/[^\w-]/', '', str_replace(' ', '_', $id));
assert(is_string($str_id));

View File

@ -121,7 +121,8 @@ function loadCache(?string $dsn): CacheInterface
], ['prefix' => 'shm:']);
$c = new \Naroga\RedisCache\Redis($redis);
}
} else {
}
if(is_null($c)) {
$c = new \Sabre\Cache\Memory();
}
global $_tracer;

View File

@ -43,12 +43,15 @@ class Database
$this->dsn = $dsn;
}
private function connect_db(): void
private function get_db(): PDO
{
$this->db = new PDO($this->dsn);
$this->connect_engine();
$this->get_engine()->init($this->db);
$this->begin_transaction();
if(is_null($this->db)) {
$this->db = new PDO($this->dsn);
$this->connect_engine();
$this->get_engine()->init($this->db);
$this->begin_transaction();
}
return $this->db;
}
private function connect_engine(): void
@ -76,7 +79,7 @@ class Database
public function begin_transaction(): void
{
if ($this->is_transaction_open() === false) {
$this->db->beginTransaction();
$this->get_db()->beginTransaction();
}
}
@ -88,7 +91,7 @@ class Database
public function commit(): bool
{
if ($this->is_transaction_open()) {
return $this->db->commit();
return $this->get_db()->commit();
} else {
throw new SCoreException("Unable to call commit() as there is no transaction currently open.");
}
@ -97,7 +100,7 @@ class Database
public function rollback(): bool
{
if ($this->is_transaction_open()) {
return $this->db->rollback();
return $this->get_db()->rollback();
} else {
throw new SCoreException("Unable to call rollback() as there is no transaction currently open.");
}
@ -123,7 +126,7 @@ class Database
public function get_version(): string
{
return $this->get_engine()->get_version($this->db);
return $this->get_engine()->get_version($this->get_db());
}
private function count_time(string $method, float $start, string $query, ?array $args): void
@ -144,21 +147,18 @@ class Database
public function set_timeout(?int $time): void
{
$this->get_engine()->set_timeout($this->db, $time);
$this->get_engine()->set_timeout($this->get_db(), $time);
}
public function notify(string $channel, ?string $data=null): void
{
$this->get_engine()->notify($this->db, $channel, $data);
$this->get_engine()->notify($this->get_db(), $channel, $data);
}
public function execute(string $query, array $args = []): PDOStatement
public function _execute(string $query, array $args = []): PDOStatement
{
try {
if (is_null($this->db)) {
$this->connect_db();
}
$ret = $this->db->execute(
$ret = $this->get_db()->execute(
"-- " . str_replace("%2F", "/", urlencode($_GET['q'] ?? '')). "\n" .
$query,
$args
@ -173,13 +173,24 @@ class Database
}
}
/**
* Execute an SQL query with no return
*/
public function execute(string $query, array $args = []): PDOStatement
{
$_start = ftime();
$st = $this->_execute($query, $args);
$this->count_time("execute", $_start, $query, $args);
return $st;
}
/**
* Execute an SQL query and return a 2D array.
*/
public function get_all(string $query, array $args = []): array
{
$_start = ftime();
$data = $this->execute($query, $args)->fetchAll();
$data = $this->_execute($query, $args)->fetchAll();
$this->count_time("get_all", $_start, $query, $args);
return $data;
}
@ -190,7 +201,7 @@ class Database
public function get_all_iterable(string $query, array $args = []): PDOStatement
{
$_start = ftime();
$data = $this->execute($query, $args);
$data = $this->_execute($query, $args);
$this->count_time("get_all_iterable", $_start, $query, $args);
return $data;
}
@ -201,7 +212,7 @@ class Database
public function get_row(string $query, array $args = []): ?array
{
$_start = ftime();
$row = $this->execute($query, $args)->fetch();
$row = $this->_execute($query, $args)->fetch();
$this->count_time("get_row", $_start, $query, $args);
return $row ? $row : null;
}
@ -212,7 +223,7 @@ class Database
public function get_col(string $query, array $args = []): array
{
$_start = ftime();
$res = $this->execute($query, $args)->fetchAll(PDO::FETCH_COLUMN);
$res = $this->_execute($query, $args)->fetchAll(PDO::FETCH_COLUMN);
$this->count_time("get_col", $_start, $query, $args);
return $res;
}
@ -223,7 +234,7 @@ class Database
public function get_col_iterable(string $query, array $args = []): \Generator
{
$_start = ftime();
$stmt = $this->execute($query, $args);
$stmt = $this->_execute($query, $args);
$this->count_time("get_col_iterable", $_start, $query, $args);
foreach ($stmt as $row) {
yield $row[0];
@ -236,7 +247,7 @@ class Database
public function get_pairs(string $query, array $args = []): array
{
$_start = ftime();
$res = $this->execute($query, $args)->fetchAll(PDO::FETCH_KEY_PAIR);
$res = $this->_execute($query, $args)->fetchAll(PDO::FETCH_KEY_PAIR);
$this->count_time("get_pairs", $_start, $query, $args);
return $res;
}
@ -248,7 +259,7 @@ class Database
public function get_pairs_iterable(string $query, array $args = []): \Generator
{
$_start = ftime();
$stmt = $this->execute($query, $args);
$stmt = $this->_execute($query, $args);
$this->count_time("get_pairs_iterable", $_start, $query, $args);
foreach ($stmt as $row) {
yield $row[0] => $row[1];
@ -261,7 +272,7 @@ class Database
public function get_one(string $query, array $args = [])
{
$_start = ftime();
$row = $this->execute($query, $args)->fetch();
$row = $this->_execute($query, $args)->fetch();
$this->count_time("get_one", $_start, $query, $args);
return $row ? $row[0] : null;
}
@ -272,7 +283,7 @@ class Database
public function exists(string $query, array $args = []): bool
{
$_start = ftime();
$row = $this->execute($query, $args)->fetch();
$row = $this->_execute($query, $args)->fetch();
$this->count_time("exists", $_start, $query, $args);
if ($row==null) {
return false;
@ -286,9 +297,9 @@ class Database
public function get_last_insert_id(string $seq): int
{
if ($this->get_engine()->id == DatabaseDriverID::PGSQL) {
$id = $this->db->lastInsertId($seq);
$id = $this->get_db()->lastInsertId($seq);
} else {
$id = $this->db->lastInsertId();
$id = $this->get_db()->lastInsertId();
}
assert(is_numeric($id));
return (int)$id;
@ -313,10 +324,6 @@ class Database
*/
public function count_tables(): int
{
if (is_null($this->db) || is_null($this->engine)) {
$this->connect_db();
}
if ($this->get_engine()->id === DatabaseDriverID::MYSQL) {
return count(
$this->get_all("SHOW TABLES")
@ -336,10 +343,7 @@ class Database
public function raw_db(): PDO
{
if (is_null($this->db)) {
$this->connect_db();
}
return $this->db;
return $this->get_db();
}
public function standardise_boolean(string $table, string $column, bool $include_postgres=false): void

View File

@ -19,7 +19,7 @@ namespace Shimmie2;
abstract class Extension
{
public string $key;
protected ?Themelet $theme;
protected Themelet $theme;
public ExtensionInfo $info;
private static array $enabled_extensions = [];
@ -35,7 +35,7 @@ abstract class Extension
/**
* Find the theme object for a given extension.
*/
private function get_theme_object(string $base): ?Themelet
private function get_theme_object(string $base): Themelet
{
$base = str_replace("Shimmie2\\", "", $base);
$custom = "Shimmie2\Custom{$base}Theme";
@ -46,7 +46,7 @@ abstract class Extension
} elseif (class_exists($normal)) {
return new $normal();
} else {
return null;
return new Themelet();
}
}

View File

@ -149,7 +149,7 @@ class Image
if ($start < 0) {
$start = 0;
}
if ($limit!=null && $limit < 1) {
if ($limit !== null && $limit < 1) {
$limit = 1;
}
@ -166,11 +166,11 @@ class Image
/**
* Search for an array of images
*
* @param String[] $tags
* @param string[] $tags
* @return Image[]
*/
#[Query(name: "posts", type: "[Post!]!", args: ["tags" => "[string!]"])]
public static function find_images(?int $offset = 0, ?int $limit = null, array $tags=[]): array
public static function find_images(int $offset = 0, ?int $limit = null, array $tags=[]): array
{
$result = self::find_images_internal($offset, $limit, $tags);
@ -586,7 +586,7 @@ class Image
{
$this->mime = $mime;
$ext = FileExtension::get_for_mime($this->get_mime());
assert($ext != null);
assert($ext !== null);
$this->ext = $ext;
}
@ -640,27 +640,15 @@ class Image
public function delete_tags_from_image(): void
{
global $database;
if ($database->get_driver_id() == DatabaseDriverID::MYSQL) {
//mysql < 5.6 has terrible subquery optimization, using EXISTS / JOIN fixes this
$database->execute(
"
UPDATE tags t
INNER JOIN image_tags it ON t.id = it.tag_id
SET count = count - 1
WHERE it.image_id = :id",
["id"=>$this->id]
);
} else {
$database->execute("
UPDATE tags
SET count = count - 1
WHERE id IN (
SELECT tag_id
FROM image_tags
WHERE image_id = :id
)
", ["id"=>$this->id]);
}
$database->execute("
UPDATE tags
SET count = count - 1
WHERE id IN (
SELECT tag_id
FROM image_tags
WHERE image_id = :id
)
", ["id"=>$this->id]);
$database->execute("
DELETE
FROM image_tags
@ -695,55 +683,23 @@ class Image
throw new SCoreException('Tried to set zero tags');
}
if (Tag::implode($tags) != $this->get_tag_list()) {
if (strtolower(Tag::implode($tags)) != strtolower($this->get_tag_list())) {
// delete old
$this->delete_tags_from_image();
$written_tags = [];
// insert each new tags
foreach ($tags as $tag) {
$id = $database->get_one(
"
SELECT id
FROM tags
WHERE LOWER(tag) = LOWER(:tag)
",
["tag"=>$tag]
);
if (empty($id)) {
// a new tag
$database->execute(
"INSERT INTO tags(tag) VALUES (:tag)",
["tag"=>$tag]
);
$database->execute(
"INSERT INTO image_tags(image_id, tag_id)
VALUES(:id, (SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)))",
["id"=>$this->id, "tag"=>$tag]
);
} else {
// check if tag has already been written
if (in_array($id, $written_tags)) {
continue;
}
$database->execute("
INSERT INTO image_tags(image_id, tag_id)
VALUES(:iid, :tid)
", ["iid"=>$this->id, "tid"=>$id]);
$written_tags[] = $id;
}
$database->execute(
"
UPDATE tags
SET count = count + 1
WHERE LOWER(tag) = LOWER(:tag)
",
["tag"=>$tag]
);
}
$ids = array_map(fn ($tag) => Tag::get_or_create_id($tag), $tags);
$values = implode(", ", array_map(fn ($id) => "({$this->id}, $id)", $ids));
$database->execute("INSERT INTO image_tags(image_id, tag_id) VALUES $values");
$database->execute("
UPDATE tags
SET count = count + 1
WHERE id IN (
SELECT tag_id
FROM image_tags
WHERE image_id = :id
)
", ["id"=>$this->id]);
log_info("core_image", "Tags for Post #{$this->id} set to: ".Tag::implode($tags));
$cache->delete("image-{$this->id}-tags");

View File

@ -196,7 +196,7 @@ function redirect_to_next_image(Image $image): void
$target_image = $image->get_next($search_terms);
if ($target_image == null) {
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);

View File

@ -90,12 +90,42 @@ class TagUsage
*/
class Tag
{
private static $tag_id_cache = [];
public static function get_or_create_id(string $tag): int
{
global $database;
// don't cache in unit tests, because the test suite doesn't
// reset static variables but it does reset the database
if (!defined("UNITTEST") && array_key_exists($tag, self::$tag_id_cache)) {
return self::$tag_id_cache[$tag];
}
$id = $database->get_one(
"SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)",
["tag"=>$tag]
);
if (empty($id)) {
// a new tag
$database->execute(
"INSERT INTO tags(tag) VALUES (:tag)",
["tag"=>$tag]
);
$id = $database->get_one(
"SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)",
["tag"=>$tag]
);
}
self::$tag_id_cache[$tag] = $id;
return $id;
}
public static function implode(array $tags): string
{
sort($tags);
$tags = implode(' ', $tags);
return $tags;
sort($tags, SORT_FLAG_CASE|SORT_STRING);
return implode(' ', $tags);
}
/**

153
core/microhtml.php Normal file
View File

@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
use MicroHTML\HTMLElement;
use function MicroHTML\emptyHTML;
use function MicroHTML\A;
use function MicroHTML\FORM;
use function MicroHTML\INPUT;
use function MicroHTML\DIV;
use function MicroHTML\OPTION;
use function MicroHTML\PRE;
use function MicroHTML\P;
use function MicroHTML\SELECT;
use function MicroHTML\TABLE;
use function MicroHTML\THEAD;
use function MicroHTML\TFOOT;
use function MicroHTML\TR;
use function MicroHTML\TH;
use function MicroHTML\TD;
function SHM_FORM(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit="", string $name=""): HTMLElement
{
global $user;
$attrs = [
"action"=>make_link($target),
"method"=>$method
];
if ($form_id) {
$attrs["id"] = $form_id;
}
if ($multipart) {
$attrs["enctype"] = 'multipart/form-data';
}
if ($onsubmit) {
$attrs["onsubmit"] = $onsubmit;
}
if ($name) {
$attrs["name"] = $name;
}
return FORM(
$attrs,
INPUT(["type"=>"hidden", "name"=>"q", "value"=>$target]),
$method == "GET" ? "" : $user->get_auth_microhtml()
);
}
function SHM_SIMPLE_FORM($target, ...$children): HTMLElement
{
$form = SHM_FORM($target);
$form->appendChild(emptyHTML(...$children));
return $form;
}
function SHM_SUBMIT(string $text, array $args=[]): HTMLElement
{
$args["type"] = "submit";
$args["value"] = $text;
return INPUT($args);
}
function SHM_A(string $href, string|HTMLElement $text, string $id="", string $class="", array $args=[]): HTMLElement
{
$args["href"] = make_link($href);
if ($id) {
$args["id"] = $id;
}
if ($class) {
$args["class"] = $class;
}
return A($args, $text);
}
function SHM_COMMAND_EXAMPLE(string $ex, string $desc): HTMLElement
{
return DIV(
["class"=>"command_example"],
PRE($ex),
P($desc)
);
}
function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot): HTMLElement
{
if (is_string($foot)) {
$foot = TFOOT(TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>$foot]))));
}
return SHM_SIMPLE_FORM(
$target,
P(
INPUT(["type"=>'hidden', "name"=>'id', "value"=>$duser->id]),
TABLE(
["class"=>"form"],
THEAD(TR(TH(["colspan"=>"2"], $title))),
$body,
$foot
)
)
);
}
/**
* Generates a <select> element and sets up the given options.
*
* @param string $name The name attribute of <select>.
* @param array $options An array of pairs of parameters for <option> tags. First one is value, second one is text. Example: ('optionA', 'Choose Option A').
* @param array $selected_options The values of options that should be pre-selected.
* @param bool $required Wether the <select> element is required.
* @param bool $multiple Wether the <select> element is multiple-choice.
* @param bool $empty_option Whether the first option should be an empty one.
* @param array $attrs Additional attributes dict for <select>. Example: ["id"=>"some_id", "class"=>"some_class"].
*/
function SHM_SELECT(string $name, array $options, array $selected_options=[], bool $required=false, bool $multiple=false, bool $empty_option=false, array $attrs=[]): HTMLElement
{
if ($required) {
$attrs["required"] = "";
}
if ($multiple) {
if (!str_ends_with($name, "[]")) {
$name = $name . "[]";
}
$attrs["multiple"] = "";
}
$attrs["name"] = $name;
$_options = [];
if ($empty_option) {
$_options[] = OPTION();
}
foreach ($options as $value => $text) {
$_options[] = SHM_OPTION((string)$value, (string)$text, in_array($value, $selected_options));
}
return SELECT($attrs, ...$_options);
}
function SHM_OPTION(string $value, string $text, bool $selected=false): HTMLElement
{
if ($selected) {
return OPTION(["value"=>$value, "selected"=>""], $text);
}
return OPTION(["value"=>$value], $text);
}

View File

@ -12,32 +12,40 @@ use GQLA\Enum;
#[Enum(name: "Permission")]
abstract class Permissions
{
public const CHANGE_SETTING = "change_setting"; # modify web-level settings, eg the config table
public const OVERRIDE_CONFIG = "override_config"; # modify sys-level settings, eg shimmie.conf.php
public const 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
/** modify web-level settings, eg the config table */
public const CHANGE_SETTING = "change_setting";
/** modify sys-level settings, eg shimmie.conf.php */
public const OVERRIDE_CONFIG = "override_config";
/** modify own user-level settings */
public const CHANGE_USER_SETTING = "change_user_setting";
public const CHANGE_OTHER_USER_SETTING = "change_other_user_setting";
public const BIG_SEARCH = "big_search"; # search for more than 3 tags at once (speed mode only)
/** search for more than 3 tags at once (only applies if SPEED_HAX is active) */
public const BIG_SEARCH = "big_search";
/** enable or disable extensions */
public const MANAGE_EXTENSION_LIST = "manage_extension_list";
public const MANAGE_ALIAS_LIST = "manage_alias_list";
public const MANAGE_AUTO_TAG = "manage_auto_tag";
public const MASS_TAG_EDIT = "mass_tag_edit";
public const VIEW_IP = "view_ip"; # view IP addresses associated with things
/** View which IP address posted a comment / image / etc */
public const VIEW_IP = "view_ip";
public const BAN_IP = "ban_ip";
public const CREATE_USER = "create_user";
public const CREATE_OTHER_USER = "create_other_user";
public const EDIT_USER_NAME = "edit_user_name";
public const EDIT_USER_PASSWORD = "edit_user_password";
public const EDIT_USER_INFO = "edit_user_info"; # email address, etc
/** Edit metadata about a user (eg email address) */
public const EDIT_USER_INFO = "edit_user_info";
public const EDIT_USER_CLASS = "edit_user_class";
public const DELETE_USER = "delete_user";
public const CREATE_COMMENT = "create_comment";
public const DELETE_COMMENT = "delete_comment";
public const BYPASS_COMMENT_CHECKS = "bypass_comment_checks"; # spam etc
/** Allow a user to make comments even if the spam-detector disapproves */
public const BYPASS_COMMENT_CHECKS = "bypass_comment_checks";
public const REPLACE_IMAGE = "replace_image";
public const CREATE_IMAGE = "create_image";
@ -59,7 +67,8 @@ abstract class Permissions
public const VIEW_REGISTRATIONS = "view_registrations";
public const CREATE_IMAGE_REPORT = "create_image_report";
public const VIEW_IMAGE_REPORT = "view_image_report"; # deal with reported images
/** deal with reported images */
public const VIEW_IMAGE_REPORT = "view_image_report";
public const WIKI_ADMIN = "wiki_admin";
public const EDIT_WIKI_PAGE = "edit_wiki_page";
@ -84,7 +93,8 @@ abstract class Permissions
public const HELLBANNED = "hellbanned";
public const VIEW_HELLBANNED = "view_hellbanned";
public const PROTECTED = "protected"; # only admins can modify protected users (stops a moderator changing an admin's password)
/** only admins can modify protected users (stops a moderator from changing an admin's password) */
public const PROTECTED = "protected";
public const EDIT_IMAGE_RATING = "edit_image_rating";
public const BULK_EDIT_IMAGE_RATING = "bulk_edit_image_rating";
@ -110,6 +120,7 @@ abstract class Permissions
public const CRON_ADMIN = "cron_admin";
public const APPROVE_IMAGE = "approve_image";
public const APPROVE_COMMENT = "approve_comment";
public const BYPASS_IMAGE_APPROVAL = "bypass_image_approval";
public const SET_PRIVATE_IMAGE = "set_private_image";
public const SET_OTHERS_PRIVATE_IMAGES = "set_others_private_images";

View File

@ -173,7 +173,6 @@ function stream_file(string $file, int $start, int $end): void
{
$fp = fopen($file, 'r');
try {
set_time_limit(0);
fseek($fp, $start);
$buffer = 1024 * 1024;
while (!feof($fp) && ($p = ftell($fp)) <= $end) {
@ -229,10 +228,6 @@ if (!function_exists('http_parse_headers')) {
*/
function find_header(array $headers, string $name): ?string
{
if (!is_array($headers)) {
return null;
}
$header = null;
if (array_key_exists($name, $headers)) {
@ -458,9 +453,9 @@ function page_number(string $input, ?int $max=null): int
return $pageNumber;
}
function clamp(?int $val, ?int $min=null, ?int $max=null): int
function clamp(int $val, ?int $min=null, ?int $max=null): int
{
if (!is_numeric($val) || (!is_null($min) && $val < $min)) {
if (!is_null($min) && $val < $min) {
$val = $min;
}
if (!is_null($max) && $val > $max) {

View File

@ -4,6 +4,11 @@ declare(strict_types=1);
namespace Shimmie2;
class TimeoutException extends \RuntimeException
{
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Event API *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@ -93,6 +98,22 @@ function _dump_event_listeners(array $event_listeners, string $path): void
/** @private */
global $_shm_event_count;
$_shm_event_count = 0;
$_shm_timeout = null;
function shm_set_timeout(?int $timeout=null): void
{
global $_shm_timeout;
if ($timeout) {
$_shm_timeout = ftime() + $timeout;
} else {
$_shm_timeout = null;
}
set_time_limit(is_null($timeout) ? 0 : $timeout);
}
if (ini_get('max_execution_time')) {
shm_set_timeout((int)ini_get('max_execution_time') - 3);
}
/**
* Send an event to all registered Extensions.
@ -105,7 +126,7 @@ function send_event(Event $event): Event
{
global $tracer_enabled;
global $_shm_event_listeners, $_shm_event_count, $_tracer;
global $_shm_event_listeners, $_shm_event_count, $_tracer, $_shm_timeout;
$event_name = _namespaced_class_name(get_class($event));
if (!isset($_shm_event_listeners[$event_name])) {
return $event;
@ -122,6 +143,9 @@ function send_event(Event $event): Event
ksort($my_event_listeners);
foreach ($my_event_listeners as $listener) {
if ($_shm_timeout && ftime() > $_shm_timeout) {
throw new TimeoutException("Timeout while sending $event_name");
}
if ($tracer_enabled) {
$_tracer->begin(get_class($listener));
}

View File

@ -73,11 +73,14 @@ class PolyfillsTest extends TestCase
public function test_clamp()
{
$this->assertEquals(5, clamp(0, 5, 10));
$this->assertEquals(5, clamp(5, 5, 10));
$this->assertEquals(7, clamp(7, 5, 10));
$this->assertEquals(10, clamp(10, 5, 10));
$this->assertEquals(10, clamp(15, 5, 10));
$this->assertEquals(5, clamp(0, 5, 10)); // too small
$this->assertEquals(5, clamp(5, 5, 10)); // lower limit
$this->assertEquals(7, clamp(7, 5, 10)); // ok
$this->assertEquals(10, clamp(10, 5, 10)); // upper limit
$this->assertEquals(10, clamp(15, 5, 10)); // too large
$this->assertEquals(0, clamp(0, null, 10)); // no lower limit
$this->assertEquals(10, clamp(10, 0, null)); // no upper limit
$this->assertEquals(42, clamp(42, null, null)); // no limit
}
public function test_truncate()

View File

@ -7,6 +7,9 @@ namespace Shimmie2;
use GQLA\Type;
use GQLA\Field;
use GQLA\Query;
use MicroHTML\HTMLElement;
use function MicroHTML\INPUT;
function _new_user(array $row): User
{
@ -277,6 +280,13 @@ class User
return '<input type="hidden" name="auth_token" value="'.$at.'">';
}
// Temporary? This should eventually become get_auth_html (probably with a different name?).
public function get_auth_microhtml(): HTMLElement
{
$at = $this->get_auth_token();
return INPUT(["type"=>"hidden", "name"=>"auth_token", "value"=>$at]);
}
public function check_auth_token(): bool
{
if (defined("UNITTEST")) {

View File

@ -217,6 +217,7 @@ new UserClass("admin", "base", [
Permissions::APPROVE_IMAGE => true,
Permissions::APPROVE_COMMENT => true,
Permissions::BYPASS_IMAGE_APPROVAL => true,
Permissions::CRON_RUN =>true,

View File

@ -4,22 +4,6 @@ declare(strict_types=1);
namespace Shimmie2;
use MicroHTML\HTMLElement;
use function MicroHTML\emptyHTML;
use function MicroHTML\rawHTML;
use function MicroHTML\FORM;
use function MicroHTML\INPUT;
use function MicroHTML\DIV;
use function MicroHTML\PRE;
use function MicroHTML\P;
use function MicroHTML\TABLE;
use function MicroHTML\THEAD;
use function MicroHTML\TFOOT;
use function MicroHTML\TR;
use function MicroHTML\TH;
use function MicroHTML\TD;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Misc *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@ -270,7 +254,7 @@ function load_balance_url(string $tmpl, string $hash, int $n=0): string
$opt_weight = 0;
if ($parts_count === 2) {
$opt_val = $parts[0];
$opt_weight = $parts[1];
$opt_weight = (int)$parts[1];
} elseif ($parts_count === 1) {
$opt_val = $parts[0];
$opt_weight = 1;
@ -754,71 +738,6 @@ function make_form(string $target, string $method="POST", bool $multipart=false,
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
{
global $user;
$attrs = [
"action"=>make_link($target),
"method"=>$method
];
if ($form_id) {
$attrs["id"] = $form_id;
}
if ($multipart) {
$attrs["enctype"] = 'multipart/form-data';
}
if ($onsubmit) {
$attrs["onsubmit"] = $onsubmit;
}
return FORM(
$attrs,
INPUT(["type"=>"hidden", "name"=>"q", "value"=>$target]),
$method == "GET" ? "" : rawHTML($user->get_auth_html())
);
}
function SHM_SIMPLE_FORM($target, ...$children): HTMLElement
{
$form = SHM_FORM($target);
$form->appendChild(emptyHTML(...$children));
return $form;
}
function SHM_SUBMIT(string $text): HTMLElement
{
return INPUT(["type"=>"submit", "value"=>$text]);
}
function SHM_COMMAND_EXAMPLE(string $ex, string $desc): HTMLElement
{
return DIV(
["class"=>"command_example"],
PRE($ex),
P($desc)
);
}
function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot): HTMLElement
{
if (is_string($foot)) {
$foot = TFOOT(TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>$foot]))));
}
return SHM_SIMPLE_FORM(
$target,
P(
INPUT(["type"=>'hidden', "name"=>'id', "value"=>$duser->id]),
TABLE(
["class"=>"form"],
THEAD(TR(TH(["colspan"=>"2"], $title))),
$body,
$foot
)
)
);
}
const BYTE_DENOMINATIONS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
function human_filesize(int $bytes, $decimals = 2): string
{

View File

@ -33,7 +33,7 @@ class AdminActionEvent extends Event
class AdminPage extends Extension
{
/** @var AdminPageTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{
@ -51,7 +51,7 @@ class AdminPage extends Extension
if ($user->check_auth_token()) {
log_info("admin", "Util: $action");
set_time_limit(0);
shm_set_timeout(null);
$database->set_timeout(null);
send_event($aae);
}

View File

@ -59,7 +59,7 @@ class AddAliasException extends SCoreException
class AliasEditor extends Extension
{
/** @var AliasEditorTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{

View File

@ -4,36 +4,35 @@ declare(strict_types=1);
namespace Shimmie2;
use MicroHTML\HTMLElement;
use function MicroHTML\emptyHTML;
use function MicroHTML\{BR,INPUT};
class AliasEditorTheme extends Themelet
{
/**
* Show a page of aliases.
*
* Note: $can_manage = whether things like "add new alias" should be shown
*/
public function display_aliases($table, $paginator): void
public function display_aliases(HTMLElement $table, HTMLElement $paginator): void
{
global $page, $user;
$can_manage = $user->can(Permissions::MANAGE_ALIAS_LIST);
$html = "
$table
$paginator
<p><a href='".make_link("alias/export/aliases.csv")."' download='aliases.csv'>Download as CSV</a></p>
";
$html = emptyHTML($table, BR(), $paginator, BR(), SHM_A("alias/export/aliases.csv", "Download as CSV", args: ["download"=>"aliases.csv"]));
$bulk_html = "
".make_form(make_link("alias/import"), 'post', true)."
<input type='file' name='alias_file'>
<input type='submit' value='Upload List'>
</form>
";
$bulk_form = SHM_FORM("alias/import", multipart: true);
$bulk_form->appendChild(
INPUT(["type"=>"file", "name"=>"alias_file"]),
SHM_SUBMIT("Upload List")
);
$bulk_html = emptyHTML($bulk_form);
$page->set_title("Alias List");
$page->set_heading("Alias List");
$page->add_block(new NavBlock());
$page->add_block(new Block("Aliases", $html));
if ($can_manage) {
if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
$page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51));
}
}

View File

@ -14,7 +14,7 @@ abstract class ApprovalConfig
class Approval extends Extension
{
/** @var ApprovalTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onInitExt(InitExtEvent $event)
{
@ -26,6 +26,15 @@ class Approval extends Extension
Image::$bool_props[] = "approved";
}
public function onImageAddition(ImageAdditionEvent $event)
{
global $user, $config;
if ($config->get_bool(ApprovalConfig::IMAGES) && $user->can(Permissions::BYPASS_IMAGE_APPROVAL)) {
self::approve_image($event->image->id);
}
}
public function onPageRequest(PageRequestEvent $event)
{
global $page, $user;
@ -159,10 +168,7 @@ class Approval extends Extension
global $user, $config;
if ($event->key===HelpPages::SEARCH) {
if ($user->can(Permissions::APPROVE_IMAGE) && $config->get_bool(ApprovalConfig::IMAGES)) {
$block = new Block();
$block->header = "Approval";
$block->body = $this->theme->get_help_html();
$event->add_block($block);
$event->add_block(new Block("Approval", $this->theme->get_help_html()));
}
}
}
@ -222,7 +228,7 @@ class Approval extends Extension
{
global $user, $config;
if ($user->can(Permissions::APPROVE_IMAGE) && $config->get_bool(ApprovalConfig::IMAGES)) {
$event->add_part($this->theme->get_image_admin_html($event->image));
$event->add_part((string)$this->theme->get_image_admin_html($event->image));
}
}

View File

@ -4,43 +4,40 @@ declare(strict_types=1);
namespace Shimmie2;
use function MicroHTML\BR;
use function MicroHTML\BUTTON;
use function MicroHTML\INPUT;
use MicroHTML\HTMLElement;
use function MicroHTML\emptyHTML;
use function MicroHTML\{BUTTON,INPUT,P};
class ApprovalTheme extends Themelet
{
public function get_image_admin_html(Image $image): string
public function get_image_admin_html(Image $image): HTMLElement
{
if ($image->approved===true) {
$html = SHM_SIMPLE_FORM(
$form = SHM_SIMPLE_FORM(
'disapprove_image/'.$image->id,
INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image->id]),
SHM_SUBMIT("Disapprove")
);
} else {
$html = SHM_SIMPLE_FORM(
$form = SHM_SIMPLE_FORM(
'approve_image/'.$image->id,
INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image->id]),
SHM_SUBMIT("Approve")
);
}
return (string)$html;
return $form;
}
public function get_help_html(): string
public function get_help_html(): HTMLElement
{
return '<p>Search for posts that are approved/not approved.</p>
<div class="command_example">
<pre>approved:yes</pre>
<p>Returns posts that have been approved.</p>
</div>
<div class="command_example">
<pre>approved:no</pre>
<p>Returns posts that have not been approved.</p>
</div>
';
return emptyHTML(
P("Search for posts that are approved/not approved."),
SHM_COMMAND_EXAMPLE("approved:yes", "Returns posts that have been approved."),
SHM_COMMAND_EXAMPLE("approved:no", "Returns posts that have not been approved.")
);
}
public function display_admin_block(SetupBuildingEvent $event)
@ -53,12 +50,13 @@ class ApprovalTheme extends Themelet
{
global $page;
$html = (string)SHM_SIMPLE_FORM(
$form = SHM_SIMPLE_FORM(
"admin/approval",
BUTTON(["name"=>'approval_action', "value"=>'approve_all'], "Approve All Posts"),
BR(),
" ",
BUTTON(["name"=>'approval_action', "value"=>'disapprove_all'], "Disapprove All Posts"),
);
$page->add_block(new Block("Approval", $html));
$page->add_block(new Block("Approval", $form));
}
}

View File

@ -22,7 +22,7 @@ class AuthorSetEvent extends Event
class Artists extends Extension
{
/** @var ArtistsTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onImageInfoSet(ImageInfoSetEvent $event)
{
@ -57,10 +57,7 @@ class Artists extends Extension
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
{
if ($event->key===HelpPages::SEARCH) {
$block = new Block();
$block->header = "Artist";
$block->body = $this->theme->get_help_html();
$event->add_block($block);
$event->add_block(new Block("Artist", $this->theme->get_help_html()));
}
}
@ -212,7 +209,7 @@ class Artists extends Extension
$userIsLogged = !$user->is_anonymous();
$userIsAdmin = $user->can(Permissions::ARTISTS_ADMIN);
$images = Image::find_images(0, 4, Tag::explode($artist['name']));
$images = Image::find_images(limit: 4, tags: Tag::explode($artist['name']));
$this->theme->show_artist($artist, $aliases, $members, $urls, $images, $userIsLogged, $userIsAdmin);
/*

View File

@ -4,20 +4,20 @@ declare(strict_types=1);
namespace Shimmie2;
use MicroHTML\HTMLElement;
use function MicroHTML\emptyHTML;
use function MicroHTML\{INPUT,P,SPAN,TD,TH,TR};
class ArtistsTheme extends Themelet
{
public function get_author_editor_html(string $author): string
{
$h_author = html_escape($author);
return "
<tr>
<th>Author</th>
<td>
<span class='view'>$h_author</span>
<input class='edit' type='text' name='tag_edit__author' value='$h_author'>
</td>
</tr>
";
return (string)TR(TH("Author", TD(
SPAN(["class"=>"view"], $h_author),
INPUT(["class"=>"edit", "type"=>"text", "name"=>"tag_edit__author", "value"=>$h_author])
)));
}
public function sidebar_options(string $mode, ?int $artistID=null, $is_admin=false): void
@ -554,13 +554,11 @@ class ArtistsTheme extends Themelet
return $html;
}
public function get_help_html(): string
public function get_help_html(): HTMLElement
{
return '<p>Search for posts with a particular artist.</p>
<div class="command_example">
<pre>artist=leonardo</pre>
<p>Returns posts with the artist "leonardo".</p>
</div>
';
return emptyHTML(
P("Search for posts with a particular artist."),
SHM_COMMAND_EXAMPLE("artist=leonardo", "Returns posts with the artist \"leonardo\".")
);
}
}

View File

@ -65,7 +65,7 @@ class AddAutoTagException extends SCoreException
class AutoTagger extends Extension
{
/** @var AutoTaggerTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class AutoComplete extends Extension
{
/** @var AutoCompleteTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function get_priority(): int
{

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class Biography extends Extension
{
/** @var BiographyTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onUserPageBuilding(UserPageBuildingEvent $event)
{

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class Blocks extends Extension
{
/** @var BlocksTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{
@ -19,10 +19,15 @@ class Blocks extends Extension
title VARCHAR(128) NOT NULL,
area VARCHAR(16) NOT NULL,
priority INTEGER NOT NULL,
content TEXT NOT NULL
content TEXT NOT NULL,
userclass TEXT
");
$database->execute("CREATE INDEX blocks_pages_idx ON blocks(pages)", []);
$this->set_version("ext_blocks_version", 1);
$this->set_version("ext_blocks_version", 2);
}
if ($this->get_version("ext_blocks_version") < 2) {
$database->execute("ALTER TABLE blocks ADD COLUMN userclass TEXT");
$this->set_version("ext_blocks_version", 2);
}
}
@ -58,7 +63,12 @@ class Blocks extends Extension
if (strlen($path) < 4000 && fnmatch($block['pages'], $path)) {
$b = new Block($block['title'], $block['content'], $block['area'], (int)$block['priority']);
$b->is_content = false;
$page->add_block($b);
# Split by comma, trimming whitespaces, and not allowing empty elements.
$userclasses = preg_split('/\s*,+\s*/', strtolower($block['userclass'] ?? ""), 0, PREG_SPLIT_NO_EMPTY);
if (empty($userclasses) || in_array(strtolower($user->class->name), $userclasses)) {
$page->add_block($b);
}
}
}
@ -66,9 +76,9 @@ class Blocks extends Extension
if ($event->get_arg(0) == "add") {
if ($user->check_auth_token()) {
$database->execute("
INSERT INTO blocks (pages, title, area, priority, content)
VALUES (:pages, :title, :area, :priority, :content)
", ['pages'=>$_POST['pages'], 'title'=>$_POST['title'], 'area'=>$_POST['area'], 'priority'=>(int)$_POST['priority'], 'content'=>$_POST['content']]);
INSERT INTO blocks (pages, title, area, priority, content, userclass)
VALUES (:pages, :title, :area, :priority, :content, :userclass)
", ['pages'=>$_POST['pages'], 'title'=>$_POST['title'], 'area'=>$_POST['area'], 'priority'=>(int)$_POST['priority'], 'content'=>$_POST['content'], 'userclass'=>$_POST['userclass']]);
log_info("blocks", "Added Block #".($database->get_last_insert_id('blocks_id_seq'))." (".$_POST['title'].")");
$cache->delete("blocks");
$page->set_mode(PageMode::REDIRECT);
@ -85,9 +95,9 @@ class Blocks extends Extension
log_info("blocks", "Deleted Block #".$_POST['id']);
} else {
$database->execute("
UPDATE blocks SET pages=:pages, title=:title, area=:area, priority=:priority, content=:content
UPDATE blocks SET pages=:pages, title=:title, area=:area, priority=:priority, content=:content, userclass=:userclass
WHERE id=:id
", ['pages'=>$_POST['pages'], 'title'=>$_POST['title'], 'area'=>$_POST['area'], 'priority'=>(int)$_POST['priority'], 'content'=>$_POST['content'], 'id'=>$_POST['id']]);
", ['pages'=>$_POST['pages'], 'title'=>$_POST['title'], 'area'=>$_POST['area'], 'priority'=>(int)$_POST['priority'], 'content'=>$_POST['content'], 'userclass'=>$_POST['userclass'], 'id'=>$_POST['id']]);
log_info("blocks", "Updated Block #".$_POST['id']." (".$_POST['title'].")");
}
$cache->delete("blocks");

View File

@ -32,6 +32,8 @@ class BlocksTheme extends Themelet
TD(INPUT(["type"=>"text", "name"=>"area", "value"=>$block['area']])),
TH("Priority"),
TD(INPUT(["type"=>"text", "name"=>"priority", "value"=>$block['priority']])),
TH("User Class"),
TD(INPUT(["type"=>"text", "name"=>"userclass", "value"=>$block['userclass']])),
TH("Pages"),
TD(INPUT(["type"=>"text", "name"=>"pages", "value"=>$block['pages']])),
TH("Delete"),
@ -39,10 +41,10 @@ class BlocksTheme extends Themelet
TD(INPUT(["type"=>"submit", "value"=>"Save"]))
),
TR(
TD(["colspan"=>"11"], TEXTAREA(["rows"=>"5", "name"=>"content"], $block['content']))
TD(["colspan"=>"13"], TEXTAREA(["rows"=>"5", "name"=>"content"], $block['content']))
),
TR(
TD(["colspan"=>"11"], rawHTML("&nbsp;"))
TD(["colspan"=>"13"], rawHTML("&nbsp;"))
),
));
}
@ -56,18 +58,20 @@ class BlocksTheme extends Themelet
TD(SELECT(["name"=>"area"], OPTION("left"), OPTION("main"))),
TH("Priority"),
TD(INPUT(["type"=>"text", "name"=>"priority", "value"=>'50'])),
TH("User Class"),
TD(INPUT(["type"=>"text", "name"=>"userclass", "value"=>""])),
TH("Pages"),
TD(INPUT(["type"=>"text", "name"=>"pages", "value"=>'post/list*'])),
TD(["colspan"=>'3'], INPUT(["type"=>"submit", "value"=>"Add"]))
),
TR(
TD(["colspan"=>"11"], TEXTAREA(["rows"=>"5", "name"=>"content"]))
TD(["colspan"=>"13"], TEXTAREA(["rows"=>"5", "name"=>"content"]))
),
));
$page->set_title("Blocks");
$page->set_heading("Blocks");
$page->add_block(new NavBlock());
$page->add_block(new Block("Block Editor", (string)$html));
$page->add_block(new Block("Block Editor", $html));
}
}

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class Blotter extends Extension
{
/** @var BlotterTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onInitExt(InitExtEvent $event)
{

View File

@ -12,12 +12,8 @@ class BulkActionBlockBuildingEvent extends Event
public array $actions = [];
public array $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)
{
if ($block == null) {
$block = "";
}
if (!empty($access_key)) {
assert(strlen($access_key)==1);
foreach ($this->actions as $existing) {
@ -55,7 +51,7 @@ class BulkActionEvent extends Event
class BulkActions extends Extension
{
/** @var BulkActionsTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPostListBuilding(PostListBuildingEvent $event)
{
@ -180,7 +176,7 @@ class BulkActions extends Extension
}
} elseif (isset($_POST['bulk_query']) && $_POST['bulk_query'] != "") {
$query = $_POST['bulk_query'];
if ($query != null && $query != "") {
if (!empty($query)) {
$items = $this->yield_search_results($query);
}
} else {

View File

@ -14,7 +14,7 @@ function validate_selections(form, confirmationMessage) {
} else {
var query = $(form).find('input[name="bulk_query"]').val();
if (query == null || query === "") {
if (query === null || query === "") {
return false;
} else {
queryOnly = true;

View File

@ -26,7 +26,7 @@ class BulkActionsTheme extends Themelet
</td></tr></table>
";
$hasQuery = ($query != null && $query != "");
$hasQuery = !empty($query);
if ($hasQuery) {
$body .= "</div>";
@ -61,7 +61,7 @@ class BulkActionsTheme extends Themelet
public function render_tag_input(): string
{
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' class='autocomplete_tags' required='required' placeholder='Enter tags here' />";
}
public function render_source_input(): string

View File

@ -20,14 +20,14 @@ class BulkAddEvent extends Event
class BulkAdd extends Extension
{
/** @var BulkAddTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{
global $page, $user;
if ($event->page_matches("bulk_add")) {
if ($user->can(Permissions::BULK_ADD) && $user->check_auth_token() && isset($_POST['dir'])) {
set_time_limit(0);
shm_set_timeout(null);
$bae = send_event(new BulkAddEvent($_POST['dir']));
foreach ($bae->results as $result) {
$this->theme->add_status("Adding files", $result);

View File

@ -7,14 +7,14 @@ namespace Shimmie2;
class BulkAddCSV extends Extension
{
/** @var BulkAddCSVTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{
global $page, $user;
if ($event->page_matches("bulk_add_csv")) {
if ($user->can(Permissions::BULK_ADD) && $user->check_auth_token() && isset($_POST['csv'])) {
set_time_limit(0);
shm_set_timeout(null);
$this->add_csv($_POST['csv']);
$this->theme->display_upload_results($page);
}

View File

@ -31,7 +31,7 @@ class BulkParentChild extends Extension
($event->action == BulkParentChild::PARENT_CHILD_ACTION_NAME)) {
$prev_id = null;
foreach ($event->items as $image) {
if ($prev_id != null) {
if ($prev_id !== null) {
send_event(new ImageRelationshipSetEvent($image->id, $prev_id));
}
$prev_id = $image->id;

View File

@ -114,7 +114,7 @@ class Comment
class CommentList extends Extension
{
/** @var CommentListTheme $theme */
public ?Themelet $theme;
public Themelet $theme;
public function onInitExt(InitExtEvent $event)
{

View File

@ -26,7 +26,7 @@ class CronUploaderInfo extends ExtensionInfo
public function __construct()
{
$this->documentation = "Installation guide: activate this extension and navigate to System Config screen.</a>";
$this->documentation = "Installation guide: activate this extension and navigate to Board Config screen.</a>";
parent::__construct();
}
}

View File

@ -9,7 +9,7 @@ require_once "config.php";
class CronUploader extends Extension
{
/** @var CronUploaderTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public const NAME = "cron_uploader";
@ -350,7 +350,7 @@ class CronUploader extends Extension
self::$IMPORT_RUNNING = true;
try {
//set_time_limit(0);
//shm_set_timeout(null);
$output_subdir = date('Ymd-His', time());
$image_queue = $this->generate_image_queue();

View File

@ -74,10 +74,10 @@ class CronUploaderTheme extends Themelet
$install_html = "
This cron uploader is fairly easy to use but has to be configured first.
<ol>
<li>Install & activate this plugin.</li>
<li>Go to the <a href='".make_link("setup")."'>Board Config</a> and change any settings to match your preference.</li>
<li>Copy the cron command above.</li>
<li>Create a cron job or something else that can open a url on specified times.
<li style='text-align: left;'>Install & activate this plugin.</li>
<li style='text-align: left;'>Go to the <a href='".make_link("setup")."'>Board Config</a> and change any settings to match your preference.</li>
<li style='text-align: left;'>Copy the cron command above.</li>
<li style='text-align: left;'>Create a cron job or something else that can open a url on specified times.
<br/>cron is a service that runs commands over and over again on a a schedule. You can set up cron (or any similar tool) to run the command above to trigger the import on whatever schedule you desire.
<br />If you're not sure how to do this, you can give the command to your web host and you can ask them to create the cron job for you.
<br />When you create the cron job, you choose when to upload new posts.</li>
@ -89,11 +89,11 @@ class CronUploaderTheme extends Themelet
$usage_html = "Upload your images you want to be uploaded to the queue directory using your FTP client or other means.
<br />(<b>{$queue_dirinfo['path']}</b>)
<ol>
<li>Any sub-folders will be turned into tags.</li>
<li>If the file name matches \"## - tag1 tag2.png\" the tags will be used.</li>
<li>If both are found, they will all be used.</li>
<li>The character \";\" will be changed into \":\" in any tags.</li>
<li>You can inherit categories by creating a folder that ends with \";\". For instance category;\\tag1 would result in the tag category:tag1. This allows creating a category folder, then creating many subfolders that will use that category.</li>
<li style='text-align: left;'>Any sub-folders will be turned into tags.</li>
<li style='text-align: left;'>If the file name matches \"## - tag1 tag2.png\" the tags will be used.</li>
<li style='text-align: left;'>If both are found, they will all be used.</li>
<li style='text-align: left;'>The character \";\" will be changed into \":\" in any tags.</li>
<li style='text-align: left;'>You can inherit categories by creating a folder that ends with \";\". For instance category;\\tag1 would result in the tag category:tag1. This allows creating a category folder, then creating many subfolders that will use that category.</li>
</ol>
The cron uploader works by importing files from the queue folder whenever this url is visited:
<br/><pre><a href='$cron_url'>$cron_url</a></pre>

View File

@ -195,6 +195,10 @@ class DanbooruApi extends Extension
}
$tags = isset($_GET['tags']) ? Tag::explode($_GET['tags']) : [];
// danbooru API clients often set tags=*
$tags = array_filter($tags, static function ($element) {
return $element !== "*";
});
$count = Image::count_images($tags);
$results = Image::find_images(max($start, 0), min($limit, 100), $tags);
}

View File

@ -15,6 +15,7 @@ class DanbooruApiTest extends ShimmiePHPUnitTestCase
$this->get_page("api/danbooru/find_posts");
$this->get_page("api/danbooru/find_posts", ["id"=>$image_id]);
$this->get_page("api/danbooru/find_posts", ["md5"=>"17fc89f372ed3636e28bd25cc7f3bac1"]);
$this->get_page("api/danbooru/find_posts", ["tags"=>"*"]);
$this->get_page("api/danbooru/find_tags");
$this->get_page("api/danbooru/find_tags", ["id"=>1]);

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class Downtime extends Extension
{
/** @var DowntimeTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function get_priority(): int
{

View File

@ -10,7 +10,7 @@ namespace Shimmie2;
class EmoticonList extends Extension
{
/** @var EmoticonListTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class ET extends Extension
{
/** @var ETTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{

View File

@ -37,7 +37,7 @@ class ExtensionAuthor
class ExtManager extends Extension
{
/** @var ExtManagerTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{

View File

@ -25,7 +25,7 @@ class FavoriteSetEvent extends Event
class Favorites extends Extension
{
/** @var FavoritesTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
{

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class Featured extends Extension
{
/** @var FeaturedTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onInitExt(InitExtEvent $event)
{

View File

@ -15,7 +15,7 @@ Todo:
class Forum extends Extension
{
/** @var ForumTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{

View File

@ -11,7 +11,7 @@ class SVGFileHandler extends DataHandlerExtension
protected array $SUPPORTED_MIME = [MimeType::SVG];
/** @var SVGFileHandlerTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{

View File

@ -66,53 +66,51 @@ class VideoFileHandler extends DataHandlerExtension
try {
$data = Media::get_ffprobe_data($event->image->get_image_filename());
if (is_array($data)) {
if (array_key_exists("streams", $data)) {
$video = false;
$audio = false;
$video_codec = null;
$streams = $data["streams"];
if (is_array($streams)) {
foreach ($streams as $stream) {
if (is_array($stream)) {
if (array_key_exists("codec_type", $stream)) {
$type = $stream["codec_type"];
switch ($type) {
case "audio":
$audio = true;
break;
case "video":
$video = true;
$video_codec = $stream["codec_name"];
break;
}
}
if (array_key_exists("width", $stream) && !empty($stream["width"])
&& is_numeric($stream["width"]) && intval($stream["width"]) > ($event->image->width) ?? 0) {
$event->image->width = intval($stream["width"]);
}
if (array_key_exists("height", $stream) && !empty($stream["height"])
&& is_numeric($stream["height"]) && intval($stream["height"]) > ($event->image->height) ?? 0) {
$event->image->height = intval($stream["height"]);
if (array_key_exists("streams", $data)) {
$video = false;
$audio = false;
$video_codec = null;
$streams = $data["streams"];
if (is_array($streams)) {
foreach ($streams as $stream) {
if (is_array($stream)) {
if (array_key_exists("codec_type", $stream)) {
$type = $stream["codec_type"];
switch ($type) {
case "audio":
$audio = true;
break;
case "video":
$video = true;
$video_codec = $stream["codec_name"];
break;
}
}
if (array_key_exists("width", $stream) && !empty($stream["width"])
&& is_numeric($stream["width"]) && intval($stream["width"]) > ($event->image->width) ?? 0) {
$event->image->width = intval($stream["width"]);
}
if (array_key_exists("height", $stream) && !empty($stream["height"])
&& is_numeric($stream["height"]) && intval($stream["height"]) > ($event->image->height) ?? 0) {
$event->image->height = intval($stream["height"]);
}
}
$event->image->video = $video;
$event->image->video_codec = $video_codec;
$event->image->audio = $audio;
if ($event->image->get_mime()==MimeType::MKV &&
VideoContainers::is_video_codec_supported(VideoContainers::WEBM, $event->image->video_codec)) {
// WEBMs are MKVs with the VP9 or VP8 codec
// For browser-friendliness, we'll just change the mime type
$event->image->set_mime(MimeType::WEBM);
}
}
$event->image->video = $video;
$event->image->video_codec = $video_codec;
$event->image->audio = $audio;
if ($event->image->get_mime()==MimeType::MKV &&
VideoContainers::is_video_codec_supported(VideoContainers::WEBM, $event->image->video_codec)) {
// WEBMs are MKVs with the VP9 or VP8 codec
// For browser-friendliness, we'll just change the mime type
$event->image->set_mime(MimeType::WEBM);
}
}
if (array_key_exists("format", $data)&& is_array($data["format"])) {
$format = $data["format"];
if (array_key_exists("duration", $format) && is_numeric($format["duration"])) {
$event->image->length = (int)floor(floatval($format["duration"]) * 1000);
}
}
if (array_key_exists("format", $data)&& is_array($data["format"])) {
$format = $data["format"];
if (array_key_exists("duration", $format) && is_numeric($format["duration"])) {
$event->image->length = (int)floor(floatval($format["duration"]) * 1000);
}
}
} catch (MediaException $e) {

View File

@ -37,25 +37,15 @@ class HelpPageBuildingEvent extends Event
class HelpPages extends Extension
{
/** @var HelpPagesTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public const SEARCH = "search";
private ?array $pages = null;
private function get_pages(): array
{
if ($this->pages==null) {
$this->pages = send_event(new HelpPageListBuildingEvent())->pages;
}
return $this->pages;
}
public function onPageRequest(PageRequestEvent $event)
{
global $page;
$pages = $this->get_pages();
if ($event->page_matches("help")) {
$pages = send_event(new HelpPageListBuildingEvent())->pages;
if ($event->count_args() == 0) {
$name = array_key_first($pages);
$page->set_mode(PageMode::REDIRECT);
@ -98,7 +88,7 @@ class HelpPages extends Extension
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
if ($event->parent=="help") {
$pages = $this->get_pages();
$pages = send_event(new HelpPageListBuildingEvent())->pages;
foreach ($pages as $key=>$value) {
$event->add_nav_link("help_".$key, new Link('help/'.$key), $value);
}

View File

@ -29,5 +29,6 @@ class HelpPagesTheme extends Themelet
$page->set_title("Help - $title");
$page->set_heading("Help - $title");
$page->add_block(new NavBlock());
}
}

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class Holiday extends Extension
{
/** @var HolidayTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onInitExt(InitExtEvent $event)
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class Home extends Extension
{
/** @var HomeTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{
@ -26,6 +26,8 @@ class Home extends Extension
public function onSetupBuilding(SetupBuildingEvent $event)
{
$counters = [];
$counters["None"] = "none";
$counters["Text-only"] = "text-only";
foreach (glob("ext/home/counters/*") as $counter_dirname) {
$name = str_replace("ext/home/counters/", "", $counter_dirname);
$counters[ucfirst($name)] = $name;
@ -50,22 +52,21 @@ class Home extends Extension
}
$counter_dir = $config->get_string('home_counter', 'default');
$total = Image::count_images();
$strtotal = "$total";
$num_comma = number_format($total);
$num_comma = "";
$counter_text = "";
$length = strlen($strtotal);
for ($n=0; $n<$length; $n++) {
$cur = $strtotal[$n];
if ($cur == 6 && rand(0, 2500) == 0) {
$counter_text .= "<img alt='$cur' src='$base_href/ext/home/counters/$counter_dir/Ϫ.gif' />";
} elseif ($cur == 7 && rand(0, 2500) == 0) {
$counter_text .= "<img alt='$cur' src='$base_href/ext/home/counters/$counter_dir/Ϫ.gif' />";
} else {
$counter_text .= "<img alt='$cur' src='$base_href/ext/home/counters/$counter_dir/$cur.gif' />";
}
if ($counter_dir != 'none') {
$total = Image::count_images();
$num_comma = number_format($total);
if ($counter_dir != 'text-only') {
$strtotal = "$total";
$length = strlen($strtotal);
for ($n=0; $n<$length; $n++) {
$cur = $strtotal[$n];
$counter_text .= "<img alt='$cur' src='$base_href/ext/home/counters/$counter_dir/Ϫ.gif' />";
$counter_text .= "<img alt='$cur' src='$base_href/ext/home/counters/$counter_dir/$cur.gif' />";
}
}
}
// get the homelinks and process them

View File

@ -53,7 +53,7 @@ EOD
$counter_html
<div class='space' id='foot'>
<small><small>
$contact_link Serving $num_comma posts &ndash;
$contact_link" . (empty($num_comma) ? "" : " Serving $num_comma posts &ndash;") . "
Running <a href='https://code.shishnet.org/shimmie2/'>Shimmie2</a>
</small></small>
</div>

View File

@ -12,7 +12,7 @@ require_once "config.php";
class ImageIO extends Extension
{
/** @var ImageIOTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public const COLLISION_OPTIONS = [
'Error'=>ImageConfig::COLLISION_ERROR,

View File

@ -60,7 +60,7 @@ class AddImageHashBanEvent extends Event
class ImageBan extends Extension
{
/** @var ImageBanTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class ImageViewCounter extends Extension
{
/** @var ImageViewCounterTheme */
protected ?Themelet $theme;
protected Themelet $theme;
private int $view_interval = 3600; # allows views to be added each hour
# Add Setup Block with options for view counter

View File

@ -10,7 +10,7 @@ require_once "events.php";
class Index extends Extension
{
/** @var IndexTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onInitExt(InitExtEvent $event)
{
@ -150,10 +150,7 @@ class Index extends Extension
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
{
if ($event->key===HelpPages::SEARCH) {
$block = new Block();
$block->header = "General";
$block->body = $this->theme->get_help_html();
$event->add_block($block, 0);
$event->add_block(new Block("General", $this->theme->get_help_html()), 0);
}
}
@ -166,7 +163,7 @@ class Index extends Extension
}
if ($event->cmd == "search") {
$query = count($event->args) > 0 ? Tag::explode($event->args[0]) : [];
$items = Image::find_images(0, 1000, $query);
$items = Image::find_images(limit: 1000, tags: $query);
foreach ($items as $item) {
print("{$item->hash}\n");
}
@ -259,7 +256,7 @@ class Index extends Extension
}
// If we've reached this far, and nobody else has done anything with this term, then treat it as a tag
if ($event->order == null && $event->img_conditions == [] && $event->tag_conditions == []) {
if ($event->order === null && $event->img_conditions == [] && $event->tag_conditions == []) {
$event->add_tag_condition(new TagCondition($event->term, $event->positive));
}
}

View File

@ -49,7 +49,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
function select_blocked_tags() {
var blocked_tags = prompt("Enter tags to ignore", Cookies.get("ui-blocked-tags") || "My_Little_Pony");
var blocked_tags = prompt("Enter tags to ignore", Cookies.get("ui-blocked-tags") || "AI-generated");
if(blocked_tags !== null) {
Cookies.set("ui-blocked-tags", blocked_tags.toLowerCase(), {expires: 365});
location.reload(true);

View File

@ -4,6 +4,11 @@ declare(strict_types=1);
namespace Shimmie2;
use MicroHTML\HTMLElement;
use function MicroHTML\emptyHTML;
use function MicroHTML\{BR,H3,HR,P};
class IndexTheme extends Themelet
{
protected int $page_number;
@ -82,7 +87,7 @@ and of course start organising your images :-)
$h_next = ($page_number >= $total_pages) ? "Next" : '<a href="'.make_link('post/list'.$query.'/'.$next).'">Next</a>';
$h_search_string = html_escape(Tag::implode($search_terms));
$h_search_link = make_link();
$h_search_link = make_link("post/list");
$h_search = "
<p><form action='$h_search_link' method='GET'>
<input type='search' name='search' value='$h_search_string' placeholder='Search' class='autocomplete_tags' autocomplete='off' />
@ -182,227 +187,95 @@ and of course start organising your images :-)
}
}
public function get_help_html(): string
public function get_help_html(): HTMLElement
{
return '<p>Searching is largely based on tags, with a number of special keywords available that allow searching based on properties of the posts.</p>
<div class="command_example">
<pre>tagname</pre>
<p>Returns posts that are tagged with "tagname".</p>
</div>
<div class="command_example">
<pre>tagname othertagname</pre>
<p>Returns posts that are tagged with "tagname" and "othertagname".</p>
</div>
<p>Most tags and keywords can be prefaced with a negative sign (-) to indicate that you want to search for posts that do not match something.</p>
<div class="command_example">
<pre>-tagname</pre>
<p>Returns posts that are not tagged with "tagname".</p>
</div>
<div class="command_example">
<pre>-tagname -othertagname</pre>
<p>Returns posts that are not tagged with "tagname" and "othertagname". This is different than without the negative sign, as posts with "tagname" or "othertagname" can still be returned as long as the other one is not present.</p>
</div>
<div class="command_example">
<pre>tagname -othertagname</pre>
<p>Returns posts that are tagged with "tagname", but are not tagged with "othertagname".</p>
</div>
<p>Wildcard searches are possible as well using * for "any one, more, or none" and ? for "any one".</p>
<div class="command_example">
<pre>tagn*</pre>
<p>Returns posts that are tagged with "tagname", "tagnot", or anything else that starts with "tagn".</p>
</div>
<div class="command_example">
<pre>tagn?me</pre>
<p>Returns posts that are tagged with "tagname", "tagnome", or anything else that starts with "tagn", has one character, and ends with "me".</p>
</div>
<div class="command_example">
<pre>tags=1</pre>
<p>Returns posts with exactly 1 tag.</p>
</div>
<div class="command_example">
<pre>tags>0</pre>
<p>Returns posts with 1 or more tags. </p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
<hr/>
<p>Search for posts by aspect ratio</p>
<div class="command_example">
<pre>ratio=4:3</pre>
<p>Returns posts with an aspect ratio of 4:3.</p>
</div>
<div class="command_example">
<pre>ratio>16:9</pre>
<p>Returns posts with an aspect ratio greater than 16:9. </p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =. The relation is calculated by dividing width by height.</p>
<hr/>
<p>Search for posts by file size</p>
<div class="command_example">
<pre>filesize=1</pre>
<p>Returns posts exactly 1 byte in size.</p>
</div>
<div class="command_example">
<pre>filesize>100mb</pre>
<p>Returns posts greater than 100 megabytes in size. </p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =. Supported suffixes are kb, mb, and gb. Uses multiples of 1024.</p>
<hr/>
<p>Search for posts by MD5 hash</p>
<div class="command_example">
<pre>hash=0D3512CAA964B2BA5D7851AF5951F33B</pre>
<p>Returns post with an MD5 hash 0D3512CAA964B2BA5D7851AF5951F33B.</p>
</div>
<hr/>
<p>Search for posts by file name</p>
<div class="command_example">
<pre>filename=picasso.jpg</pre>
<p>Returns posts that are named "picasso.jpg".</p>
</div>
<hr/>
<p>Search for posts by source</p>
<div class="command_example">
<pre>source=https:///google.com/</pre>
<p>Returns posts with a source of "https://google.com/".</p>
</div>
<div class="command_example">
<pre>source=any</pre>
<p>Returns posts with a source set.</p>
</div>
<div class="command_example">
<pre>source=none</pre>
<p>Returns posts without a source set.</p>
</div>
<hr/>
<p>Search for posts by date posted.</p>
<div class="command_example">
<pre>posted>=2019-07-19</pre>
<p>Returns posts posted on or after 2019-07-19.</p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =. Date format is yyyy-mm-dd. Date posted includes time component, so = will not work unless the time is exact.</p>
<hr/>
<p>Search for posts by length.</p>
<div class="command_example">
<pre>length>=1h</pre>
<p>Returns posts that are longer than an hour.</p>
</div>
<div class="command_example">
<pre>length<=10h15m</pre>
<p>Returns posts that are shorter than 10 hours and 15 minutes.</p>
</div>
<div class="command_example">
<pre>length>=10000</pre>
<p>Returns posts that are longer than 10,000 milliseconds, or 10 seconds.</p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =. Available suffixes are ms, s, m, h, d, and y. A number by itself will be interpreted as milliseconds. Searches using = are not likely to work unless time is specified down to the millisecond.</p>
<hr/>
<p>Search for posts by dimensions</p>
<div class="command_example">
<pre>size=640x480</pre>
<p>Returns posts exactly 640 pixels wide by 480 pixels high.</p>
</div>
<div class="command_example">
<pre>size>1920x1080</pre>
<p>Returns posts with a width larger than 1920 and a height larger than 1080.</p>
</div>
<div class="command_example">
<pre>width=1000</pre>
<p>Returns posts exactly 1000 pixels wide.</p>
</div>
<div class="command_example">
<pre>height=1000</pre>
<p>Returns posts exactly 1000 pixels high.</p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
<hr/>
<p>Search for posts by ID</p>
<div class="command_example">
<pre>id=1234</pre>
<p>Find the 1234th thing uploaded.</p>
</div>
<div class="command_example">
<pre>id>1234</pre>
<p>Find more recently posted things</p>
</div>
<p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
<hr/>
<p>Sorting search results can be done using the pattern order:field_direction. _direction can be either _asc or _desc, indicating ascending (123) or descending (321) order.</p>
<div class="command_example">
<pre>order:id_asc</pre>
<p>Returns posts sorted by ID, smallest first.</p>
</div>
<div class="command_example">
<pre>order:width_desc</pre>
<p>Returns posts sorted by width, largest first.</p>
</div>
<p>These fields are supported:
<ul>
<li>id</li>
<li>width</li>
<li>height</li>
<li>filesize</li>
<li>filename</li>
</ul>
</p>
';
return emptyHTML(
H3("Tag Searching"),
P("Searching is largely based on tags, with a number of special keywords available that allow searching based on properties of the posts."),
SHM_COMMAND_EXAMPLE("tagname", 'Returns posts that are tagged with "tagname".'),
SHM_COMMAND_EXAMPLE("tagname othertagname", 'Returns posts that are tagged with "tagname" and "othertagme".'),
//
BR(),
P("Most tags and keywords can be prefaced with a negative sign (-) to indicate that you want to search for posts that do not match something."),
SHM_COMMAND_EXAMPLE("-tagname", 'Returns posts that are not tagged with "tagname".'),
SHM_COMMAND_EXAMPLE("-tagname -othertagname", 'Returns posts that are not tagged with "tagname" or "othertagname".'),
SHM_COMMAND_EXAMPLE("tagname -othertagname", 'Returns posts that are tagged with "tagname", but are not tagged with "othertagname".'),
//
BR(),
P('Wildcard searches are possible as well using * for "any one, more, or none" and ? for "any one".'),
SHM_COMMAND_EXAMPLE("tagn*", 'Returns posts that are tagged with "tagname", "tagnot", or anything else that starts with "tagn".'),
SHM_COMMAND_EXAMPLE("tagn?me", 'Returns posts that are tagged with "tagname", "tagnome", or anything else that starts with "tagn", has one character, and ends with "me".'),
//
//
//
HR(),
H3("Comparing values (<, <=, >, >=, or =)"),
P("For example, you can use this to count tags."),
SHM_COMMAND_EXAMPLE("tags=1", "Returns posts with exactly 1 tag."),
SHM_COMMAND_EXAMPLE("tags>0", "Returns posts with 1 or more tags."),
//
BR(),
P("Searching for posts by aspect ratio."),
P("The relation is calculated as: width / height."),
SHM_COMMAND_EXAMPLE("ratio=4:3", "Returns posts with an aspect ratio of 4:3."),
SHM_COMMAND_EXAMPLE("ratio>16:9", "Returns posts with an aspect ratio greater than 16:9."),
//
BR(),
P("Searching by dimentions."),
SHM_COMMAND_EXAMPLE("size=640x480", "Returns posts exactly 640 pixels wide by 480 pixels high."),
SHM_COMMAND_EXAMPLE("size>1920x1080", "Returns posts with a width larger than 1920 and a height larger than 1080."),
SHM_COMMAND_EXAMPLE("width=1000", "Returns posts exactly 1000 pixels wide."),
SHM_COMMAND_EXAMPLE("height=1000", "Returns posts exactly 1000 pixels high."),
//
BR(),
P("Searching by file size."),
P("Supported suffixes are kb, mb, and gb. Uses multiples of 1024."),
SHM_COMMAND_EXAMPLE("filesize=1", "Returns posts exactly 1 byte in size"),
SHM_COMMAND_EXAMPLE("filesize>100mb", "Returns posts greater than 100 megabytes in size."),
//
BR(),
P("Searching by date posted."),
P("Date format is yyyy-mm-dd. Date posted includes time component, so = will not work unless the time is exact."),
SHM_COMMAND_EXAMPLE("posted>=2019-07-19", "Returns posts posted on or after 2019-07-19."),
//
BR(),
P("Searching posts by media length."),
P("Available suffixes are ms, s, m, h, d, and y. A number by itself will be interpreted as milliseconds. Searches using = are not likely to work unless time is specified down to the millisecond."),
SHM_COMMAND_EXAMPLE("length>=1h", "Returns posts that are longer than an hour."),
SHM_COMMAND_EXAMPLE("length<=10h15m", "Returns posts that are shorter than 10 hours and 15 minutes."),
SHM_COMMAND_EXAMPLE("length>=10000", "Returns posts that are longer than 10,000 milliseconds, or 10 seconds."),
//
BR(),
P("Searching posts by ID."),
SHM_COMMAND_EXAMPLE("id=1234", "Find the 1234th thing uploaded."),
SHM_COMMAND_EXAMPLE("id>1234", "Find more recently posted things."),
//
//
//
HR(),
H3("Post attributes."),
P("Searching by MD5 hash."),
SHM_COMMAND_EXAMPLE("hash=0D3512CAA964B2BA5D7851AF5951F33B", "Returns post with MD5 hash 0D3512CAA964B2BA5D7851AF5951F33B."),
//
BR(),
P("Searching by file name."),
SHM_COMMAND_EXAMPLE("filename=picasso.jpg", 'Returns posts that are named "picasso.jpg".'),
//
BR(),
P("Searching for posts by source."),
SHM_COMMAND_EXAMPLE("source=https:///google.com/", 'Returns posts with a source of "https://google.com/".'),
SHM_COMMAND_EXAMPLE("source=any", "Returns posts with a source set."),
SHM_COMMAND_EXAMPLE("source=none", "Returns posts without a source set."),
//
//
//
HR(),
H3("Sorting search results"),
P("Sorting can be done using the pattern order:field_direction."),
P("Supported fields: id, width, height, filesize, filename."),
P("Direction can be either asc or desc, indicating ascending (123) or descending (321) order."),
SHM_COMMAND_EXAMPLE("order:id_asc", "Returns posts sorted by ID, smallest first."),
SHM_COMMAND_EXAMPLE("order:width_desc", "Returns posts sorted by width, largest first."),
);
}
}

View File

@ -81,7 +81,7 @@ class AddIPBanEvent extends Event
class IPBan extends Extension
{
/** @var IPBanTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function get_priority(): int
{

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class LinkImage extends Extension
{
/** @var LinkImageTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onDisplayingImage(DisplayingImageEvent $event)
{

View File

@ -61,7 +61,7 @@ class LinkImageTheme extends Themelet
protected function url(string $url, string $content, string $type): string
{
if ($content == null) {
if (empty($content)) {
$content=$url;
}

View File

@ -112,7 +112,13 @@ class MessageColumn extends Column
public function get_sql_filter(): string
{
return "({$this->name} LIKE :{$this->name}_0 AND priority >= :{$this->name}_1)";
$driver = $this->table->db->getAttribute(\PDO::ATTR_DRIVER_NAME);
switch ($driver) {
case "pgsql":
return "(LOWER({$this->name}) LIKE LOWER(:{$this->name}_0) AND priority >= :{$this->name}_1)";
default:
return "({$this->name} LIKE :{$this->name}_0 AND priority >= :{$this->name}_1)";
}
}
public function read_input(array $inputs)
@ -222,7 +228,7 @@ class LogTable extends Table
class LogDatabase extends Extension
{
/** @var LogDatabaseTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onInitExt(InitExtEvent $event)
{

View File

@ -19,7 +19,7 @@ class MediaException extends SCoreException
class Media extends Extension
{
/** @var MediaTheme */
protected ?Themelet $theme;
protected Themelet $theme;
private const LOSSLESS_FORMATS = [
MimeType::WEBP_LOSSLESS,
@ -83,7 +83,7 @@ class Media extends Extension
public function onSetupBuilding(SetupBuildingEvent $event)
{
$sb = $event->panel->create_new_block("Media Engines");
$sb = $event->panel->create_new_block("Media Engine Commands");
// if (self::imagick_available()) {
// try {
@ -95,7 +95,6 @@ class Media extends Extension
// }
// } else {
$sb->start_table();
$sb->add_table_header("Commands");
$sb->add_text_option(MediaConfig::CONVERT_PATH, "convert", true);
// }
@ -369,8 +368,8 @@ class Media extends Extension
global $config;
$ffprobe = $config->get_string(MediaConfig::FFPROBE_PATH);
if ($ffprobe == null || $ffprobe == "") {
throw new MediaException("ffprobe command configured");
if (empty($ffprobe)) {
throw new MediaException("ffprobe command not configured");
}
$args = [
@ -657,7 +656,7 @@ class Media extends Extension
$width = $info[0];
$height = $info[1];
if ($output_mime == null) {
if ($output_mime === null) {
/* If not specified, output to the same format as the original image */
switch ($info[2]) {
case IMAGETYPE_GIF:

View File

@ -11,7 +11,7 @@ require_once "mime_type.php";
class MimeSystem extends Extension
{
/** @var MimeSystemTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public const VERSION = "ext_mime_version";

View File

@ -166,7 +166,7 @@ class MimeType
for ($i = 0; $i < $cc; $i++) {
$byte = $comparison[$i];
if ($byte == null) {
if ($byte === null) {
continue;
} else {
$fileByte = $chunk[$i + 1];

View File

@ -33,7 +33,7 @@ class NotATagTable extends Table
class NotATag extends Extension
{
/** @var NotATagTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function get_priority(): int
{

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class Notes extends Extension
{
/** @var NotesTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{

View File

@ -102,7 +102,7 @@ class NumericScoreSetEvent extends Event
class NumericScore extends Extension
{
/** @var NumericScoreTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onDisplayingImage(DisplayingImageEvent $event)
{

View File

@ -9,6 +9,8 @@ use GQLA\Field;
use GQLA\Query;
use GQLA\Mutation;
use function MicroHTML\{emptyHTML, SPAN};
class SendPMEvent extends Event
{
public PM $pm;
@ -139,7 +141,7 @@ class PM
class PrivMsg extends Extension
{
/** @var PrivMsgTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{
@ -185,8 +187,8 @@ class PrivMsg extends Extension
if ($event->parent==="user") {
if ($user->can(Permissions::READ_PM)) {
$count = $this->count_pms($user);
$h_count = $count > 0 ? " <span class='unread'>($count)</span>" : "";
$event->add_nav_link("pm", new Link('user#private-messages'), "Private Messages$h_count");
$h_count = $count > 0 ? SPAN(["class"=>'unread'], "($count)") : "";
$event->add_nav_link("pm", new Link('user#private-messages'), emptyHTML("Private Messages", $h_count));
}
}
}
@ -196,8 +198,8 @@ class PrivMsg extends Extension
global $user;
if ($user->can(Permissions::READ_PM)) {
$count = $this->count_pms($user);
$h_count = $count > 0 ? " <span class='unread'>($count)</span>" : "";
$event->add_link("Private Messages$h_count", make_link("user", null, "private-messages"));
$h_count = $count > 0 ? SPAN(["class"=>'unread'], "($count)") : "";
$event->add_link(emptyHTML("Private Messages", $h_count), make_link("user", null, "private-messages"));
}
}

View File

@ -108,7 +108,7 @@ function _image_to_id(Image $image): int
class Pools extends Extension
{
/** @var PoolsTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onInitExt(InitExtEvent $event)
{
@ -357,9 +357,8 @@ class Pools extends Extension
case "import":
if ($this->have_permission($user, $pool)) {
$images = Image::find_images(
0,
$config->get_int(PoolsConfig::MAX_IMPORT_RESULTS, 1000),
Tag::explode($_POST["pool_tag"])
limit: $config->get_int(PoolsConfig::MAX_IMPORT_RESULTS, 1000),
tags: Tag::explode($_POST["pool_tag"])
);
$this->theme->pool_result($page, $images, $pool);
} else {
@ -467,14 +466,14 @@ class Pools extends Extension
{
global $config, $database, $user;
if ($config->get_bool(PoolsConfig::ADDER_ON_VIEW_IMAGE) && !$user->is_anonymous()) {
$pools = [];
if ($user->can(Permissions::POOLS_ADMIN)) {
$rows = $database->get_all("SELECT * FROM pools");
$pools = $database->get_pairs("SELECT id,title FROM pools ORDER BY title");
} else {
$rows = $database->get_all("SELECT * FROM pools WHERE user_id=:id", ["id" => $user->id]);
$pools = $database->get_pairs("SELECT id,title FROM pools WHERE user_id=:id ORDER BY title", ["id" => $user->id]);
}
if (count($rows) > 0) {
$pools = array_map([Pool::class, "makePool"], $rows);
$event->add_part($this->theme->get_adder_html($event->image, $pools));
if (count($pools) > 0) {
$event->add_part((string)$this->theme->get_adder_html($event->image, $pools));
}
}
}
@ -482,10 +481,7 @@ class Pools extends Extension
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
{
if ($event->key===HelpPages::SEARCH) {
$block = new Block();
$block->header = "Pools";
$block->body = $this->theme->get_help_html();
$event->add_block($block);
$event->add_block(new Block("Pools", $this->theme->get_help_html()));
}
}
@ -554,13 +550,11 @@ class Pools extends Extension
{
global $database;
$pools = array_map(
[Pool::class, "makePool"],
$database->get_all("SELECT * FROM pools ORDER BY title ")
);
$options = $database->get_pairs("SELECT id,title FROM pools ORDER BY title");
$event->add_action("bulk_pool_add_existing", "Add To (P)ool", "p", "", $this->theme->get_bulk_pool_selector($pools));
$event->add_action("bulk_pool_add_new", "Create Pool", "", "", $this->theme->get_bulk_pool_input($event->search_terms));
// TODO: Don't cast into strings, make BABBE accept HTMLElement instead.
$event->add_action("bulk_pool_add_existing", "Add To (P)ool", "p", "", (string)$this->theme->get_bulk_pool_selector($options));
$event->add_action("bulk_pool_add_new", "Create Pool", "", "", (string)$this->theme->get_bulk_pool_input($event->search_terms));
}
public function onBulkAction(BulkActionEvent $event)

View File

@ -4,6 +4,12 @@ declare(strict_types=1);
namespace Shimmie2;
use MicroHTML\HTMLElement;
use function MicroHTML\emptyHTML;
use function MicroHTML\rawHTML;
use function MicroHTML\{A,BR,DIV,INPUT,P,SCRIPT,SPAN,TABLE,TBODY,TD,TEXTAREA,TH,THEAD,TR};
class PoolsTheme extends Themelet
{
/**
@ -14,44 +20,36 @@ class PoolsTheme extends Themelet
{
global $page;
$linksPools = [];
//TODO: Use a 3 column table?
$linksPools = emptyHTML();
foreach ($navIDs as $poolID => $poolInfo) {
$linksPools[] = "<a href='" . make_link("pool/view/" . $poolID) . "'>" . html_escape($poolInfo['info']->title) . "</a>";
$div = DIV(SHM_A("pool/view/" . $poolID, $poolInfo["info"]->title));
if (!empty($poolInfo['nav'])) {
$navlinks = "";
if (!empty($poolInfo['nav']['prev'])) {
$navlinks .= '<a href="' . make_link('post/view/' . $poolInfo['nav']['prev']) . '" class="pools_prev_img">Prev</a>';
if (!empty($poolInfo["nav"])) {
if (!empty($poolInfo["nav"]["prev"])) {
$div->appendChild(SHM_A("post/view/" . $poolInfo["nav"]["prev"], "Prev", class: "pools_prev_img"));
}
if (!empty($poolInfo['nav']['next'])) {
$navlinks .= '<a href="' . make_link('post/view/' . $poolInfo['nav']['next']) . '" class="pools_next_img">Next</a>';
}
if (!empty($navlinks)) {
$navlinks .= "<div style='height: 5px'></div>";
$linksPools[] = $navlinks;
if (!empty($poolInfo["nav"]["next"])) {
$div->appendChild(SHM_A("post/view/" . $poolInfo["nav"]["next"], "Next", class: "pools_next_img"));
}
}
$linksPools->appendChild($div);
}
if (count($linksPools) > 0) {
$page->add_block(new Block("Pools", implode("<br>", $linksPools), "left"));
if (!empty($navIDs)) {
$page->add_block(new Block("Pools", $linksPools, "left"));
}
}
public function get_adder_html(Image $image, array $pools): string
public function get_adder_html(Image $image, array $pools): HTMLElement
{
$h = "";
foreach ($pools as $pool) {
$h .= "<option value='" . $pool->id . "'>" . html_escape($pool->title) . "</option>";
}
return "\n" . make_form(make_link("pool/add_post")) . "
<select name='pool_id'>
$h
</select>
<input type='hidden' name='image_id' value='{$image->id}'>
<input type='submit' value='Add Post to Pool'>
</form>
";
return SHM_SIMPLE_FORM(
"pool/add_post",
SHM_SELECT("pool_id", $pools),
INPUT(["type"=>"hidden", "name"=>"image_id", "value"=>$image->id]),
SHM_SUBMIT("Add Post to Pool")
);
}
/**
@ -59,45 +57,34 @@ class PoolsTheme extends Themelet
*/
public function list_pools(Page $page, array $pools, int $pageNumber, int $totalPages)
{
$html = '
<table id="poolsList" class="zebra">
<thead><tr>
<th>Name</th>
<th>Creator</th>
<th>Posts</th>
<th>Public</th>
</tr></thead><tbody>';
// Build up the list of pools.
$pool_rows = [];
foreach ($pools as $pool) {
$pool_link = '<a href="' . make_link("pool/view/" . $pool->id) . '">' . html_escape($pool->title) . "</a>";
$user_link = '<a href="' . make_link("user/" . url_escape($pool->user_name)) . '">' . html_escape($pool->user_name) . "</a>";
$public = ($pool->public ? "Yes" : "No");
$pool_link = SHM_A("pool/view/" . $pool->id, $pool->title);
$user_link = SHM_A("user/" . url_escape($pool->user_name), $pool->user_name);
$html .= "<tr>" .
"<td class='left'>" . $pool_link . "</td>" .
"<td>" . $user_link . "</td>" .
"<td>" . $pool->posts . "</td>" .
"<td>" . $public . "</td>" .
"</tr>";
$pool_rows[] = TR(
TD(["class"=>"left"], $pool_link),
TD($user_link),
TD($pool->posts),
TD($pool->public ? "Yes" : "No")
);
}
$html .= "</tbody></table>";
$table = TABLE(
["id"=>"poolsList", "class"=>"zebra"],
THEAD(TR(TH("Name"), TH("Creator"), TH("Posts"), TH("Public"))),
TBODY(...$pool_rows)
);
$order_html = '<select id="order_pool">';
$order_selected = $page->get_cookie('ui-order-pool');
$order_arr = ['created' => 'Recently created', 'updated' => 'Last updated', 'name' => 'Name', 'count' => 'Post Count'];
foreach ($order_arr as $value => $text) {
$selected = ($value == $order_selected ? "selected" : "");
$order_html .= "<option value=\"{$value}\" {$selected}>{$text}</option>\n";
}
$order_html .= '</select>';
$order_selected = $page->get_cookie('ui-order-pool');
$order_sel = SHM_SELECT("order_pool", $order_arr, selected_options: [$order_selected], attrs: ["id"=>"order_pool"]);
$this->display_top(null, "Pools");
$page->add_block(new Block("Order By", $order_html, "left", 15));
$page->add_block(new Block("Pools", $html, "main", 10));
$page->add_block(new Block("Order By", $order_sel, "left", 15));
$page->add_block(new Block("Pools", $table, position: 10));
$this->display_paginator($page, "pool/list", null, $pageNumber, $totalPages);
}
@ -107,19 +94,15 @@ class PoolsTheme extends Themelet
*/
public function new_pool_composer(Page $page)
{
$create_html = "
" . make_form(make_link("pool/create")) . "
<table>
<tr><td>Title:</td><td><input type='text' name='title'></td></tr>
<tr><td>Public?</td><td><input name='public' type='checkbox' value='Y' checked='checked'/></td></tr>
<tr><td>Description:</td><td><textarea name='description'></textarea></td></tr>
<tr><td colspan='2'><input type='submit' value='Create' /></td></tr>
</table>
</form>
";
$form = SHM_SIMPLE_FORM("pool/create", TABLE(
TR(TD("Title:"), TD(INPUT(["type"=>"text", "name"=>"title"]))),
TR(TD("Public?:"), TD(INPUT(["type"=>"checkbox", "name"=>"public", "value"=>"Y", "checked"=>"checked"]))),
TR(TD("Description:"), TD(TEXTAREA(["name"=>"description"]))),
TR(TD(["colspan"=>"2"], SHM_SUBMIT("Create")))
));
$this->display_top(null, "Create Pool");
$page->add_block(new Block("Create Pool", $create_html, "main", 20));
$page->add_block(new Block("Create Pool", $form, position: 20));
}
private function display_top(?Pool $pool, string $heading, bool $check_all = false)
@ -129,14 +112,16 @@ class PoolsTheme extends Themelet
$page->set_title($heading);
$page->set_heading($heading);
$poolnav_html = '
<a href="' . make_link("pool/list") . '">Pool Index</a>
<br><a href="' . make_link("pool/new") . '">Create Pool</a>
<br><a href="' . make_link("pool/updated") . '">Pool Changes</a>
';
$poolnav = emptyHTML(
SHM_A("pool/list", "Pool Index"),
BR(),
SHM_A("pool/new", "Create Pool"),
BR(),
SHM_A("pool/updated", "Pool Changes")
);
$page->add_block(new NavBlock());
$page->add_block(new Block("Pool Navigation", $poolnav_html, "left", 10));
$page->add_block(new Block("Pool Navigation", $poolnav, "left", 10));
if (!is_null($pool)) {
if ($pool->public || $user->can(Permissions::POOLS_ADMIN)) {// IF THE POOL IS PUBLIC OR IS ADMIN SHOW EDIT PANEL
@ -158,10 +143,9 @@ class PoolsTheme extends Themelet
$this->display_top($pool, "Pool: " . html_escape($pool->title));
$pool_images = '';
$pool_images = emptyHTML();
foreach ($images as $image) {
$thumb_html = $this->build_thumb_html($image);
$pool_images .= "\n" . $thumb_html . "\n";
$pool_images->appendChild($this->build_thumb_html($image));
}
$page->add_block(new Block("Viewing Posts", $pool_images, "main", 30));
@ -176,63 +160,77 @@ class PoolsTheme extends Themelet
{
global $user;
$editor = "\n" . make_form(make_link('pool/import')) . '
<input type="text" name="pool_tag" id="edit_pool_tag" placeholder="Please enter a tag"/>
<input type="submit" name="edit" id="edit_pool_import_btn" value="Import"/>
<input type="hidden" name="pool_id" value="' . $pool->id . '">
</form>
// This could become a SHM_INPUT function that also accepts 'type' and other attributes.
$_hidden=function (string $name, $value) {
return INPUT(["type"=>"hidden", "name"=>$name, "value"=>$value]);
};
' . make_form(make_link('pool/edit')) . '
<input type="submit" name="edit" id="edit_pool_btn" value="Edit Pool"/>
<input type="hidden" name="edit_pool" value="yes">
<input type="hidden" name="pool_id" value="' . $pool->id . '">
</form>
$_input_id = $_hidden("pool_id", $pool->id);
' . make_form(make_link('pool/order')) . '
<input type="submit" name="edit" id="edit_pool_order_btn" value="Order Pool"/>
<input type="hidden" name="order_view" value="yes">
<input type="hidden" name="pool_id" value="' . $pool->id . '">
</form>
' . make_form(make_link('pool/reverse')) . '
<input type="submit" name="edit" id="reverse_pool_order_btn" value="Reverse Order"/>
<input type="hidden" name="reverse_view" value="yes">
<input type="hidden" name="pool_id" value="' . $pool->id . '">
</form>
' . make_form(make_link('post/list/pool_id%3A' . $pool->id . '/1')) . '
<input type="submit" name="edit" id="postlist_pool_btn" value="Post/List View"/>
</form>
';
$editor = emptyHTML(
SHM_SIMPLE_FORM(
"pool/import",
INPUT(["type"=>"text", "name"=>"pool_tag", "id"=>"edit_pool_tag", "placeholder"=>"Please enter a tag"]),
$_input_id,
SHM_SUBMIT("Import", ["name"=>"edit", "id"=>"edit_pool_import_btn"])
),
SHM_SIMPLE_FORM(
"pool/edit",
$_hidden("edit_pool", "yes"),
$_input_id,
SHM_SUBMIT("Edit Pool", ["name"=>"edit", "id"=>"edit_pool_btn"]),
),
SHM_SIMPLE_FORM(
"pool/order",
$_hidden("order_view", "yes"),
$_input_id,
SHM_SUBMIT("Order Pool", ["name"=>"edit", "id"=>"edit_pool_order_btn"])
),
SHM_SIMPLE_FORM(
"pool/reverse",
$_hidden("reverse_view", "yes"),
$_input_id,
SHM_SUBMIT("Reverse Order", ["name"=>"edit", "id"=>"reverse_pool_order_btn"])
),
SHM_SIMPLE_FORM(
"pool/list/pool_id%3A" . $pool->id . "/1",
SHM_SUBMIT("Post/List View", ["name"=>"edit", "id"=>"postlist_pool_btn"])
)
);
if ($user->id == $pool->user_id || $user->can(Permissions::POOLS_ADMIN)) {
$editor .= "
<script type='text/javascript'>
<!--
function confirm_action() {
return confirm('Are you sure that you want to delete this pool?');
}
//-->
</script>
" . make_form(make_link("pool/nuke")) . "
<input type='submit' name='delete' id='delete_pool_btn' value='Delete Pool' onclick='return confirm_action()' />
<input type='hidden' name='pool_id' value='" . $pool->id . "'>
</form>
";
$editor->appendChild(
SCRIPT(
["type"=>"text/javascript"],
rawHTML("<!--
function confirm_action() {
return confirm('Are you sure that you want to delete this pool?');
}
//-->")
),
SHM_SIMPLE_FORM(
"pool/nuke",
$_input_id,
SHM_SUBMIT("Delete Pool", ["name"=>"delete", "id"=>"delete_pool_btn", "onclick"=>"return confirm_action()"])
)
);
}
if ($check_all) {
$editor .= "
<script type='text/javascript'>
<!--
function setAll(value) {
$('[name=\"check[]\"]').attr('checked', value);
}
//-->
</script>
<br><input type='button' name='CheckAll' value='Check All' onClick='setAll(true)'>
<input type='button' name='UnCheckAll' value='Uncheck All' onClick='setAll(false)'>
";
$editor->appendChild(
SCRIPT(
["type"=>"text/javascript"],
rawHTML("<!--
function setAll(value) {
$('[name=\"check[]\"]').attr('checked', value);
}
//-->")
),
INPUT(["type"=>"button", "name"=>"CheckAll", "value"=>"Check All", "onclick"=>"setAll(true)"]),
INPUT(["type"=>"button", "name"=>"UnCheckAll", "value"=>"Uncheck All", "onclick"=>"setAll(false)"])
);
}
$page->add_block(new Block("Manage Pool", $editor, "left", 15));
}
@ -242,30 +240,33 @@ class PoolsTheme extends Themelet
public function pool_result(Page $page, array $images, Pool $pool)
{
$this->display_top($pool, "Importing Posts", true);
$pool_images = "
<script type='text/javascript'>
function confirm_action() {
return confirm('Are you sure you want to add selected posts to this pool?');
}
</script>
";
$pool_images .= "<form action='" . make_link("pool/add_posts") . "' method='POST' name='checks'>";
$import = emptyHTML(
SCRIPT(
["type"=>"text/javascript"],
rawHTML("
function confirm_action() {
return confirm('Are you sure you want to add selected posts to this pool?');
}")
)
);
$form = SHM_FORM("pool/add_posts", name: "checks");
foreach ($images as $image) {
$thumb_html = $this->build_thumb_html($image);
$pool_images .= '<span class="thumb">' . $thumb_html . '<br>' .
'<input name="check[]" type="checkbox" value="' . $image->id . '" />' .
'</span>';
$form->appendChild(
SPAN(["class"=>"thumb"], $this->build_thumb_html($image), BR(), INPUT(["type"=>"checkbox", "name"=>"check[]", "value"=>$image->id])),
);
}
$pool_images .= "<br>" .
"<input type='submit' name='edit' id='edit_pool_add_btn' value='Add Selected' onclick='return confirm_action()'/>" .
"<input type='hidden' name='pool_id' value='" . $pool->id . "'>" .
"</form>";
$form->appendChild(
BR(),
SHM_SUBMIT("Add Selected", ["name"=>"edit", "id"=>"edit_pool_add_btn", "onclick"=>"return confirm_action()"]),
INPUT(["type"=>"hidden", "name"=>"pool_id", "value"=>$pool->id])
);
$page->add_block(new Block("Import", $pool_images, "main", 30));
$import->appendChild($form);
$page->add_block(new Block("Import", $import, "main", 30));
}
@ -277,23 +278,22 @@ class PoolsTheme extends Themelet
{
$this->display_top($pool, "Sorting Pool");
$pool_images = "\n<form action='" . make_link("pool/order") . "' method='POST' name='checks'>";
$i = 0;
foreach ($images as $image) {
$thumb_html = $this->build_thumb_html($image);
$pool_images .= '<span class="thumb">' . "\n" . $thumb_html . "\n" .
'<br><input name="imgs[' . $i . '][]" type="number" style="max-width:50px;" value="' . $image->image_order . '" />' .
'<input name="imgs[' . $i . '][]" type="hidden" value="' . $image->id . '" />' .
'</span>';
$i++;
$form = SHM_FORM("pool/order", name: "checks");
foreach ($images as $i=>$image) {
$form->appendChild(SPAN(
["class"=>"thumb"],
$this->build_thumb_html($image),
INPUT(["type"=>"number", "name"=>"imgs[$i][]", "value"=>$image->image_order, "style"=>"max-width: 50px;"]),
INPUT(["type"=>"hidden", "name"=>"imgs[$i][]", "value"=>$image->id])
));
}
$pool_images .= "<br>" .
"<input type='submit' name='edit' id='edit_pool_order' value='Order'/>" .
"<input type='hidden' name='pool_id' value='" . $pool->id . "'>" .
"</form>";
$form->appendChild(
INPUT(["type"=>"hidden", "name"=>"pool_id", "value"=>$pool->id]),
SHM_SUBMIT("Order", ["name"=>"edit", "id"=>"edit_pool_order"])
);
$page->add_block(new Block("Sorting Posts", $pool_images, "main", 30));
$page->add_block(new Block("Sorting Posts", $form, position: 30));
}
/**
@ -304,35 +304,35 @@ class PoolsTheme extends Themelet
*/
public function edit_pool(Page $page, Pool $pool, array $images)
{
/* EDIT POOL DESCRIPTION */
$desc_html = "
" . make_form(make_link("pool/edit_description")) . "
<textarea name='description'>" . $pool->description . "</textarea><br />
<input type='hidden' name='pool_id' value='" . $pool->id . "'>
<input type='submit' value='Change Description' />
</form>
";
$_input_id = INPUT(["type"=>"hidden", "name"=>"pool_id", "value"=>$pool->id]);
/* REMOVE POOLS */
$pool_images = "\n<form action='" . make_link("pool/remove_posts") . "' method='POST' name='checks'>";
$desc_form = SHM_SIMPLE_FORM(
"pool/edit/description",
TEXTAREA(["name"=>"description"], $pool->description),
BR(),
$_input_id,
SHM_SUBMIT("Change Description")
);
$images_form = SHM_FORM("pool/remove_posts", name: "checks");
foreach ($images as $image) {
$thumb_html = $this->build_thumb_html($image);
$pool_images .= '<span class="thumb">' . "\n" . $thumb_html . "\n" .
'<br><input name="check[]" type="checkbox" value="' . $image->id . '" />' .
'</span>';
$images_form->appendChild(SPAN(
["class"=>"thumb"],
$this->build_thumb_html($image),
INPUT(["type"=>"checkbox", "name"=>"check[]", "value"=>$image->id])
));
}
$pool_images .= "<br>" .
"<input type='submit' name='edit' id='edit_pool_remove_sel' value='Remove Selected'/>" .
"<input type='hidden' name='pool_id' value='" . $pool->id . "'>" .
"</form>";
$images_form->appendChild(
BR(),
$_input_id,
SHM_SUBMIT("Remove Selected", ["name"=>"edit", "id"=>"edit_pool_remove_sel"])
);
$pool->description = ""; //This is a rough fix to avoid showing the description twice.
$this->display_top($pool, "Editing Pool", true);
$page->add_block(new Block("Editing Description", $desc_html, "main", 28));
$page->add_block(new Block("Editing Posts", $pool_images, "main", 30));
$page->add_block(new Block("Editing Description", $desc_form, position: 28));
$page->add_block(new Block("Editing Posts", $images_form, position: 30));
}
/**
@ -341,21 +341,17 @@ class PoolsTheme extends Themelet
public function show_history(array $histories, int $pageNumber, int $totalPages)
{
global $page;
$html = '
<table id="poolsList" class="zebra">
<thead><tr>
<th>Pool</th>
<th>Post Count</th>
<th>Changes</th>
<th>Updater</th>
<th>Date</th>
<th>Action</th>
</tr></thead><tbody>';
$table = TABLE(
["id"=>"poolsList", "class"=>"zebra"],
THEAD(TR(TH("Pool"), TH("Post Count"), TH("Changes"), TH("Updater"), TH("Date"), TH("Action")))
);
$body = [];
foreach ($histories as $history) {
$pool_link = "<a href='" . make_link("pool/view/" . $history['pool_id']) . "'>" . html_escape($history['title']) . "</a>";
$user_link = "<a href='" . make_link("user/" . url_escape($history['user_name'])) . "'>" . html_escape($history['user_name']) . "</a>";
$revert_link = "<a href='" . make_link("pool/revert/" . $history['id']) . "'>Revert</a>";
$pool_link = SHM_A("pool/view/" . $history["pool_id"], $history["title"]);
$user_link = SHM_A("user/" . url_escape($history["user_name"]), $history["user_name"]);
$revert_link = SHM_A(("pool/revert/" . $history["id"]), "Revert");
if ($history['action'] == 1) {
$prefix = "+";
@ -365,69 +361,74 @@ class PoolsTheme extends Themelet
throw new \RuntimeException("history['action'] not in {0, 1}");
}
$images = trim($history['images']);
$images = trim($history["images"]);
$images = explode(" ", $images);
$image_link = "";
$image_links = emptyHTML();
foreach ($images as $image) {
$image_link .= "<a href='" . make_link("post/view/" . $image) . "'>" . $prefix . $image . " </a>";
$image_links->appendChild(" ", SHM_A("post/view/" . $image, $prefix . $image));
}
$html .= "<tr>" .
"<td class='left'>" . $pool_link . "</td>" .
"<td>" . $history['count'] . "</td>" .
"<td>" . $image_link . "</td>" .
"<td>" . $user_link . "</td>" .
"<td>" . $history['date'] . "</td>" .
"<td>" . $revert_link . "</td>" .
"</tr>";
$body[] = TR(
TD(["class"=>"left"], $pool_link),
TD($history["count"]),
TD($image_links),
TD($user_link),
TD($history["date"]),
TD($revert_link)
);
}
$html .= "</tbody></table>";
$table->appendChild(TBODY(...$body));
$this->display_top(null, "Recent Changes");
$page->add_block(new Block("Recent Changes", $html, "main", 10));
$page->add_block(new Block("Recent Changes", $table, position: 10));
$this->display_paginator($page, "pool/updated", null, $pageNumber, $totalPages);
}
public function get_bulk_pool_selector(array $pools): string
public function get_bulk_pool_selector(array $options): HTMLElement
{
$output = "<select name='bulk_pool_select' required='required'><option></option>";
foreach ($pools as $pool) {
$output .= "<option value='" . $pool->id . "' >" . $pool->title . "</option>";
}
return $output . "</select>";
return SHM_SELECT("bulk_pool_select", $options, required: true, empty_option: true);
}
public function get_bulk_pool_input(array $search_terms): string
public function get_bulk_pool_input(array $search_terms): HTMLElement
{
return "<input type='text' name='bulk_pool_new' placeholder='New pool' required='required' value='".(implode(" ", $search_terms))."' />";
return INPUT(
[
"type"=>"text",
"name"=>"bulk_pool_new",
"placeholder"=>"New Pool",
"required"=>"",
"value"=>implode(" ", $search_terms)
]
);
}
public function get_help_html(): string
public function get_help_html(): HTMLElement
{
return '<p>Search for posts that are in a pool.</p>
<div class="command_example">
<pre>pool=1</pre>
<p>Returns posts in pool #1.</p>
</div>
<div class="command_example">
<pre>pool=any</pre>
<p>Returns posts in any pool.</p>
</div>
<div class="command_example">
<pre>pool=none</pre>
<p>Returns posts not in any pool.</p>
</div>
<div class="command_example">
<pre>pool_by_name=swimming</pre>
<p>Returns posts in the "swimming" pool.</p>
</div>
<div class="command_example">
<pre>pool_by_name=swimming_pool</pre>
<p>Returns posts in the "swimming pool" pool. Note that the underscore becomes a space</p>
</div>
';
return emptyHTML(
P("Search for posts that are in a pool."),
SHM_COMMAND_EXAMPLE(
"pool=1",
"Returns posts in pool #1."
),
SHM_COMMAND_EXAMPLE(
"pool=any",
"Returns posts in any pool."
),
SHM_COMMAND_EXAMPLE(
"pool=none",
"Returns posts not in any pool."
),
SHM_COMMAND_EXAMPLE(
"pool_by_name=swimming",
"Returns posts in the \"swimming\" pool."
),
SHM_COMMAND_EXAMPLE(
"pool_by_name=swimming_pool",
"Returns posts in the \"swimming pool\" pool. Note that the underscore becomes a space."
)
);
}
}

View File

@ -10,7 +10,7 @@ require_once "events/post_title_set_event.php";
class PostTitles extends Extension
{
/** @var PostTitlesTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function get_priority(): int
{

View File

@ -14,11 +14,11 @@ abstract class PrivateImageConfig
class PrivateImage extends Extension
{
/** @var PrivateImageTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onInitExt(InitExtEvent $event)
{
Image::$bool_props[] = "private ";
Image::$bool_props[] = "private";
}
public function onInitUserConfig(InitUserConfigEvent $event)
@ -29,9 +29,12 @@ class PrivateImage extends Extension
public function onUserOptionsBuilding(UserOptionsBuildingEvent $event)
{
global $user;
$sb = $event->panel->create_new_block("Private Posts");
$sb->start_table();
$sb->add_bool_option(PrivateImageConfig::USER_SET_DEFAULT, "Mark posts private by default", true);
if ($user->can(Permissions::SET_PRIVATE_IMAGE)) {
$sb->add_bool_option(PrivateImageConfig::USER_SET_DEFAULT, "Mark posts private by default", true);
}
$sb->add_bool_option(PrivateImageConfig::USER_VIEW_DEFAULT, "View private posts by default", true);
$sb->end_table();
}
@ -114,7 +117,7 @@ class PrivateImage extends Extension
{
global $user, $page;
if ($event->image->private==true && $event->image->owner_id!=$user->id && !$user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) {
if ($event->image->private===true && $event->image->owner_id!=$user->id && !$user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/list"));
}
@ -228,8 +231,8 @@ class PrivateImage extends Extension
public function onImageAddition(ImageAdditionEvent $event)
{
global $user_config;
if ($user_config->get_bool(PrivateImageConfig::USER_SET_DEFAULT)) {
global $user, $user_config;
if ($user_config->get_bool(PrivateImageConfig::USER_SET_DEFAULT) && $user->can(Permissions::SET_PRIVATE_IMAGE)) {
self::privatize_image($event->image->id);
}
}

View File

@ -10,7 +10,7 @@ class PrivateImageTheme extends Themelet
{
public function get_image_admin_html(Image $image): string
{
if ($image->private==false) {
if ($image->private===false) {
$html = SHM_SIMPLE_FORM(
'privatize_image/'.$image->id,
INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image->id]),

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class QRImage extends Extension
{
/** @var QRImageTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onDisplayingImage(DisplayingImageEvent $event)
{

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class RandomImage extends Extension
{
/** @var RandomImageTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class RandomList extends Extension
{
/** @var RandomListTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{

View File

@ -73,7 +73,7 @@ abstract class RatingsConfig
class Ratings extends Extension
{
/** @var RatingsTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public const UNRATED_KEYWORDS = ["unknown", "unrated"];
@ -183,7 +183,7 @@ class Ratings extends Extension
public function onBulkImport(BulkImportEvent $event)
{
if (array_key_exists("rating", $event->fields)
&& $event->fields['rating'] != null
&& $event->fields['rating'] !== null
&& Ratings::rating_is_valid($event->fields['rating'])) {
$this->set_rating($event->image->id, $event->fields['rating'], "");
}
@ -203,7 +203,7 @@ class Ratings extends Extension
{
global $user;
$event->add_part(
$this->theme->get_rater_html(
(string)$this->theme->get_rater_html(
$event->image->id,
$event->image->rating,
$user->can(Permissions::EDIT_IMAGE_RATING)
@ -231,13 +231,8 @@ class Ratings extends Extension
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
{
if ($event->key===HelpPages::SEARCH) {
$block = new Block();
$block->header = "Ratings";
$ratings = self::get_sorted_ratings();
$block->body = $this->theme->get_help_html($ratings);
$event->add_block($block);
$event->add_block(new Block("Ratings", $this->theme->get_help_html($ratings)));
}
}
@ -314,7 +309,7 @@ class Ratings extends Extension
}
}
$this->theme->display_form($original_values, self::get_sorted_ratings());
$this->theme->display_form($original_values);
}
public function onAdminAction(AdminActionEvent $event)
@ -346,7 +341,7 @@ class Ratings extends Extension
global $user;
if ($user->can(Permissions::BULK_EDIT_IMAGE_RATING)) {
$event->add_action("bulk_rate", "Set (R)ating", "r", "", $this->theme->get_selection_rater_html(["?"]));
$event->add_action("bulk_rate", "Set (R)ating", "r", "", (string)$this->theme->get_selection_rater_html(selected_options: ["?"]));
}
}
@ -416,6 +411,21 @@ class Ratings extends Extension
return $ratings;
}
public static function get_ratings_dict(array $ratings=null): array
{
if (!isset($ratings)) {
$ratings = self::get_sorted_ratings();
}
return array_combine(
array_map(function ($o) {
return $o->code;
}, $ratings),
array_map(function ($o) {
return $o->name;
}, $ratings)
);
}
public static function get_user_class_privs(User $user): array
{
global $config;

View File

@ -4,96 +4,83 @@ declare(strict_types=1);
namespace Shimmie2;
use MicroHTML\HTMLElement;
use function MicroHTML\emptyHTML;
use function MicroHTML\{P,SPAN,TABLE,TD,TH,TR};
class RatingsTheme extends Themelet
{
public function get_rater_html(int $image_id, string $rating, bool $can_rate): string
public function get_selection_rater_html(string $name = "rating", array $ratings = [], array $selected_options = []): HTMLElement
{
return SHM_SELECT($name, !empty($ratings) ? $ratings : Ratings::get_ratings_dict(), required: true, selected_options: $selected_options);
}
public function get_rater_html(int $image_id, string $rating, bool $can_rate): HTMLElement
{
$human_rating = Ratings::rating_to_human($rating);
$html = "
<tr>
<th>Rating</th>
<td>
".($can_rate ? "
<span class='view'>$human_rating</span>
<span class='edit'>
".$this->get_selection_rater_html([$rating])."
</span>
" : "
$human_rating
")."
</td>
</tr>
";
$html = TR(TH("Rating"));
if ($can_rate) {
$selector = $this->get_selection_rater_html(selected_options: [$rating]);
$html->appendChild(TD(
SPAN(["class"=>"view"], $human_rating),
SPAN(["class"=>"edit"], $selector)
));
} else {
$html->appendChild(TD($human_rating));
}
return $html;
}
public function display_form(array $current_ratings, array $available_ratings)
public function display_form(array $current_ratings)
{
global $page;
$html = make_form(make_link("admin/update_ratings"))."<table class='form'><tr>
<th>Change</th><td><select name='rating_old' required='required'><option></option>";
foreach ($current_ratings as $key=>$value) {
$html .= "<option value='$key'>$value</option>";
}
$html .= "</select></td></tr>
<tr><th>To</th><td><select name='rating_new' required='required'><option></option>";
foreach ($available_ratings as $value) {
$html .= "<option value='$value->code'>$value->name</option>";
}
$html .= "</select></td></tr>
<tr><td colspan='2'><input type='submit' value='Update'></td></tr></table>
</form>\n";
$page->add_block(new Block("Update Ratings", $html));
$table = TABLE(
["class"=>"form"],
TR(TH("Change"), TD($this->get_selection_rater_html("rating_old", $current_ratings))),
TR(TH("To"), TD($this->get_selection_rater_html("rating_new"))),
TR(TD(["colspan"=>"2"], SHM_SUBMIT("Update")))
);
$page->add_block(new Block("Update Ratings", SHM_SIMPLE_FORM("admin/update_ratings", $table)));
}
public function get_selection_rater_html(array $selected_options, bool $multiple = false, array $available_options = null): string
public function get_help_html(array $ratings): HTMLElement
{
$output = "<select name='rating".($multiple ? "[]' multiple='multiple'" : "' ")." >";
$options = Ratings::get_sorted_ratings();
foreach ($options as $option) {
if ($available_options!=null && !in_array($option->code, $available_options)) {
continue;
}
$output .= "<option value='".$option->code."' ".
(in_array($option->code, $selected_options) ? "selected='selected'" : "")
.">".$option->name."</option>";
}
return $output."</select>";
}
public function get_help_html(array $ratings): string
{
$output = '<p>Search for posts with one or more possible ratings.</p>
<div class="command_example">
<pre>rating:'.$ratings[0]->search_term.'</pre>
<p>Returns posts with the '.$ratings[0]->name.' rating.</p>
</div>
<p>Ratings can be abbreviated to a single letter as well</p>
<div class="command_example">
<pre>rating:'.$ratings[0]->code.'</pre>
<p>Returns posts with the '.$ratings[0]->name.' rating.</p>
</div>
<p>If abbreviations are used, multiple ratings can be searched for.</p>
<div class="command_example">
<pre>rating:'.$ratings[0]->code.$ratings[1]->code.'</pre>
<p>Returns posts with the '.$ratings[0]->name.' or '.$ratings[1]->name.' rating.</p>
</div>
<p>Available ratings:</p>
<table>
<tr><th>Name</th><th>Search Term</th><th>Abbreviation</th></tr>
';
$rating_rows = [TR(TH("Name"), TH("Search Term"), TH("Abbreviation"))];
foreach ($ratings as $rating) {
$output .= "<tr><td>{$rating->name}</td><td>{$rating->search_term}</td><td>{$rating->code}</td></tr>";
$rating_rows[] = TR(TD($rating->name), TD($rating->search_term), TD($rating->code));
}
$output .= "</table>";
return $output;
return emptyHTML(
P("Search for posts with one or more possible ratings."),
SHM_COMMAND_EXAMPLE(
"rating:" . $ratings[0]->search_term,
"Returns posts with the " . $ratings[0]->name . " rating."
),
P("Ratings can be abbreviated to a single letter as well."),
SHM_COMMAND_EXAMPLE(
"rating:" . $ratings[0]->code,
"Returns posts with the " . $ratings[0]->name . " rating."
),
P("If abbreviations are used, multiple ratings can be searched for."),
SHM_COMMAND_EXAMPLE(
"rating:" . $ratings[0]->code . $ratings[1]->code,
"Returns posts with the " . $ratings[0]->name . " or " . $ratings[1]->name . " rating."
),
P("Available ratings:"),
TABLE(...$rating_rows)
);
}
public function get_user_options(User $user, array $selected_ratings, array $available_ratings): string
// This wasn't being used at all
/* public function get_user_options(User $user, array $selected_ratings, array $available_ratings): string
{
$html = "
<p>".make_form(make_link("user_admin/default_ratings"))."
@ -105,7 +92,7 @@ class RatingsTheme extends Themelet
<tbody>
<tr><td>This controls the default rating search results will be filtered by, and nothing else. To override in your search results, add rating:* to your search.</td></tr>
<tr><td>
".$this->get_selection_rater_html($selected_ratings, true, $available_ratings)."
".SHM_SELECT("ratings", selected_options: $selected_ratings, multiple: true, options: $available_ratings)."
</td></tr>
</tbody>
<tfoot>
@ -115,5 +102,5 @@ class RatingsTheme extends Themelet
</form>
";
return $html;
}
} */
}

View File

@ -7,7 +7,7 @@ namespace Shimmie2;
class RegenThumb extends Extension
{
/** @var RegenThumbTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function regenerate_thumbnail(Image $image, bool $force = true): bool
{
@ -30,7 +30,7 @@ class RegenThumb extends Extension
}
if ($event->page_matches("regen_thumb/mass") && $user->can(Permissions::DELETE_IMAGE) && isset($_POST['tags'])) {
$tags = Tag::explode(strtolower($_POST['tags']), false);
$images = Image::find_images(0, 10000, $tags);
$images = Image::find_images(limit: 10000, tags: $tags);
foreach ($images as $image) {
$this->regenerate_thumbnail($image);

View File

@ -21,7 +21,7 @@ class ImageRelationshipSetEvent extends Event
class Relationships extends Extension
{
/** @var RelationshipsTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public const NAME = "Relationships";

View File

@ -43,7 +43,7 @@ class ImageReport
class ReportImage extends Extension
{
/** @var ReportImageTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onPageRequest(PageRequestEvent $event)
{

View File

@ -20,7 +20,7 @@ abstract class ResizeConfig
class ResizeImage extends Extension
{
/** @var ResizeImageTheme */
protected ?Themelet $theme;
protected Themelet $theme;
/**
* Needs to be after the data processing extensions

View File

@ -19,7 +19,7 @@ class ImageRotateException extends SCoreException
class RotateImage extends Extension
{
/** @var RotateImageTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public const SUPPORTED_MIME = [MimeType::JPEG, MimeType::PNG, MimeType::GIF, MimeType::WEBP];

View File

@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
/* needed for access to build_thumb_html */
class RSSImagesTheme extends Themelet
{
}

View File

@ -20,7 +20,7 @@ if ( // kill these glitched requests immediately
class Rule34 extends Extension
{
/** @var Rule34Theme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onImageDeletion(ImageDeletionEvent $event)
{
@ -114,7 +114,7 @@ class Rule34 extends Extension
global $database, $page, $user;
# Database might not be connected at this point...
#$database->set_timeout(DATABASE_TIMEOUT+15000); // deleting users can take a while
#$database->set_timeout(null); // deleting users can take a while
if (function_exists("sd_notify_watchdog")) {
\sd_notify_watchdog();

View File

@ -294,7 +294,7 @@ class SetupBlock extends Block
class Setup extends Extension
{
/** @var SetupTheme */
protected ?Themelet $theme;
protected Themelet $theme;
public function onInitExt(InitExtEvent $event)
{

View File

@ -21,7 +21,7 @@ class _SafeImage
public function __construct(Image $img)
{
$_id = $img->id;
assert($_id != null);
assert($_id !== null);
$this->id = $_id;
$this->height = $img->height;
$this->width = $img->width;

View File

@ -42,7 +42,7 @@ class XMLSitemap extends Extension
private function handle_smaller_sitemap()
{
/* --- Add latest images to sitemap with higher priority --- */
$latestimages = Image::find_images(0, 50, []);
$latestimages = Image::find_images(limit: 50);
if (empty($latestimages)) {
return;
}
@ -85,7 +85,7 @@ class XMLSitemap extends Extension
$this->add_sitemap_queue($popular_tags, "monthly", "0.9" /* not sure how to deal with date here */);
/* --- Add latest images to sitemap with higher priority --- */
$latestimages = Image::find_images(0, 50, []);
$latestimages = Image::find_images(limit: 50);
$latestimages_urllist = [];
$latest_image = null;
foreach ($latestimages as $arrayid => $image) {
@ -107,7 +107,7 @@ class XMLSitemap extends Extension
$this->add_sitemap_queue($other_tags, "monthly", "0.7" /* not sure how to deal with date here */);
/* --- Add all other images to sitemap with lower priority --- */
$otherimages = Image::find_images(51, 10000000, []);
$otherimages = Image::find_images(offset: 51, limit: 10000000);
$image = null;
foreach ($otherimages as $arrayid => $image) {
// create url from image id's

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