From 1257d87e511682376d3e414fbcff7a48e3b91b0d Mon Sep 17 00:00:00 2001 From: Michael Yick Date: Sun, 10 Sep 2023 09:16:26 -0500 Subject: [PATCH] Merge and integrate upstream fixes --- .github/workflows/main.yml | 26 + .github/workflows/publish.yml | 10 +- .scrutinizer.yml | 20 - Dockerfile | 42 +- core/basepage.php | 16 +- core/basethemelet.php | 87 +++- core/block.php | 2 +- core/cacheengine.php | 3 +- core/database.php | 74 +-- core/extension.php | 6 +- core/imageboard/image.php | 96 +--- core/imageboard/misc.php | 2 +- core/imageboard/tag.php | 38 +- core/microhtml.php | 153 ++++++ core/permissions.php | 31 +- core/polyfills.php | 9 +- core/send_event.php | 26 +- core/tests/polyfills.test.php | 13 +- core/user.php | 10 + core/userclass.php | 1 + core/util.php | 83 +-- ext/admin/main.php | 4 +- ext/alias_editor/main.php | 2 +- ext/alias_editor/theme.php | 31 +- ext/approval/main.php | 18 +- ext/approval/theme.php | 40 +- ext/artists/main.php | 9 +- ext/artists/theme.php | 30 +- ext/auto_tagger/main.php | 2 +- ext/autocomplete/main.php | 2 +- ext/biography/main.php | 2 +- .../script.js => ext/biography/main.php: | 0 ext/blocks/main.php | 28 +- ext/blocks/theme.php | 12 +- ext/blotter/main.php | 2 +- ext/bulk_actions/main.php | 10 +- ext/bulk_actions/script.js | 2 +- ext/bulk_actions/theme.php | 4 +- ext/bulk_add/main.php | 4 +- ext/bulk_add_csv/main.php | 4 +- ext/bulk_parent_child/main.php | 2 +- ext/comment/main.php | 2 +- ext/cron_uploader/info.php | 2 +- ext/cron_uploader/main.php | 4 +- ext/cron_uploader/theme.php | 18 +- ext/danbooru_api/main.php | 4 + ext/danbooru_api/test.php | 1 + ext/downtime/main.php | 2 +- ext/emoticons_list/main.php | 2 +- ext/et/main.php | 2 +- ext/ext_manager/main.php | 2 +- ext/favorites/main.php | 2 +- ext/featured/main.php | 2 +- ext/forum/main.php | 2 +- ext/handle_svg/main.php | 2 +- ext/handle_video/main.php | 82 ++- ext/help_pages/main.php | 16 +- ext/help_pages/theme.php | 1 + ext/holiday/main.php | 2 +- ext/home/counters/cavemanon/Ϫ.gif | Bin 3327 -> 0 bytes ext/home/counters/cavemanon/Ϫ.png | Bin 8018 -> 0 bytes ext/home/main.php | 33 +- ext/home/theme.php | 2 +- ext/image/main.php | 2 +- ext/image_hash_ban/main.php | 2 +- ext/image_view_counter/main.php | 2 +- ext/index/main.php | 11 +- ext/index/script.js | 2 +- ext/index/theme.php | 317 ++++-------- ext/ipban/main.php | 2 +- ext/link_image/main.php | 2 +- ext/link_image/theme.php | 2 +- ext/log_db/main.php | 10 +- ext/media/main.php | 11 +- ext/mime/main.php | 2 +- ext/mime/mime_type.php | 2 +- ext/not_a_tag/main.php | 2 +- ext/notes/main.php | 2 +- ext/numeric_score/main.php | 2 +- ext/pm/main.php | 12 +- ext/pools/main.php | 32 +- ext/pools/theme.php | 471 +++++++++--------- ext/post_titles/main.php | 2 +- ext/private_image/main.php | 15 +- ext/private_image/theme.php | 2 +- ext/qr_code/main.php | 2 +- ext/random_image/main.php | 2 +- ext/random_list/main.php | 2 +- ext/rating/main.php | 32 +- ext/rating/theme.php | 137 +++-- ext/regen_thumb/main.php | 4 +- ext/relationships/main.php | 2 +- ext/report_image/main.php | 2 +- ext/resize/main.php | 2 +- ext/rotate/main.php | 2 +- ext/rss_images/theme.php | 10 - ext/rule34/main.php | 4 +- ext/setup/main.php | 2 +- ext/shimmie_api/main.php | 2 +- ext/sitemap/main.php | 6 +- ext/source_history/main.php | 4 +- ext/tag_categories/main.php | 2 +- ext/tag_edit/main.php | 48 +- ext/tag_editcloud/info.php | 2 +- ext/tag_editcloud/main.php | 2 +- ext/tag_history/main.php | 4 +- ext/tag_history/theme.php | 2 +- ext/tag_list/main.php | 2 +- ext/tag_list/theme.php | 2 +- ext/tag_tools/main.php | 2 +- ext/tag_tools/theme.php | 2 +- ext/tips/main.php | 2 +- ext/transcode/main.php | 2 +- ext/transcode_video/main.php | 2 +- ext/trash/main.php | 10 +- ext/update/main.php | 2 +- ext/upload/main.php | 2 +- ext/upload/theme.php | 82 ++- ext/user/events.php | 4 +- ext/user/main.php | 15 +- ext/user/test.php | 16 +- ext/user/theme.php | 60 ++- ext/user_config/main.php | 10 +- ext/user_config/theme.php | 1 + ext/view/main.php | 2 +- ext/wiki/info.php | 2 +- ext/wiki/main.php | 2 +- index.php | 1 + tests/docker-init.sh | 10 +- tests/router.php | 4 +- themes/danbooru/themelet.class.php | 30 +- themes/danbooru/view.theme.php | 2 +- themes/danbooru2/themelet.class.php | 30 +- themes/danbooru2/view.theme.php | 2 +- themes/default/background.svg | 222 --------- themes/default/style.css | 108 ++-- themes/futaba/style.css | 45 +- themes/lite/page.class.php | 25 +- themes/lite/style.css | 25 +- themes/lite/themelet.class.php | 50 +- themes/lite/view.theme.php | 2 +- themes/material/home.theme.php | 77 --- themes/material/material.min.css | 9 - themes/material/material.min.js | 10 - themes/material/mdl-LICENSE | 212 -------- themes/material/page.class.php | 278 ----------- themes/material/script0.js | 95 ---- themes/material/style.css | 17 - themes/material/themelet.class.php | 9 - themes/material/upload.theme.php | 16 - themes/material/user.theme.php | 36 -- themes/material/view.theme.php | 75 --- themes/rule34v2/header.inc | 2 +- themes/rule34v2/tag_edit.theme.php | 15 + themes/rule34v2/themelet.class.php | 39 +- themes/rule34v2/upload.theme.php | 78 ++- themes/rule34v2/user.theme.php | 5 +- 157 files changed, 1684 insertions(+), 2388 deletions(-) create mode 100644 .github/workflows/main.yml delete mode 100644 .scrutinizer.yml create mode 100644 core/microhtml.php rename themes/material/script.js => ext/biography/main.php: (100%) delete mode 100644 ext/home/counters/cavemanon/Ϫ.gif delete mode 100644 ext/home/counters/cavemanon/Ϫ.png delete mode 100644 ext/rss_images/theme.php delete mode 100644 themes/default/background.svg delete mode 100644 themes/material/home.theme.php delete mode 100644 themes/material/material.min.css delete mode 100644 themes/material/material.min.js delete mode 100644 themes/material/mdl-LICENSE delete mode 100644 themes/material/page.class.php delete mode 100644 themes/material/script0.js delete mode 100644 themes/material/style.css delete mode 100644 themes/material/themelet.class.php delete mode 100644 themes/material/upload.theme.php delete mode 100644 themes/material/user.theme.php delete mode 100644 themes/material/view.theme.php diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..5d471d6f --- /dev/null +++ b/.github/workflows/main.yml @@ -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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 89f1fb79..2e8fd122 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 3f425840..00000000 --- a/.scrutinizer.yml +++ /dev/null @@ -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 diff --git a/Dockerfile b/Dockerfile index 944d0439..75508f59 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/core/basepage.php b/core/basepage.php index 410ae35b..2acdb953 100644 --- a/core/basepage.php +++ b/core/basepage.php @@ -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; Shimmie © Shish & The Team - 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; diff --git a/core/basethemelet.php b/core/basethemelet.php index 2338d34f..d646e851 100644 --- a/core/basethemelet.php +++ b/core/basethemelet.php @@ -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 "". - "$h_tip". - "\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(""); } - 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 ''.$name.''; + 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 .= ""; - } - $paginator .= $this->gen_page_link($base_url, $query, $page, $name); - if ($page == $current_page) { - $paginator .= ""; + $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 - .'
<< '.$pages_html.' >>'; + return emptyHTML( + $this->implode(" | ", [ + $first_html, + $prev_html, + $random_html, + $next_html, + $last_html, + ]), + BR(), + '<< ', + $pages_html, + ' >>' + ); } } diff --git a/core/block.php b/core/block.php index 8cf31329..0242656a 100644 --- a/core/block.php +++ b/core/block.php @@ -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)); diff --git a/core/cacheengine.php b/core/cacheengine.php index 18c379cc..01a66f90 100644 --- a/core/cacheengine.php +++ b/core/cacheengine.php @@ -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; diff --git a/core/database.php b/core/database.php index 78f8cf80..72dd60a9 100644 --- a/core/database.php +++ b/core/database.php @@ -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 diff --git a/core/extension.php b/core/extension.php index 7ef28fb2..c5b7edec 100644 --- a/core/extension.php +++ b/core/extension.php @@ -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(); } } diff --git a/core/imageboard/image.php b/core/imageboard/image.php index af2483df..df4e5de7 100644 --- a/core/imageboard/image.php +++ b/core/imageboard/image.php @@ -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"); diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php index e9944606..09def27e 100644 --- a/core/imageboard/misc.php +++ b/core/imageboard/misc.php @@ -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); diff --git a/core/imageboard/tag.php b/core/imageboard/tag.php index c5997586..0780aaaa 100644 --- a/core/imageboard/tag.php +++ b/core/imageboard/tag.php @@ -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); } /** diff --git a/core/microhtml.php b/core/microhtml.php new file mode 100644 index 00000000..4cb1a943 --- /dev/null +++ b/core/microhtml.php @@ -0,0 +1,153 @@ +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 . + * @param array $options An array of pairs of parameters for