diff --git a/core/imageboard/search.php b/core/imageboard/search.php index 0377f9cc6..b89b70a68 100644 --- a/core/imageboard/search.php +++ b/core/imageboard/search.php @@ -6,6 +6,21 @@ use GQLA\Query; +/** + * A small chunk of SQL code + parameters, to be used in a larger query + * + * eg + * + * $q = new Querylet("SELECT * FROM images"); + * $q->append(new Querylet(" WHERE id = :id", ["id" => 123])); + * $q->append(new Querylet(" AND rating = :rating", ["rating" => "safe"])); + * $q->append(new Querylet(" ORDER BY id DESC")); + * + * becomes + * + * SELECT * FROM images WHERE id = :id AND rating = :rating ORDER BY id DESC + * ["id" => 123, "rating" => "safe"] + */ class Querylet { /** @@ -25,6 +40,9 @@ public function append(Querylet $querylet): void } } +/** + * When somebody has searched for a tag, "cat", "cute", "-angry", etc + */ class TagCondition { public function __construct( @@ -34,6 +52,10 @@ public function __construct( } } +/** + * When somebody has searched for a specific image property, like "rating:safe", + * "id:123", "width:100", etc + */ class ImgCondition { public function __construct( @@ -45,10 +67,19 @@ public function __construct( class Search { - /** @var list */ + /** + * The search code is dark and full of horrors, and it's not always clear + * what's going on. This is a list of the steps that the search code took + * to find the images that it returned. + * + * @var list + */ public static array $_search_path = []; /** + * Build a search query for a given set of tags and return + * the results as a PDOStatement (raw SQL rows) + * * @param list $tags */ private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags = []): \FFSPHP\PDOStatement @@ -203,10 +234,13 @@ private static function tag_or_wildcard_to_ids(string $tag): array /** * Turn a human input string into a an abstract search query * + * (This is only public for testing purposes, nobody should be calling this + * directly from outside this class) + * * @param string[] $terms * @return array{0: TagCondition[], 1: ImgCondition[], 2: string} */ - private static function terms_to_conditions(array $terms): array + public static function terms_to_conditions(array $terms): array { global $config; @@ -234,10 +268,13 @@ private static function terms_to_conditions(array $terms): array /** * Turn an abstract search query into an SQL Querylet * + * (This is only public for testing purposes, nobody should be calling this + * directly from outside this class) + * * @param TagCondition[] $tag_conditions * @param ImgCondition[] $img_conditions */ - private static function build_search_querylet( + public static function build_search_querylet( array $tag_conditions, array $img_conditions, ?string $order = null, diff --git a/core/imageboard/tag.php b/core/imageboard/tag.php index 1f6bc6f8d..fab6b1f2c 100644 --- a/core/imageboard/tag.php +++ b/core/imageboard/tag.php @@ -210,9 +210,6 @@ public static function sanitize(string $tag): string $tag = ""; } // hard-code one bad case... - if (mb_strlen($tag, 'UTF-8') > 255) { - throw new InvalidInput("The tag below is longer than 255 characters, please use a shorter tag.\n$tag\n"); - } return $tag; } diff --git a/ext/image_hash_ban/main.php b/ext/image_hash_ban/main.php index 58ca13935..08c66a823 100644 --- a/ext/image_hash_ban/main.php +++ b/ext/image_hash_ban/main.php @@ -128,7 +128,7 @@ public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void global $user; if ($event->parent === "system") { if ($user->can(Permissions::BAN_IMAGE)) { - $event->add_nav_link("image_bans", new Link('image_hash_ban/list/1'), "Post Bans", NavLink::is_active(["image_hash_ban"])); + $event->add_nav_link("image_bans", new Link('image_hash_ban/list'), "Post Bans", NavLink::is_active(["image_hash_ban"])); } } } @@ -137,7 +137,7 @@ public function onUserBlockBuilding(UserBlockBuildingEvent $event): void { global $user; if ($user->can(Permissions::BAN_IMAGE)) { - $event->add_link("Post Bans", make_link("image_hash_ban/list/1")); + $event->add_link("Post Bans", make_link("image_hash_ban/list")); } } diff --git a/ext/index/main.php b/ext/index/main.php index 3bf49f95b..d1bbfdf39 100644 --- a/ext/index/main.php +++ b/ext/index/main.php @@ -6,6 +6,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputInterface,InputArgument}; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; require_once "config.php"; @@ -149,6 +150,46 @@ public function onCliGen(CliGenEvent $event): void foreach ($items as $item) { $output->writeln($item->hash); } + return Command::SUCCESS; + }); + $event->app->register('debug:search') + ->addArgument('query', InputArgument::REQUIRED) + ->addOption('count', null, InputOption::VALUE_NONE, 'Generate a count-only query') + ->addOption('page', null, InputOption::VALUE_REQUIRED, 'Page number', default: 1) + ->addOption('limit', null, InputOption::VALUE_REQUIRED, 'Number of results per page', default: 25) + ->setDescription('Show the SQL generated for a given search query') + ->setCode(function (InputInterface $input, OutputInterface $output): int { + $search = Tag::explode($input->getArgument('query'), false); + $page = $input->getOption('page'); + $limit = $input->getOption('limit'); + $count = $input->getOption('count'); + + [$tag_conditions, $img_conditions, $order] = Search::terms_to_conditions($search); + if($count) { + $order = null; + $page = null; + $limit = null; + } + + $q = Search::build_search_querylet( + $tag_conditions, + $img_conditions, + $order, + $limit, + (int)(($page - 1) * $limit), + ); + + $sql_str = $q->sql; + $sql_str = preg_replace("/\s+/", " ", $sql_str); + foreach($q->variables as $key => $val) { + if(is_string($val)) { + $sql_str = str_replace(":$key", "'$val'", $sql_str); + } else { + $sql_str = str_replace(":$key", (string)$val, $sql_str); + } + } + $output->writeln(trim($sql_str)); + return Command::SUCCESS; }); } diff --git a/ext/not_a_tag/main.php b/ext/not_a_tag/main.php index 72a4200d9..d180da5c4 100644 --- a/ext/not_a_tag/main.php +++ b/ext/not_a_tag/main.php @@ -109,7 +109,7 @@ public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void global $user; if ($event->parent === "tags") { if ($user->can(Permissions::BAN_IMAGE)) { - $event->add_nav_link("untags", new Link('untag/list/1'), "UnTags"); + $event->add_nav_link("untags", new Link('untag/list'), "UnTags"); } } } @@ -118,7 +118,7 @@ public function onUserBlockBuilding(UserBlockBuildingEvent $event): void { global $user; if ($user->can(Permissions::BAN_IMAGE)) { - $event->add_link("UnTags", make_link("untag/list/1")); + $event->add_link("UnTags", make_link("untag/list")); } } @@ -126,32 +126,30 @@ public function onPageRequest(PageRequestEvent $event): void { global $database, $page, $user; - if ($event->page_matches("untag", permission: Permissions::BAN_IMAGE)) { - if ($event->page_matches("untag/add", method: "POST")) { - $input = validate_input(["c_tag" => "string", "c_redirect" => "string"]); - $database->execute( - "INSERT INTO untags(tag, redirect) VALUES (:tag, :redirect)", - ["tag" => $input['c_tag'], "redirect" => $input['c_redirect']] - ); - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(referer_or(make_link())); - } - if ($event->page_matches("untag/remove", method: "POST")) { - $input = validate_input(["d_tag" => "string"]); - $database->execute( - "DELETE FROM untags WHERE LOWER(tag) = LOWER(:tag)", - ["tag" => $input['d_tag']] - ); - $page->flash("Post ban removed"); - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(referer_or(make_link())); - } - if ($event->page_matches("untag/list")) { - $t = new NotATagTable($database->raw_db()); - $t->token = $user->get_auth_token(); - $t->inputs = $event->GET; - $this->theme->display_crud("UnTags", $t->table($t->query()), $t->paginator()); - } + if ($event->page_matches("untag/add", method: "POST", permission: Permissions::BAN_IMAGE)) { + $input = validate_input(["c_tag" => "string", "c_redirect" => "string"]); + $database->execute( + "INSERT INTO untags(tag, redirect) VALUES (:tag, :redirect)", + ["tag" => $input['c_tag'], "redirect" => $input['c_redirect']] + ); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(referer_or(make_link())); + } + if ($event->page_matches("untag/remove", method: "POST", permission: Permissions::BAN_IMAGE)) { + $input = validate_input(["d_tag" => "string"]); + $database->execute( + "DELETE FROM untags WHERE LOWER(tag) = LOWER(:tag)", + ["tag" => $input['d_tag']] + ); + $page->flash("Post ban removed"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(referer_or(make_link())); + } + if ($event->page_matches("untag/list")) { + $t = new NotATagTable($database->raw_db()); + $t->token = $user->get_auth_token(); + $t->inputs = $event->GET; + $this->theme->display_crud("UnTags", $t->table($t->query()), $t->paginator()); } } } diff --git a/ext/post_tags/main.php b/ext/post_tags/main.php index ed3327185..ff21433c5 100644 --- a/ext/post_tags/main.php +++ b/ext/post_tags/main.php @@ -141,6 +141,7 @@ public function onImageInfoSet(ImageInfoSetEvent $event): void } else { $page->flash($e->getMessage()); } + throw $e; } } } diff --git a/ext/post_tags/test.php b/ext/post_tags/test.php index 583af3127..885d68d15 100644 --- a/ext/post_tags/test.php +++ b/ext/post_tags/test.php @@ -42,8 +42,8 @@ public function testInvalidChange(): void public function testTagEdit_tooLong(): void { $this->log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", str_repeat("a", 500)); - $this->get_page("post/view/$image_id"); - $this->assert_title("Post $image_id: tagme"); + $this->assertException(TagSetException::class, function () { + $this->post_image("tests/pbx_screenshot.jpg", str_repeat("a", 500)); + }); } } diff --git a/themes/default/style.css b/themes/default/style.css index 46e7a2c54..0746f9ac0 100644 --- a/themes/default/style.css +++ b/themes/default/style.css @@ -97,8 +97,9 @@ TD { TABLE.zebra {border-spacing: 0; border: 2px solid var(--zebra-border);} TABLE.zebra TD, TABLE.zebra TH {vertical-align: middle; padding: 4px;} -TABLE.zebra THEAD TD, TABLE.zebra THEAD TH {border-bottom: 2px solid var(--zebra-border);} -TABLE.zebra TFOOT TD, TABLE.zebra TFOOT TH {border-top: 2px solid var(--zebra-border);} +TABLE.zebra THEAD {position: sticky;top: 0;} +TABLE.zebra THEAD TR:last-child TD, TABLE.zebra TR:last-child THEAD TH {border-bottom: 2px solid var(--zebra-border);} +TABLE.zebra TFOOT TR:first-child TD, TABLE.zebra TFOOT TR:first-child TH {border-top: 2px solid var(--zebra-border);} TABLE.zebra TR TD {border-bottom: 1px solid var(--zebra-header);} TABLE.zebra TR:nth-child(odd) {background: var(--zebra-odd);} TABLE.zebra TR:nth-child(even) {background: var(--zebra-even);} diff --git a/themes/futaba/comment.theme.php b/themes/futaba/comment.theme.php index c4ea8c888..dec2abd01 100644 --- a/themes/futaba/comment.theme.php +++ b/themes/futaba/comment.theme.php @@ -7,6 +7,7 @@ class CustomCommentListTheme extends CommentListTheme { public int $inner_id = 0; + public bool $post_page = true; /** * @param array $images @@ -25,6 +26,7 @@ public function display_comment_list(array $images, int $page_number, int $total $page->add_block(new Block(null, $this->build_upload_box(), "main", 0)); $page->add_block(new Block(null, "
", "main", 80)); $this->display_paginator($page, "comment/list", null, $page_number, $total_pages); + $this->post_page = false; // parts for each image $position = 10; @@ -58,7 +60,8 @@ public function display_comment_list(array $images, int $page_number, int $total public function display_recent_comments(array $comments): void { - // sidebar fails in this theme + $this->post_page = false; + parent::display_recent_comments($comments); } public function build_upload_box(): string @@ -69,7 +72,9 @@ public function build_upload_box(): string protected function comment_to_html(Comment $comment, bool $trim = false): string { - $inner_id = $this->inner_id; // because custom themes can't add params, because PHP + // because custom themes can't add params, because PHP + $post_page = $this->post_page; + $inner_id = $this->inner_id; global $user; $tfe = send_event(new TextFormattingEvent($comment->comment)); @@ -82,6 +87,10 @@ protected function comment_to_html(Comment $comment, bool $trim = false): string } else { $h_comment = $tfe->formatted; } + $h_comment = preg_replace("/(^|>)(>[^<\n]*)(<|\n|$)/", '${1}${2}${3}', $h_comment); + // handles discrepency in comment page and homepage + $h_comment = str_replace("
", "", $h_comment); + $h_comment = str_replace("\n", "
", $h_comment); $i_comment_id = $comment->comment_id; $i_image_id = $comment->image_id; @@ -89,15 +98,23 @@ protected function comment_to_html(Comment $comment, bool $trim = false): string $h_date = $comment->posted; $h_del = ""; if ($user->can(Permissions::DELETE_COMMENT)) { - $h_del = " - " . $this->delete_link($i_comment_id, $i_image_id, $comment->owner_name, $tfe->stripped); + $comment_preview = substr(html_unescape($tfe->stripped), 0, 50); + $j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview"); + $h_delete_script = html_escape("return confirm($j_delete_confirm_message);"); + $h_delete_link = make_link("comment/delete/$i_comment_id/$i_image_id"); + $h_del = " - [Delete]"; + } + if ($this->post_page) { + $h_reply = "[Reply]"; + } else { + $h_reply = "[Reply]"; } - $h_reply = "[Reply]"; if ($inner_id == 0) { return "
$h_userlink$h_del $h_date No.$i_comment_id $h_reply

$h_comment

"; } else { return "
>>". - "
$h_userlink$h_del $h_date No.$i_comment_id $h_reply

$h_comment

" . + "
$h_userlink$h_del $h_date No.$i_comment_id

$h_comment

" . "
"; } } diff --git a/themes/futaba/style.css b/themes/futaba/style.css index 3d07afa8c..19f127e23 100644 --- a/themes/futaba/style.css +++ b/themes/futaba/style.css @@ -160,3 +160,6 @@ TABLE.tag_list>TBODY>TR>TD:after { .thumb { margin: 16px; } +.greentext { + color: green; +} \ No newline at end of file