From 53152bf9f0bf9c010891ac596f8b15ad702322e3 Mon Sep 17 00:00:00 2001 From: Shish Date: Thu, 4 Jan 2024 16:08:53 +0000 Subject: [PATCH 01/10] [forum] use microhtml, avoid double-escaping text, fixes #835 --- ext/forum/main.php | 68 ++++++----- ext/forum/theme.php | 282 +++++++++++++++++++++++--------------------- 2 files changed, 186 insertions(+), 164 deletions(-) diff --git a/ext/forum/main.php b/ext/forum/main.php index 4bba03985..060735a7f 100644 --- a/ext/forum/main.php +++ b/ext/forum/main.php @@ -111,10 +111,10 @@ public function onPageRequest(PageRequestEvent $event) case "view": $threadID = int_escape($event->get_arg(1)); // $pageNumber = int_escape($event->get_arg(2)); - list($errors) = $this->sanity_check_viewed_thread($threadID); + $errors = $this->sanity_check_viewed_thread($threadID); - if ($errors != null) { - $this->theme->display_error(500, "Error", $errors); + if (count($errors) > 0) { + $this->theme->display_error(500, "Error", implode("
", $errors)); break; } @@ -133,10 +133,10 @@ public function onPageRequest(PageRequestEvent $event) case "create": $redirectTo = "forum/index"; if (!$user->is_anonymous()) { - list($errors) = $this->sanity_check_new_thread(); + $errors = $this->sanity_check_new_thread(); - if ($errors != null) { - $this->theme->display_error(500, "Error", $errors); + if (count($errors) > 0) { + $this->theme->display_error(500, "Error", implode("
", $errors)); break; } @@ -174,10 +174,10 @@ public function onPageRequest(PageRequestEvent $event) $threadID = int_escape($_POST["threadID"]); $total_pages = $this->get_total_pages_for_thread($threadID); if (!$user->is_anonymous()) { - list($errors) = $this->sanity_check_new_post(); + $errors = $this->sanity_check_new_post(); - if ($errors != null) { - $this->theme->display_error(500, "Error", $errors); + if (count($errors) > 0) { + $this->theme->display_error(500, "Error", implode("
", $errors)); break; } $this->save_new_post($threadID, $user); @@ -197,63 +197,69 @@ public function onPageRequest(PageRequestEvent $event) private function get_total_pages_for_thread(int $threadID): int { global $database, $config; - $result = $database->get_row("SELECT COUNT(1) AS count FROM forum_posts WHERE thread_id = :thread_id", ['thread_id' => $threadID]); + $result = $database->get_row(" + SELECT COUNT(1) AS count + FROM forum_posts + WHERE thread_id = :thread_id + ", ['thread_id' => $threadID]); return (int)ceil($result["count"] / $config->get_int("forumPostsPerPage")); } private function sanity_check_new_thread(): array { - $errors = null; + $errors = []; if (!array_key_exists("title", $_POST)) { - $errors .= "
No title supplied.
"; + $errors[] = "No title supplied."; } elseif (strlen($_POST["title"]) == 0) { - $errors .= "
You cannot have an empty title.
"; - } elseif (strlen(html_escape($_POST["title"])) > 255) { - $errors .= "
Your title is too long.
"; + $errors[] = "You cannot have an empty title."; + } elseif (strlen($_POST["title"]) > 255) { + $errors[] = "Your title is too long."; } if (!array_key_exists("message", $_POST)) { - $errors .= "
No message supplied.
"; + $errors[] = "No message supplied."; } elseif (strlen($_POST["message"]) == 0) { - $errors .= "
You cannot have an empty message.
"; + $errors[] = "You cannot have an empty message."; } - return [$errors]; + return $errors; } private function sanity_check_new_post(): array { - $errors = null; + $errors = []; if (!array_key_exists("threadID", $_POST)) { - $errors = "
No thread ID supplied.
"; + $errors[] = "No thread ID supplied."; } elseif (strlen($_POST["threadID"]) == 0) { - $errors = "
No thread ID supplied.
"; + $errors[] = "No thread ID supplied."; } elseif (is_numeric($_POST["threadID"])) { if (!array_key_exists("message", $_POST)) { - $errors .= "
No message supplied.
"; + $errors[] = "No message supplied."; } elseif (strlen($_POST["message"]) == 0) { - $errors .= "
You cannot have an empty message.
"; + $errors[] = "You cannot have an empty message."; } } - return [$errors]; + return $errors; } + /** + * @return string[] + */ private function sanity_check_viewed_thread(int $threadID): array { - $errors = null; + $errors = []; if (!$this->threadExists($threadID)) { - $errors = "
Inexistent thread.
"; + $errors[] = "Inexistent thread."; } - return [$errors]; + return $errors; } private function get_thread_title(int $threadID): string { global $database; - $result = $database->get_row("SELECT t.title FROM forum_threads AS t WHERE t.id = :id ", ['id' => $threadID]); - return $result["title"]; + return $database->get_one("SELECT t.title FROM forum_threads AS t WHERE t.id = :id ", ['id' => $threadID]); } private function show_last_threads(Page $page, PageRequestEvent $event, bool $showAdminOptions = false): void @@ -312,7 +318,7 @@ private function show_posts(PageRequestEvent $event, bool $showAdminOptions = fa private function save_new_thread(User $user): int { - $title = html_escape($_POST["title"]); + $title = $_POST["title"]; $sticky = !empty($_POST["sticky"]); global $database; @@ -336,7 +342,7 @@ private function save_new_post(int $threadID, User $user): void { global $config; $userID = $user->id; - $message = html_escape($_POST["message"]); + $message = $_POST["message"]; $max_characters = $config->get_int('forumMaxCharsPerPost'); $message = substr($message, 0, $max_characters); diff --git a/ext/forum/theme.php b/ext/forum/theme.php index 93def4775..2d49021dc 100644 --- a/ext/forum/theme.php +++ b/ext/forum/theme.php @@ -4,6 +4,10 @@ namespace Shimmie2; +use MicroHTML\HTMLElement; + +use function MicroHTML\{INPUT, LABEL, SMALL, TEXTAREA, TR, TD, TABLE, TH, TBODY, THEAD, DIV, A, BR, emptyHTML, SUP, rawHTML}; + class ForumTheme extends Themelet { public function display_thread_list(Page $page, $threads, $showAdminOptions, $pageNumber, $totalPages) @@ -27,29 +31,41 @@ public function display_new_thread_composer(Page $page, $threadText = null, $thr { global $config, $user; $max_characters = $config->get_int('forumMaxCharsPerPost'); - $html = make_form(make_link("forum/create")); - - - if (!is_null($threadTitle)) { - $threadTitle = html_escape($threadTitle); - } - if (!is_null($threadText)) { - $threadText = html_escape($threadText); - } - - $html .= " - - - - "; - if ($user->can(Permissions::FORUM_ADMIN)) { - $html .= ""; - } - $html .= " -
Title:
Message:
Max characters alowed: $max_characters.
- - "; + $html = SHM_SIMPLE_FORM( + "forum/create", + TABLE( + ["style" => "width: 500px;"], + TR( + TD("Title:"), + TD(INPUT(["type" => "text", "name" => "title", "value" => $threadTitle])) + ), + TR( + TD("Message:"), + TD(TEXTAREA( + ["id" => "message", "name" => "message"], + $threadText + )) + ), + TR( + TD(), + TD(SMALL("Max characters allowed: $max_characters.")) + ), + $user->can(Permissions::FORUM_ADMIN) ? TR( + TD(), + TD( + LABEL(["for" => "sticky"], "Sticky:"), + INPUT(["name" => "sticky", "id" => "sticky", "type" => "checkbox", "value" => "Y"]) + ) + ) : null, + TR( + TD( + ["colspan" => 2], + INPUT(["type" => "submit", "value" => "Submit"]) + ) + ) + ) + ); $blockTitle = "Write a new thread"; $page->set_title(html_escape($blockTitle)); @@ -65,20 +81,27 @@ public function display_new_post_composer(Page $page, $threadID) $max_characters = $config->get_int('forumMaxCharsPerPost'); - $html = make_form(make_link("forum/answer")); - - $html .= ''; - - $html .= " - - - "; - - $html .= " -
Message: -
Max characters alowed: $max_characters.
- - "; + $html = SHM_SIMPLE_FORM( + "forum/answer", + INPUT(["type" => "hidden", "name" => "threadID", "value" => $threadID]), + TABLE( + ["style" => "width: 500px;"], + TR( + TD("Message:"), + TD(TEXTAREA(["id" => "message", "name" => "message"])) + ), + TR( + TD(), + TD(SMALL("Max characters allowed: $max_characters.")) + ), + TR( + TD( + ["colspan" => 2], + INPUT(["type" => "submit", "value" => "Submit"]) + ) + ) + ) + ); $blockTitle = "Answer to this thread"; $page->add_block(new Block($blockTitle, $html, "main", 130)); @@ -94,68 +117,70 @@ public function display_thread($posts, $showAdminOptions, $threadTitle, $threadI $current_post = 0; - $html = - "

