diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 968d7a846..1594d85b1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,12 +17,13 @@ // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [8000], - // Uncomment the next line to run commands after the container is created. + "updateContentCommand": "composer install", "postCreateCommand": "./.docker/entrypoint.sh unitd --no-daemon --control unix:/var/run/control.unit.sock", "containerEnv": { "UID": "2000", "GID": "2000", "UPLOAD_MAX_FILESIZE": "50M", + "INSTALL_DSN": "sqlite:data/shimmie.dev.sqlite" }, "customizations": { "vscode": { diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 43a213284..889e7e845 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -85,25 +85,7 @@ jobs: extensions: mbstring - name: Set up database - run: | - mkdir -p data/config - if [[ "${{ matrix.database }}" == "pgsql" ]]; then - sudo systemctl start postgresql ; - psql --version ; - sudo -u postgres psql -c "SELECT set_config('log_statement', 'all', false);" -U postgres ; - sudo -u postgres psql -c "CREATE USER shimmie WITH PASSWORD 'shimmie';" -U postgres ; - sudo -u postgres psql -c "CREATE DATABASE shimmie WITH OWNER shimmie;" -U postgres ; - fi - if [[ "${{ matrix.database }}" == "mysql" ]]; then - sudo systemctl start mysql ; - mysql --version ; - mysql -e "SET GLOBAL general_log = 'ON';" -uroot -proot ; - mysql -e "CREATE DATABASE shimmie;" -uroot -proot ; - fi - if [[ "${{ matrix.database }}" == "sqlite" ]]; then - sudo apt update && sudo apt-get install -y sqlite3 ; - sqlite3 --version ; - fi + run: ./tests/setup-db.sh "${{ matrix.database }}" - name: Check versions run: php -v && composer -V @@ -116,15 +98,6 @@ jobs: - name: Run test suite run: | - if [[ "${{ matrix.database }}" == "pgsql" ]]; then - export TEST_DSN="pgsql:user=shimmie;password=shimmie;host=127.0.0.1;dbname=shimmie" - fi - if [[ "${{ matrix.database }}" == "mysql" ]]; then - export TEST_DSN="mysql:user=root;password=root;host=127.0.0.1;dbname=shimmie" - fi - if [[ "${{ matrix.database }}" == "sqlite" ]]; then - export TEST_DSN="sqlite:data/shimmie.sqlite" - fi if [[ "${{ matrix.php }}" == "8.3" ]]; then vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover else diff --git a/Dockerfile b/Dockerfile index 3b391da1b..17b42fc81 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ RUN apt update && \ php${PHP_VERSION}-pgsql php${PHP_VERSION}-mysql php${PHP_VERSION}-sqlite3 php${PHP_VERSION}-curl \ curl imagemagick zip unzip unit unit-php gettext && \ rm -rf /var/lib/apt/lists/* +RUN ln -sf /dev/stderr /var/log/unit.log # Install dev packages # Things which are only needed during development - Composer has 100MB of diff --git a/README.md b/README.md index dcbcf3d7b..91d22d14d 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ To Dos: [![Code Coverage](https://scrutinizer-ci.com/g/shish/shimmie2/badges/coverage.png?b=main)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=main) [![Matrix](https://matrix.to/img/matrix-badge.svg)](https://matrix.to/#/#shimmie:matrix.org) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/shish/shimmie2?quickstart=1) # Documentation diff --git a/core/basepage.php b/core/basepage.php index f151e2f95..a4d57157b 100644 --- a/core/basepage.php +++ b/core/basepage.php @@ -6,6 +6,8 @@ use MicroHTML\HTMLElement; +use function MicroHTML\{emptyHTML,rawHTML,HTML,HEAD,BODY}; + require_once "core/event.php"; enum PageMode: string @@ -530,16 +532,23 @@ protected function get_nav_links(): array */ public function render() { - $head_html = $this->head_html(); - $body_html = $this->body_html(); + global $config, $user; + + $head = $this->head_html(); + $body = $this->body_html(); - print << - - $head_html - $body_html - -EOD; + $body_attrs = [ + "data-userclass" => $user->class->name, + ]; + + print emptyHTML( + rawHTML(""), + HTML( + ["lang" => "en"], + HEAD(rawHTML($head)), + BODY($body_attrs, rawHTML($body)) + ) + ); } protected function head_html(): string @@ -547,10 +556,8 @@ protected function head_html(): string $html_header_html = $this->get_all_html_headers(); return " - - {$this->title} - $html_header_html - + {$this->title} + $html_header_html "; } @@ -585,22 +592,20 @@ protected function body_html(): string $footer_html = $this->footer_html(); $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; return " - -
- {$this->heading} - $sub_block_html -
- -
- $flash_html - $main_block_html -
- - +
+ {$this->heading} + $sub_block_html +
+ +
+ $flash_html + $main_block_html +
+ "; } @@ -615,7 +620,7 @@ protected function footer_html(): string Shimmie © Shish & The Team - 2007-2023, + 2007-2024, based on the Danbooru concept. $debug $contact diff --git a/core/basethemelet.php b/core/basethemelet.php index 30fc6372c..010b2e1b4 100644 --- a/core/basethemelet.php +++ b/core/basethemelet.php @@ -79,16 +79,21 @@ public function build_thumb_html(Image $image): HTMLElement } } + $attrs = [ + "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, + ]; + if(Extension::is_enabled(RatingsInfo::KEY)) { + $attrs["data-rating"] = $image->rating; + } + 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, - ], + $attrs, IMG( [ "id" => "thumb_$id", diff --git a/core/extension.php b/core/extension.php index 65d906942..82c939cd1 100644 --- a/core/extension.php +++ b/core/extension.php @@ -325,6 +325,7 @@ public function onDataUpload(DataUploadEvent $event) $event->metadata['tags'] = $existing->get_tag_list(); $image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata); + $image->posted = $existing->posted; send_event(new ImageReplaceEvent($event->replace_id, $image)); $_id = $event->replace_id; assert(!is_null($_id)); diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php index 6a96baeec..083fdcdc2 100644 --- a/core/imageboard/misc.php +++ b/core/imageboard/misc.php @@ -14,7 +14,7 @@ * @param string $base * @return array */ -function add_dir(string $base): array +function add_dir(string $base, ?array $extra_tags = []): array { $results = []; @@ -22,8 +22,8 @@ function add_dir(string $base): array $short_path = str_replace($base, "", $full_path); $filename = basename($full_path); - $tags = path_to_tags($short_path); - $result = "$short_path (".str_replace(" ", ", ", $tags).")... "; + $tags = array_merge(path_to_tags($short_path), $extra_tags); + $result = "$short_path (".implode(", ", $tags).")... "; try { add_image($full_path, $filename, $tags); $result .= "ok"; @@ -39,11 +39,11 @@ function add_dir(string $base): array /** * Sends a DataUploadEvent for a file. */ -function add_image(string $tmpname, string $filename, string $tags, ?string $source = null): DataUploadEvent +function add_image(string $tmpname, string $filename, array $tags, ?string $source = null): DataUploadEvent { return send_event(new DataUploadEvent($tmpname, [ 'filename' => pathinfo($filename, PATHINFO_BASENAME), - 'tags' => Tag::explode($tags), + 'tags' => $tags, 'source' => $source, ])); } diff --git a/core/imageboard/search.php b/core/imageboard/search.php index 2b9dfff57..7c526e956 100644 --- a/core/imageboard/search.php +++ b/core/imageboard/search.php @@ -104,6 +104,26 @@ public static function find_images_iterable(int $start = 0, ?int $limit = null, } } + /** + * Get a specific set of images, in the order that the set specifies, + * with all the search stuff (rating filters etc) taken into account + * + * @param int[] $ids + * @return Image[] + */ + public static function get_images(array $ids): array + { + $visible_images = []; + foreach(Search::find_images(tags: ["id=" . implode(",", $ids)]) as $image) { + $visible_images[$image->id] = $image; + } + $visible_ids = array_keys($visible_images); + + $visible_popular_ids = array_filter($ids, fn ($id) => in_array($id, $visible_ids)); + $images = array_map(fn ($id) => $visible_images[$id], $visible_popular_ids); + return $images; + } + /* * Image-related utility functions */ diff --git a/core/install.php b/core/install.php index b18c14f84..8014bed62 100644 --- a/core/install.php +++ b/core/install.php @@ -114,9 +114,9 @@ function ask_questions() "; } + $db_s = in_array(DatabaseDriverID::SQLITE->value, $drivers) ? '' : ""; $db_m = in_array(DatabaseDriverID::MYSQL->value, $drivers) ? '' : ""; $db_p = in_array(DatabaseDriverID::PGSQL->value, $drivers) ? '' : ""; - $db_s = in_array(DatabaseDriverID::SQLITE->value, $drivers) ? '' : ""; $warn_msg = $warnings ? "

Warnings

".implode("\n

", $warnings) : ""; $err_msg = $errors ? "

Errors

".implode("\n

", $errors) : ""; @@ -132,9 +132,9 @@ function ask_questions() Type: @@ -161,13 +161,9 @@ function q(n) { return document.querySelectorAll(n); } function update_qs() { - Array.prototype.forEach.call(q('.dbconf'), function(el, i){ - el.style.display = 'none'; - }); + q('.dbconf').forEach(el => el.style.display = 'none'); let seldb = q("#database_type")[0].value || "none"; - Array.prototype.forEach.call(q('.'+seldb), function(el, i){ - el.style.display = null; - }); + q('.'+seldb).forEach(el => el.style.display = null); } @@ -178,8 +174,9 @@ function update_qs() { The username provided must have access to create tables within the database.

- For SQLite the database name will be a filename on disk, relative to - where shimmie was installed. + SQLite with default settings is fine for tens of users with thousands + of images. For thousands of users or millions of images, postgres is + recommended.

Drivers can generally be downloaded with your OS package manager; diff --git a/core/microhtml.php b/core/microhtml.php index c7848900b..f3eb3681f 100644 --- a/core/microhtml.php +++ b/core/microhtml.php @@ -6,7 +6,7 @@ use MicroHTML\HTMLElement; -use function MicroHTML\emptyHTML; +use function MicroHTML\{emptyHTML}; use function MicroHTML\A; use function MicroHTML\FORM; use function MicroHTML\INPUT; @@ -16,12 +16,7 @@ use function MicroHTML\P; use function MicroHTML\SELECT; use function MicroHTML\SPAN; -use function MicroHTML\TABLE; -use function MicroHTML\THEAD; -use function MicroHTML\TFOOT; -use function MicroHTML\TR; -use function MicroHTML\TH; -use function MicroHTML\TD; +use function MicroHTML\{TABLE,THEAD,TFOOT,TR,TH,TD}; function SHM_FORM(string $target, string $method = "POST", bool $multipart = false, string $form_id = "", string $onsubmit = "", string $name = ""): HTMLElement { diff --git a/core/polyfills.php b/core/polyfills.php index 6609945d8..0e6f6afdb 100644 --- a/core/polyfills.php +++ b/core/polyfills.php @@ -36,7 +36,11 @@ function array_iunique(array $array): array */ function ip_in_range(string $IP, string $CIDR): bool { - list($net, $mask) = explode("/", $CIDR); + $parts = explode("/", $CIDR); + if(count($parts) == 1) { + $parts[1] = "32"; + } + list($net, $mask) = $parts; $ip_net = ip2long($net); $ip_mask = ~((1 << (32 - (int)$mask)) - 1); diff --git a/core/sys_config.php b/core/sys_config.php index 2f5cb0c25..1ed2cd49f 100644 --- a/core/sys_config.php +++ b/core/sys_config.php @@ -37,4 +37,4 @@ function _d(string $name, $value): void _d("BASE_HREF", null); // string force a specific base URL (default is auto-detect) _d("TRACE_FILE", null); // string file to log performance data into _d("TRACE_THRESHOLD", 0.0); // float log pages which take more time than this many seconds -_d("REVERSE_PROXY_X_HEADERS", false); // boolean get request IPs from "X-Real-IP" and protocol from "X-Forwarded-Proto" HTTP headers +_d("TRUSTED_PROXIES", []); // array trust "X-Real-IP" / "X-Forwarded-For" / "X-Forwarded-Proto" headers from these IP ranges diff --git a/core/testcase.php b/core/testcase.php index f536e92b4..74cd686a9 100644 --- a/core/testcase.php +++ b/core/testcase.php @@ -161,6 +161,9 @@ protected function page_to_text(string $section = null): string } } + /** + * Assert that the page contains the given text somewhere in the blocks + */ protected function assert_text(string $text, string $section = null): void { $this->assertStringContainsString($text, $this->page_to_text($section)); @@ -171,6 +174,9 @@ protected function assert_no_text(string $text, string $section = null): void $this->assertStringNotContainsString($text, $this->page_to_text($section)); } + /** + * Assert that the page contains the given text somewhere in the binary data + */ protected function assert_content(string $content): void { global $page; diff --git a/core/tests/PolyfillsTest.php b/core/tests/PolyfillsTest.php index 649ca68af..f8e023f01 100644 --- a/core/tests/PolyfillsTest.php +++ b/core/tests/PolyfillsTest.php @@ -226,4 +226,13 @@ public function test_stringer() stringer(["foo" => "bar", "baz" => [1,2,3], "qux" => ["a" => "b"]]) ); } + + public function test_ip_in_range() + { + $this->assertTrue(ip_in_range("1.2.3.4", "1.2.0.0/16")); + $this->assertFalse(ip_in_range("4.3.2.1", "1.2.0.0/16")); + + // A single IP should be interpreted as a /32 + $this->assertTrue(ip_in_range("1.2.3.4", "1.2.3.4")); + } } diff --git a/core/tests/SearchTest.php b/core/tests/SearchTest.php index 303072f7b..c95197d76 100644 --- a/core/tests/SearchTest.php +++ b/core/tests/SearchTest.php @@ -487,4 +487,19 @@ public function testBSQ_TagCondWithImgCond($image_ids) path: ["general", "some_positives"], ); } + + /** + * get_images + */ + #[Depends('testUpload')] + public function test_get_images() + { + $image_ids = $this->testUpload(); + + $res = Search::get_images($image_ids); + $this->assertGreaterThan($res[0]->id, $res[1]->id); + + $res = Search::get_images(array_reverse($image_ids)); + $this->assertLessThan($res[0]->id, $res[1]->id); + } } diff --git a/core/tests/UtilTest.php b/core/tests/UtilTest.php index 30fffbf72..e4312307d 100644 --- a/core/tests/UtilTest.php +++ b/core/tests/UtilTest.php @@ -126,39 +126,39 @@ public function test_load_balance_url() public function test_path_to_tags() { $this->assertEquals( - "", + [], path_to_tags("nope.jpg") ); $this->assertEquals( - "", + [], path_to_tags("\\") ); $this->assertEquals( - "", + [], path_to_tags("/") ); $this->assertEquals( - "", + [], path_to_tags("C:\\") ); $this->assertEquals( - "test tag", + ["test", "tag"], path_to_tags("123 - test tag.jpg") ); $this->assertEquals( - "foo bar", + ["foo", "bar"], path_to_tags("/foo/bar/baz.jpg") ); $this->assertEquals( - "cake pie foo bar", + ["cake", "pie", "foo", "bar"], path_to_tags("/foo/bar/123 - cake pie.jpg") ); $this->assertEquals( - "bacon lemon", + ["bacon", "lemon"], path_to_tags("\\bacon\\lemon\\baz.jpg") ); $this->assertEquals( - "category:tag", + ["category:tag"], path_to_tags("/category:/tag/baz.jpg") ); } diff --git a/core/util.php b/core/util.php index 9c695e7d3..62081a4df 100644 --- a/core/util.php +++ b/core/util.php @@ -54,7 +54,7 @@ function contact_link(): ?string function is_https_enabled(): bool { // check forwarded protocol - if (REVERSE_PROXY_X_HEADERS && !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') { + if (is_trusted_proxy() && !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') { $_SERVER['HTTPS'] = 'on'; } return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); @@ -148,25 +148,37 @@ function check_im_version(): int return (empty($convert_check) ? 0 : 1); } -/** - * Get request IP - */ - -function get_remote_addr() +function is_trusted_proxy(): bool { - return $_SERVER['REMOTE_ADDR']; + $ra = $_SERVER['REMOTE_ADDR'] ?? "0.0.0.0"; + foreach(TRUSTED_PROXIES as $proxy) { + if(ip_in_range($ra, $proxy)) { + return true; + } + } + return false; } + /** * Get real IP if behind a reverse proxy */ - function get_real_ip() { - $ip = get_remote_addr(); - if (REVERSE_PROXY_X_HEADERS && isset($_SERVER['HTTP_X_REAL_IP'])) { - $ip = $_SERVER['HTTP_X_REAL_IP']; - if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { - $ip = "0.0.0.0"; + $ip = $_SERVER['REMOTE_ADDR']; + + if(is_trusted_proxy()) { + if (isset($_SERVER['HTTP_X_REAL_IP'])) { + if(filter_var($ip, FILTER_VALIDATE_IP)) { + $ip = $_SERVER['HTTP_X_REAL_IP']; + } + } + + if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); + $last_ip = $ips[count($ips) - 1]; + if(filter_var($last_ip, FILTER_VALIDATE_IP)) { + $ip = $last_ip; + } } } @@ -334,7 +346,10 @@ function fetch_url(string $url, string $mfile): ?array return null; } -function path_to_tags(string $path): string +/** + * @return string[] + */ +function path_to_tags(string $path): array { $matches = []; $tags = []; @@ -378,7 +393,7 @@ function path_to_tags(string $path): string $category = $category_to_inherit; } - return implode(" ", $tags); + return $tags; } function get_dir_contents(string $dir): array diff --git a/ext/bulk_add_csv/main.php b/ext/bulk_add_csv/main.php index f04e93edc..f49e889f7 100644 --- a/ext/bulk_add_csv/main.php +++ b/ext/bulk_add_csv/main.php @@ -48,7 +48,7 @@ public function onAdminBuilding(AdminBuildingEvent $event) /** * Generate the necessary DataUploadEvent for a given image and tags. */ - private function add_image(string $tmpname, string $filename, string $tags, string $source, string $rating, string $thumbfile) + private function add_image(string $tmpname, string $filename, array $tags, string $source, string $rating, string $thumbfile) { $event = add_image($tmpname, $filename, $tags, $source); if ($event->image_id == -1) { @@ -91,12 +91,12 @@ private function add_csv(string $csvfile) } } $fullpath = $csvdata[0]; - $tags = trim($csvdata[1]); + $tags = Tag::explode(trim($csvdata[1])); $source = $csvdata[2]; $rating = $csvdata[3]; $thumbfile = $csvdata[4]; $shortpath = pathinfo($fullpath, PATHINFO_BASENAME); - $list .= "
".html_escape("$shortpath (".str_replace(" ", ", ", $tags).")... "); + $list .= "
".html_escape("$shortpath (".implode(", ", $tags).")... "); if (file_exists($csvdata[0]) && is_file($csvdata[0])) { try { $this->add_image($fullpath, $shortpath, $tags, $source, $rating, $thumbfile); diff --git a/ext/bulk_import_export/main.php b/ext/bulk_import_export/main.php index 3dd921cb6..eddd27af3 100644 --- a/ext/bulk_import_export/main.php +++ b/ext/bulk_import_export/main.php @@ -52,7 +52,7 @@ public function onDataUpload(DataUploadEvent $event) file_put_contents($tmpfile, $stream); - $id = add_image($tmpfile, $item->filename, Tag::implode($item->tags))->image_id; + $id = add_image($tmpfile, $item->filename, $item->tags)->image_id; if ($id == -1) { throw new SCoreException("Unable to import file $item->hash"); diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php index 22ef18ed8..1f97a0f91 100644 --- a/ext/cron_uploader/main.php +++ b/ext/cron_uploader/main.php @@ -512,7 +512,7 @@ private function generate_image_queue(): \Generator foreach (new \RecursiveIteratorIterator($ite) as $fullpath => $cur) { if (!is_link($fullpath) && !is_dir($fullpath) && !$this->is_skippable_file($fullpath)) { $relativePath = substr($fullpath, strlen($base)); - $tags = path_to_tags($relativePath); + $tags = Tag::implode(path_to_tags($relativePath)); yield [ 0 => $fullpath, diff --git a/ext/download/main.php b/ext/download/main.php index 52f428886..5bf5facc2 100644 --- a/ext/download/main.php +++ b/ext/download/main.php @@ -14,17 +14,13 @@ public function get_priority(): int return 99; } - public function onImageDownloading(ImageDownloadingEvent $event) { global $page; $page->set_mime($event->mime); - $page->set_mode(PageMode::FILE); - $page->set_file($event->path, $event->file_modified); - $event->stop_processing = true; } } diff --git a/ext/download/test.php b/ext/download/test.php new file mode 100644 index 000000000..b0ac2949e --- /dev/null +++ b/ext/download/test.php @@ -0,0 +1,16 @@ +post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + $this->get_page("/image/$image_id"); + $this->assertEquals(PageMode::FILE, $page->mode); + } +} diff --git a/ext/et_server/main.php b/ext/et_server/main.php index c88ac6cc2..dbc314c6f 100644 --- a/ext/et_server/main.php +++ b/ext/et_server/main.php @@ -12,7 +12,6 @@ public function onPageRequest(PageRequestEvent $event) { global $database, $page, $user; if ($event->page_matches("register.php")) { - error_log("register.php"); if (isset($_POST["data"])) { $database->execute( "INSERT INTO registration(data) VALUES(:data)", diff --git a/ext/et_server/test.php b/ext/et_server/test.php new file mode 100644 index 000000000..9b80e6373 --- /dev/null +++ b/ext/et_server/test.php @@ -0,0 +1,21 @@ +post_page("register.php", ["data" => "test entry"]); + + $this->log_in_as_user(); + $this->get_page("register.php"); + $this->assert_no_text("test entry"); + + $this->log_in_as_admin(); + $this->get_page("register.php"); + $this->assert_text("test entry"); + } +} diff --git a/ext/handle_archive/main.php b/ext/handle_archive/main.php index a975643b1..291a95d4f 100644 --- a/ext/handle_archive/main.php +++ b/ext/handle_archive/main.php @@ -34,7 +34,7 @@ public function onDataUpload(DataUploadEvent $event) exec($cmd); if (file_exists($tmpdir)) { try { - $results = add_dir($tmpdir); + $results = add_dir($tmpdir, $event->metadata['tags']); if (count($results) > 0) { $page->flash("Adding files" . implode("\n", $results)); } diff --git a/ext/handle_archive/test.php b/ext/handle_archive/test.php new file mode 100644 index 000000000..682820987 --- /dev/null +++ b/ext/handle_archive/test.php @@ -0,0 +1,27 @@ +log_in_as_user(); + system("zip -q tests/test.zip tests/pbx_screenshot.jpg tests/favicon.png"); + $this->post_image("tests/test.zip", "a z"); + + $images = Search::find_images(); + $this->assertEquals(2, count($images)); + $this->assertEquals("a tests z", $images[0]->get_tag_list()); + $this->assertEquals("a tests z", $images[1]->get_tag_list()); + } + + public function tearDown(): void + { + if(file_exists("tests/test.zip")) { + unlink("tests/test.zip"); + } + } +} diff --git a/ext/image/main.php b/ext/image/main.php index a0acad933..04cecd6d9 100644 --- a/ext/image/main.php +++ b/ext/image/main.php @@ -79,7 +79,12 @@ public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) public function onPageRequest(PageRequestEvent $event) { - global $config; + global $config, $page; + + $thumb_width = $config->get_int(ImageConfig::THUMB_WIDTH, 192); + $thumb_height = $config->get_int(ImageConfig::THUMB_HEIGHT, 192); + $page->add_html_header(""); + if ($event->page_matches("image/delete")) { global $page, $user; if ($user->can(Permissions::DELETE_IMAGE) && isset($_POST['image_id']) && $user->check_auth_token()) { diff --git a/ext/image_view_counter/main.php b/ext/image_view_counter/main.php index cb4b945a4..7df1a2850 100644 --- a/ext/image_view_counter/main.php +++ b/ext/image_view_counter/main.php @@ -94,19 +94,16 @@ public function onPageRequest(PageRequestEvent $event) global $database; if ($event->page_matches("popular_images")) { - $sql = " + $popular_ids = $database->get_col(" SELECT image_id, count(*) AS total_views FROM image_views, images WHERE image_views.image_id = image_views.image_id AND image_views.image_id = images.id GROUP BY image_views.image_id ORDER BY total_views DESC - "; - $result = $database->get_col($sql); - $images = []; - foreach ($result as $id) { - $images[] = Image::by_id(intval($id)); - } + LIMIT 100 + "); + $images = Search::get_images($popular_ids); $this->theme->view_popular($images); } } diff --git a/ext/image_view_counter/test.php b/ext/image_view_counter/test.php new file mode 100644 index 000000000..fc6e1c84d --- /dev/null +++ b/ext/image_view_counter/test.php @@ -0,0 +1,24 @@ +post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + $this->log_in_as_admin(); + $this->get_page("post/view/$image_id"); + $this->assert_text("Views"); + } + + public function testPopular() + { + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + $this->get_page("post/view/$image_id"); + $this->get_page("popular_images"); + $this->assert_text("$image_id"); + } +} diff --git a/ext/image_view_counter/theme.php b/ext/image_view_counter/theme.php index d6c07f349..5ba946566 100644 --- a/ext/image_view_counter/theme.php +++ b/ext/image_view_counter/theme.php @@ -6,7 +6,10 @@ class ImageViewCounterTheme extends Themelet { - public function view_popular($images) + /** + * @param Image[] $images + */ + public function view_popular(array $images) { global $page, $config; $pop_images = ""; diff --git a/ext/index/main.php b/ext/index/main.php index 9bfd6bfe9..8ad07eb40 100644 --- a/ext/index/main.php +++ b/ext/index/main.php @@ -188,11 +188,18 @@ public function onSearchTermParse(SearchTermParseEvent $event) $cmp = preg_replace('/^:/', '=', $matches[1]); $args = ["width{$event->id}" => int_escape($matches[2]), "height{$event->id}" => int_escape($matches[3])]; $event->add_querylet(new Querylet("width / :width{$event->id} $cmp height / :height{$event->id}", $args)); - } elseif (preg_match("/^(filesize|id)([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+[kmg]?b?)$/i", $event->term, $matches)) { - $col = $matches[1]; - $cmp = ltrim($matches[2], ":") ?: "="; - $val = parse_shorthand_int($matches[3]); - $event->add_querylet(new Querylet("images.$col $cmp :val{$event->id}", ["val{$event->id}" => $val])); + } elseif (preg_match("/^filesize([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+[kmg]?b?)$/i", $event->term, $matches)) { + $cmp = ltrim($matches[1], ":") ?: "="; + $val = parse_shorthand_int($matches[2]); + $event->add_querylet(new Querylet("images.filesize $cmp :val{$event->id}", ["val{$event->id}" => $val])); + } elseif (preg_match("/^id=([\d,]+)$/i", $event->term, $matches)) { + $val = array_map(fn ($x) => int_escape($x), explode(",", $matches[1])); + $set = implode(",", $val); + $event->add_querylet(new Querylet("images.id IN ($set)")); + } elseif (preg_match("/^id([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { + $cmp = ltrim($matches[1], ":") ?: "="; + $val = int_escape($matches[2]); + $event->add_querylet(new Querylet("images.id $cmp :val{$event->id}", ["val{$event->id}" => $val])); } elseif (preg_match("/^(hash|md5)[=|:]([0-9a-fA-F]*)$/i", $event->term, $matches)) { $hash = strtolower($matches[2]); $event->add_querylet(new Querylet('images.hash = :hash', ["hash" => $hash])); diff --git a/ext/index/script.js b/ext/index/script.js index a599a97e1..1ee59f9f9 100644 --- a/ext/index/script.js +++ b/ext/index/script.js @@ -1,14 +1,15 @@ /*jshint bitwise:false, curly:true, eqeqeq:true, evil:true, forin:false, noarg:true, noempty:true, nonew:true, undef:false, strict:false, browser:true, jquery:true */ document.addEventListener('DOMContentLoaded', () => { - var blocked_tags = (shm_cookie_get("ui-blocked-tags") || "").split(" "); - var needs_refresh = false; - for(var i=0; i tag.length > 0) + .map(tag => tag.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")) + .map(tag => `.shm-thumb[data-tags~="${tag}"]`).join(", "); + if(blocked_css) { + let style = document.createElement("style"); + style.innerHTML = blocked_css + " { display: none; }"; + document.head.appendChild(style); } //Generate a random seed when using order:random diff --git a/ext/index/style.css b/ext/index/style.css index 8fdee55d7..3bebb4bfa 100644 --- a/ext/index/style.css +++ b/ext/index/style.css @@ -7,3 +7,11 @@ text-align: left; margin: 0 10px 10px 0; } +.shm-image-list { + display: grid; + grid-template-columns: repeat( auto-fill, calc(var(--thumb-width) + 42px) ); + place-items: center; +} +.shm-image-list .thumb { + margin-bottom: 8px; +} diff --git a/ext/numeric_score/main.php b/ext/numeric_score/main.php index 028eca694..7f8f3f63a 100644 --- a/ext/numeric_score/main.php +++ b/ext/numeric_score/main.php @@ -224,12 +224,8 @@ public function onPageRequest(PageRequestEvent $event) //filter images by score != 0 + date > limit to max images on one page > order from highest to lowest score - $result = $database->get_col($sql, $args); - $images = []; - foreach ($result as $id) { - $images[] = Image::by_id((int)$id); - } - + $ids = $database->get_col($sql, $args); + $images = Search::get_images($ids); $this->theme->view_popular($images, $dte); } } diff --git a/ext/setup/main.php b/ext/setup/main.php index 1587060e3..c942644d0 100644 --- a/ext/setup/main.php +++ b/ext/setup/main.php @@ -222,7 +222,7 @@ public function add_int_option(string $name, string $label = null, bool $table_r { $val = $this->config->get_int($name); - $html = "\n"; + $html = "\n"; $html .= "\n"; $this->format_option($name, $html, $label, $table_row); @@ -231,7 +231,7 @@ public function add_int_option(string $name, string $label = null, bool $table_r public function add_shorthand_int_option(string $name, string $label = null, bool $table_row = false) { $val = to_shorthand_int($this->config->get_int($name)); - $html = "\n"; + $html = "\n"; $html .= "\n"; $this->format_option($name, $html, $label, $table_row); diff --git a/ext/setup/style.css b/ext/setup/style.css index c8904a5b1..56d9a40fe 100644 --- a/ext/setup/style.css +++ b/ext/setup/style.css @@ -24,3 +24,9 @@ margin: 0; padding: 0; } +.setupblock .form { + width: 100%; +} +.setupblock .form TH { + font-weight: normal; +} \ No newline at end of file diff --git a/ext/system/test.php b/ext/system/test.php new file mode 100644 index 000000000..170d3b08e --- /dev/null +++ b/ext/system/test.php @@ -0,0 +1,15 @@ +get_page("system"); + $this->assertEquals(PageMode::REDIRECT, $page->mode); + } +} diff --git a/ext/upload/test.php b/ext/upload/test.php index be099ba6f..56347e161 100644 --- a/ext/upload/test.php +++ b/ext/upload/test.php @@ -56,6 +56,9 @@ public function testRawReplace() $this->log_in_as_admin(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + $original_posted = $database->get_one("SELECT posted FROM images WHERE id = $image_id"); + + sleep(1); // make sure the timestamp changes (see bug #903) $_FILES = [ 'data' => [ @@ -70,8 +73,13 @@ public function testRawReplace() $page = $this->post_page("replace/$image_id"); $this->assert_response(302); $this->assertEquals("/test/post/view/$image_id", $page->redirect); + $new_posted = $database->get_one("SELECT posted FROM images WHERE id = $image_id"); $this->assertEquals(1, $database->get_one("SELECT COUNT(*) FROM images")); + + // check that the original timestamp is left alone, despite the + // file being replaced (see bug #903) + $this->assertEquals($original_posted, $new_posted); } public function testUpload() diff --git a/ext/wiki/main.php b/ext/wiki/main.php index 179eacfc9..671d0bb7e 100644 --- a/ext/wiki/main.php +++ b/ext/wiki/main.php @@ -135,9 +135,9 @@ public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Wiki"); $sb->add_bool_option(WikiConfig::ENABLE_REVISIONS, "Enable wiki revisions: "); - $sb->add_longtext_option(WikiConfig::TAG_PAGE_TEMPLATE, "Tag page template: "); - $sb->add_text_option(WikiConfig::EMPTY_TAGINFO, "Empty list text: "); - $sb->add_bool_option(WikiConfig::TAG_SHORTWIKIS, "Show shortwiki entry when searching for a single tag: "); + $sb->add_longtext_option(WikiConfig::TAG_PAGE_TEMPLATE, "
Tag page template: "); + $sb->add_text_option(WikiConfig::EMPTY_TAGINFO, "
Empty list text: "); + $sb->add_bool_option(WikiConfig::TAG_SHORTWIKIS, "
Show shortwiki entry when searching for a single tag: "); } public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) diff --git a/tests/defines.php b/tests/defines.php index dc6ad8643..5744f4270 100644 --- a/tests/defines.php +++ b/tests/defines.php @@ -21,4 +21,4 @@ define("BASE_HREF", "/test"); define("CLI_LOG_LEVEL", 50); define("STATSD_HOST", null); -define("REVERSE_PROXY_X_HEADERS", false); +define("TRUSTED_PROXIES", []); diff --git a/tests/setup-db.sh b/tests/setup-db.sh new file mode 100755 index 000000000..f3370c3bd --- /dev/null +++ b/tests/setup-db.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +DATABASE=$1 + +mkdir -p data/config + +if [[ "$DATABASE" == "pgsql" ]]; then + sudo systemctl start postgresql + psql --version + sudo -u postgres psql -c "SELECT set_config('log_statement', 'all', false);" -U postgres + sudo -u postgres psql -c "CREATE USER shimmie WITH PASSWORD 'shimmie';" -U postgres + sudo -u postgres psql -c "CREATE DATABASE shimmie WITH OWNER shimmie;" -U postgres + export TEST_DSN="pgsql:user=shimmie;password=shimmie;host=127.0.0.1;dbname=shimmie" +fi +if [[ "$DATABASE" == "mysql" ]]; then + sudo systemctl start mysql + mysql --version + mysql -e "SET GLOBAL general_log = 'ON';" -uroot -proot + mysql -e "CREATE DATABASE shimmie;" -uroot -proot + export TEST_DSN="mysql:user=root;password=root;host=127.0.0.1;dbname=shimmie" +fi +if [[ "$DATABASE" == "sqlite" ]]; then + sudo apt update && sudo apt-get install -y sqlite3 + sqlite3 --version + export TEST_DSN="sqlite:data/shimmie.sqlite" +fi + +if [[ -n "$GITHUB_ENV" ]]; then + echo "TEST_DSN=$TEST_DSN" >> $GITHUB_ENV +fi diff --git a/themes/danbooru/page.class.php b/themes/danbooru/page.class.php index 6ff8dba7f..d0bafacc4 100644 --- a/themes/danbooru/page.class.php +++ b/themes/danbooru/page.class.php @@ -50,7 +50,7 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ class Page extends BasePage { - public function render() + public function body_html(): string { global $config; @@ -123,33 +123,26 @@ public function render() } $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; - $head_html = $this->head_html(); $footer_html = $this->footer_html(); - print << - - $head_html - -

- $title_link - - -
- $subheading - $sub_block_html - $left -
- $flash_html - $main_block_html -
- - - + return << + $title_link + + + + $subheading + $sub_block_html + $left +
+ $flash_html + $main_block_html +
+
$footer_html
EOD; } diff --git a/themes/danbooru/style.css b/themes/danbooru/style.css index b912670b4..32946a14d 100644 --- a/themes/danbooru/style.css +++ b/themes/danbooru/style.css @@ -200,13 +200,6 @@ background:blue none repeat scroll 0 0; border:1px solid #EEEEEE; color:white; } -span.thumb { -display:inline-block; -float:left; -height:220px; -text-align:center; -width:220px; -} #pagelist { margin-top:32px; } diff --git a/themes/danbooru2/page.class.php b/themes/danbooru2/page.class.php index d34354a32..d714cbdac 100644 --- a/themes/danbooru2/page.class.php +++ b/themes/danbooru2/page.class.php @@ -51,7 +51,7 @@ class Page extends BasePage { - public function render() + public function body_html(): string { global $config; @@ -124,14 +124,9 @@ public function render() } $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; - $head_html = $this->head_html(); $footer_html = $this->footer_html(); - print << - - $head_html - + return << $title_link