". - "". - "". - "". - "". - ""; - + $tbody = TBODY(); foreach ($posts as $post) { $current_post++; - $message = $post["message"]; - - $message = send_event(new TextFormattingEvent($message))->formatted; - $message = str_replace('\n\r', '
', $message); - $message = str_replace('\r\n', '
', $message); - $message = str_replace('\n', '
', $message); - $message = str_replace('\r', '
', $message); - - $message = stripslashes($message); - - $userLink = "".$post["user_name"].""; - - $poster = User::by_name($post["user_name"]); - $gravatar = $poster->get_avatar_html(); - - $rank = "{$post["user_class"]}"; - - $postID = $post['id']; - - //if($user->can(Permissions::FORUM_ADMIN)){ - //$delete_link = "Delete"; - //} else { - //$delete_link = ""; - //} - - if ($showAdminOptions) { - $delete_link = "Delete"; - } else { - $delete_link = ""; - } $post_number = (($pageNumber - 1) * $posts_per_page) + $current_post; - $html .= " - - - - - - - - - - - - "; + $tbody->appendChild( + emptyHTML( + TR( + ["class" => "postHead"], + TD(["class" => "forumSupuser"]), + TD( + ["class" => "forumSupmessage"], + DIV( + ["class" => "deleteLink"], + $showAdminOptions ? A(["href" => make_link("forum/delete/".$threadID."/".$post['id'])], "Delete") : null + ) + ) + ), + TR( + ["class" => "posBody"], + TD( + ["class" => "forumUser"], + A(["href" => make_link("user/".$post["user_name"])], $post["user_name"]), + BR(), + SUP(["class" => "user_rank"], $post["user_class"]), + BR(), + rawHTML(User::by_name($post["user_name"])->get_avatar_html()), + BR() + ), + TD( + ["class" => "forumMessage"], + DIV(["class" => "postDate"], SMALL(rawHTML(autodate($post['date'])))), + DIV(["class" => "postNumber"], " #".$post_number), + BR(), + DIV(["class" => "postMessage"], rawHTML(send_event(new TextFormattingEvent($post["message"]))->formatted)) + ) + ), + TR( + ["class" => "postFoot"], + TD(["class" => "forumSubuser"]), + TD(["class" => "forumSubmessage"]) + ) + ) + ); } - $html .= "
UserMessage
".$userLink."
".$rank."
".$gravatar."
- -
#".$post_number."
-
-
".$message."
"; + $html = emptyHTML( + DIV( + ["id" => "returnLink"], + A(["href" => make_link("forum/index/")], "Return") + ), + BR(), + BR(), + TABLE( + ["id" => "threadPosts", "class" => "zebra"], + THEAD( + TR( + TH(["id" => "threadHeadUser"], "User"), + TH("Message") + ) + ), + $tbody + ) + ); $this->display_paginator($page, "forum/view/".$threadID, null, $pageNumber, $totalPages); @@ -164,37 +189,32 @@ public function display_thread($posts, $showAdminOptions, $threadTitle, $threadI $page->add_block(new Block($threadTitle, $html, "main", 20)); } - - public function add_actions_block(Page $page, $threadID) { - $html = 'Delete this thread and its posts.'; - + $html = A(["href" => make_link("forum/nuke/".$threadID)], "Delete this thread and its posts."); $page->add_block(new Block("Admin Actions", $html, "main", 140)); } - - - private function make_thread_list($threads, $showAdminOptions): string + private function make_thread_list($threads, $showAdminOptions): HTMLElement { - $html = "". - "". - "". - "". - "". - ""; - - if ($showAdminOptions) { - $html .= ""; - } + global $config; - $html .= ""; + $tbody = TBODY(); + $html = TABLE( + ["id" => "threadList", "class" => "zebra"], + THEAD( + TR( + TH("Title"), + TH("Author"), + TH("Updated"), + TH("Responses"), + $showAdminOptions ? TH("Actions") : null + ) + ), + $tbody + ); - $current_post = 0; foreach ($threads as $thread) { - $oe = ($current_post++ % 2 == 0) ? "even" : "odd"; - - global $config; $titleSubString = $config->get_int('forumTitleSubString'); if ($titleSubString < strlen($thread["title"])) { @@ -204,27 +224,23 @@ private function make_thread_list($threads, $showAdminOptions): string $title = $thread["title"]; } - if (bool_escape($thread["sticky"])) { - $sticky = "Sticky: "; - } else { - $sticky = ""; - } - - $html .= "". - '". - '". - "". - ""; - - if ($showAdminOptions) { - $html .= ''; - } - - $html .= ""; + $tbody->appendChild( + TR( + TD( + ["class" => "left"], + bool_escape($thread["sticky"]) ? "Sticky: " : "", + A(["href" => make_link("forum/view/".$thread["id"])], $title) + ), + TD( + A(["href" => make_link("user/".$thread["user_name"])], $thread["user_name"]) + ), + TD(rawHTML(autodate($thread["uptodate"]))), + TD($thread["response_count"]), + $showAdminOptions ? TD(A(["href" => make_link("forum/nuke/".$thread["id"])], "Delete")) : null + ) + ); } - $html .= "
TitleAuthorUpdatedResponsesActions
'.$sticky.''.$title."'.$thread["user_name"]."".autodate($thread["uptodate"])."".$thread["response_count"]."Delete
"; - return $html; } } From d8986159fe858d265151f62df9ff39b65d805018 Mon Sep 17 00:00:00 2001 From: Shish Date: Thu, 4 Jan 2024 18:40:02 +0000 Subject: [PATCH 02/10] avoid passing an array as a database parameter, fixes #820 --- ext/tag_editcloud/main.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ext/tag_editcloud/main.php b/ext/tag_editcloud/main.php index 1158d8a4b..5f7b52ae2 100644 --- a/ext/tag_editcloud/main.php +++ b/ext/tag_editcloud/main.php @@ -88,6 +88,8 @@ private function build_tag_map(Image $image): ?HTMLElement if (count($relevant_tags) == 0) { return null; } + $relevant_tag_ids = implode(',', array_map(fn ($t) => Tag::get_or_create_id($t), $relevant_tags)); + $tag_data = $database->get_all( " SELECT t2.tag AS tag, COUNT(image_id) AS count, FLOOR(LN(LN(COUNT(image_id) - :tag_min1 + 1)+1)*150)/200 AS scaled @@ -95,11 +97,11 @@ private function build_tag_map(Image $image): ?HTMLElement JOIN image_tags it2 USING(image_id) JOIN tags t1 ON it1.tag_id = t1.id JOIN tags t2 ON it2.tag_id = t2.id - WHERE t1.count >= :tag_min2 AND t1.tag IN(:relevant_tags) + WHERE t1.count >= :tag_min2 AND t1.tag_id IN ($relevant_tag_ids) GROUP BY t2.tag ORDER BY count DESC LIMIT :limit", - ["tag_min1" => $tags_min, "tag_min2" => $tags_min, "limit" => $max_count, "relevant_tags" => $relevant_tags] + ["tag_min1" => $tags_min, "tag_min2" => $tags_min, "limit" => $max_count] ); break; /** @noinspection PhpMissingBreakStatementInspection */ From 7524abe040cf1b1b3871409462c22f7393c4b0db Mon Sep 17 00:00:00 2001 From: Shish Date: Thu, 4 Jan 2024 18:41:35 +0000 Subject: [PATCH 03/10] bumps --- composer.lock | 85 ++++++++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/composer.lock b/composer.lock index 3183e5b35..e67148368 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.295.4", + "version": "3.295.5", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "2372661db989fe4229abd95f4434b37252076d58" + "reference": "cd9d48ebfdfc8fb5f6df9fe95dced622287f3412" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2372661db989fe4229abd95f4434b37252076d58", - "reference": "2372661db989fe4229abd95f4434b37252076d58", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/cd9d48ebfdfc8fb5f6df9fe95dced622287f3412", + "reference": "cd9d48ebfdfc8fb5f6df9fe95dced622287f3412", "shasum": "" }, "require": { @@ -151,16 +151,16 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.295.4" + "source": "https://github.com/aws/aws-sdk-php/tree/3.295.5" }, - "time": "2023-12-29T19:07:49+00:00" + "time": "2024-01-03T19:12:43+00:00" }, { "name": "bower-asset/jquery", "version": "1.12.4", "source": { "type": "git", - "url": "https://github.com/jquery/jquery-dist.git", + "url": "git@github.com:jquery/jquery-dist.git", "reference": "5e89585e0121e72ff47de177c5ef604f3089a53d" }, "dist": { @@ -1322,21 +1322,21 @@ }, { "name": "shish/ffsphp", - "version": "v1.3.0", + "version": "v1.3.2", "source": { "type": "git", "url": "https://github.com/shish/ffsphp.git", - "reference": "26eea8149fda5f20bed7399b8b4a84946448bec0" + "reference": "d69223f4317de302b6cd485d0a43709788dd6f69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/shish/ffsphp/zipball/26eea8149fda5f20bed7399b8b4a84946448bec0", - "reference": "26eea8149fda5f20bed7399b8b4a84946448bec0", + "url": "https://api.github.com/repos/shish/ffsphp/zipball/d69223f4317de302b6cd485d0a43709788dd6f69", + "reference": "d69223f4317de302b6cd485d0a43709788dd6f69", "shasum": "" }, "require": { "ext-pdo": "*", - "php": "^8.0" + "php": "^8.1" }, "require-dev": { "friendsofphp/php-cs-fixer": "3.41.1", @@ -1365,9 +1365,9 @@ "homepage": "https://github.com/shish/ffsphp", "support": { "issues": "https://github.com/shish/ffsphp/issues", - "source": "https://github.com/shish/ffsphp/tree/v1.3.0" + "source": "https://github.com/shish/ffsphp/tree/v1.3.2" }, - "time": "2024-01-01T14:48:00+00:00" + "time": "2024-01-04T18:38:54+00:00" }, { "name": "shish/gqla", @@ -1589,12 +1589,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "reference": "2c438b99bb2753c1628c1e6f523991edea5b03a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/2c438b99bb2753c1628c1e6f523991edea5b03a4", + "reference": "2c438b99bb2753c1628c1e6f523991edea5b03a4", "shasum": "" }, "require": { @@ -1604,7 +1604,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -1633,7 +1633,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/main" }, "funding": [ { @@ -1649,7 +1649,7 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-01-02T14:07:37+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -2418,12 +2418,12 @@ "source": { "type": "git", "url": "https://github.com/schmittjoh/serializer.git", - "reference": "4ff99e4cbba44cdbe57028ec2ccc874c8f61bbe9" + "reference": "39096dd64dd6c66afc7eb7f294918243fd80d383" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/4ff99e4cbba44cdbe57028ec2ccc874c8f61bbe9", - "reference": "4ff99e4cbba44cdbe57028ec2ccc874c8f61bbe9", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/39096dd64dd6c66afc7eb7f294918243fd80d383", + "reference": "39096dd64dd6c66afc7eb7f294918243fd80d383", "shasum": "" }, "require": { @@ -2446,6 +2446,7 @@ "phpstan/phpstan": "^1.0.2", "phpunit/phpunit": "^9.0 || ^10.0", "psr/container": "^1.0 || ^2.0", + "rector/rector": "^0.18.13", "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", "symfony/expression-language": "^5.4 || ^6.0 || ^7.0", "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", @@ -2507,7 +2508,7 @@ "type": "github" } ], - "time": "2023-12-22T14:51:09+00:00" + "time": "2024-01-03T21:21:26+00:00" }, { "name": "myclabs/deep-copy", @@ -2826,16 +2827,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.5", + "version": "1.25.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc" + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fedf211ff14ec8381c9bf5714e33a7a552dd1acc", - "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", "shasum": "" }, "require": { @@ -2867,9 +2868,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.5" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0" }, - "time": "2023-12-16T09:33:33+00:00" + "time": "2024-01-04T17:06:16+00:00" }, { "name": "phpstan/phpstan", @@ -4716,12 +4717,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" + "reference": "705c57c64120840dc3043ef1d43916f46af4f986" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", - "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/705c57c64120840dc3043ef1d43916f46af4f986", + "reference": "705c57c64120840dc3043ef1d43916f46af4f986", "shasum": "" }, "require": { @@ -4732,7 +4733,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -4769,7 +4770,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/main" }, "funding": [ { @@ -4785,7 +4786,7 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-01-02T14:07:37+00:00" }, { "name": "symfony/filesystem", @@ -5462,12 +5463,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + "reference": "cea2eccfcd27ac3deb252bd67f78b9b8ffc4da84" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/cea2eccfcd27ac3deb252bd67f78b9b8ffc4da84", + "reference": "cea2eccfcd27ac3deb252bd67f78b9b8ffc4da84", "shasum": "" }, "require": { @@ -5481,7 +5482,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -5521,7 +5522,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" + "source": "https://github.com/symfony/service-contracts/tree/main" }, "funding": [ { @@ -5537,7 +5538,7 @@ "type": "tidelift" } ], - "time": "2023-12-26T14:02:43+00:00" + "time": "2024-01-02T14:07:37+00:00" }, { "name": "symfony/stopwatch", From e3febc54886a4adea25f9fb515c5b0ee0d7747c1 Mon Sep 17 00:00:00 2001 From: Shish Date: Thu, 4 Jan 2024 19:21:29 +0000 Subject: [PATCH 04/10] [cron_uploader] minor fixes --- ext/cron_uploader/main.php | 4 ++-- ext/cron_uploader/theme.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php index 1f97a0f91..de6a0de9d 100644 --- a/ext/cron_uploader/main.php +++ b/ext/cron_uploader/main.php @@ -464,7 +464,7 @@ private function move_uploaded(string $path, string $filename, string $output_su /** * Generate the necessary DataUploadEvent for a given image and tags. */ - private function add_image(string $tmpname, string $filename, string $tags): DataUploadEvent + private function add_image(string $tmpname, string $filename, array $tags): DataUploadEvent { $event = add_image($tmpname, $filename, $tags, null); @@ -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 = Tag::implode(path_to_tags($relativePath)); + $tags = path_to_tags($relativePath); yield [ 0 => $fullpath, diff --git a/ext/cron_uploader/theme.php b/ext/cron_uploader/theme.php index 977f40f34..6d89890c5 100644 --- a/ext/cron_uploader/theme.php +++ b/ext/cron_uploader/theme.php @@ -34,7 +34,7 @@ public function display_documentation( $page->set_heading("Cron Uploader"); if (!$config->get_bool(UserConfig::ENABLE_API_KEYS)) { - $info_html .= "THIS EXTENSION REQUIRES USER API KEYS TO BE ENABLED IN BOARD ADMIN"; + $info_html .= "THIS EXTENSION REQUIRES USER API KEYS TO BE ENABLED IN BOARD ADMIN
"; } $info_html .= "Information From b060accc4453697661e15824117fde277023d926 Mon Sep 17 00:00:00 2001 From: Shish Date: Thu, 4 Jan 2024 19:21:55 +0000 Subject: [PATCH 05/10] [view] don't show prev/next when the user has searched for order:... - fixes #790 --- ext/view/test.php | 18 ++++++++++++++++++ ext/view/theme.php | 37 ++++++++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/ext/view/test.php b/ext/view/test.php index fc6898a07..e272c06dd 100644 --- a/ext/view/test.php +++ b/ext/view/test.php @@ -66,6 +66,24 @@ public function testPrevNext() $this->assertEquals(404, $page->code); } + public function testPrevNextDisabledWhenOrdered() + { + $this->log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test"); + + $this->get_page("post/view/$image_id"); + $this->assert_text("Prev"); + + $this->get_page("post/view/$image_id", ["search" => "test"]); + $this->assert_text("Prev"); + + $this->get_page("post/view/$image_id", ["search" => "cake_order:_the_cakening"]); + $this->assert_text("Prev"); + + $this->get_page("post/view/$image_id", ["search" => "order:score"]); + $this->assert_no_text("Prev"); + } + public function testView404() { $this->log_in_as_user(); diff --git a/ext/view/theme.php b/ext/view/theme.php index 657a5404f..84efbf014 100644 --- a/ext/view/theme.php +++ b/ext/view/theme.php @@ -35,8 +35,10 @@ public function display_page(Image $image, $editor_parts) //$page->add_block(new Block(null, $this->build_pin($image), "main", 11)); $query = $this->get_query(); - $page->add_html_header(""); - $page->add_html_header(""); + if(!$this->is_ordered_search()) { + $page->add_html_header(""); + $page->add_html_header(""); + } } public function display_admin_block(Page $page, $parts) @@ -56,14 +58,35 @@ protected function get_query(): ?string return $query; } + /** + * prev/next only work for default-ordering searches - if the user + * has specified a custom order, we can't show prev/next. + */ + protected function is_ordered_search(): bool + { + if(isset($_GET['search'])) { + $tags = Tag::explode($_GET['search']); + foreach($tags as $tag) { + if(preg_match("/^order[=:]/", $tag) == 1) { + return true; + } + } + } + return false; + } + protected function build_pin(Image $image): HTMLElement { $query = $this->get_query(); - return joinHTML(" | ", [ - A(["href" => make_link("post/prev/{$image->id}", $query), "id" => "prevlink"], "Prev"), - A(["href" => make_link()], "Index"), - A(["href" => make_link("post/next/{$image->id}", $query), "id" => "nextlink"], "Next"), - ]); + if($this->is_ordered_search()) { + return A(["href" => make_link()], "Index"); + } else { + return joinHTML(" | ", [ + A(["href" => make_link("post/prev/{$image->id}", $query), "id" => "prevlink"], "Prev"), + A(["href" => make_link()], "Index"), + A(["href" => make_link("post/next/{$image->id}", $query), "id" => "nextlink"], "Next"), + ]); + } } protected function build_navigation(Image $image): string From 48b3de3c6eeb6d6ea329911d4095b524b5c39502 Mon Sep 17 00:00:00 2001 From: Shish Date: Thu, 4 Jan 2024 22:48:56 +0000 Subject: [PATCH 06/10] [core] fix error in error handling --- core/database.php | 25 ++++++++++++++++++------- core/exceptions.php | 4 +--- core/util.php | 16 +++++++++++----- ext/random_image/main.php | 5 +---- ext/tag_edit/main.php | 2 +- 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/core/database.php b/core/database.php index 5d53ae698..e09161c25 100644 --- a/core/database.php +++ b/core/database.php @@ -7,6 +7,8 @@ use FFSPHP\PDO; use FFSPHP\PDOStatement; +require_once __DIR__ . '/exceptions.php'; + enum DatabaseDriverID: string { case MYSQL = "mysql"; @@ -14,6 +16,20 @@ enum DatabaseDriverID: string case SQLITE = "sqlite"; } +class DatabaseException extends SCoreException +{ + public string $query; + public array $args; + + public function __construct(string $msg, string $query, array $args) + { + parent::__construct($msg); + $this->error = $msg; + $this->query = $query; + $this->args = $args; + } +} + /** * A class for controlled database access */ @@ -158,18 +174,13 @@ public function notify(string $channel, ?string $data = null): void public function _execute(string $query, array $args = []): PDOStatement { try { - $ret = $this->get_db()->execute( + return $this->get_db()->execute( "-- " . str_replace("%2F", "/", urlencode($_GET['q'] ?? '')). "\n" . $query, $args ); - if ($ret === false) { - throw new SCoreException("Query failed", $query); - } - /** @noinspection PhpIncompatibleReturnTypeInspection */ - return $ret; } catch (\PDOException $pdoe) { - throw new SCoreException($pdoe->getMessage(), $query); + throw new DatabaseException($pdoe->getMessage(), $query, $args); } } diff --git a/core/exceptions.php b/core/exceptions.php index 139b86700..9683d33cf 100644 --- a/core/exceptions.php +++ b/core/exceptions.php @@ -9,15 +9,13 @@ */ class SCoreException extends \RuntimeException { - public ?string $query; public string $error; public int $http_code = 500; - public function __construct(string $msg, ?string $query = null) + public function __construct(string $msg) { parent::__construct($msg); $this->error = $msg; - $this->query = $query; } } diff --git a/core/util.php b/core/util.php index 62081a4df..432b89951 100644 --- a/core/util.php +++ b/core/util.php @@ -627,8 +627,6 @@ function _fatal_error(\Exception $e): void $version = VERSION; $message = $e->getMessage(); $phpver = phpversion(); - $query = is_subclass_of($e, "Shimmie2\SCoreException") ? $e->query : null; - $code = is_subclass_of($e, "Shimmie2\SCoreException") ? $e->http_code : 500; //$hash = exec("git rev-parse HEAD"); //$h_hash = $hash ? "

Hash: $hash" : ""; @@ -646,13 +644,21 @@ function _fatal_error(\Exception $e): void print("Message: $message\n"); - if ($query) { - print("Query: {$query}\n"); + if (is_a($e, DatabaseException::class)) { + print("Query: {$e->query}\n"); + print("Args: ".var_export($e->args, true)."\n"); } print("Version: $version (on $phpver)\n"); } else { - $q = $query ? "" : "

Query: " . html_escape($query); + $query = is_a($e, DatabaseException::class) ? $e->query : null; + $code = is_a($e, SCoreException::class) ? $e->http_code : 500; + + $q = ""; + if(is_a($e, DatabaseException::class)) { + $q .= "

Query: " . html_escape($query); + $q .= "

Args: " . html_escape(var_export($e->args, true)); + } if ($code >= 500) { error_log("Shimmie Error: $message (Query: $query)\n{$e->getTraceAsString()}"); } diff --git a/ext/random_image/main.php b/ext/random_image/main.php index 05c89294d..759512153 100644 --- a/ext/random_image/main.php +++ b/ext/random_image/main.php @@ -25,10 +25,7 @@ public function onPageRequest(PageRequestEvent $event) } $image = Image::by_random($search_terms); if (!$image) { - throw new SCoreException( - "Couldn't find any posts randomly", - Tag::implode($search_terms) - ); + throw new SCoreException("Couldn't find any posts randomly"); } if ($action === "download") { diff --git a/ext/tag_edit/main.php b/ext/tag_edit/main.php index 309c4ae7f..986b54185 100644 --- a/ext/tag_edit/main.php +++ b/ext/tag_edit/main.php @@ -44,7 +44,7 @@ class TagSetException extends UserErrorException public function __construct(string $msg, ?string $redirect = null) { - parent::__construct($msg, null); + parent::__construct($msg); $this->redirect = $redirect; } } From 05a981d935bdd06083cebf9514cd71c6c8c1016c Mon Sep 17 00:00:00 2001 From: Shish Date: Fri, 5 Jan 2024 02:03:41 +0000 Subject: [PATCH 07/10] [notes] replace all the javascript --- ext/notes/border-h.gif | Bin 72 -> 0 bytes ext/notes/border-v.gif | Bin 72 -> 0 bytes .../lib/jquery.imgareaselect-1.0.0-rc1.min.js | 2 - ext/notes/lib/jquery.imgnotes-1.0.min.css | 2 - ext/notes/lib/jquery.imgnotes-1.0.min.js | 15 - ext/notes/lib/spacer.gif | Bin 43 -> 0 bytes ext/notes/main.php | 108 +++--- ext/notes/script.js | 315 ++++++++++++++---- ext/notes/style.css | 112 +++---- ext/notes/theme.php | 131 ++------ .../image_admin_block_building_event.php | 5 +- 11 files changed, 381 insertions(+), 309 deletions(-) delete mode 100644 ext/notes/border-h.gif delete mode 100644 ext/notes/border-v.gif delete mode 100644 ext/notes/lib/jquery.imgareaselect-1.0.0-rc1.min.js delete mode 100644 ext/notes/lib/jquery.imgnotes-1.0.min.css delete mode 100644 ext/notes/lib/jquery.imgnotes-1.0.min.js delete mode 100644 ext/notes/lib/spacer.gif diff --git a/ext/notes/border-h.gif b/ext/notes/border-h.gif deleted file mode 100644 index a2aa5b0d09bf7215199dbf9a291b34b11d0352d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72 zcmZ?wbhEHbWMg1tSjfQe|Nnmm1_m=TGay6ppOkY^YGO%hib8p2NrpmVR$@g?eqxGV XW?ou8gAPy~14tJG6LU)|D}yxvaY_|= diff --git a/ext/notes/border-v.gif b/ext/notes/border-v.gif deleted file mode 100644 index 4bfd55564099537d38d4112f550b8974744de3ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72 zcmZ?wbhEHbWMp7tSjfQe|Nnmm1_m=TGay6ppOkY^YGO%hib8p2NrpmVR$@g?eqxGV XW?ou8gAPy~14tJG6LU)|D}yxvaW)ls diff --git a/ext/notes/lib/jquery.imgareaselect-1.0.0-rc1.min.js b/ext/notes/lib/jquery.imgareaselect-1.0.0-rc1.min.js deleted file mode 100644 index c842914da..000000000 --- a/ext/notes/lib/jquery.imgareaselect-1.0.0-rc1.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! imgareaselect 1.0.0-rc.1 */ -(function(e){function t(){return e("

")}var o=Math.abs,n=Math.max,i=Math.min,s=Math.round;e.imgAreaSelect=function(a,c){function r(e){return e+bt.left-St.left}function d(e){return e+bt.top-St.top}function u(e){return e-bt.left+St.left}function h(e){return e-bt.top+St.top}function f(e){var t,o=m(e)||e;return(t=parseInt(o.pageX))?t-St.left:void 0}function l(e){var t,o=m(e)||e;return(t=parseInt(o.pageY))?t-St.top:void 0}function m(e){var t=e.originalEvent||{};return t.touches&&t.touches.length?t.touches[0]:!1}function p(e){var t=e||G,o=e||J;return{x1:s(At.x1*t),y1:s(At.y1*o),x2:s(At.x2*t)-1,y2:s(At.y2*o)-1,width:s(At.x2*t)-s(At.x1*t),height:s(At.y2*o)-s(At.y1*o)}}function v(e,t,o,n,i){var a=i||G,c=i||J;At={x1:s(e/a||0),y1:s(t/c||0),x2:s(++o/a||0),y2:s(++n/c||0)},At.width=At.x2-At.x1,At.height=At.y2-At.y1}function y(){$&&pt.width()&&(bt={left:s(pt.offset().left),top:s(pt.offset().top)},Q=pt.innerWidth(),R=pt.innerHeight(),bt.top+=pt.outerHeight()-R>>1,bt.left+=pt.outerWidth()-Q>>1,V=s(c.minWidth/G)||0,Z=s(c.minHeight/J)||0,_=s(i(c.maxWidth/G||1<<24,Q)),et=s(i(c.maxHeight/J||1<<24,R)),St="fixed"==kt?{left:e(document).scrollLeft(),top:e(document).scrollTop()}:/static|^$/.test(X.css("position"))?{left:0,top:0}:{left:s(X.offset().left)-X.scrollLeft(),top:s(X.offset().top)-X.scrollTop()},L=r(0),j=d(0),(At.x2>Q||At.y2>R)&&P())}function g(t){if(ot){switch(vt.css({left:r(At.x1),top:d(At.y1)}).add(yt).width(ft=At.width).height(lt=At.height),yt.add(gt).add(wt).css({left:0,top:0}),gt.add(xt).width(n(ft-gt.outerWidth()+gt.innerWidth(),0)).height(n(lt-gt.outerHeight()+gt.innerHeight(),0)),xt.css({left:L,top:j,width:ft,height:lt,borderStyle:"solid",borderWidth:At.y1+"px "+(Q-At.x2)+"px "+(R-At.y2)+"px "+At.x1+"px"}),ft-=wt.outerWidth(),lt-=wt.outerHeight(),wt.length){case 8:e(wt[4]).css({left:ft>>1}),e(wt[5]).css({left:ft,top:lt>>1}),e(wt[6]).css({left:ft>>1,top:lt}),e(wt[7]).css({top:lt>>1});case 4:wt.slice(1,3).css({left:ft}),wt.slice(2,4).css({top:lt})}t!==!1&&(e.imgAreaSelect.keyPress!=Pt&&e(document).unbind(e.imgAreaSelect.keyPress,e.imgAreaSelect.onKeyPress),c.keys&&e(document)[e.imgAreaSelect.keyPress](e.imgAreaSelect.onKeyPress=Pt))}}function x(e){y(),g(e),nt=r(At.x1),it=d(At.y1),st=r(At.x2),at=d(At.y2)}function w(e,t){c.fadeDuration?e.fadeOut(c.fadeDuration,t):e.hide()}function b(e){return ct&&!/^touch/.test(e.type)}function S(e){var t=u(f(e))-At.x1,o=h(l(e))-At.y1;U="",c.resizable&&(c.resizeMargin>=o?U="n":o>=At.height-c.resizeMargin&&(U="s"),c.resizeMargin>=t?U+="w":t>=At.width-c.resizeMargin&&(U+="e")),vt.css("cursor",U?U+"-resize":c.movable?"move":"")}function z(e){b(e)||(mt||(y(),mt=!0,vt.one("mouseout",function(){mt=!1})),S(e))}function k(t){ct=!1,e("body").css("cursor",""),(c.autoHide||0==At.width*At.height)&&w(vt.add(xt),function(){e(this).hide()}),e(document).off("mousemove touchmove",N),vt.on("mousemove touchmove",z),t&&c.onSelectEnd(a,p())}function A(t){return"mousedown"==t.type&&1!=t.which?!1:("touchstart"==t.type?(ct&&k(),ct=!0,S(t)):y(),U?(nt=r(At["x"+(1+/w/.test(U))]),it=d(At["y"+(1+/n/.test(U))]),st=r(At["x"+(1+!/w/.test(U))]),at=d(At["y"+(1+!/n/.test(U))]),B=st-f(t),F=at-l(t),e(document).on("mousemove touchmove",N).one("mouseup touchend",k),vt.off("mousemove touchmove",z)):c.movable?(Y=L+At.x1-f(t),q=j+At.y1-l(t),vt.off("mousemove touchmove",z),e(document).on("mousemove touchmove",H).one("mouseup touchend",function(){ct=!1,c.onSelectEnd(a,p()),e(document).off("mousemove touchmove",H),vt.on("mousemove touchmove",z)})):pt.mousedown(t),!1)}function I(e){tt&&(e?(st=n(L,i(L+Q,nt+o(at-it)*tt*(st>nt||-1))),at=s(n(j,i(j+R,it+o(st-nt)/tt*(at>it||-1)))),st=s(st)):(at=n(j,i(j+R,it+o(st-nt)/tt*(at>it||-1))),st=s(n(L,i(L+Q,nt+o(at-it)*tt*(st>nt||-1)))),at=s(at)))}function P(){nt=i(nt,L+Q),it=i(it,j+R),V>o(st-nt)&&(st=nt-V*(nt>st||-1),L>st?nt=L+V:st>L+Q&&(nt=L+Q-V)),Z>o(at-it)&&(at=it-Z*(it>at||-1),j>at?it=j+Z:at>j+R&&(it=j+R-Z)),st=n(L,i(st,L+Q)),at=n(j,i(at,j+R)),I(o(st-nt)_&&(st=nt-_*(nt>st||-1),I()),o(at-it)>et&&(at=it-et*(it>at||-1),I(!0)),At={x1:u(i(nt,st)),x2:u(n(nt,st)),y1:h(i(it,at)),y2:h(n(it,at)),width:o(st-nt),height:o(at-it)}}function K(){P(),g(),c.onSelectChange(a,p())}function N(e){return b(e)?void 0:(P(),st=/w|e|^$/.test(U)||tt?f(e)+B:r(At.x2),at=/n|s|^$/.test(U)||tt?l(e)+F:d(At.y2),K(),!1)}function C(t,o){st=(nt=t)+At.width,at=(it=o)+At.height,e.extend(At,{x1:u(nt),y1:h(it),x2:u(st),y2:h(at)}),g(),c.onSelectChange(a,p())}function H(e){return b(e)?void 0:(nt=n(L,i(Y+f(e),L+Q-At.width)),it=n(j,i(q+l(e),j+R-At.height)),C(nt,it),e.preventDefault(),!1)}function M(){e(document).off("mousemove touchmove",M),y(),st=nt,at=it,K(),U="",xt.is(":visible")||vt.add(xt).hide().fadeIn(c.fadeDuration||0),ot=!0,e(document).off("mouseup touchend",W).on("mousemove touchmove",N).one("mouseup touchend",k),vt.off("mousemove touchmove",z),c.onSelectStart(a,p())}function W(){e(document).off("mousemove touchmove",M).off("mouseup touchend",W),w(vt.add(xt)),v(u(nt),h(it),u(nt),h(it)),this instanceof e.imgAreaSelect||(c.onSelectChange(a,p()),c.onSelectEnd(a,p()))}function D(t){return"mousedown"==t.type&&1!=t.which||xt.is(":animated")?!1:(ct="touchstart"==t.type,y(),Y=nt=f(t),q=it=l(t),B=F=0,e(document).on({"mousemove touchmove":M,"mouseup touchend":W}),!1)}function E(){x(!1)}function O(){$=!0,T(c=e.extend({classPrefix:"imgareaselect",movable:!0,parent:"body",resizable:!0,resizeMargin:10,onInit:function(){},onSelectStart:function(){},onSelectChange:function(){},onSelectEnd:function(){}},c)),c.show&&(ot=!0,y(),g(),vt.add(xt).hide().fadeIn(c.fadeDuration||0)),setTimeout(function(){c.onInit(a,p())},0)}function T(o){if(o.parent&&(X=e(o.parent)).append(vt).append(xt),e.extend(c,o),y(),null!=o.handles){for(wt.remove(),wt=e([]),ut=o.handles?"corners"==o.handles?4:8:0;ut--;)wt=wt.add(t());wt.addClass(c.classPrefix+"-handle").css({position:"absolute",fontSize:0,zIndex:zt+1||1}),!parseInt(wt.css("width"))>=0&&wt.width(5).height(5)}for(G=c.imageWidth/Q||1,J=c.imageHeight/R||1,null!=o.x1&&(v(o.x1,o.y1,o.x2,o.y2),o.show=!o.hide),o.keys&&(c.keys=e.extend({shift:1,ctrl:"resize"},o.keys)),xt.addClass(c.classPrefix+"-outer"),yt.addClass(c.classPrefix+"-selection"),ut=0;4>ut++;)e(gt[ut-1]).addClass(c.classPrefix+"-border"+ut);vt.append(yt.add(gt)).append(wt),Kt&&((ht=(xt.css("filter")||"").match(/opacity=(\d+)/))&&xt.css("opacity",ht[1]/100),(ht=(gt.css("filter")||"").match(/opacity=(\d+)/))&>.css("opacity",ht[1]/100)),o.hide?w(vt.add(xt)):o.show&&$&&(ot=!0,vt.add(xt).fadeIn(c.fadeDuration||0),x()),tt=(dt=(c.aspectRatio||"").split(/:/))[0]/dt[1],pt.add(xt).off("mousedown touchstart",D),c.disable||c.enable===!1?(vt.off({"mousemove touchmove":z,"mousedown touchstart":A}),e(window).off("resize",E)):((c.enable||c.disable===!1)&&((c.resizable||c.movable)&&vt.on({"mousemove touchmove":z,"mousedown touchstart":A}),e(window).resize(E)),c.persistent||pt.add(xt).on("mousedown touchstart",D)),c.enable=c.disable=void 0}var $,L,j,Q,R,X,Y,q,B,F,G,J,U,V,Z,_,et,tt,ot,nt,it,st,at,ct,rt,dt,ut,ht,ft,lt,mt,pt=e(a),vt=t(),yt=t(),gt=t().add(t()).add(t()).add(t()),xt=t(),wt=e([]),bt={left:0,top:0},St={left:0,top:0},zt=0,kt="absolute",At={x1:0,y1:0,x2:0,y2:0,width:0,height:0},It=navigator.userAgent,Pt=function(e){var t,o,s=c.keys,a=e.keyCode;if(t=isNaN(s.alt)||!e.altKey&&!e.originalEvent.altKey?!isNaN(s.ctrl)&&e.ctrlKey?s.ctrl:!isNaN(s.shift)&&e.shiftKey?s.shift:isNaN(s.arrows)?10:s.arrows:s.alt,"resize"==s.arrows||"resize"==s.shift&&e.shiftKey||"resize"==s.ctrl&&e.ctrlKey||"resize"==s.alt&&(e.altKey||e.originalEvent.altKey)){switch(a){case 37:t=-t;case 39:o=n(nt,st),nt=i(nt,st),st=n(o+t,nt),I();break;case 38:t=-t;case 40:o=n(it,at),it=i(it,at),at=n(o+t,it),I(!0);break;default:return}K()}else switch(nt=i(nt,st),it=i(it,at),a){case 37:C(n(nt-t,L),it);break;case 38:C(nt,n(it-t,j));break;case 39:C(nt+i(t,Q-u(st)),it);break;case 40:C(nt,it+i(t,R-h(at)));break;default:return}return!1};this.remove=function(){T({disable:!0}),vt.add(xt).remove()},this.getOptions=function(){return c},this.setOptions=T,this.getSelection=p,this.setSelection=v,this.cancelSelection=W,this.update=x;var Kt=(/msie ([\w.]+)/i.exec(It)||[])[1],Nt=/webkit/i.test(It)&&!/chrome/i.test(It);for(rt=pt;rt.length;)zt=n(zt,isNaN(rt.css("z-index"))?zt:rt.css("z-index")),c.parent||"fixed"!=rt.css("position")||(kt="fixed"),rt=rt.parent(":not(body)");zt=c.zIndex||zt,e.imgAreaSelect.keyPress=Kt||Nt?"keydown":"keypress",vt.add(xt).hide().css({position:kt,overflow:"hidden",zIndex:zt||"0"}),vt.css({zIndex:zt+2||2}),yt.add(gt).css({position:"absolute",fontSize:0}),a.complete||"complete"==a.readyState||!pt.is("img")?O():pt.one("load",O),!$&&Kt&&Kt>=7&&(a.src=a.src)},e.fn.imgAreaSelect=function(t){return t=t||{},this.each(function(){e(this).data("imgAreaSelect")?t.remove?(e(this).data("imgAreaSelect").remove(),e(this).removeData("imgAreaSelect")):e(this).data("imgAreaSelect").setOptions(t):t.remove||(void 0===t.enable&&void 0===t.disable&&(t.enable=!0),e(this).data("imgAreaSelect",new e.imgAreaSelect(this,t)))}),t.instance?e(this).data("imgAreaSelect"):this}})(jQuery); diff --git a/ext/notes/lib/jquery.imgnotes-1.0.min.css b/ext/notes/lib/jquery.imgnotes-1.0.min.css deleted file mode 100644 index 214f3c7a0..000000000 --- a/ext/notes/lib/jquery.imgnotes-1.0.min.css +++ /dev/null @@ -1,2 +0,0 @@ -/** imgnotes jQuery plugin v1.0.0 **/ -p{font-family:"Times New Roman";font-size:14px}.ui-widget-content{font-family:"Times New Roman"}.ui-widget-content a{color:blue}.marker{position:absolute;width:27px;height:40px}.marker-text{position:absolute;top:10%;width:100%;margin:0 0 0 0;z-index:1;font-size:12px;font-weight:700;text-align:center;color:#fff}.pin{position:absolute;width:20px;height:30px}.pin-text{position:absolute;top:10%;width:100%;margin:0 0 0 0;font-size:14px;font-weight:700;text-align:center;color:#000}.tooltip{position:relative;display:inline-block}.tooltip .tooltiptext{visibility:visible;width:180px;background-color:#fff;color:#000;text-align:center;padding:5px 0;border-radius:6px;position:absolute;z-index:1;bottom:7px;left:50%;margin-left:-90px}.tooltip .tooltiptext::after{content:"";position:absolute;top:100%;left:50%;margin-left:-7px;border-width:7px;border-style:solid;border-color:#fff transparent transparent transparent}table.gridtable{font-family:verdana,arial,sans-serif;font-size:11px;color:#333;border-width:1px;border-color:#666;border-collapse:collapse}table.gridtable th{border-width:1px;padding:8px;border-style:solid;border-color:#666;background-color:#dedede}table.gridtable td{border-width:1px;padding:8px;border-style:solid;border-color:#666;background-color:#fff} \ No newline at end of file diff --git a/ext/notes/lib/jquery.imgnotes-1.0.min.js b/ext/notes/lib/jquery.imgnotes-1.0.min.js deleted file mode 100644 index 241769f7e..000000000 --- a/ext/notes/lib/jquery.imgnotes-1.0.min.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * imgnotes jQuery plugin - * version 1.0 - * - * Copyright (c) 2008 - Dr. Tarique Sani - * - * Dual licensed under the MIT (MIT-LICENSE.txt) - * and GPL (GPL-LICENSE.txt) licenses. - * - * @URL http://www.sanisoft.com/blog/2008/05/26/img-notes-jquery-plugin/ - * @Example example.html - * - **/ - -(function(e){function t(){e(".note").hover(function(){e(".note").show();e(this).next(".notep").show();e(this).next(".notep").css("z-index",1e4)},function(){e(".note").show();e(this).next(".notep").hide();e(this).next(".notep").css("z-index",0)})}function n(t){note_left=parseInt(imgOffset.left)+parseInt(t.x1);note_top=parseInt(imgOffset.top)+parseInt(t.y1);note_p_top=note_top+parseInt(t.height)+5;note_area_div=e("
").css({left:note_left+"px",top:note_top+"px",width:t.width+"px",height:t.height+"px"});note_text_div=e('
'+t.note+"
").css({left:note_left+"px",top:note_p_top+"px"});e("body").append(note_area_div);e("body").append(note_text_div)}function r(t){if(true!==t){return}notes_icon_left=parseInt(imgOffset.left)+parseInt(imgWidth)-36;notes_icon_top=parseInt(imgOffset.top)+parseInt(imgHieght)-40;notes_icon_div=note_area_div=e("
").css({left:notes_icon_left+"px",top:notes_icon_top+"px"});e("body").append(notes_icon_div);e(".notesicon").toggle(function(){e.fn.imgNotes.showAll()},function(){e.fn.imgNotes.hideAll()})}e.fn.imgNotes=function(i){if(undefined==s){var s}if(undefined!=i.notes){s=i.notes}if(i.url){e.ajaxSetup({async:false});e.getJSON(i.url,function(e){s=e})}image=this;imgOffset=e(image).offset();imgHieght=e(image).height();imgWidth=e(image).width();e(s).each(function(){n(this)});e(image).hover(function(){e(".note").show()},function(){e(".note").hide();e(".notep").hide()});t();r(i.isMobile);e(window).resize(function(){e(".note").remove();e(".notep").remove();e(".notesicon").remove();imgOffset=e(image).offset();imgHieght=e(image).height();imgWidth=e(image).width();e(s).each(function(){n(this)});t();r(i.isMobile)})};e.fn.imgNotes.showAll=function(){e(".note").show();e(".notep").show()};e.fn.imgNotes.hideAll=function(){e(".note").hide();e(".notep").hide()}})(jQuery); diff --git a/ext/notes/lib/spacer.gif b/ext/notes/lib/spacer.gif deleted file mode 100644 index e565824aafafe632011b281cba976baf8b3ba89a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43 qcmZ?wbhEHbWMp7uXkcLY4+e@qSs1y10y+#p0Fq%~V)9{Rum%7ZWeN!Z diff --git a/ext/notes/main.php b/ext/notes/main.php index e6292e0b7..6ccb1ed5e 100644 --- a/ext/notes/main.php +++ b/ext/notes/main.php @@ -98,31 +98,14 @@ public function onPageRequest(PageRequestEvent $event) if (!$user->is_anonymous()) { $this->revert_history($noteID, $reviewID); } - $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("note/updated")); break; - case "add_note": - if (!$user->is_anonymous()) { - $this->add_new_note(); - } - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("post/view/".$_POST["image_id"])); - break; case "add_request": if (!$user->is_anonymous()) { $this->add_note_request(); } - - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("post/view/".$_POST["image_id"])); - break; - case "nuke_notes": - if ($user->can(Permissions::NOTES_ADMIN)) { - $this->nuke_notes(); - } - $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("post/view/".$_POST["image_id"])); break; @@ -130,24 +113,43 @@ public function onPageRequest(PageRequestEvent $event) if ($user->can(Permissions::NOTES_ADMIN)) { $this->nuke_requests(); } - $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("post/view/".$_POST["image_id"])); break; - case "edit_note": + + case "create_note": + $page->set_mode(PageMode::DATA); + if (!$user->is_anonymous()) { + $note_id = $this->add_new_note(); + $page->set_data(json_encode([ + 'status' => 'success', + 'note_id' => $note_id, + ])); + } + break; + case "update_note": + $page->set_mode(PageMode::DATA); if (!$user->is_anonymous()) { $this->update_note(); - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("post/view/" . $_POST["image_id"])); + $page->set_data(json_encode(['status' => 'success'])); } break; case "delete_note": + $page->set_mode(PageMode::DATA); if ($user->can(Permissions::NOTES_ADMIN)) { $this->delete_note(); - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("post/view/".$_POST["image_id"])); + $page->set_data(json_encode(['status' => 'success'])); } break; + case "nuke_notes": + if ($user->can(Permissions::NOTES_ADMIN)) { + $this->nuke_notes(); + } + + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$_POST["image_id"])); + break; + default: $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("note/list")); @@ -243,32 +245,43 @@ private function get_notes(int $imageID): array /* * HERE WE ADD A NOTE TO DATABASE */ - private function add_new_note() + private function add_new_note(): int { global $database, $user; - $imageID = int_escape($_POST["image_id"]); - $user_id = $user->id; - $noteX1 = int_escape($_POST["note_x1"]); - $noteY1 = int_escape($_POST["note_y1"]); - $noteHeight = int_escape($_POST["note_height"]); - $noteWidth = int_escape($_POST["note_width"]); - $noteText = html_escape($_POST["note_text"]); + $note = json_decode(file_get_contents('php://input'), true); $database->execute( " INSERT INTO notes (enable, image_id, user_id, user_ip, date, x1, y1, height, width, note) VALUES (:enable, :image_id, :user_id, :user_ip, now(), :x1, :y1, :height, :width, :note)", - ['enable' => 1, 'image_id' => $imageID, 'user_id' => $user_id, 'user_ip' => get_real_ip(), 'x1' => $noteX1, 'y1' => $noteY1, 'height' => $noteHeight, 'width' => $noteWidth, 'note' => $noteText] + [ + 'enable' => 1, + 'image_id' => $note['image_id'], + 'user_id' => $user->id, + 'user_ip' => get_real_ip(), + 'x1' => $note['x1'], + 'y1' => $note['y1'], + 'height' => $note['height'], + 'width' => $note['width'], + 'note' => $note['note'], + ] ); $noteID = $database->get_last_insert_id('notes_id_seq'); log_info("notes", "Note added {$noteID} by {$user->name}"); - $database->execute("UPDATE images SET notes=(SELECT COUNT(*) FROM notes WHERE image_id=:id1) WHERE id=:id2", ['id1' => $imageID, 'id2' => $imageID]); + $database->execute("UPDATE images SET notes=(SELECT COUNT(*) FROM notes WHERE image_id=:id) WHERE id=:id", ['id' => $note['image_id']]); - $this->add_history(1, $noteID, $imageID, $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText); + $this->add_history( + 1, $noteID, $note['image_id'], + $note['x1'], $note['y1'], + $note['height'], $note['width'], + $note['note'] + ); + + return $noteID; } private function add_note_request() @@ -294,15 +307,7 @@ private function update_note() { global $database; - $note = [ - "x1" => int_escape($_POST["note_x1"]), - "y1" => int_escape($_POST["note_y1"]), - "height" => int_escape($_POST["note_height"]), - "width" => int_escape($_POST["note_width"]), - "note" => $_POST["note_text"], - "image_id" => int_escape($_POST["image_id"]), - "id" => int_escape($_POST["note_id"]) - ]; + $note = json_decode(file_get_contents('php://input'), true); // validate parameters if (empty($note['note'])) { @@ -312,29 +317,22 @@ private function update_note() $database->execute(" UPDATE notes SET x1 = :x1, y1 = :y1, height = :height, width = :width, note = :note - WHERE image_id = :image_id AND id = :id", $note); + WHERE image_id = :image_id AND id = :note_id", $note); - $this->add_history(1, $note['id'], $note['image_id'], $note['x1'], $note['y1'], $note['height'], $note['width'], $note['note']); + $this->add_history(1, $note['note_id'], $note['image_id'], $note['x1'], $note['y1'], $note['height'], $note['width'], $note['note']); } private function delete_note() { global $user, $database; - $imageID = int_escape($_POST["image_id"]); - $noteID = int_escape($_POST["note_id"]); - - // validate parameters - if (is_null($imageID) || !is_numeric($imageID) || is_null($noteID) || !is_numeric($noteID)) { - return; - } - + $note = json_decode(file_get_contents('php://input'), true); $database->execute(" UPDATE notes SET enable = :enable WHERE image_id = :image_id AND id = :id - ", ['enable' => 0, 'image_id' => $imageID, 'id' => $noteID]); + ", ['enable' => 0, 'image_id' => $note["image_id"], 'id' => $note["note_id"]]); - log_info("notes", "Note deleted {$noteID} by {$user->name}"); + log_info("notes", "Note deleted {$note["note_id"]} by {$user->name}"); } private function nuke_notes() diff --git a/ext/notes/script.js b/ext/notes/script.js index 7b5be0548..ea2bb87c3 100644 --- a/ext/notes/script.js +++ b/ext/notes/script.js @@ -1,81 +1,274 @@ -/*jshint bitwise:true, curly:true, forin:false, noarg:true, noempty:true, nonew:true, undef:true, strict:false, browser:true, jquery:true */ +let notesContainer = null; +let noteImage = document.getElementById('main_image'); +let noteBeingEdited = null; +let dragStart = null; document.addEventListener('DOMContentLoaded', () => { if(window.notes) { - $('#main_image').load(function(){ - $('#main_image').imgNotes({notes: window.notes}); + if(noteImage.complete) { + renderNotes(); + } else { + noteImage.addEventListener('load', () => { + renderNotes(); + }); + } - //Make sure notes are always shown - $('#main_image').off('mouseenter mouseleave'); + let resizeObserver = new ResizeObserver(entries => { + renderNotes(); }); + resizeObserver.observe(noteImage); } +}); - $('#cancelnote').click(function(){ - $('#main_image').imgAreaSelect({ hide: true }); - $('#noteform').hide(); - }); +function renderNotes() { + // reset the DOM to empty + if(notesContainer) { + notesContainer.remove(); + } + + // check the image we're adding notes on top of + let br = noteImage.getBoundingClientRect(); + let scale = br.width / noteImage.getAttribute("data-width"); + + // render a container full of notes + notesContainer = document.createElement('div'); + notesContainer.className = 'notes-container'; + notesContainer.style.left = window.scrollX + br.left + 'px'; + notesContainer.style.top = window.scrollY + br.top + 'px'; + notesContainer.style.width = br.width + 'px'; + notesContainer.style.height = br.height + 'px'; + + // render each note + window.notes.forEach(note => { + let noteDiv = document.createElement('div'); + noteDiv.classList.add('note'); + noteDiv.style.left = note.x1 * scale + 'px'; + noteDiv.style.top = note.y1 * scale + 'px'; + noteDiv.style.width = note.width * scale + 'px'; + noteDiv.style.height = note.height * scale + 'px'; + let text = document.createElement('div'); + text.innerText = note.note; + noteDiv.addEventListener('click', (e) => { + noteBeingEdited = note.note_id; + renderNotes(); + }); + noteDiv.appendChild(text); + notesContainer.appendChild(noteDiv); - $('#EditCancelNote').click(function() { - $('#main_image').imgAreaSelect({ hide: true }); - $('#noteEditForm').hide(); + // if the current note is being edited, render the editor + if(note.note_id == noteBeingEdited) { + let editor = renderEditor(noteDiv, note); + notesContainer.appendChild(editor); + } }); - $('#addnote').click(function(){ - $('#noteEditForm').hide(); - $('#main_image').imgAreaSelect({ onSelectChange: showaddnote, x1: 120, y1: 90, x2: 280, y2: 210 }); - return false; + noteImage.parentNode.appendChild(notesContainer); +} + +/** + * + * @param {HTMLElement} noteDiv + * @param {*} note + * @returns + */ +function renderEditor(noteDiv, note) { + // check the image we're adding notes on top of + let br = noteImage.getBoundingClientRect(); + let scale = br.width / noteImage.getAttribute("data-width"); + + // set the note itself into drag & resize mode + // NOTE: to avoid re-rendering the whole DOM every time the mouse + // moves, we directly edit the style of the noteDiv, and then when + // the mouse is released, we update the note object and re-render + noteDiv.classList.add('editing'); + noteDiv.addEventListener('mousedown', (e) => { + dragStart = { + x: e.pageX, + y: e.pageY, + mode: getArea(e.offsetX, e.offsetY, noteDiv.offsetWidth, noteDiv.offsetHeight), + }; + noteDiv.classList.add("dragging"); + }); + noteDiv.addEventListener('mousemove', (e) => { + if(dragStart) { + if(dragStart.mode == "c") { + noteDiv.style.left = (note.x1 * scale) + (e.pageX - dragStart.x) + 'px'; + noteDiv.style.top = (note.y1 * scale) + (e.pageY - dragStart.y) + 'px'; + } + if(dragStart.mode.indexOf("n") >= 0) { + noteDiv.style.top = (note.y1 * scale) + (e.pageY - dragStart.y) + 'px'; + noteDiv.style.height = (note.height * scale) - (e.pageY - dragStart.y) + 'px'; + } + if(dragStart.mode.indexOf("s") >= 0) { + noteDiv.style.height = (note.height * scale) + (e.pageY - dragStart.y) + 'px'; + } + if(dragStart.mode.indexOf("w") >= 0) { + noteDiv.style.left = (note.x1 * scale) + (e.pageX - dragStart.x) + 'px'; + noteDiv.style.width = (note.width * scale) - (e.pageX - dragStart.x) + 'px'; + } + if(dragStart.mode.indexOf("e") >= 0) { + noteDiv.style.width = (note.width * scale) + (e.pageX - dragStart.x) + 'px'; + } + } else { + let area = getArea(e.offsetX, e.offsetY, noteDiv.offsetWidth, noteDiv.offsetHeight); + if(area == "c") { + noteDiv.style.cursor = 'move'; + } else { + noteDiv.style.cursor = area + '-resize'; + } + } }); + function _commit() { + noteDiv.classList.remove("dragging"); + dragStart = null; + note.x1 = noteDiv.offsetLeft / scale; + note.y1 = noteDiv.offsetTop / scale; + note.width = noteDiv.offsetWidth / scale; + note.height = noteDiv.offsetHeight / scale; + renderNotes(); + } + noteDiv.addEventListener('mouseup', _commit); + noteDiv.addEventListener('mouseleave', _commit); - $('.note').click(function() { - $('#noteform').hide(); - var imgOffset = $('#main_image').offset(); + // add textarea / save / cancel / delete buttons + let editor = document.createElement('div'); + editor.classList.add('editor'); + editor.style.left = note.x1 * scale + 'px'; + editor.style.top = (note.y1 + note.height) * scale + 'px'; - var x1 = parseInt(this.style.left) - imgOffset.left; - var y1 = parseInt(this.style.top) - imgOffset.top; - var width = parseInt(this.style.width); - var height = parseInt(this.style.height); - var text = $(this).next('.notep').text().replace(/([^>]?)\\n{2}/g, '$1\\n'); - var id = $(this).next('.notep').next('.noteID').text(); + let textarea = document.createElement('textarea'); + textarea.value = note.note; + textarea.addEventListener('input', () => { + note.note = textarea.value; + }); + editor.appendChild(textarea); - $('#main_image').imgAreaSelect({ onSelectChange: showeditnote, x1: x1, y1: y1, x2: x1 + width, y2: y1 + height }); - setEditNoteData(x1, y1, width, height, text, id); + let save = document.createElement('button'); + save.innerText = 'Save'; + save.addEventListener('click', () => { + if(note.note_id == null) { + fetch('/note/create_note', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(note) + }).then(response => { + if(response.ok) { + return response.json(); + } else { + throw new Error('Failed to create note'); + } + }).then(data => { + note.note_id = data.note_id; + renderNotes(); + }).catch(error => { + alert(error); + }); + } else { + fetch('/note/update_note', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(note) + }).then(response => { + if(!response.ok) { + throw new Error('Failed to update note'); + } + }).catch(error => { + alert(error); + }); + } + noteBeingEdited = null; + renderNotes(); }); -}); - -function showaddnote (img, area) { - var imgOffset = $(img).offset(); - var form_left = parseInt(imgOffset.left) + parseInt(area.x1); - var form_top = parseInt(imgOffset.top) + parseInt(area.y1) + parseInt(area.height)+5; - - $('#noteform').css({ left: form_left + 'px', top: form_top + 'px'}); - $('#noteform').show(); - $('#noteform').css('z-index', 10000); - $('#NoteX1').val(area.x1); - $('#NoteY1').val(area.y1); - $('#NoteHeight').val(area.height); - $('#NoteWidth').val(area.width); + editor.appendChild(save); + + let cancel = document.createElement('button'); + cancel.innerText = 'Cancel'; + cancel.addEventListener('click', () => { + noteBeingEdited = null; + if(note.note_id == null) { + // delete the un-saved note + window.notes = window.notes.filter(n => n.note_id != null); + } + renderNotes(); + }); + editor.appendChild(cancel); + + if(window.notes_admin && note.note_id != null) { + let deleteNote = document.createElement('button'); + deleteNote.innerText = 'Delete'; + deleteNote.addEventListener('click', () => { + // TODO: delete note from server + fetch('/note/delete_note', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(note) + }).then(response => { + if(!response.ok) { + throw new Error('Failed to delete note'); + } + }).catch(error => { + alert(error); + }); + noteBeingEdited = null; + window.notes = window.notes.filter(n => n.note_id != note.note_id); + renderNotes(); + }); + editor.appendChild(deleteNote); + } + + return editor; } -function showeditnote (img, area) { - var imgOffset = $(img).offset(); - var form_left = parseInt(imgOffset.left) + area.x1; - var form_top = parseInt(imgOffset.top) + area.y2; - - $('#noteEditForm').css({ left: form_left + 'px', top: form_top + 'px'}); - $('#noteEditForm').show(); - $('#noteEditForm').css('z-index', 10000); - $('#EditNoteX1').val(area.x1); - $('#EditNoteY1').val(area.y1); - $('#EditNoteHeight').val(area.height); - $('#EditNoteWidth').val(area.width); +function addNewNote() { + if(window.notes.filter(note => note.note_id == null).length > 0) { + alert("Please save all notes before adding a new one."); + return; + } + window.notes.push( + { + x1: 10, + y1: 10, + width: 100, + height: 40, + note: "new note", + note_id: null, + image_id: window.notes_image_id, + } + ); + noteBeingEdited = null; + renderNotes(); } -function setEditNoteData(x1, y1, width, height, text, id) { - $('#EditNoteX1').val(x1); - $('#EditNoteY1').val(y1); - $('#EditNoteHeight').val(height); - $('#EditNoteWidth').val(width); - $('#EditNoteNote').text(text); - $('#EditNoteID').val(id); - $('#DeleteNoteNoteID').val(id); +function getArea(x, y, width, height) { + let border = 10; + + if(y < border) { + if(x < border) { + return "nw"; + } else if(x > width - border) { + return "ne"; + } else { + return "n"; + } + } else if(y > height - border) { + if(x < border) { + return "sw"; + } else if(x > width - border) { + return "se"; + } else { + return "s"; + } + } else if(x < border) { + return "w"; + } else if(x > width - border) { + return "e"; + } else { + return "c"; + } } diff --git a/ext/notes/style.css b/ext/notes/style.css index ccdb7e97c..7153ebacf 100644 --- a/ext/notes/style.css +++ b/ext/notes/style.css @@ -1,87 +1,57 @@ -.note { - display: block; +.notes-container { + position: absolute; + border: 1px solid red; +} +.notes-container .note { + display: flex; + justify-content: center; + align-items: center; + color: black; background-color: #FFE; border: 1px dashed black; overflow: hidden; position: absolute; - z-index: 0; - - filter:alpha(opacity=50); - -moz-opacity:0.5; - -khtml-opacity: 0.5; opacity: 0.5; + z-index: 1; } - -.notep { - display: none; - color: #412a21; - background-color: #fffdef; - border: #412a21 1px solid; - font-size: 8pt; - margin-top: 0; - padding: 2px; - position: absolute; - width: 175px; -} - -#noteform, #noteEditForm { - display: none; - position: absolute; - width: 250px; -} - -#noteform textarea, #noteEditForm textarea { - width: 100%; -} - -/* - * imgAreaSelect default style - */ - -.imgareaselect-border1 { - background: url(border-v.gif) repeat-y left top; +.notes-container .note.editing { + opacity: 1; + border: 1px dashed red; + z-index: 2; } - -.imgareaselect-border2 { - background: url(border-h.gif) repeat-x left top; -} - -.imgareaselect-border3 { - background: url(border-v.gif) repeat-y right top; -} - -.imgareaselect-border4 { - background: url(border-h.gif) repeat-x left bottom; -} - -.imgareaselect-border1, .imgareaselect-border2, -.imgareaselect-border3, .imgareaselect-border4 { - filter: alpha(opacity=50); +.notes-container .note.editing.dragging { opacity: 0.5; + z-index: 2; } - -.imgareaselect-handle { - background-color: #fff; - border: solid 1px #000; - filter: alpha(opacity=50); - opacity: 0.5; +.notes-container .note:hover { + opacity: 1; + z-index: 3; } -.imgareaselect-outer { - /*background-color: #000;*/ - filter: alpha(opacity=50); - opacity: 0.5; +.notes-container .editor { + display: grid; + color: black; + background-color: #EFE; + border: 1px dashed blue; + position: absolute; + grid-template-columns: 1fr 1fr; + grid-template-areas: + "text text" + "save cancel" + "delete delete"; + z-index: 4; } - -.imgareaselect-selection { +.notes-container .editor TEXTAREA { + grid-area: text; + // resize: none; } - -/* Makes sure the note block is hidden */ -section#note_system { - height: 0; +.notes-container .editor BUTTON[value="Save"] { + grid-area: save; +} +.notes-container .editor BUTTON[value="Cancel"] { + grid-area: cancel; } -section#note_system > .blockbody { - padding: 0; - border: 0; +.notes-container .editor BUTTON[value="Delete"] { + grid-area: delete; } diff --git a/ext/notes/theme.php b/ext/notes/theme.php index a1204ea2f..f7ea09036 100644 --- a/ext/notes/theme.php +++ b/ext/notes/theme.php @@ -4,41 +4,38 @@ namespace Shimmie2; +use MicroHTML\HTMLElement; +use function MicroHTML\INPUT; + class NotesTheme extends Themelet { - public function note_button(int $image_id): string + public function note_button(int $image_id): HTMLElement { - return ' - -
- - -
- '; + return SHM_SIMPLE_FORM("", INPUT(["type"=>"button", "value"=>"Add Note", "onclick"=>"addNewNote()"])); } - public function request_button(int $image_id): string + public function request_button(int $image_id): HTMLElement { - return make_form(make_link("note/add_request")) . ' - - - - '; + return SHM_SIMPLE_FORM( + "note/add_request", + INPUT(["type"=>"hidden", "name"=>"image_id", "value"=>$image_id]), + INPUT(["type"=>"submit", "value"=>"Add Note Request"]), + ); } - public function nuke_notes_button(int $image_id): string + public function nuke_notes_button(int $image_id): HTMLElement { - return make_form(make_link("note/nuke_notes")) . ' - - - - '; + return SHM_SIMPLE_FORM( + "note/nuke_notes", + INPUT(["type"=>"hidden", "name"=>"image_id", "value"=>$image_id]), + INPUT(["type"=>"submit", "value"=>"Nuke Notes", "onclick"=>"return confirm_action('Are you sure?')"]), + ); } - public function nuke_requests_button(int $image_id): string + public function nuke_requests_button(int $image_id): HTMLElement { - return make_form(make_link("note/nuke_requests")) . ' - - - - '; + return SHM_SIMPLE_FORM( + "note/nuke_requests", + INPUT(["type"=>"hidden", "name"=>"image_id", "value"=>$image_id]), + INPUT(["type"=>"submit", "value"=>"Nuke Requests", "onclick"=>"return confirm_action('Are you sure?')"]), + ); } public function search_notes_page(Page $page): void @@ -56,91 +53,23 @@ public function search_notes_page(Page $page): void // check action POST on form public function display_note_system(Page $page, int $image_id, array $recovered_notes, bool $adminOptions): void { - $base_href = get_base_href(); - - $page->add_html_header(""); - $page->add_html_header(""); - $page->add_html_header(""); - $to_json = []; foreach ($recovered_notes as $note) { - $parsedNote = $note["note"]; - $parsedNote = str_replace("\n", "\\n", $parsedNote); - $parsedNote = str_replace("\r", "\\r", $parsedNote); - $to_json[] = [ + 'image_id' => $image_id, 'x1' => $note["x1"], 'y1' => $note["y1"], 'height' => $note["height"], 'width' => $note["width"], - 'note' => $parsedNote, + 'note' => $note["note"], 'note_id' => $note["id"], ]; } - - $html = ""; - - $html .= " -
- ".make_form(make_link("note/add_note"))." - - - - - - - - - - - - - - -
- -
- - -
-
- ".make_form(make_link("note/edit_note"))." - - - - - - - - - - - - - - -
- -
- "; - - if ($adminOptions) { - $html .= " - ".make_form(make_link("note/delete_note"))." - - - - - - -
- -"; - } - - $html .= "
"; - - $page->add_block(new Block(null, $html, "main", 1, 'note_system')); + $page->add_html_header(""); } diff --git a/ext/view/events/image_admin_block_building_event.php b/ext/view/events/image_admin_block_building_event.php index 6a1d458d2..87c8b8bb5 100644 --- a/ext/view/events/image_admin_block_building_event.php +++ b/ext/view/events/image_admin_block_building_event.php @@ -3,10 +3,11 @@ declare(strict_types=1); namespace Shimmie2; +use MicroHTML\HTMLElement; class ImageAdminBlockBuildingEvent extends Event { - /** @var string[] */ + /** @var HTMLElement[]|string[] */ public array $parts = []; public Image $image; public User $user; @@ -20,7 +21,7 @@ public function __construct(Image $image, User $user, string $context) $this->context = $context; } - public function add_part(string $html, int $position = 50) + public function add_part(HTMLElement|string $html, int $position = 50) { while (isset($this->parts[$position])) { $position++; From 4edf985a737c568f6c8a490019148c805f223240 Mon Sep 17 00:00:00 2001 From: Shish Date: Fri, 5 Jan 2024 04:06:19 +0000 Subject: [PATCH 08/10] fmt --- ext/notes/main.php | 10 ++++++--- ext/notes/theme.php | 21 ++++++++++--------- .../image_admin_block_building_event.php | 1 + 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/ext/notes/main.php b/ext/notes/main.php index 6ccb1ed5e..f190e374c 100644 --- a/ext/notes/main.php +++ b/ext/notes/main.php @@ -275,9 +275,13 @@ private function add_new_note(): int $database->execute("UPDATE images SET notes=(SELECT COUNT(*) FROM notes WHERE image_id=:id) WHERE id=:id", ['id' => $note['image_id']]); $this->add_history( - 1, $noteID, $note['image_id'], - $note['x1'], $note['y1'], - $note['height'], $note['width'], + 1, + $noteID, + $note['image_id'], + $note['x1'], + $note['y1'], + $note['height'], + $note['width'], $note['note'] ); diff --git a/ext/notes/theme.php b/ext/notes/theme.php index f7ea09036..0755eba66 100644 --- a/ext/notes/theme.php +++ b/ext/notes/theme.php @@ -5,36 +5,37 @@ namespace Shimmie2; use MicroHTML\HTMLElement; + use function MicroHTML\INPUT; class NotesTheme extends Themelet { public function note_button(int $image_id): HTMLElement { - return SHM_SIMPLE_FORM("", INPUT(["type"=>"button", "value"=>"Add Note", "onclick"=>"addNewNote()"])); + return SHM_SIMPLE_FORM("", INPUT(["type" => "button", "value" => "Add Note", "onclick" => "addNewNote()"])); } public function request_button(int $image_id): HTMLElement { return SHM_SIMPLE_FORM( - "note/add_request", - INPUT(["type"=>"hidden", "name"=>"image_id", "value"=>$image_id]), - INPUT(["type"=>"submit", "value"=>"Add Note Request"]), + "note/add_request", + INPUT(["type" => "hidden", "name" => "image_id", "value" => $image_id]), + INPUT(["type" => "submit", "value" => "Add Note Request"]), ); } public function nuke_notes_button(int $image_id): HTMLElement { return SHM_SIMPLE_FORM( - "note/nuke_notes", - INPUT(["type"=>"hidden", "name"=>"image_id", "value"=>$image_id]), - INPUT(["type"=>"submit", "value"=>"Nuke Notes", "onclick"=>"return confirm_action('Are you sure?')"]), + "note/nuke_notes", + INPUT(["type" => "hidden", "name" => "image_id", "value" => $image_id]), + INPUT(["type" => "submit", "value" => "Nuke Notes", "onclick" => "return confirm_action('Are you sure?')"]), ); } public function nuke_requests_button(int $image_id): HTMLElement { return SHM_SIMPLE_FORM( - "note/nuke_requests", - INPUT(["type"=>"hidden", "name"=>"image_id", "value"=>$image_id]), - INPUT(["type"=>"submit", "value"=>"Nuke Requests", "onclick"=>"return confirm_action('Are you sure?')"]), + "note/nuke_requests", + INPUT(["type" => "hidden", "name" => "image_id", "value" => $image_id]), + INPUT(["type" => "submit", "value" => "Nuke Requests", "onclick" => "return confirm_action('Are you sure?')"]), ); } diff --git a/ext/view/events/image_admin_block_building_event.php b/ext/view/events/image_admin_block_building_event.php index 87c8b8bb5..9f2fe701f 100644 --- a/ext/view/events/image_admin_block_building_event.php +++ b/ext/view/events/image_admin_block_building_event.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace Shimmie2; + use MicroHTML\HTMLElement; class ImageAdminBlockBuildingEvent extends Event From f7025069ad127b34fe2f140a42f6c3f22e0ede2f Mon Sep 17 00:00:00 2001 From: Shish Date: Fri, 5 Jan 2024 04:09:01 +0000 Subject: [PATCH 09/10] [notes] remove debug border --- ext/notes/style.css | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/notes/style.css b/ext/notes/style.css index 7153ebacf..112659370 100644 --- a/ext/notes/style.css +++ b/ext/notes/style.css @@ -1,6 +1,5 @@ .notes-container { position: absolute; - border: 1px solid red; } .notes-container .note { From 87368ac56a86381521f8de030d467d44ee7d5330 Mon Sep 17 00:00:00 2001 From: Shish Date: Fri, 5 Jan 2024 04:19:42 +0000 Subject: [PATCH 10/10] [upload] fix empty-vs-null confusion, fixes #989 --- ext/upload/main.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ext/upload/main.php b/ext/upload/main.php index bc53e1d05..e64299abf 100644 --- a/ext/upload/main.php +++ b/ext/upload/main.php @@ -292,7 +292,17 @@ private function tags_for_upload_slot(int $id): array private function source_for_upload_slot(int $id): ?string { - return $_POST["source$id"] ?? $_POST['source'] ?? null; + global $config; + if(!empty($_POST["source$id"])) { + return $_POST["source$id"]; + } + if(!empty($_POST['source'])) { + return $_POST['source']; + } + if($config->get_bool(UploadConfig::TLSOURCE) && !empty($_POST["url$id"])) { + return $_POST["url$id"]; + } + return null; } /** @@ -409,7 +419,7 @@ private function try_transload(string $url, array $tags, string $source = null, $metadata = []; $metadata['filename'] = $filename; $metadata['tags'] = $tags; - $metadata['source'] = (($url == $source) && !$config->get_bool(UploadConfig::TLSOURCE) ? "" : $source); + $metadata['source'] = $source; if ($user->can(Permissions::EDIT_IMAGE_LOCK) && !empty($_GET['locked'])) { $metadata['locked'] = bool_escape($_GET['locked']) ? "on" : ""; }