From cb1e9c00752463a58e02cd77478a6393f2cd83db Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Tue, 9 Jul 2019 09:10:21 -0500
Subject: [PATCH 01/10] Permissions to constants

---
 README.markdown                    |  14 +-
 core/imageboard/image.php          |   2 +-
 core/permissions.php               |  67 ++++++++++
 core/userclass.php                 | 197 ++++++++++++++---------------
 core/util.php                      |   4 +-
 ext/admin/main.php                 |   4 +-
 ext/alias_editor/main.php          |   8 +-
 ext/alias_editor/theme.php         |   2 +-
 ext/ban_words/main.php             |   2 +-
 ext/blocks/main.php                |   4 +-
 ext/bulk_actions/main.php          |  12 +-
 ext/comment/main.php               |  12 +-
 ext/comment/theme.php              |   8 +-
 ext/danbooru_api/main.php          |   2 +-
 ext/downtime/main.php              |   2 +-
 ext/et/main.php                    |   4 +-
 ext/ext_manager/main.php           |   4 +-
 ext/featured/main.php              |   4 +-
 ext/hellban/main.php               |   4 +-
 ext/image/main.php                 |   8 +-
 ext/image_hash_ban/main.php        |   6 +-
 ext/ipban/main.php                 |   4 +-
 ext/log_db/main.php                |   4 +-
 ext/media/main.php                 |   2 +-
 ext/not_a_tag/main.php             |   4 +-
 ext/numeric_score/main.php         |   6 +-
 ext/numeric_score/theme.php        |   2 +-
 ext/oekaki/main.php                |   4 +-
 ext/ouroboros_api/main.php         |   2 +-
 ext/pm/main.php                    |   6 +-
 ext/pm/theme.php                   |   2 +-
 ext/rating/main.php                |   6 +-
 ext/regen_thumb/main.php           |  12 +-
 ext/report_image/main.php          |  12 +-
 ext/rule34/main.php                |  12 +-
 ext/setup/main.php                 |   4 +-
 ext/source_history/main.php        |   6 +-
 ext/source_history/theme.php       |   4 +-
 ext/tag_edit/main.php              |  22 ++--
 ext/tag_edit/theme.php             |  10 +-
 ext/tag_editcloud/main.php         |   2 +-
 ext/tag_history/main.php           |   6 +-
 ext/tag_history/theme.php          |   4 +-
 ext/tagger/main.php                |   2 +-
 ext/trash/main.php                 |  12 +-
 ext/upload/main.php                |   8 +-
 ext/user/main.php                  |  18 +--
 ext/user/theme.php                 |  12 +-
 ext/view/theme.php                 |   4 +-
 ext/wiki/main.php                  |   2 +-
 themes/danbooru/comment.theme.php  |   2 +-
 themes/danbooru/view.theme.php     |   2 +-
 themes/danbooru2/comment.theme.php |   2 +-
 themes/danbooru2/view.theme.php    |   2 +-
 themes/futaba/comment.theme.php    |   2 +-
 themes/lite/view.theme.php         |   2 +-
 themes/material/view.theme.php     |   4 +-
 57 files changed, 323 insertions(+), 257 deletions(-)
 create mode 100644 core/permissions.php

diff --git a/README.markdown b/README.markdown
index 23dec0ee..853a37ee 100644
--- a/README.markdown
+++ b/README.markdown
@@ -100,10 +100,10 @@ permissions like so:
 
 ```php
 new UserClass("anonymous", "base", [
-	"create_comment" => True,
-	"edit_image_tag" => True,
-	"edit_image_source" => True,
-	"create_image_report" => True,
+	Permissions::CREATE_COMMENT => True,
+	Permissions::EDIT_IMAGE_TAG => True,
+	Permissions::EDIT_IMAGE_SOURCE => True,
+	Permissions::CREATE_IMAGE_REPORT => True,
 ]);
 ```
 
@@ -111,12 +111,12 @@ For a moderator class, being a regular user who can delete images and comments:
 
 ```php
 new UserClass("moderator", "user", [
-	"delete_image" => True,
-	"delete_comment" => True,
+	Permissions::DELETE_IMAGE => True,
+	Permissions::DELETE_COMMENT => True,
 ]);
 ```
 
-For a list of permissions, see `core/userclass.php`
+For a list of permissions, see `core/permissions.php`
 
 
 # Development Info
diff --git a/core/imageboard/image.php b/core/imageboard/image.php
index 90ab9e6a..3e2529b8 100644
--- a/core/imageboard/image.php
+++ b/core/imageboard/image.php
@@ -129,7 +129,7 @@ class Image
         }
 
         if (SPEED_HAX) {
-            if (!$user->can("big_search") and count($tags) > 3) {
+            if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > 3) {
                 throw new SCoreException("Anonymous users may only search for up to 3 tags at a time");
             }
         }
diff --git a/core/permissions.php b/core/permissions.php
new file mode 100644
index 00000000..f3ab7c6b
--- /dev/null
+++ b/core/permissions.php
@@ -0,0 +1,67 @@
+<?php
+
+abstract class Permissions
+{
+    public const CHANGE_SETTING = "change_setting";  # modify web-level settings, eg the config table
+    public const OVERRIDE_CONFIG = "override_config"; # modify sys-level settings, eg shimmie.conf.php
+    public const BIG_SEARCH = "big_search";      # search for more than 3 tags at once (speed mode only)
+
+    public const MANAGE_EXTENSION_LIST = "manage_extension_list";
+    public const MANAGE_ALIAS_LIST = "manage_alias_list";
+    public const MASS_TAG_EDIT = "mass_tag_edit";
+
+    public const VIEW_IP = "view_ip";         # view IP addresses associated with things
+    public const BAN_IP = "ban_ip";
+
+    public const EDIT_USER_NAME = "edit_user_name";
+    public const EDIT_USER_PASSWORD = "edit_user_password";
+    public const EDIT_USER_INFO = "edit_user_info";  # email address, etc
+    public const EDIT_USER_CLASS = "edit_user_class";
+    public const DELETE_USER = "delete_user";
+
+    public const CREATE_COMMENT = "create_comment";
+    public const DELETE_COMMENT = "delete_comment";
+    public const BYPASS_COMMENT_CHECKS = "bypass_comment_checks";  # spam etc
+
+    public const REPLACE_IMAGE = "replace_image";
+    public const CREATE_IMAGE = "create_image";
+    public const EDIT_IMAGE_TAG = "edit_image_tag";
+    public const EDIT_IMAGE_SOURCE = "edit_image_source";
+    public const EDIT_IMAGE_OWNER = "edit_image_owner";
+    public const EDIT_IMAGE_LOCK = "edit_image_lock";
+    public const BULK_EDIT_IMAGE_TAG = "bulk_edit_image_tag";
+    public const BULK_EDIT_IMAGE_SOURCE = "bulk_edit_image_source";
+    public const DELETE_IMAGE = "delete_image";
+
+    public const BAN_IMAGE = "ban_image";
+
+    public const VIEW_EVENTLOG = "view_eventlog";
+    public const IGNORE_DOWNTIME = "ignore_downtime";
+
+    public const CREATE_IMAGE_REPORT = "create_image_report";
+    public const VIEW_IMAGE_REPORT = "view_image_report";  # deal with reported images
+
+    public const EDIT_WIKI_PAGE = "edit_wiki_page";
+    public const DELETE_WIKI_PAGE = "delete_wiki_page";
+
+    public const MANAGE_BLOCKS = "manage_blocks";
+
+    public const MANAGE_ADMINTOOLS = "manage_admintools";
+
+    public const VIEW_OTHER_PMS = "view_other_pms";
+    public const EDIT_FEATURE = "edit_feature";
+    public const BULK_EDIT_VOTE = "bulk_edit_vote";
+    public const EDIT_OTHER_VOTE = "edit_other_vote";
+    public const VIEW_SYSINTO = "view_sysinfo";
+
+    public const HELLBANNED = "hellbanned";
+    public const VIEW_HELLBANNED = "view_hellbanned";
+
+    public const PROTECTED = "protected";          # only admins can modify protected users (stops a moderator changing an admin's password)
+
+    public const EDIT_IMAGE_RATING = "edit_image_rating";
+    public const BULK_EDIT_IMAGE_RATING = "bulk_edit_image_rating";
+
+    public const VIEW_TRASH = "view_trash";
+
+}
\ No newline at end of file
diff --git a/core/userclass.php b/core/userclass.php
index bcd05c36..4ae2511b 100644
--- a/core/userclass.php
+++ b/core/userclass.php
@@ -72,134 +72,133 @@ class UserClass
 // action = create / view / edit / delete
 // object = image / user / tag / setting
 new UserClass("base", null, [
-    "change_setting" => false,  # modify web-level settings, eg the config table
-    "override_config" => false, # modify sys-level settings, eg shimmie.conf.php
-    "big_search" => false,      # search for more than 3 tags at once (speed mode only)
+    Permissions::CHANGE_SETTING => false,  # modify web-level settings, eg the config table
+    Permissions::OVERRIDE_CONFIG => false, # modify sys-level settings, eg shimmie.conf.php
+    Permissions::BIG_SEARCH => false,      # search for more than 3 tags at once (speed mode only)
 
-    "manage_extension_list" => false,
-    "manage_alias_list" => false,
-    "mass_tag_edit" => false,
+    Permissions::MANAGE_EXTENSION_LIST => false,
+    Permissions::MANAGE_ALIAS_LIST => false,
+    Permissions::MASS_TAG_EDIT => false,
 
-    "view_ip" => false,         # view IP addresses associated with things
-    "ban_ip" => false,
+    Permissions::VIEW_IP => false,         # view IP addresses associated with things
+    Permissions::BAN_IP => false,
 
-    "edit_user_name" => false,
-    "edit_user_password" => false,
-    "edit_user_info" => false,  # email address, etc
-    "edit_user_class" => false,
-    "delete_user" => false,
+    Permissions::EDIT_USER_NAME => false,
+    Permissions::EDIT_USER_PASSWORD => false,
+    Permissions::EDIT_USER_INFO => false,  # email address, etc
+    Permissions::EDIT_USER_CLASS => false,
+    Permissions::DELETE_USER => false,
 
-    "create_comment" => false,
-    "delete_comment" => false,
-    "bypass_comment_checks" => false,  # spam etc
+    Permissions::CREATE_COMMENT => false,
+    Permissions::DELETE_COMMENT => false,
+    Permissions::BYPASS_COMMENT_CHECKS => false,  # spam etc
 
-    "replace_image" => false,
-    "create_image" => false,
-    "edit_image_tag" => false,
-    "edit_image_source" => false,
-    "edit_image_owner" => false,
-    "edit_image_lock" => false,
-    "bulk_edit_image_tag" => false,
-    "bulk_edit_image_source" => false,
-    "delete_image" => false,
+    Permissions::REPLACE_IMAGE => false,
+    Permissions::CREATE_IMAGE => false,
+    Permissions::EDIT_IMAGE_TAG => false,
+    Permissions::EDIT_IMAGE_SOURCE => false,
+    Permissions::EDIT_IMAGE_OWNER => false,
+    Permissions::EDIT_IMAGE_LOCK => false,
+    Permissions::BULK_EDIT_IMAGE_TAG => false,
+    Permissions::BULK_EDIT_IMAGE_SOURCE => false,
+    Permissions::DELETE_IMAGE => false,
 
-    "ban_image" => false,
+    Permissions::BAN_IMAGE => false,
 
-    "view_eventlog" => false,
-    "ignore_downtime" => false,
+    Permissions::VIEW_EVENTLOG => false,
+    Permissions::IGNORE_DOWNTIME => false,
 
-    "create_image_report" => false,
-    "view_image_report" => false,  # deal with reported images
+    Permissions::CREATE_IMAGE_REPORT => false,
+    Permissions::VIEW_IMAGE_REPORT => false,  # deal with reported images
 
-    "edit_wiki_page" => false,
-    "delete_wiki_page" => false,
+    Permissions::EDIT_WIKI_PAGE => false,
+    Permissions::DELETE_WIKI_PAGE => false,
 
-    "manage_blocks" => false,
+    Permissions::MANAGE_BLOCKS => false,
 
-    "manage_admintools" => false,
+    Permissions::MANAGE_ADMINTOOLS => false,
 
-    "view_other_pms" => false,
-    "edit_feature" => false,
-    "bulk_edit_vote" => false,
-    "edit_other_vote" => false,
-    "view_sysinfo" => false,
+    Permissions::VIEW_OTHER_PMS => false,
+    Permissions::EDIT_FEATURE => false,
+    Permissions::BULK_EDIT_VOTE => false,
+    Permissions::EDIT_OTHER_VOTE => false,
+    Permissions::VIEW_SYSINTO => false,
 
-    "hellbanned" => false,
-    "view_hellbanned" => false,
+    Permissions::HELLBANNED => false,
+    Permissions::VIEW_HELLBANNED => false,
 
-    "protected" => false,          # only admins can modify protected users (stops a moderator changing an admin's password)
+    Permissions::PROTECTED => false,          # only admins can modify protected users (stops a moderator changing an admin's password)
 
-    "edit_image_rating" => false,
-    "bulk_edit_image_rating" => false,
+    Permissions::EDIT_IMAGE_RATING => false,
+    Permissions::BULK_EDIT_IMAGE_RATING => false,
 
-    "view_trash" => false,
-    "perform_bulk_actions" => false,
+    Permissions::VIEW_TRASH => false,
 ]);
 
 new UserClass("anonymous", "base", [
 ]);
 
 new UserClass("user", "base", [
-    "big_search" => true,
-    "create_image" => true,
-    "create_comment" => true,
-    "edit_image_tag" => true,
-    "edit_image_source" => true,
-    "create_image_report" => true,
-    "edit_image_rating" => true,
+    Permissions::BIG_SEARCH => true,
+    Permissions::CREATE_IMAGE => true,
+    Permissions::CREATE_COMMENT => true,
+    Permissions::EDIT_IMAGE_TAG => true,
+    Permissions::EDIT_IMAGE_SOURCE => true,
+    Permissions::CREATE_IMAGE_REPORT => true,
+    Permissions::EDIT_IMAGE_RATING => true,
 
 ]);
 
 new UserClass("admin", "base", [
-    "change_setting" => true,
-    "override_config" => true,
-    "big_search" => true,
-    "edit_image_lock" => true,
-    "view_ip" => true,
-    "ban_ip" => true,
-    "edit_user_name" => true,
-    "edit_user_password" => true,
-    "edit_user_info" => true,
-    "edit_user_class" => true,
-    "delete_user" => true,
-    "create_image" => true,
-    "delete_image" => true,
-    "ban_image" => true,
-    "create_comment" => true,
-    "delete_comment" => true,
-    "bypass_comment_checks" => true,
-    "replace_image" => true,
-    "manage_extension_list" => true,
-    "manage_alias_list" => true,
-    "edit_image_tag" => true,
-    "edit_image_source" => true,
-    "edit_image_owner" => true,
-    "bulk_edit_image_tag" => true,
-    "bulk_edit_image_source" => true,
-    "mass_tag_edit" => true,
-    "create_image_report" => true,
-    "view_image_report" => true,
-    "edit_wiki_page" => true,
-    "delete_wiki_page" => true,
-    "view_eventlog" => true,
-    "manage_blocks" => true,
-    "manage_admintools" => true,
-    "ignore_downtime" => true,
-    "view_other_pms" => true,
-    "edit_feature" => true,
-    "bulk_edit_vote" => true,
-    "edit_other_vote" => true,
-    "view_sysinfo" => true,
-    "view_hellbanned" => true,
-    "protected" => true,
-    "edit_image_rating" => true,
-    "bulk_edit_image_rating" => true,
-    "view_trash" => true,
-    "perform_bulk_actions" => true,
+    Permissions::CHANGE_SETTING => true,
+    Permissions::OVERRIDE_CONFIG => true,
+    Permissions::BIG_SEARCH => true,
+    Permissions::EDIT_IMAGE_LOCK => true,
+    Permissions::VIEW_IP => true,
+    Permissions::BAN_IP => true,
+    Permissions::EDIT_USER_NAME => true,
+    Permissions::EDIT_USER_PASSWORD => true,
+    Permissions::EDIT_USER_INFO => true,
+    Permissions::EDIT_USER_CLASS => true,
+    Permissions::DELETE_USER => true,
+    Permissions::CREATE_IMAGE => true,
+    Permissions::DELETE_IMAGE => true,
+    Permissions::BAN_IMAGE => true,
+    Permissions::CREATE_COMMENT => true,
+    Permissions::DELETE_COMMENT => true,
+    Permissions::BYPASS_COMMENT_CHECKS => true,
+    Permissions::REPLACE_IMAGE => true,
+    Permissions::MANAGE_EXTENSION_LIST => true,
+    Permissions::MANAGE_ALIAS_LIST => true,
+    Permissions::EDIT_IMAGE_TAG => true,
+    Permissions::EDIT_IMAGE_SOURCE => true,
+    Permissions::EDIT_IMAGE_OWNER => true,
+    Permissions::BULK_EDIT_IMAGE_TAG => true,
+    Permissions::BULK_EDIT_IMAGE_SOURCE => true,
+    Permissions::MASS_TAG_EDIT => true,
+    Permissions::CREATE_IMAGE_REPORT => true,
+    Permissions::VIEW_IMAGE_REPORT => true,
+    Permissions::EDIT_WIKI_PAGE => true,
+    Permissions::DELETE_WIKI_PAGE => true,
+    Permissions::VIEW_EVENTLOG => true,
+    Permissions::MANAGE_BLOCKS => true,
+    Permissions::MANAGE_ADMINTOOLS => true,
+    Permissions::IGNORE_DOWNTIME => true,
+    Permissions::VIEW_OTHER_PMS => true,
+    Permissions::EDIT_FEATURE => true,
+    Permissions::BULK_EDIT_VOTE => true,
+    Permissions::EDIT_OTHER_VOTE => true,
+    Permissions::VIEW_SYSINTO => true,
+    Permissions::VIEW_HELLBANNED => true,
+    Permissions::PROTECTED => true,
+    Permissions::EDIT_IMAGE_RATING => true,
+    Permissions::BULK_EDIT_IMAGE_RATING => true,
+    Permissions::VIEW_TRASH => true,
+
 ]);
 
 new UserClass("hellbanned", "user", [
-    "hellbanned" => true,
+    Permissions::HELLBANNED => true,
 ]);
 
 @include_once "data/config/user-classes.conf.php";
diff --git a/core/util.php b/core/util.php
index 91e467ff..aa310c45 100644
--- a/core/util.php
+++ b/core/util.php
@@ -588,8 +588,8 @@ function show_ip(string $ip, string $ban_reason): string
     global $user;
     $u_reason = url_escape($ban_reason);
     $u_end = url_escape("+1 week");
-    $ban = $user->can("ban_ip") ? ", <a href='".make_link("ip_ban/list", "ip=$ip&reason=$u_reason&end=$u_end#add")."'>Ban</a>" : "";
-    $ip = $user->can("view_ip") ? $ip.$ban : "";
+    $ban = $user->can(Permissions::BAN_IP) ? ", <a href='".make_link("ip_ban/list", "ip=$ip&reason=$u_reason&end=$u_end#add")."'>Ban</a>" : "";
+    $ip = $user->can(Permissions::VIEW_IP) ? $ip.$ban : "";
     return $ip;
 }
 
diff --git a/ext/admin/main.php b/ext/admin/main.php
index 4ebae35e..eef919d7 100644
--- a/ext/admin/main.php
+++ b/ext/admin/main.php
@@ -54,7 +54,7 @@ class AdminPage extends Extension
         global $page, $user;
 
         if ($event->page_matches("admin")) {
-            if (!$user->can("manage_admintools")) {
+            if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) {
                 $this->theme->display_permission_denied();
             } else {
                 if ($event->count_args() == 0) {
@@ -111,7 +111,7 @@ class AdminPage extends Extension
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("manage_admintools")) {
+        if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
             $event->add_link("Board Admin", make_link("admin"));
         }
     }
diff --git a/ext/alias_editor/main.php b/ext/alias_editor/main.php
index 07f9f289..5e8e4c11 100644
--- a/ext/alias_editor/main.php
+++ b/ext/alias_editor/main.php
@@ -36,7 +36,7 @@ class AliasEditor extends Extension
 
         if ($event->page_matches("alias")) {
             if ($event->get_arg(0) == "add") {
-                if ($user->can("manage_alias_list")) {
+                if ($user->can(Permissions::MANAGE_ALIAS_LIST))) {
                     if (isset($_POST['oldtag']) && isset($_POST['newtag'])) {
                         try {
                             $aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']);
@@ -49,7 +49,7 @@ class AliasEditor extends Extension
                     }
                 }
             } elseif ($event->get_arg(0) == "remove") {
-                if ($user->can("manage_alias_list")) {
+                if ($user->can(Permissions::MANAGE_ALIAS_LIST))) {
                     if (isset($_POST['oldtag'])) {
                         $database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", ["oldtag" => $_POST['oldtag']]);
                         log_info("alias_editor", "Deleted alias for ".$_POST['oldtag'], "Deleted alias");
@@ -85,7 +85,7 @@ class AliasEditor extends Extension
                 $page->set_filename("aliases.csv");
                 $page->set_data($this->get_alias_csv($database));
             } elseif ($event->get_arg(0) == "import") {
-                if ($user->can("manage_alias_list")) {
+                if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
                     if (count($_FILES) > 0) {
                         $tmp = $_FILES['alias_file']['tmp_name'];
                         $contents = file_get_contents($tmp);
@@ -120,7 +120,7 @@ class AliasEditor extends Extension
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("manage_alias_list")) {
+        if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
             $event->add_link("Alias Editor", make_link("alias/list"));
         }
     }
diff --git a/ext/alias_editor/theme.php b/ext/alias_editor/theme.php
index ec12348e..732139d4 100644
--- a/ext/alias_editor/theme.php
+++ b/ext/alias_editor/theme.php
@@ -11,7 +11,7 @@ class AliasEditorTheme extends Themelet
     {
         global $page, $user;
 
-        $can_manage = $user->can("manage_alias_list");
+        $can_manage = $user->can(Permissions::MANAGE_ALIAS_LIST);
         if ($can_manage) {
             $h_action = "<th width='10%'>Action</th>";
             $h_add = "
diff --git a/ext/ban_words/main.php b/ext/ban_words/main.php
index c668e514..5e0761b3 100644
--- a/ext/ban_words/main.php
+++ b/ext/ban_words/main.php
@@ -58,7 +58,7 @@ xanax
     public function onCommentPosting(CommentPostingEvent $event)
     {
         global $user;
-        if (!$user->can("bypass_comment_checks")) {
+        if (!$user->can(Permissions::BYPASS_COMMENT_CHECKS)) {
             $this->test_text($event->comment, new CommentPostingException("Comment contains banned terms"));
         }
     }
diff --git a/ext/blocks/main.php b/ext/blocks/main.php
index cb9c375c..b444b03a 100644
--- a/ext/blocks/main.php
+++ b/ext/blocks/main.php
@@ -29,7 +29,7 @@ class Blocks extends Extension
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("manage_blocks")) {
+        if ($user->can(Permissions::MANAGE_BLOCKS)) {
             $event->add_link("Blocks Editor", make_link("blocks/list"));
         }
     }
@@ -52,7 +52,7 @@ class Blocks extends Extension
             }
         }
 
-        if ($event->page_matches("blocks") && $user->can("manage_blocks")) {
+        if ($event->page_matches("blocks") && $user->can(Permissions::MANAGE_BLOCKS)) {
             if ($event->get_arg(0) == "add") {
                 if ($user->check_auth_token()) {
                     $database->execute("
diff --git a/ext/bulk_actions/main.php b/ext/bulk_actions/main.php
index d8dde6d9..67f652e2 100644
--- a/ext/bulk_actions/main.php
+++ b/ext/bulk_actions/main.php
@@ -85,11 +85,11 @@ class BulkActions extends Extension
     {
         global $user;
 
-        if ($user->can("delete_image")) {
+        if ($user->can(Permissions::DELETE_IMAGE)) {
             $event->add_action("bulk_delete", "(D)elete", "d", "Delete selected images?", $this->theme->render_ban_reason_input(), 10);
         }
 
-        if ($user->can("bulk_edit_image_tag")) {
+        if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) {
 
             $event->add_action(
                 "bulk_tag",
@@ -100,7 +100,7 @@ class BulkActions extends Extension
                 10);
         }
 
-        if ($user->can("bulk_edit_image_source")) {
+        if ($user->can(Permissions::BULK_EDIT_IMAGE_SOURCE)) {
             $event->add_action("bulk_source", "Set (S)ource", "s","", $this->theme->render_source_input(), 10);
         }
     }
@@ -111,7 +111,7 @@ class BulkActions extends Extension
 
         switch ($event->action) {
             case "bulk_delete":
-                if ($user->can("delete_image")) {
+                if ($user->can(Permissions::DELETE_IMAGE)) {
                     $i = $this->delete_items($event->items);
                     flash_message("Deleted $i items");
                 }
@@ -120,7 +120,7 @@ class BulkActions extends Extension
                 if (!isset($_POST['bulk_tags'])) {
                     return;
                 }
-                if ($user->can("bulk_edit_image_tag")) {
+                if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) {
                     $tags = $_POST['bulk_tags'];
                     $replace = false;
                     if (isset($_POST['bulk_tags_replace']) &&  $_POST['bulk_tags_replace'] == "true") {
@@ -135,7 +135,7 @@ class BulkActions extends Extension
                 if (!isset($_POST['bulk_source'])) {
                     return;
                 }
-                if ($user->can("bulk_edit_image_source")) {
+                if ($user->can(Permissions::BULK_EDIT_IMAGE_SOURCE)) {
                     $source = $_POST['bulk_source'];
                     $i = $this->set_source($event->items, $source);
                     flash_message("Set source for $i items");
diff --git a/ext/comment/main.php b/ext/comment/main.php
index 1dfdc03b..2540ef4a 100644
--- a/ext/comment/main.php
+++ b/ext/comment/main.php
@@ -189,7 +189,7 @@ class CommentList extends Extension
     private function onPageRequest_delete(PageRequestEvent $event)
     {
         global $user, $page;
-        if ($user->can("delete_comment")) {
+        if ($user->can(Permissions::DELETE_COMMENT)) {
             // FIXME: post, not args
             if ($event->count_args() === 3) {
                 send_event(new CommentDeletionEvent($event->get_arg(1)));
@@ -209,7 +209,7 @@ class CommentList extends Extension
     private function onPageRequest_bulk_delete()
     {
         global $user, $database, $page;
-        if ($user->can("delete_comment") && !empty($_POST["ip"])) {
+        if ($user->can(Permissions::DELETE_COMMENT) && !empty($_POST["ip"])) {
             $ip = $_POST['ip'];
 
             $comment_ids = $database->get_col("
@@ -288,7 +288,7 @@ class CommentList extends Extension
         $this->theme->display_image_comments(
             $event->image,
             $this->get_comments($event->image->id),
-            $user->can("create_comment")
+            $user->can(Permissions::CREATE_COMMENT)
         );
     }
 
@@ -399,7 +399,7 @@ class CommentList extends Extension
             }
         }
 
-        $this->theme->display_comment_list($images, $current_page, $total_pages, $user->can("create_comment"));
+        $this->theme->display_comment_list($images, $current_page, $total_pages, $user->can(Permissions::CREATE_COMMENT));
     }
     // }}}
 
@@ -574,7 +574,7 @@ class CommentList extends Extension
     {
         global $database, $page;
 
-        if (!$user->can("bypass_comment_checks")) {
+        if (!$user->can(Permissions::BYPASS_COMMENT_CHECKS)) {
             // will raise an exception if anything is wrong
             $this->comment_checks($image_id, $user, $comment);
         }
@@ -600,7 +600,7 @@ class CommentList extends Extension
         global $config, $page;
 
         // basic sanity checks
-        if (!$user->can("create_comment")) {
+        if (!$user->can(Permissions::CREATE_COMMENT)) {
             throw new CommentPostingException("Anonymous posting has been disabled");
         } elseif (is_null(Image::by_id($image_id))) {
             throw new CommentPostingException("The image does not exist");
diff --git a/ext/comment/theme.php b/ext/comment/theme.php
index a17131af..1bdc70cb 100644
--- a/ext/comment/theme.php
+++ b/ext/comment/theme.php
@@ -218,9 +218,9 @@ class CommentListTheme extends Themelet
                 if (!array_key_exists($comment->poster_ip, $this->anon_map)) {
                     $this->anon_map[$comment->poster_ip] = $this->anon_id;
                 }
-                #if($user->can("view_ip")) {
+                #if($user->can(UserAbilities::VIEW_IP)) {
                 #$style = " style='color: ".$this->get_anon_colour($comment->poster_ip).";'";
-                if ($user->can("view_ip") || $config->get_bool("comment_samefags_public", false)) {
+                if ($user->can(Permissions::VIEW_IP) || $config->get_bool("comment_samefags_public", false)) {
                     if ($this->anon_map[$comment->poster_ip] != $this->anon_id) {
                         $anoncode2 = '<sup>('.$this->anon_map[$comment->poster_ip].')</sup>';
                     }
@@ -248,9 +248,9 @@ class CommentListTheme extends Themelet
                 $h_avatar = "<img src=\"//www.gravatar.com/avatar/$hash.jpg?cacheBreak=$cb\"><br>";
             }
             $h_reply = " - <a href='javascript: replyTo($i_image_id, $i_comment_id, \"$h_name\")'>Reply</a>";
-            $h_ip = $user->can("view_ip") ? "<br>".show_ip($comment->poster_ip, "Comment posted {$comment->posted}") : "";
+            $h_ip = $user->can(Permissions::VIEW_IP) ? "<br>".show_ip($comment->poster_ip, "Comment posted {$comment->posted}") : "";
             $h_del = "";
-            if ($user->can("delete_comment")) {
+            if ($user->can(Permissions::DELETE_COMMENT)) {
                 $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);");
diff --git a/ext/danbooru_api/main.php b/ext/danbooru_api/main.php
index ce13295b..cb55766f 100644
--- a/ext/danbooru_api/main.php
+++ b/ext/danbooru_api/main.php
@@ -297,7 +297,7 @@ class DanbooruApi extends Extension
         // Now we check if a file was uploaded or a url was provided to transload
         // Much of this code is borrowed from /ext/upload
 
-        if (!$user->can("create_image")) {
+        if (!$user->can(Permissions::CREATE_IMAGE)) {
             $page->set_code(409);
             $page->add_http_header("X-Danbooru-Errors: authentication error");
             return;
diff --git a/ext/downtime/main.php b/ext/downtime/main.php
index 891d87c3..97f8682e 100644
--- a/ext/downtime/main.php
+++ b/ext/downtime/main.php
@@ -32,7 +32,7 @@ class Downtime extends Extension
         global $config, $page, $user;
 
         if ($config->get_bool("downtime")) {
-            if (!$user->can("ignore_downtime") && !$this->is_safe_page($event)) {
+            if (!$user->can(Permissions::IGNORE_DOWNTIME) && !$this->is_safe_page($event)) {
                 $msg = $config->get_string("downtime_message");
                 $this->theme->display_message($msg);
                 if (!defined("UNITTEST")) {  // hax D:
diff --git a/ext/et/main.php b/ext/et/main.php
index e3e9b9c7..e344710d 100644
--- a/ext/et/main.php
+++ b/ext/et/main.php
@@ -18,7 +18,7 @@ class ET extends Extension
     {
         global $user;
         if ($event->page_matches("system_info")) {
-            if ($user->can("view_sysinfo")) {
+            if ($user->can(Permissions::VIEW_SYSINTO)) {
                 $this->theme->display_info_page($this->get_info());
             }
         }
@@ -27,7 +27,7 @@ class ET extends Extension
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("view_sysinfo")) {
+        if ($user->can(Permissions::VIEW_SYSINTO)) {
             $event->add_link("System Info", make_link("system_info"));
         }
     }
diff --git a/ext/ext_manager/main.php b/ext/ext_manager/main.php
index 03ee3bb1..e024f286 100644
--- a/ext/ext_manager/main.php
+++ b/ext/ext_manager/main.php
@@ -118,7 +118,7 @@ class ExtManager extends Extension
     {
         global $page, $user;
         if ($event->page_matches("ext_manager")) {
-            if ($user->can("manage_extension_list")) {
+            if ($user->can(Permissions::MANAGE_EXTENSION_LIST)) {
                 if ($event->get_arg(0) == "set" && $user->check_auth_token()) {
                     if (is_writable("data/config")) {
                         $this->set_things($_POST);
@@ -166,7 +166,7 @@ class ExtManager extends Extension
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("manage_extension_list")) {
+        if ($user->can(Permissions::MANAGE_EXTENSION_LIST)) {
             $event->add_link("Extension Manager", make_link("ext_manager"));
         } else {
             $event->add_link("Help", make_link("ext_doc"));
diff --git a/ext/featured/main.php b/ext/featured/main.php
index 4b713424..ae5946dd 100644
--- a/ext/featured/main.php
+++ b/ext/featured/main.php
@@ -32,7 +32,7 @@ class Featured extends Extension
         global $config, $page, $user;
         if ($event->page_matches("featured_image")) {
             if ($event->get_arg(0) == "set" && $user->check_auth_token()) {
-                if ($user->can("edit_feature") && isset($_POST['image_id'])) {
+                if ($user->can(Permissions::EDIT_FEATURE) && isset($_POST['image_id'])) {
                     $id = int_escape($_POST['image_id']);
                     if ($id > 0) {
                         $config->set_int("featured_id", $id);
@@ -86,7 +86,7 @@ class Featured extends Extension
     public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("edit_feature")) {
+        if ($user->can(Permissions::EDIT_FEATURE)) {
             $event->add_part($this->theme->get_buttons_html($event->image->id));
         }
     }
diff --git a/ext/hellban/main.php b/ext/hellban/main.php
index 42bfec27..541f7620 100644
--- a/ext/hellban/main.php
+++ b/ext/hellban/main.php
@@ -9,9 +9,9 @@ class HellBan extends Extension
     {
         global $page, $user;
 
-        if ($user->can("hellbanned")) {
+        if ($user->can(Permissions::HELLBANNED)) {
             $s = "";
-        } elseif ($user->can("view_hellbanned")) {
+        } elseif ($user->can(Permissions::VIEW_HELLBANNED)) {
             $s = "DIV.hb, TR.hb TD {border: 1px solid red !important;}";
         } else {
             $s = ".hb {display: none !important;}";
diff --git a/ext/image/main.php b/ext/image/main.php
index 099bdd65..9969c3e8 100644
--- a/ext/image/main.php
+++ b/ext/image/main.php
@@ -73,7 +73,7 @@ class ImageIO extends Extension
     {
         if ($event->page_matches("image/delete")) {
             global $page, $user;
-            if ($user->can("delete_image") && isset($_POST['image_id']) && $user->check_auth_token()) {
+            if ($user->can(Permissions::DELETE_IMAGE) && isset($_POST['image_id']) && $user->check_auth_token()) {
                 $image = Image::by_id($_POST['image_id']);
                 if ($image) {
                     send_event(new ImageDeletionEvent($image));
@@ -87,7 +87,7 @@ class ImageIO extends Extension
             }
         } elseif ($event->page_matches("image/replace")) {
             global $page, $user;
-            if ($user->can("replace_image") && isset($_POST['image_id']) && $user->check_auth_token()) {
+            if ($user->can(Permissions::REPLACE_IMAGE) && isset($_POST['image_id']) && $user->check_auth_token()) {
                 $image = Image::by_id($_POST['image_id']);
                 if ($image) {
                     $page->set_mode(PageMode::REDIRECT);
@@ -110,11 +110,11 @@ class ImageIO extends Extension
     {
         global $user;
         
-        if ($user->can("delete_image")) {
+        if ($user->can(Permissions::DELETE_IMAGE)) {
             $event->add_part($this->theme->get_deleter_html($event->image->id));
         }
         /* In the future, could perhaps allow users to replace images that they own as well... */
-        if ($user->can("replace_image")) {
+        if ($user->can(Permissions::REPLACE_IMAGE)) {
             $event->add_part($this->theme->get_replace_html($event->image->id));
         }
     }
diff --git a/ext/image_hash_ban/main.php b/ext/image_hash_ban/main.php
index c2e3ec3a..67298a89 100644
--- a/ext/image_hash_ban/main.php
+++ b/ext/image_hash_ban/main.php
@@ -64,7 +64,7 @@ class ImageBan extends Extension
         global $database, $page, $user;
 
         if ($event->page_matches("image_hash_ban")) {
-            if ($user->can("ban_image")) {
+            if ($user->can(Permissions::BAN_IMAGE)) {
                 if ($event->get_arg(0) == "add") {
                     $image = isset($_POST['image_id']) ? Image::by_id(int_escape($_POST['image_id'])) : null;
                     $hash = isset($_POST["hash"]) ? $_POST["hash"] : $image->hash;
@@ -106,7 +106,7 @@ class ImageBan extends Extension
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("ban_image")) {
+        if ($user->can(Permissions::BAN_IMAGE)) {
             $event->add_link("Image Bans", make_link("image_hash_ban/list/1"));
         }
     }
@@ -130,7 +130,7 @@ class ImageBan extends Extension
     public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("ban_image")) {
+        if ($user->can(Permissions::BAN_IMAGE)) {
             $event->add_part($this->theme->get_buttons_html($event->image));
         }
     }
diff --git a/ext/ipban/main.php b/ext/ipban/main.php
index d6feb092..bfefb813 100644
--- a/ext/ipban/main.php
+++ b/ext/ipban/main.php
@@ -66,7 +66,7 @@ class IPBan extends Extension
     {
         if ($event->page_matches("ip_ban")) {
             global $page, $user;
-            if ($user->can("ban_ip")) {
+            if ($user->can(Permissions::BAN_IP)) {
                 if ($event->get_arg(0) == "add" && $user->check_auth_token()) {
                     if (isset($_POST['ip']) && isset($_POST['reason']) && isset($_POST['end'])) {
                         if (empty($_POST['end'])) {
@@ -108,7 +108,7 @@ class IPBan extends Extension
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("ban_ip")) {
+        if ($user->can(Permissions::BAN_IP)) {
             $event->add_link("IP Bans", make_link("ip_ban/list"));
         }
     }
diff --git a/ext/log_db/main.php b/ext/log_db/main.php
index 2f1d761a..a5dd1d7f 100644
--- a/ext/log_db/main.php
+++ b/ext/log_db/main.php
@@ -48,7 +48,7 @@ class LogDatabase extends Extension
     {
         global $database, $user;
         if ($event->page_matches("log/view")) {
-            if ($user->can("view_eventlog")) {
+            if ($user->can(Permissions::VIEW_EVENTLOG)) {
                 $wheres = [];
                 $args = [];
                 $page_num = int_escape($event->get_arg(0));
@@ -123,7 +123,7 @@ class LogDatabase extends Extension
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("view_eventlog")) {
+        if ($user->can(Permissions::VIEW_EVENTLOG)) {
             $event->add_link("Event Log", make_link("log/view"));
         }
     }
diff --git a/ext/media/main.php b/ext/media/main.php
index a4fba3ba..acce17ab 100644
--- a/ext/media/main.php
+++ b/ext/media/main.php
@@ -316,7 +316,7 @@ class Media extends Extension
     public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("delete_image")) {
+        if ($user->can(Permissions::DELETE_IMAGE)) {
             $event->add_part($this->theme->get_buttons_html($event->image->id));
         }
     }
diff --git a/ext/not_a_tag/main.php b/ext/not_a_tag/main.php
index 18486e9c..29f31e75 100644
--- a/ext/not_a_tag/main.php
+++ b/ext/not_a_tag/main.php
@@ -61,7 +61,7 @@ class NotATag extends Extension
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("ban_image")) {
+        if ($user->can(Permissions::BAN_IMAGE)) {
             $event->add_link("UnTags", make_link("untag/list/1"));
         }
     }
@@ -71,7 +71,7 @@ class NotATag extends Extension
         global $database, $page, $user;
 
         if ($event->page_matches("untag")) {
-            if ($user->can("ban_image")) {
+            if ($user->can(Permissions::BAN_IMAGE)) {
                 if ($event->get_arg(0) == "add") {
                     $tag = $_POST["tag"];
                     $redirect = isset($_POST['redirect']) ? $_POST['redirect'] : "DNP";
diff --git a/ext/numeric_score/main.php b/ext/numeric_score/main.php
index 5275dfa7..446c5553 100644
--- a/ext/numeric_score/main.php
+++ b/ext/numeric_score/main.php
@@ -45,7 +45,7 @@ class NumericScore extends Extension
     public function onUserPageBuilding(UserPageBuildingEvent $event)
     {
         global $user;
-        if ($user->can("edit_other_vote")) {
+        if ($user->can(Permissions::EDIT_OTHER_VOTE)) {
             $this->theme->get_nuller($event->display_user);
         }
 
@@ -98,7 +98,7 @@ class NumericScore extends Extension
                 $page->set_redirect(make_link("post/view/$image_id"));
             }
         } elseif ($event->page_matches("numeric_score/remove_votes_on") && $user->check_auth_token()) {
-            if ($user->can("edit_other_vote")) {
+            if ($user->can(Permissions::EDIT_OTHER_VOTE)) {
                 $image_id = int_escape($_POST['image_id']);
                 $database->execute(
                     "DELETE FROM numeric_score_votes WHERE image_id=?",
@@ -112,7 +112,7 @@ class NumericScore extends Extension
                 $page->set_redirect(make_link("post/view/$image_id"));
             }
         } elseif ($event->page_matches("numeric_score/remove_votes_by") && $user->check_auth_token()) {
-            if ($user->can("edit_other_vote")) {
+            if ($user->can(Permissions::EDIT_OTHER_VOTE)) {
                 $this->delete_votes_by(int_escape($_POST['user_id']));
                 $page->set_mode(PageMode::REDIRECT);
                 $page->set_redirect(make_link());
diff --git a/ext/numeric_score/theme.php b/ext/numeric_score/theme.php
index c2dc31c7..1852548c 100644
--- a/ext/numeric_score/theme.php
+++ b/ext/numeric_score/theme.php
@@ -32,7 +32,7 @@ class NumericScoreTheme extends Themelet
 			<input type='submit' value='Vote Down'>
 			</form>
 		";
-        if ($user->can("edit_other_vote")) {
+        if ($user->can(Permissions::EDIT_OTHER_VOTE)) {
             $html .= "
 			<form action='".make_link("numeric_score/remove_votes_on")."' method='POST'>
 			".$user->get_auth_html()."
diff --git a/ext/oekaki/main.php b/ext/oekaki/main.php
index 1da9cc90..94e1b061 100644
--- a/ext/oekaki/main.php
+++ b/ext/oekaki/main.php
@@ -12,7 +12,7 @@ class Oekaki extends Extension
         global $user, $page;
 
         if ($event->page_matches("oekaki")) {
-            if ($user->can("create_image")) {
+            if ($user->can(Permissions::CREATE_IMAGE)) {
                 if ($event->get_arg(0) == "create") {
                     $this->theme->display_page();
                     $this->theme->display_block();
@@ -84,7 +84,7 @@ class Oekaki extends Extension
     public function onPostListBuilding(PostListBuildingEvent $event)
     {
         global $user;
-        if ($user->can("create_image")) {
+        if ($user->can(Permissions::CREATE_IMAGE)) {
             $this->theme->display_block();
         }
     }
diff --git a/ext/ouroboros_api/main.php b/ext/ouroboros_api/main.php
index d701fbea..d654b6f0 100644
--- a/ext/ouroboros_api/main.php
+++ b/ext/ouroboros_api/main.php
@@ -410,7 +410,7 @@ class OuroborosAPI extends Extension
             if ($event->page_matches('post')) {
                 if ($this->match('create')) {
                     // Create
-                    if ($user->can("create_image")) {
+                    if ($user->can(Permissions::CREATE_IMAGE)) {
                         $md5 = !empty($_REQUEST['md5']) ? filter_var($_REQUEST['md5'], FILTER_SANITIZE_STRING) : null;
                         $this->postCreate(new OuroborosPost($_REQUEST['post']), $md5);
                     } else {
diff --git a/ext/pm/main.php b/ext/pm/main.php
index 123d6368..d4ef16da 100644
--- a/ext/pm/main.php
+++ b/ext/pm/main.php
@@ -108,7 +108,7 @@ class PrivMsg extends Extension
         global $page, $user;
         $duser = $event->display_user;
         if (!$user->is_anonymous() && !$duser->is_anonymous()) {
-            if (($user->id == $duser->id) || $user->can("view_other_pms")) {
+            if (($user->id == $duser->id) || $user->can(Permissions::VIEW_OTHER_PMS)) {
                 $this->theme->display_pms($page, $this->get_pms($duser));
             }
             if ($user->id != $duser->id) {
@@ -128,7 +128,7 @@ class PrivMsg extends Extension
                         $pm = $database->get_row("SELECT * FROM private_message WHERE id = :id", ["id" => $pm_id]);
                         if (is_null($pm)) {
                             $this->theme->display_error(404, "No such PM", "There is no PM #$pm_id");
-                        } elseif (($pm["to_id"] == $user->id) || $user->can("view_other_pms")) {
+                        } elseif (($pm["to_id"] == $user->id) || $user->can(Permissions::VIEW_OTHER_PMS)) {
                             $from_user = User::by_id(int_escape($pm["from_id"]));
                             if ($pm["to_id"] == $user->id) {
                                 $database->execute("UPDATE private_message SET is_read='Y' WHERE id = :id", ["id" => $pm_id]);
@@ -145,7 +145,7 @@ class PrivMsg extends Extension
                             $pm = $database->get_row("SELECT * FROM private_message WHERE id = :id", ["id" => $pm_id]);
                             if (is_null($pm)) {
                                 $this->theme->display_error(404, "No such PM", "There is no PM #$pm_id");
-                            } elseif (($pm["to_id"] == $user->id) || $user->can("view_other_pms")) {
+                            } elseif (($pm["to_id"] == $user->id) || $user->can(Permissions::VIEW_OTHER_PMS)) {
                                 $database->execute("DELETE FROM private_message WHERE id = :id", ["id" => $pm_id]);
                                 $database->cache->delete("pm-count-{$user->id}");
                                 log_info("pm", "Deleted PM #$pm_id", "PM deleted");
diff --git a/ext/pm/theme.php b/ext/pm/theme.php
index bb9a0f49..f69240d9 100644
--- a/ext/pm/theme.php
+++ b/ext/pm/theme.php
@@ -27,7 +27,7 @@ class PrivMsgTheme extends Themelet
                 $h_subject = "<b>$h_subject</b>";
                 $readYN = "N";
             }
-            $hb = $from->can("hellbanned") ? "hb" : "";
+            $hb = $from->can(Permissions::HELLBANNED) ? "hb" : "";
             $html .= "<tr class='$hb'>
 			<td>$readYN</td>
 			<td><a href='$pm_url'>$h_subject</a></td>
diff --git a/ext/rating/main.php b/ext/rating/main.php
index 7b43d914..c62fd03e 100644
--- a/ext/rating/main.php
+++ b/ext/rating/main.php
@@ -169,7 +169,7 @@ class Ratings extends Extension
     {
         global $user;
 
-        if ($user->can("bulk_edit_image_rating")) {
+        if ($user->can(Permissions::BULK_EDIT_IMAGE_RATING)) {
             $event->add_action("bulk_rate","Set (R)ating", "r","",$this->theme->get_selection_rater_html("u","bulk_rating"));
         }
     }
@@ -183,7 +183,7 @@ class Ratings extends Extension
                 if (!isset($_POST['bulk_rating'])) {
                     return;
                 }
-                if ($user->can("bulk_edit_image_rating")) {
+                if ($user->can(Permissions::BULK_EDIT_IMAGE_RATING)) {
                     $rating = $_POST['bulk_rating'];
                     $total = 0;
                     foreach ($event->items as $image) {
@@ -201,7 +201,7 @@ class Ratings extends Extension
         global $user, $page;
         
         if ($event->page_matches("admin/bulk_rate")) {
-            if (!$user->can("bulk_edit_image_rating")) {
+            if (!$user->can(Permissions::BULK_EDIT_IMAGE_RATING)) {
                 throw new PermissionDeniedException();
             } else {
                 $n = 0;
diff --git a/ext/regen_thumb/main.php b/ext/regen_thumb/main.php
index 637febbf..62e80664 100644
--- a/ext/regen_thumb/main.php
+++ b/ext/regen_thumb/main.php
@@ -28,14 +28,14 @@ class RegenThumb extends Extension
     {
         global $database, $page, $user;
 
-        if ($event->page_matches("regen_thumb/one") && $user->can("delete_image") && isset($_POST['image_id'])) {
+        if ($event->page_matches("regen_thumb/one") && $user->can(Permissions::DELETE_IMAGE) && isset($_POST['image_id'])) {
             $image = Image::by_id(int_escape($_POST['image_id']));
 
             $this->regenerate_thumbnail($image);
 
             $this->theme->display_results($page, $image);
         }
-        if ($event->page_matches("regen_thumb/mass") && $user->can("delete_image") && isset($_POST['tags'])) {
+        if ($event->page_matches("regen_thumb/mass") && $user->can(Permissions::DELETE_IMAGE) && isset($_POST['tags'])) {
             $tags = Tag::explode(strtolower($_POST['tags']), false);
             $images = Image::find_images(0, 10000, $tags);
 
@@ -51,7 +51,7 @@ class RegenThumb extends Extension
     public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("delete_image")) {
+        if ($user->can(Permissions::DELETE_IMAGE)) {
             $event->add_part($this->theme->get_buttons_html($event->image->id));
         }
     }
@@ -59,7 +59,7 @@ class RegenThumb extends Extension
     // public function onPostListBuilding(PostListBuildingEvent $event)
     // {
     //     global $user;
-    //     if ($user->can("delete_image") && !empty($event->search_terms)) {
+    //     if ($user->can(UserAbilities::DELETE_IMAGE) && !empty($event->search_terms)) {
     //         $event->add_control($this->theme->mtr_html(Tag::implode($event->search_terms)));
     //     }
     // }
@@ -68,7 +68,7 @@ class RegenThumb extends Extension
     {
         global $user;
 
-        if ($user->can("delete_image")) {
+        if ($user->can(Permissions::DELETE_IMAGE)) {
             $event->add_action("bulk_regen", "Regen Thumbnails", "","", $this->theme->bulk_html());
         }
     }
@@ -79,7 +79,7 @@ class RegenThumb extends Extension
 
         switch ($event->action) {
             case "bulk_regen":
-                if ($user->can("delete_image")) {
+                if ($user->can(Permissions::DELETE_IMAGE)) {
                     $force = true;
                     if (isset($_POST["bulk_regen_thumb_missing_only"])
                         &&$_POST["bulk_regen_thumb_missing_only"]=="true") {
diff --git a/ext/report_image/main.php b/ext/report_image/main.php
index d3ee2c81..970d9466 100644
--- a/ext/report_image/main.php
+++ b/ext/report_image/main.php
@@ -74,7 +74,7 @@ class ReportImage extends Extension
                 }
             } elseif ($event->get_arg(0) == "remove") {
                 if (!empty($_POST['id'])) {
-                    if ($user->can("view_image_report")) {
+                    if ($user->can(Permissions::VIEW_IMAGE_REPORT)) {
                         send_event(new RemoveReportedImageEvent($_POST['id']));
                         $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("image_report/list"));
@@ -83,13 +83,13 @@ class ReportImage extends Extension
                     $this->theme->display_error(500, "Missing input", "Missing image ID");
                 }
             } elseif ($event->get_arg(0) == "remove_reports_by" && $user->check_auth_token()) {
-                if ($user->can("view_image_report")) {
+                if ($user->can(Permissions::VIEW_IMAGE_REPORT)) {
                     $this->delete_reports_by(int_escape($_POST['user_id']));
                     $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link());
                 }
             } elseif ($event->get_arg(0) == "list") {
-                if ($user->can("view_image_report")) {
+                if ($user->can(Permissions::VIEW_IMAGE_REPORT)) {
                     $this->theme->display_reported_images($page, $this->get_reported_images());
                 }
             }
@@ -118,7 +118,7 @@ class ReportImage extends Extension
     public function onUserPageBuilding(UserPageBuildingEvent $event)
     {
         global $user;
-        if ($user->can("view_image_report")) {
+        if ($user->can(Permissions::VIEW_IMAGE_REPORT)) {
             $this->theme->get_nuller($event->display_user);
         }
     }
@@ -126,7 +126,7 @@ class ReportImage extends Extension
     public function onDisplayingImage(DisplayingImageEvent $event)
     {
         global $user;
-        if ($user->can('create_image_report')) {
+        if ($user->can(Permissions::CREATE_IMAGE_REPORT)) {
             $reps = $this->get_reports($event->image);
             $this->theme->display_image_banner($event->image, $reps);
         }
@@ -135,7 +135,7 @@ class ReportImage extends Extension
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("view_image_report")) {
+        if ($user->can(Permissions::VIEW_IMAGE_REPORT)) {
             $count = $this->count_reported_images();
             $h_count = $count > 0 ? " ($count)" : "";
             $event->add_link("Reported Images$h_count", make_link("image_report/list"));
diff --git a/ext/rule34/main.php b/ext/rule34/main.php
index b26fb166..3e9c650c 100644
--- a/ext/rule34/main.php
+++ b/ext/rule34/main.php
@@ -50,7 +50,7 @@ class Rule34 extends Extension
     public function onUserPageBuilding(UserPageBuildingEvent $event)
     {
         global $database, $user, $config;
-        if ($user->can("change_setting") && $config->get_bool('r34_comic_integration')) {
+        if ($user->can(Permissions::CHANGE_SETTING) && $config->get_bool('r34_comic_integration')) {
             $current_state = bool_escape($database->get_one("SELECT comic_admin FROM users WHERE id=?", [$event->display_user->id]));
             $this->theme->show_comic_changer($event->display_user, $current_state);
         }
@@ -59,7 +59,7 @@ class Rule34 extends Extension
     public function onThumbnailGeneration(ThumbnailGenerationEvent $event)
     {
         global $database, $user;
-        if ($user->can("manage_admintools")) {
+        if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
             $database->execute("NOTIFY shm_image_bans, '{$event->hash}';");
         }
     }
@@ -72,7 +72,7 @@ class Rule34 extends Extension
     {
         global $database, $page, $user;
 
-        if ($user->can("delete_user")) {  // deleting users can take a while
+        if ($user->can(Permissions::DELETE_USER)) {  // deleting users can take a while
             $database->execute("SET statement_timeout TO ".(DATABASE_TIMEOUT+15000).";");
         }
 
@@ -81,7 +81,7 @@ class Rule34 extends Extension
         }
 
         if ($event->page_matches("rule34/comic_admin")) {
-            if ($user->can("change_setting") && $user->check_auth_token()) {
+            if ($user->can(Permissions::CHANGE_SETTING) && $user->check_auth_token()) {
                 $input = validate_input([
                     'user_id' => 'user_id,exists',
                     'is_admin' => 'bool',
@@ -102,7 +102,7 @@ class Rule34 extends Extension
         }
 
         if ($event->page_matches("admin/cache_purge")) {
-            if (!$user->can("manage_admintools")) {
+            if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) {
                 $this->theme->display_permission_denied();
             } else {
                 if ($user->check_auth_token()) {
@@ -130,7 +130,7 @@ class Rule34 extends Extension
 
         if ($event->page_matches("sys_ip_ban")) {
             global $page, $user;
-            if ($user->can("ban_ip")) {
+            if ($user->can(Permissions::BAN_IP)) {
                 if ($event->get_arg(0) == "list") {
                     $bans = (isset($_GET["all"])) ? $this->get_bans() : $this->get_active_bans();
                     $this->theme->display_bans($page, $bans);
diff --git a/ext/setup/main.php b/ext/setup/main.php
index 86af0141..9b8fa060 100644
--- a/ext/setup/main.php
+++ b/ext/setup/main.php
@@ -293,7 +293,7 @@ class Setup extends Extension
         }
 
         if ($event->page_matches("setup")) {
-            if (!$user->can("change_setting")) {
+            if (!$user->can(Permissions::CHANGE_SETTING)) {
                 $this->theme->display_permission_denied();
             } else {
                 if ($event->get_arg(0) == "save" && $user->check_auth_token()) {
@@ -413,7 +413,7 @@ class Setup extends Extension
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("change_setting")) {
+        if ($user->can(Permissions::CHANGE_SETTING)) {
             $event->add_link("Board Config", make_link("setup"));
         }
     }
diff --git a/ext/source_history/main.php b/ext/source_history/main.php
index 591fd217..ac5f7ae7 100644
--- a/ext/source_history/main.php
+++ b/ext/source_history/main.php
@@ -35,13 +35,13 @@ class Source_History extends Extension
 
         if ($event->page_matches("source_history/revert")) {
             // this is a request to revert to a previous version of the source
-            if ($user->can("edit_image_tag")) {
+            if ($user->can(Permissions::EDIT_IMAGE_TAG)) {
                 if (isset($_POST['revert'])) {
                     $this->process_revert_request($_POST['revert']);
                 }
             }
         } elseif ($event->page_matches("source_history/bulk_revert")) {
-            if ($user->can("bulk_edit_image_tag") && $user->check_auth_token()) {
+            if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG) && $user->check_auth_token()) {
                 $this->process_bulk_revert_request();
             }
         } elseif ($event->page_matches("source_history/all")) {
@@ -85,7 +85,7 @@ class Source_History extends Extension
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("bulk_edit_image_tag")) {
+        if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) {
             $event->add_link("Source Changes", make_link("source_history/all/1"));
         }
     }
diff --git a/ext/source_history/theme.php b/ext/source_history/theme.php
index 9d6faeac..fbe5a332 100644
--- a/ext/source_history/theme.php
+++ b/ext/source_history/theme.php
@@ -20,7 +20,7 @@ class Source_HistoryTheme extends Themelet
             $current_source = html_escape($fields['source']);
             $name = $fields['name'];
             $date_set = autodate($fields['date_set']);
-            $h_ip = $user->can("view_ip") ? " ".show_ip($fields['user_ip'], "Sourcing Image #$image_id as '$current_source'") : "";
+            $h_ip = $user->can(Permissions::VIEW_IP) ? " ".show_ip($fields['user_ip'], "Sourcing Image #$image_id as '$current_source'") : "";
             $setter = "<a href='".make_link("user/".url_escape($name))."'>".html_escape($name)."</a>$h_ip";
 
             $selected = ($n == 2) ? " checked" : "";
@@ -72,7 +72,7 @@ class Source_HistoryTheme extends Themelet
             $image_id = $fields['image_id'];
             $current_source = html_escape($fields['source']);
             $name = $fields['name'];
-            $h_ip = $user->can("view_ip") ? " ".show_ip($fields['user_ip'], "Sourcing Image #$image_id as '$current_source'") : "";
+            $h_ip = $user->can(Permissions::VIEW_IP) ? " ".show_ip($fields['user_ip'], "Sourcing Image #$image_id as '$current_source'") : "";
             $setter = "<a href='".make_link("user/".url_escape($name))."'>".html_escape($name)."</a>$h_ip";
 
             $history_list .= '
diff --git a/ext/tag_edit/main.php b/ext/tag_edit/main.php
index aa443797..7d20b7d0 100644
--- a/ext/tag_edit/main.php
+++ b/ext/tag_edit/main.php
@@ -161,7 +161,7 @@ class TagEdit extends Extension
         global $user, $page;
         if ($event->page_matches("tag_edit")) {
             if ($event->get_arg(0) == "replace") {
-                if ($user->can("mass_tag_edit") && isset($_POST['search']) && isset($_POST['replace'])) {
+                if ($user->can(Permissions::MASS_TAG_EDIT) && isset($_POST['search']) && isset($_POST['replace'])) {
                     $search = $_POST['search'];
                     $replace = $_POST['replace'];
                     $this->mass_tag_edit($search, $replace);
@@ -170,7 +170,7 @@ class TagEdit extends Extension
                 }
             }
             if ($event->get_arg(0) == "mass_source_set") {
-                if ($user->can("mass_tag_edit") && isset($_POST['tags']) && isset($_POST['source'])) {
+                if ($user->can(Permissions::MASS_TAG_EDIT) && isset($_POST['tags']) && isset($_POST['source'])) {
                     $this->mass_source_edit($_POST['tags'], $_POST['source']);
                     $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("post/list"));
@@ -182,7 +182,7 @@ class TagEdit extends Extension
     // public function onPostListBuilding(PostListBuildingEvent $event)
     // {
     //     global $user;
-    //     if ($user->can("bulk_edit_image_source") && !empty($event->search_terms)) {
+    //     if ($user->can(UserAbilities::BULK_EDIT_IMAGE_SOURCE) && !empty($event->search_terms)) {
     //         $event->add_control($this->theme->mss_html(Tag::implode($event->search_terms)));
     //     }
     // }
@@ -190,7 +190,7 @@ class TagEdit extends Extension
     public function onImageInfoSet(ImageInfoSetEvent $event)
     {
         global $user;
-        if ($user->can("edit_image_owner") && isset($_POST['tag_edit__owner'])) {
+        if ($user->can(Permissions::EDIT_IMAGE_OWNER) && isset($_POST['tag_edit__owner'])) {
             $owner = User::by_name($_POST['tag_edit__owner']);
             if ($owner instanceof User) {
                 send_event(new OwnerSetEvent($event->image, $owner));
@@ -206,7 +206,7 @@ class TagEdit extends Extension
                 send_event(new SourceSetEvent($event->image, $_POST['tag_edit__source']));
             }
         }
-        if ($user->can("edit_image_lock")) {
+        if ($user->can(Permissions::EDIT_IMAGE_LOCK)) {
             $locked = isset($_POST['tag_edit__locked']) && $_POST['tag_edit__locked']=="on";
             send_event(new LockSetEvent($event->image, $locked));
         }
@@ -215,7 +215,7 @@ class TagEdit extends Extension
     public function onOwnerSet(OwnerSetEvent $event)
     {
         global $user;
-        if ($user->can("edit_image_owner") && (!$event->image->is_locked() || $user->can("edit_image_lock"))) {
+        if ($user->can(Permissions::EDIT_IMAGE_OWNER) && (!$event->image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK))) {
             $event->image->set_owner($event->owner);
         }
     }
@@ -223,7 +223,7 @@ class TagEdit extends Extension
     public function onTagSet(TagSetEvent $event)
     {
         global $user;
-        if ($user->can("edit_image_tag") && (!$event->image->is_locked() || $user->can("edit_image_lock"))) {
+        if ($user->can(Permissions::EDIT_IMAGE_TAG) && (!$event->image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK))) {
             $event->image->set_tags($event->tags);
         }
         $event->image->parse_metatags($event->metatags, $event->image->id);
@@ -232,7 +232,7 @@ class TagEdit extends Extension
     public function onSourceSet(SourceSetEvent $event)
     {
         global $user;
-        if ($user->can("edit_image_source") && (!$event->image->is_locked() || $user->can("edit_image_lock"))) {
+        if ($user->can(Permissions::EDIT_IMAGE_SOURCE) && (!$event->image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK))) {
             $event->image->set_source($event->source);
         }
     }
@@ -240,7 +240,7 @@ class TagEdit extends Extension
     public function onLockSet(LockSetEvent $event)
     {
         global $user;
-        if ($user->can("edit_image_lock")) {
+        if ($user->can(Permissions::EDIT_IMAGE_LOCK)) {
             $event->image->set_locked($event->locked);
         }
     }
@@ -288,13 +288,13 @@ class TagEdit extends Extension
     private function can_tag(Image $image): bool
     {
         global $user;
-        return ($user->can("edit_image_tag") || !$image->is_locked());
+        return ($user->can(Permissions::EDIT_IMAGE_TAG) || !$image->is_locked());
     }
 
     private function can_source(Image $image): bool
     {
         global $user;
-        return ($user->can("edit_image_source") || !$image->is_locked());
+        return ($user->can(Permissions::EDIT_IMAGE_SOURCE) || !$image->is_locked());
     }
 
     private function mass_tag_edit(string $search, string $replace)
diff --git a/ext/tag_edit/theme.php b/ext/tag_edit/theme.php
index ee978e42..fa95d7fa 100644
--- a/ext/tag_edit/theme.php
+++ b/ext/tag_edit/theme.php
@@ -51,7 +51,7 @@ class TagEditTheme extends Themelet
 			<tr>
 				<th width='50px'>Tags</th>
 				<td>
-		".($user->can("edit_image_tag") ? "
+		".($user->can(Permissions::EDIT_IMAGE_TAG) ? "
 					<span class='view'>$h_tag_links</span>
 					<input class='edit autocomplete_tags' type='text' name='tag_edit__tags' value='$h_tags' id='tag_editor' autocomplete='off'>
 		" : "
@@ -68,12 +68,12 @@ class TagEditTheme extends Themelet
         $h_owner = html_escape($image->get_owner()->name);
         $h_av = $image->get_owner()->get_avatar_html();
         $h_date = autodate($image->posted);
-        $h_ip = $user->can("view_ip") ? " (".show_ip($image->owner_ip, "Image posted {$image->posted}").")" : "";
+        $h_ip = $user->can(Permissions::VIEW_IP) ? " (".show_ip($image->owner_ip, "Image posted {$image->posted}").")" : "";
         return "
 			<tr>
 				<th>Uploader</th>
 				<td>
-		".($user->can("edit_image_owner") ? "
+		".($user->can(Permissions::EDIT_IMAGE_OWNER) ? "
 					<span class='view'><a class='username' href='".make_link("user/$h_owner")."'>$h_owner</a>$h_ip, $h_date</span>
 					<input class='edit' type='text' name='tag_edit__owner' value='$h_owner'>
 		" : "
@@ -95,7 +95,7 @@ class TagEditTheme extends Themelet
 			<tr>
 				<th>Source</th>
 				<td>
-		".($user->can("edit_image_source") ? "
+		".($user->can(Permissions::EDIT_IMAGE_SOURCE) ? "
 					<div class='view' style='$style'>$f_source</div>
 					<input class='edit' type='text' name='tag_edit__source' value='$h_source'>
 		" : "
@@ -132,7 +132,7 @@ class TagEditTheme extends Themelet
 			<tr>
 				<th>Locked</th>
 				<td>
-		".($user->can("edit_image_lock") ? "
+		".($user->can(Permissions::EDIT_IMAGE_LOCK) ? "
 					<span class='view'>$b_locked</span>
 					<input class='edit' type='checkbox' name='tag_edit__locked'$h_locked>
 		" : "
diff --git a/ext/tag_editcloud/main.php b/ext/tag_editcloud/main.php
index 24e6d1a3..0ae9088c 100644
--- a/ext/tag_editcloud/main.php
+++ b/ext/tag_editcloud/main.php
@@ -180,6 +180,6 @@ class TagEditCloud extends Extension
     private function can_tag(Image $image): bool
     {
         global $user;
-        return ($user->can("edit_image_tag") && (!$image->is_locked() || $user->can("edit_image_lock")));
+        return ($user->can(Permissions::EDIT_IMAGE_TAG) && (!$image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK)));
     }
 }
diff --git a/ext/tag_history/main.php b/ext/tag_history/main.php
index 618bd85a..6e241c3e 100644
--- a/ext/tag_history/main.php
+++ b/ext/tag_history/main.php
@@ -35,13 +35,13 @@ class Tag_History extends Extension
 
         if ($event->page_matches("tag_history/revert")) {
             // this is a request to revert to a previous version of the tags
-            if ($user->can("edit_image_tag")) {
+            if ($user->can(Permissions::EDIT_IMAGE_TAG)) {
                 if (isset($_POST['revert'])) {
                     $this->process_revert_request($_POST['revert']);
                 }
             }
         } elseif ($event->page_matches("tag_history/bulk_revert")) {
-            if ($user->can("bulk_edit_image_tag") && $user->check_auth_token()) {
+            if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG) && $user->check_auth_token()) {
                 $this->process_bulk_revert_request();
             }
         } elseif ($event->page_matches("tag_history/all")) {
@@ -85,7 +85,7 @@ class Tag_History extends Extension
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
-        if ($user->can("bulk_edit_image_tag")) {
+        if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) {
             $event->add_link("Tag Changes", make_link("tag_history/all/1"));
         }
     }
diff --git a/ext/tag_history/theme.php b/ext/tag_history/theme.php
index dfa1ab41..9e51ccb8 100644
--- a/ext/tag_history/theme.php
+++ b/ext/tag_history/theme.php
@@ -25,7 +25,7 @@ class Tag_HistoryTheme extends Themelet
             $current_tags = html_escape($fields['tags']);
             $name = $fields['name'];
             $date_set = autodate($fields['date_set']);
-            $h_ip = $user->can("view_ip") ? " ".show_ip($fields['user_ip'], "Tagging Image #$image_id as '$current_tags'") : "";
+            $h_ip = $user->can(Permissions::VIEW_IP) ? " ".show_ip($fields['user_ip'], "Tagging Image #$image_id as '$current_tags'") : "";
             $setter = "<a href='".make_link("user/".url_escape($name))."'>".html_escape($name)."</a>$h_ip";
 
             $selected = ($n == 2) ? " checked" : "";
@@ -84,7 +84,7 @@ class Tag_HistoryTheme extends Themelet
             $image_id = $fields['image_id'];
             $current_tags = html_escape($fields['tags']);
             $name = $fields['name'];
-            $h_ip = $user->can("view_ip") ? " ".show_ip($fields['user_ip'], "Tagging Image #$image_id as '$current_tags'") : "";
+            $h_ip = $user->can(Permissions::VIEW_IP) ? " ".show_ip($fields['user_ip'], "Tagging Image #$image_id as '$current_tags'") : "";
             $setter = "<a href='".make_link("user/".url_escape($name))."'>".html_escape($name)."</a>$h_ip";
 
             $history_list .= '
diff --git a/ext/tagger/main.php b/ext/tagger/main.php
index 631da7b4..2653634c 100644
--- a/ext/tagger/main.php
+++ b/ext/tagger/main.php
@@ -12,7 +12,7 @@ class Tagger extends Extension
     {
         global $page, $user;
 
-        if ($user->can("edit_image_tag") && ($event->image->is_locked() || $user->can("edit_image_lock"))) {
+        if ($user->can(Permissions::EDIT_IMAGE_TAG) && ($event->image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK))) {
             $this->theme->build_tagger($page, $event);
         }
     }
diff --git a/ext/trash/main.php b/ext/trash/main.php
index 89be2aee..bb1b766f 100644
--- a/ext/trash/main.php
+++ b/ext/trash/main.php
@@ -37,7 +37,7 @@ class Trash extends Extension
     {
         global $page, $user;
 
-        if ($event->page_matches("trash_restore") && $user->can("view_trash")) {
+        if ($event->page_matches("trash_restore") && $user->can(Permissions::VIEW_TRASH)) {
             // Try to get the image ID
             $image_id = int_escape($event->get_arg(0));
             if (empty($image_id)) {
@@ -59,7 +59,7 @@ class Trash extends Extension
     {
         global $user, $page;
 
-        if($event->image->trash===true && !$user->can("view_trash")) {
+        if($event->image->trash===true && !$user->can(Permissions::VIEW_TRASH)) {
             $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link("post/list"));
         }
@@ -87,7 +87,7 @@ class Trash extends Extension
 
 
         if (preg_match(self::SEARCH_REGEXP, strtolower($event->term), $matches)) {
-            if($user->can("view_trash")) {
+            if($user->can(Permissions::VIEW_TRASH)) {
                 $event->add_querylet(new Querylet($database->scoreql_to_sql("trash = SCORE_BOOL_Y ")));
             }
         }
@@ -114,7 +114,7 @@ class Trash extends Extension
     public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
     {
         global $config, $database, $user;
-        if($event->image->trash===true && $user->can("view_trash")) {
+        if($event->image->trash===true && $user->can(Permissions::VIEW_TRASH)) {
             $event->add_part($this->theme->get_image_admin_html($event->image->id));
         }
     }
@@ -123,7 +123,7 @@ class Trash extends Extension
     {
         global $user;
 
-        if ($user->can("view_trash")&&in_array("in:trash", $event->search_terms)) {
+        if ($user->can(Permissions::VIEW_TRASH)&&in_array("in:trash", $event->search_terms)) {
             $event->add_action("bulk_trash_restore","(U)ndelete", "u");
         }
     }
@@ -134,7 +134,7 @@ class Trash extends Extension
 
         switch ($event->action) {
             case "bulk_trash_restore":
-                if ($user->can("view_trash")) {
+                if ($user->can(Permissions::VIEW_TRASH)) {
                     $total = 0;
                     foreach ($event->items as $image) {
                         self::set_trash($image->id, false);
diff --git a/ext/upload/main.php b/ext/upload/main.php
index e0274346..ed13c96b 100644
--- a/ext/upload/main.php
+++ b/ext/upload/main.php
@@ -155,7 +155,7 @@ class Upload extends Extension
     {
         global $database, $page, $user;
 
-        if ($user->can("create_image")) {
+        if ($user->can(Permissions::CREATE_IMAGE)) {
             if ($this->is_full) {
                 $this->theme->display_full($page);
             } else {
@@ -165,7 +165,7 @@ class Upload extends Extension
 
         if ($event->page_matches("upload/replace")) {
             // check if the user is an administrator and can upload files.
-            if (!$user->can("replace_image")) {
+            if (!$user->can(Permissions::REPLACE_IMAGE)) {
                 $this->theme->display_permission_denied();
             } else {
                 if ($this->is_full) {
@@ -221,7 +221,7 @@ class Upload extends Extension
                 }
             }
         } elseif ($event->page_matches("upload")) {
-            if (!$user->can("create_image")) {
+            if (!$user->can(Permissions::CREATE_IMAGE)) {
                 $this->theme->display_permission_denied();
             } else {
                 /* Regular Upload Image */
@@ -371,7 +371,7 @@ class Upload extends Extension
         $ok = true;
 
         // Checks if user is admin > check if you want locked.
-        if ($user->can("edit_image_lock") && !empty($_GET['locked'])) {
+        if ($user->can(Permissions::EDIT_IMAGE_LOCK) && !empty($_GET['locked'])) {
             $locked = bool_escape($_GET['locked']);
         }
 
diff --git a/ext/user/main.php b/ext/user/main.php
index b918a845..f7d202af 100644
--- a/ext/user/main.php
+++ b/ext/user/main.php
@@ -127,7 +127,7 @@ class UserPage extends Extension
                     $a["name"] = '%' . $_GET['username'] . '%';
                 }
 
-                if ($user->can('delete_user') && @$_GET['email']) {
+                if ($user->can(Permissions::DELETE_USER) && @$_GET['email']) {
                     $q .= " AND SCORE_STRNORM(email) LIKE SCORE_STRNORM(:email)";
                     $a["email"] = '%' . $_GET['email'] . '%';
                 }
@@ -212,7 +212,7 @@ class UserPage extends Extension
         global $user, $config;
 
         $h_join_date = autodate($event->display_user->join_date);
-        if ($event->display_user->can("hellbanned")) {
+        if ($event->display_user->can(Permissions::HELLBANNED)) {
             $h_class = $event->display_user->class->parent->name;
         } else {
             $h_class = $event->display_user->class->name;
@@ -250,7 +250,7 @@ class UserPage extends Extension
             $this->theme->display_user_links($page, $user, $ubbe->parts);
         }
         if (
-            ($user->can("view_ip") || ($user->is_logged_in() && $user->id == $event->display_user->id)) && # admin or self-user
+            ($user->can(Permissions::VIEW_IP) || ($user->is_logged_in() && $user->id == $event->display_user->id)) && # admin or self-user
             ($event->display_user->id != $config->get_int('anon_id')) # don't show anon's IP list, it is le huge
         ) {
             $this->theme->display_ip_list(
@@ -309,7 +309,7 @@ class UserPage extends Extension
     {
         global $user;
         $event->add_link("My Profile", make_link("user"));
-        if ($user->can("edit_user_class")) {
+        if ($user->can(Permissions::EDIT_USER_CLASS)) {
             $event->add_link("User List", make_link("user_admin/list"), 98);
         }
         $event->add_link("Log Out", make_link("user_admin/logout"), 99);
@@ -337,7 +337,7 @@ class UserPage extends Extension
         } elseif (preg_match("/^(?:poster|user)_id[=|:]([0-9]+)$/i", $event->term, $matches)) {
             $user_id = int_escape($matches[1]);
             $event->add_querylet(new Querylet("images.owner_id = $user_id"));
-        } elseif ($user->can("view_ip") && preg_match("/^(?:poster|user)_ip[=|:]([0-9\.]+)$/i", $event->term, $matches)) {
+        } elseif ($user->can(Permissions::VIEW_IP) && preg_match("/^(?:poster|user)_ip[=|:]([0-9\.]+)$/i", $event->term, $matches)) {
             $user_ip = $matches[1]; // FIXME: ip_escape?
             $event->add_querylet(new Querylet("images.owner_ip = '$user_ip'"));
         }
@@ -517,8 +517,8 @@ class UserPage extends Extension
 
         if (
             ($a->name == $b->name) ||
-            ($b->can("protected") && $a->class->name == "admin") ||
-            (!$b->can("protected") && $a->can("edit_user_info"))
+            ($b->can(Permissions::PROTECTED) && $a->class->name == "admin") ||
+            (!$b->can(Permissions::PROTECTED) && $a->can(Permissions::EDIT_USER_INFO))
         ) {
             return true;
         } else {
@@ -544,7 +544,7 @@ class UserPage extends Extension
     {
         global $user;
 
-        if ($user->can('edit_user_name') && $this->user_can_edit_user($user, $duser)) {
+        if ($user->can(Permissions::EDIT_USER_NAME) && $this->user_can_edit_user($user, $duser)) {
             $duser->set_name($name);
             flash_message("Username changed");
             // TODO: set login cookie if user changed themselves
@@ -652,7 +652,7 @@ class UserPage extends Extension
         $page->set_heading("Error");
         $page->add_block(new NavBlock());
         
-        if (!$user->can("delete_user")) {
+        if (!$user->can(Permissions::DELETE_USER)) {
             $page->add_block(new Block("Not Admin", "Only admins can delete accounts"));
         } elseif (!isset($_POST['id']) || !is_numeric($_POST['id'])) {
             $page->add_block(new Block(
diff --git a/ext/user/theme.php b/ext/user/theme.php
index 45f6f08f..c773dd0e 100644
--- a/ext/user/theme.php
+++ b/ext/user/theme.php
@@ -26,7 +26,7 @@ class UserPageTheme extends Themelet
 
         $html .= "<tr>";
         $html .= "<td>Name</td>";
-        if ($user->can('delete_user')) {
+        if ($user->can(Permissions::DELETE_USER)) {
             $html .= "<td>Email</td>";
         }
         $html .= "<td>Class</td>";
@@ -39,7 +39,7 @@ class UserPageTheme extends Themelet
 
         $html .= "<tr>" . make_form("user_admin/list", "GET");
         $html .= "<td><input type='text' name='username' value='$h_username'/></td>";
-        if ($user->can('delete_user')) {
+        if ($user->can(Permissions::DELETE_USER)) {
             $html .= "<td><input type='text' name='email' value='$h_email'/></td>";
         }
         $html .= "<td><input type='text' name='class' value='$h_class'/></td>";
@@ -55,7 +55,7 @@ class UserPageTheme extends Themelet
 
             $html .= "<tr>";
             $html .= "<td><a href='$u_link'>$h_name</a></td>";
-            if ($user->can('delete_user')) {
+            if ($user->can(Permissions::DELETE_USER)) {
                 $html .= "<td>$h_email</td>";
             }
             $html .= "<td>$h_class</td>";
@@ -256,7 +256,7 @@ class UserPageTheme extends Themelet
         $html = "";
         if ($duser->id != $config->get_int('anon_id')) {  //justa fool-admin protection so they dont mess around with anon users.
         
-            if ($user->can('edit_user_name')) {
+            if ($user->can(Permissions::EDIT_USER_NAME)) {
                 $html .= "
 				<p>".make_form(make_link("user_admin/change_name"))."
 					<input type='hidden' name='id' value='{$duser->id}'>
@@ -298,7 +298,7 @@ class UserPageTheme extends Themelet
 
             $i_user_id = int_escape($duser->id);
 
-            if ($user->can("edit_user_class")) {
+            if ($user->can(Permissions::EDIT_USER_CLASS)) {
                 global $_shm_user_classes;
                 $class_html = "";
                 foreach ($_shm_user_classes as $name => $values) {
@@ -319,7 +319,7 @@ class UserPageTheme extends Themelet
 				";
             }
 
-            if ($user->can("delete_user")) {
+            if ($user->can(Permissions::DELETE_USER)) {
                 $html .= "
 					<p>".make_form(make_link("user_admin/delete_user"))."
 						<input type='hidden' name='id' value='$i_user_id'>
diff --git a/ext/view/theme.php b/ext/view/theme.php
index 82bd51f4..de1694ac 100644
--- a/ext/view/theme.php
+++ b/ext/view/theme.php
@@ -81,8 +81,8 @@ class ViewImageTheme extends Themelet
             $html .= $part;
         }
         if (
-            (!$image->is_locked() || $user->can("edit_image_lock")) &&
-            $user->can("edit_image_tag")
+            (!$image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK)) &&
+            $user->can(Permissions::EDIT_IMAGE_TAG)
         ) {
             $html .= "
 						<tr><td colspan='4'>
diff --git a/ext/wiki/main.php b/ext/wiki/main.php
index 7279b630..65d29ae0 100644
--- a/ext/wiki/main.php
+++ b/ext/wiki/main.php
@@ -206,7 +206,7 @@ class Wiki extends Extension
         }
 
         // anon / user can edit if allowed by config
-        if ($user->can("edit_wiki_page")) {
+        if ($user->can(Permissions::EDIT_WIKI_PAGE)) {
             return true;
         }
 
diff --git a/themes/danbooru/comment.theme.php b/themes/danbooru/comment.theme.php
index 03e094bc..0b40f418 100644
--- a/themes/danbooru/comment.theme.php
+++ b/themes/danbooru/comment.theme.php
@@ -104,7 +104,7 @@ class CustomCommentListTheme extends CommentListTheme
 
         $h_userlink = "<a class='username' href='".make_link("user/$h_name")."'>$h_name</a>";
         $h_del = "";
-        if ($user->can("delete_comment")) {
+        if ($user->can(Permissions::DELETE_COMMENT)) {
             $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);");
diff --git a/themes/danbooru/view.theme.php b/themes/danbooru/view.theme.php
index f113541a..3b448c2e 100644
--- a/themes/danbooru/view.theme.php
+++ b/themes/danbooru/view.theme.php
@@ -23,7 +23,7 @@ class CustomViewImageTheme extends ViewImageTheme
         $h_filesize = to_shorthand_int($image->filesize);
 
         global $user;
-        if ($user->can("view_ip")) {
+        if ($user->can(Permissions::VIEW_IP)) {
             $h_ownerlink .= " ($h_ip)";
         }
 
diff --git a/themes/danbooru2/comment.theme.php b/themes/danbooru2/comment.theme.php
index f043f445..94d56eda 100644
--- a/themes/danbooru2/comment.theme.php
+++ b/themes/danbooru2/comment.theme.php
@@ -105,7 +105,7 @@ class CustomCommentListTheme extends CommentListTheme
 
         $h_userlink = "<a class='username' href='".make_link("user/$h_name")."'>$h_name</a>";
         $h_del = "";
-        if ($user->can("delete_comment")) {
+        if ($user->can(Permissions::DELETE_COMMENT)) {
             $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);");
diff --git a/themes/danbooru2/view.theme.php b/themes/danbooru2/view.theme.php
index 237f1a8c..3e588307 100644
--- a/themes/danbooru2/view.theme.php
+++ b/themes/danbooru2/view.theme.php
@@ -22,7 +22,7 @@ class CustomViewImageTheme extends ViewImageTheme
         $h_filesize = to_shorthand_int($image->filesize);
 
         global $user;
-        if ($user->can("view_ip")) {
+        if ($user->can(Permissions::VIEW_IP)) {
             $h_ownerlink .= " ($h_ip)";
         }
 
diff --git a/themes/futaba/comment.theme.php b/themes/futaba/comment.theme.php
index 2fd0a6ad..7647c322 100644
--- a/themes/futaba/comment.theme.php
+++ b/themes/futaba/comment.theme.php
@@ -78,7 +78,7 @@ class CustomCommentListTheme extends CommentListTheme
         $h_userlink = "<a href='".make_link("user/$h_name")."'>$h_name</a>";
         $h_date = $comment->posted;
         $h_del = "";
-        if ($user->can("delete_comment")) {
+        if ($user->can(Permissions::DELETE_COMMENT)) {
             $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);");
diff --git a/themes/lite/view.theme.php b/themes/lite/view.theme.php
index a4613467..aea9b028 100644
--- a/themes/lite/view.theme.php
+++ b/themes/lite/view.theme.php
@@ -23,7 +23,7 @@ class CustomViewImageTheme extends ViewImageTheme
         $h_filesize = to_shorthand_int($image->filesize);
 
         global $user;
-        if ($user->can("view_ip")) {
+        if ($user->can(Permissions::VIEW_IP)) {
             $h_ownerlink .= " ($h_ip)";
         }
 
diff --git a/themes/material/view.theme.php b/themes/material/view.theme.php
index eafdf3f4..71edf7c6 100644
--- a/themes/material/view.theme.php
+++ b/themes/material/view.theme.php
@@ -57,8 +57,8 @@ class CustomViewImageTheme extends ViewImageTheme
             $html .= $part;
         }
         if (
-            (!$image->is_locked() || $user->can("edit_image_lock")) &&
-            $user->can("edit_image_tag")
+            (!$image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK)) &&
+            $user->can(Permissions::EDIT_IMAGE_TAG)
         ) {
             $html .= "
   						<tr><td colspan='4'>

From 45df025e7d8ea32ed466a6a257e9350b39acdc4e Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Sat, 13 Jul 2019 17:39:27 -0500
Subject: [PATCH 02/10] Bulk action permission constant

---
 core/permissions.php      | 2 ++
 core/userclass.php        | 4 +++-
 ext/bulk_actions/main.php | 2 +-
 3 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/core/permissions.php b/core/permissions.php
index f3ab7c6b..a78af388 100644
--- a/core/permissions.php
+++ b/core/permissions.php
@@ -64,4 +64,6 @@ abstract class Permissions
 
     public const VIEW_TRASH = "view_trash";
 
+    public const PERFORM_BULK_ACTIONS = "perform_bulk_actions";
+
 }
\ No newline at end of file
diff --git a/core/userclass.php b/core/userclass.php
index 4ae2511b..d2d06c0a 100644
--- a/core/userclass.php
+++ b/core/userclass.php
@@ -133,6 +133,8 @@ new UserClass("base", null, [
     Permissions::BULK_EDIT_IMAGE_RATING => false,
 
     Permissions::VIEW_TRASH => false,
+
+    Permissions::PERFORM_BULK_ACTIONS => false,
 ]);
 
 new UserClass("anonymous", "base", [
@@ -194,7 +196,7 @@ new UserClass("admin", "base", [
     Permissions::EDIT_IMAGE_RATING => true,
     Permissions::BULK_EDIT_IMAGE_RATING => true,
     Permissions::VIEW_TRASH => true,
-
+    Permissions::PERFORM_BULK_ACTIONS => true,
 ]);
 
 new UserClass("hellbanned", "user", [
diff --git a/ext/bulk_actions/main.php b/ext/bulk_actions/main.php
index 67f652e2..0bfaf1da 100644
--- a/ext/bulk_actions/main.php
+++ b/ext/bulk_actions/main.php
@@ -147,7 +147,7 @@ class BulkActions extends Extension
     public function onPageRequest(PageRequestEvent $event)
     {
         global $page, $user;
-        if ($event->page_matches("bulk_action") && $user->can("perform_bulk_actions")) {
+        if ($event->page_matches("bulk_action") && $user->can(Permissions::PERFORM_BULK_ACTIONS)) {
             if (!isset($_POST['bulk_action'])) {
                 return;
             }

From c3f2d2e1bd4dc9fc28c6b0616f1ad914f1cee778 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Tue, 9 Jul 2019 09:23:46 -0500
Subject: [PATCH 03/10] New post titles extension, resolves #19

---
 core/permissions.php                          |  1 +
 core/userclass.php                            |  3 +
 ext/post_titles/config.php                    |  8 ++
 .../events/post_title_set_event.php           |  1 +
 ext/post_titles/main.php                      | 85 +++++++++++++++++++
 ext/post_titles/theme.php                     | 25 ++++++
 6 files changed, 123 insertions(+)
 create mode 100644 ext/post_titles/config.php
 create mode 100644 ext/post_titles/events/post_title_set_event.php
 create mode 100644 ext/post_titles/main.php
 create mode 100644 ext/post_titles/theme.php

diff --git a/core/permissions.php b/core/permissions.php
index a78af388..4378a9c0 100644
--- a/core/permissions.php
+++ b/core/permissions.php
@@ -29,6 +29,7 @@ abstract class Permissions
     public const EDIT_IMAGE_SOURCE = "edit_image_source";
     public const EDIT_IMAGE_OWNER = "edit_image_owner";
     public const EDIT_IMAGE_LOCK = "edit_image_lock";
+    public const EDIT_IMAGE_TITLE = "edit_image_title";
     public const BULK_EDIT_IMAGE_TAG = "bulk_edit_image_tag";
     public const BULK_EDIT_IMAGE_SOURCE = "bulk_edit_image_source";
     public const DELETE_IMAGE = "delete_image";
diff --git a/core/userclass.php b/core/userclass.php
index d2d06c0a..a66ad6f7 100644
--- a/core/userclass.php
+++ b/core/userclass.php
@@ -99,6 +99,7 @@ new UserClass("base", null, [
     Permissions::EDIT_IMAGE_SOURCE => false,
     Permissions::EDIT_IMAGE_OWNER => false,
     Permissions::EDIT_IMAGE_LOCK => false,
+    Permissions::EDIT_IMAGE_TITLE => false,
     Permissions::BULK_EDIT_IMAGE_TAG => false,
     Permissions::BULK_EDIT_IMAGE_SOURCE => false,
     Permissions::DELETE_IMAGE => false,
@@ -146,6 +147,7 @@ new UserClass("user", "base", [
     Permissions::CREATE_COMMENT => true,
     Permissions::EDIT_IMAGE_TAG => true,
     Permissions::EDIT_IMAGE_SOURCE => true,
+    Permissions::EDIT_IMAGE_TITLE => true,
     Permissions::CREATE_IMAGE_REPORT => true,
     Permissions::EDIT_IMAGE_RATING => true,
 
@@ -175,6 +177,7 @@ new UserClass("admin", "base", [
     Permissions::EDIT_IMAGE_TAG => true,
     Permissions::EDIT_IMAGE_SOURCE => true,
     Permissions::EDIT_IMAGE_OWNER => true,
+    Permissions::EDIT_IMAGE_TITLE => true,
     Permissions::BULK_EDIT_IMAGE_TAG => true,
     Permissions::BULK_EDIT_IMAGE_SOURCE => true,
     Permissions::MASS_TAG_EDIT => true,
diff --git a/ext/post_titles/config.php b/ext/post_titles/config.php
new file mode 100644
index 00000000..47156d0d
--- /dev/null
+++ b/ext/post_titles/config.php
@@ -0,0 +1,8 @@
+<?php
+
+
+abstract class PostTitlesConfig
+{
+    public const VERSION = "ext_post_titles_version";
+    public const DEFAULT_TO_FILENAME = "post_titles_default_to_filename";
+}
\ No newline at end of file
diff --git a/ext/post_titles/events/post_title_set_event.php b/ext/post_titles/events/post_title_set_event.php
new file mode 100644
index 00000000..b3d9bbc7
--- /dev/null
+++ b/ext/post_titles/events/post_title_set_event.php
@@ -0,0 +1 @@
+<?php
diff --git a/ext/post_titles/main.php b/ext/post_titles/main.php
new file mode 100644
index 00000000..654d581d
--- /dev/null
+++ b/ext/post_titles/main.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Name: Post Titles
+ * Author: Matthew Barbour <matthew@darkholme.net>
+ * License: MIT
+ * Description: Add titles to media posts
+ */
+
+require_once "config.php";
+require_once "events/post_title_set_event.php";
+
+class PostTitles extends Extension
+{
+    public function onInitExt(InitExtEvent $event)
+    {
+        global $config, $database;
+
+        $config->set_default_bool(PostTitlesConfig::DEFAULT_TO_FILENAME, false);
+
+        if ($config->get_int(PostTitlesConfig::VERSION) < 1) {
+            $this->install();
+        }
+    }
+
+    private function install()
+    {
+        global $config, $database;
+
+        if ($config->get_int(PostTitlesConfig::VERSION) < 1) {
+            $database->Execute("ALTER TABLE images ADD COLUMN title varchar(255) NULL");
+            $config->set_int(PostTitlesConfig::VERSION, 1);
+        }
+    }
+
+    public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event)
+    {
+        global $user;
+
+        $event->add_part($this->theme->get_title_set_html(self::get_title($event->image), $user->can(Permissions::EDIT_IMAGE_TITLE)), 10);
+    }
+
+    public function onImageInfoSet(ImageInfoSetEvent $event)
+    {
+        global $user;
+
+        if ($user->can(Permissions::EDIT_IMAGE_TITLE) && isset($_POST["post_title"])) {
+            $title = $_POST["post_title"];
+            send_event(new PostTitleSetEvent($event->image, $title));
+        }
+    }
+
+    public function onPostTitleSet(PostTitleSetEvent $event)
+    {
+        $this->set_title($event->image->id, $event->title);
+    }
+
+    public function onSetupBuilding(SetupBuildingEvent $event)
+    {
+        $sb = new SetupBlock("Post Titles");
+        $sb->start_table();
+        $sb->add_bool_option(PostTitlesConfig::DEFAULT_TO_FILENAME,"Default to filename", true);
+        $sb->end_table();
+
+        $event->panel->add_block($sb);
+    }
+
+    private function set_title(int $image_id, string $title)
+    {
+        global $database;
+        $database->Execute("UPDATE images SET title=? WHERE id=?", [$title, $image_id]);
+        log_info("post_titles", "Title for Image #{$image_id} set to: ".$title);
+    }
+
+    public static function get_title(Image $image): string
+    {
+        global $config;
+
+        $title = $image->title??"";
+        if(empty($title) && $config->get_bool(PostTitlesConfig::DEFAULT_TO_FILENAME)) {
+            $info = pathinfo($image->filename);
+            $title =  basename($image->filename,'.'.$info['extension']);
+        }
+        return $title;
+    }
+}
diff --git a/ext/post_titles/theme.php b/ext/post_titles/theme.php
new file mode 100644
index 00000000..53a6c1b5
--- /dev/null
+++ b/ext/post_titles/theme.php
@@ -0,0 +1,25 @@
+<?php
+class PostTitlesTheme extends Themelet
+{
+    public function get_title_set_html(string $title, bool $can_set): string
+    {
+
+
+        $html = "
+			<tr>
+				<th>Title</th>
+				<td>
+		".($can_set ? "
+					<span class='view'>".html_escape($title)."</span>
+					<span class='edit'>
+						<input class='edit'  type='text' name='post_title' value='".html_escape($title)."' />
+					</span>
+		" : html_escape("
+					 $title
+		"))."
+				</td>
+			</tr>
+		";
+        return $html;
+    }
+}

From dd6c3b232149046c768fad400bff93207cf9fcdf Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Sat, 13 Jul 2019 17:12:03 -0500
Subject: [PATCH 04/10] Added window title option to post title extension

---
 ext/post_titles/config.php                    |  1 +
 .../events/post_title_set_event.php           | 12 +++
 ext/post_titles/main.php                      | 24 +++++-
 ext/view/events/displaying_image_event.php    | 23 +++++
 .../image_admin_block_building_event.php      | 25 ++++++
 .../events/image_info_box_building_event.php  | 25 ++++++
 ext/view/events/image_info_set_event.php      | 12 +++
 ext/view/main.php                             | 83 +++----------------
 ext/view/theme.php                            |  2 -
 themes/danbooru/view.theme.php                |  1 -
 themes/danbooru2/view.theme.php               |  1 -
 themes/futaba/view.theme.php                  |  1 -
 themes/lite/view.theme.php                    |  1 -
 themes/material/view.theme.php                |  1 -
 14 files changed, 131 insertions(+), 81 deletions(-)
 create mode 100644 ext/view/events/displaying_image_event.php
 create mode 100644 ext/view/events/image_admin_block_building_event.php
 create mode 100644 ext/view/events/image_info_box_building_event.php
 create mode 100644 ext/view/events/image_info_set_event.php

diff --git a/ext/post_titles/config.php b/ext/post_titles/config.php
index 47156d0d..5499bfad 100644
--- a/ext/post_titles/config.php
+++ b/ext/post_titles/config.php
@@ -5,4 +5,5 @@ abstract class PostTitlesConfig
 {
     public const VERSION = "ext_post_titles_version";
     public const DEFAULT_TO_FILENAME = "post_titles_default_to_filename";
+    public const SHOW_IN_WINDOW_TITLE = "post_titles_show_in_window_title";
 }
\ No newline at end of file
diff --git a/ext/post_titles/events/post_title_set_event.php b/ext/post_titles/events/post_title_set_event.php
index b3d9bbc7..57f942f7 100644
--- a/ext/post_titles/events/post_title_set_event.php
+++ b/ext/post_titles/events/post_title_set_event.php
@@ -1 +1,13 @@
 <?php
+
+class PostTitleSetEvent extends Event
+{
+    public $image;
+    public $title;
+
+    public function __construct(Image $image, String $title)
+    {
+        $this->image = $image;
+        $this->title = $title;
+    }
+}
\ No newline at end of file
diff --git a/ext/post_titles/main.php b/ext/post_titles/main.php
index 654d581d..50f550c3 100644
--- a/ext/post_titles/main.php
+++ b/ext/post_titles/main.php
@@ -11,11 +11,17 @@ require_once "events/post_title_set_event.php";
 
 class PostTitles extends Extension
 {
+    public function get_priority(): int
+    {
+        return 60;
+    }
+
     public function onInitExt(InitExtEvent $event)
     {
         global $config, $database;
 
         $config->set_default_bool(PostTitlesConfig::DEFAULT_TO_FILENAME, false);
+        $config->set_default_bool(PostTitlesConfig::SHOW_IN_WINDOW_TITLE, false);
 
         if ($config->get_int(PostTitlesConfig::VERSION) < 1) {
             $this->install();
@@ -32,6 +38,15 @@ class PostTitles extends Extension
         }
     }
 
+    public function onDisplayingImage(DisplayingImageEvent $event)
+    {
+        global $config;
+
+        if($config->get_bool(PostTitlesConfig::SHOW_IN_WINDOW_TITLE)) {
+            $event->set_title(self::get_title($event->get_image()));
+        }
+    }
+
     public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event)
     {
         global $user;
@@ -59,11 +74,14 @@ class PostTitles extends Extension
         $sb = new SetupBlock("Post Titles");
         $sb->start_table();
         $sb->add_bool_option(PostTitlesConfig::DEFAULT_TO_FILENAME,"Default to filename", true);
+        $sb->add_bool_option(PostTitlesConfig::SHOW_IN_WINDOW_TITLE,"Show in window title", true);
         $sb->end_table();
 
         $event->panel->add_block($sb);
     }
 
+
+
     private function set_title(int $image_id, string $title)
     {
         global $database;
@@ -78,7 +96,11 @@ class PostTitles extends Extension
         $title = $image->title??"";
         if(empty($title) && $config->get_bool(PostTitlesConfig::DEFAULT_TO_FILENAME)) {
             $info = pathinfo($image->filename);
-            $title =  basename($image->filename,'.'.$info['extension']);
+            if(array_key_exists("extension",$info)) {
+                $title = basename($image->filename, '.' . $info['extension']);
+            } else {
+                $title = $image->filename;
+            }
         }
         return $title;
     }
diff --git a/ext/view/events/displaying_image_event.php b/ext/view/events/displaying_image_event.php
new file mode 100644
index 00000000..3ac3090f
--- /dev/null
+++ b/ext/view/events/displaying_image_event.php
@@ -0,0 +1,23 @@
+<?php
+
+class DisplayingImageEvent extends Event
+{
+    /** @var Image  */
+    public $image;
+
+    public $title;
+
+    public function __construct(Image $image)
+    {
+        $this->image = $image;
+    }
+
+    public function get_image(): Image
+    {
+        return $this->image;
+    }
+
+    public function set_title(String $title) {
+        $this->title = $title;
+    }
+}
\ No newline at end of file
diff --git a/ext/view/events/image_admin_block_building_event.php b/ext/view/events/image_admin_block_building_event.php
new file mode 100644
index 00000000..0211065a
--- /dev/null
+++ b/ext/view/events/image_admin_block_building_event.php
@@ -0,0 +1,25 @@
+<?php
+
+class ImageAdminBlockBuildingEvent extends Event
+{
+    /** @var string[] */
+    public $parts = [];
+    /** @var ?Image  */
+    public $image = null;
+    /** @var ?User  */
+    public $user = null;
+
+    public function __construct(Image $image, User $user)
+    {
+        $this->image = $image;
+        $this->user = $user;
+    }
+
+    public function add_part(string $html, int $position=50)
+    {
+        while (isset($this->parts[$position])) {
+            $position++;
+        }
+        $this->parts[$position] = $html;
+    }
+}
\ No newline at end of file
diff --git a/ext/view/events/image_info_box_building_event.php b/ext/view/events/image_info_box_building_event.php
new file mode 100644
index 00000000..cb626349
--- /dev/null
+++ b/ext/view/events/image_info_box_building_event.php
@@ -0,0 +1,25 @@
+<?php
+
+class ImageInfoBoxBuildingEvent extends Event
+{
+    /** @var array  */
+    public $parts = [];
+    /** @var Image  */
+    public $image;
+    /** @var User  */
+    public $user;
+
+    public function __construct(Image $image, User $user)
+    {
+        $this->image = $image;
+        $this->user = $user;
+    }
+
+    public function add_part(string $html, int $position=50)
+    {
+        while (isset($this->parts[$position])) {
+            $position++;
+        }
+        $this->parts[$position] = $html;
+    }
+}
\ No newline at end of file
diff --git a/ext/view/events/image_info_set_event.php b/ext/view/events/image_info_set_event.php
new file mode 100644
index 00000000..e6d77d59
--- /dev/null
+++ b/ext/view/events/image_info_set_event.php
@@ -0,0 +1,12 @@
+<?php
+
+class ImageInfoSetEvent extends Event
+{
+    /** @var Image */
+    public $image;
+
+    public function __construct(Image $image)
+    {
+        $this->image = $image;
+    }
+}
\ No newline at end of file
diff --git a/ext/view/main.php b/ext/view/main.php
index bb2ee8c6..11c9f74e 100644
--- a/ext/view/main.php
+++ b/ext/view/main.php
@@ -14,80 +14,12 @@
  * wish to appear on the "view" page should listen for this,
  * which only appears when an image actually exists.
  */
-class DisplayingImageEvent extends Event
-{
-    /** @var Image  */
-    public $image;
 
-    public function __construct(Image $image)
-    {
-        $this->image = $image;
-    }
+require_once "events/displaying_image_event.php";
+require_once "events/image_info_box_building_event.php";
+require_once "events/image_info_set_event.php";
+require_once "events/image_admin_block_building_event.php";
 
-    public function get_image(): Image
-    {
-        return $this->image;
-    }
-}
-
-class ImageInfoBoxBuildingEvent extends Event
-{
-    /** @var array  */
-    public $parts = [];
-    /** @var Image  */
-    public $image;
-    /** @var User  */
-    public $user;
-
-    public function __construct(Image $image, User $user)
-    {
-        $this->image = $image;
-        $this->user = $user;
-    }
-
-    public function add_part(string $html, int $position=50)
-    {
-        while (isset($this->parts[$position])) {
-            $position++;
-        }
-        $this->parts[$position] = $html;
-    }
-}
-
-class ImageInfoSetEvent extends Event
-{
-    /** @var Image */
-    public $image;
-
-    public function __construct(Image $image)
-    {
-        $this->image = $image;
-    }
-}
-
-class ImageAdminBlockBuildingEvent extends Event
-{
-    /** @var string[] */
-    public $parts = [];
-    /** @var ?Image  */
-    public $image = null;
-    /** @var ?User  */
-    public $user = null;
-
-    public function __construct(Image $image, User $user)
-    {
-        $this->image = $image;
-        $this->user = $user;
-    }
-
-    public function add_part(string $html, int $position=50)
-    {
-        while (isset($this->parts[$position])) {
-            $position++;
-        }
-        $this->parts[$position] = $html;
-    }
-}
 
 class ViewImage extends Extension
 {
@@ -140,7 +72,9 @@ class ViewImage extends Extension
             $image = Image::by_id($image_id);
 
             if (!is_null($image)) {
-                send_event(new DisplayingImageEvent($image));
+                $die = new DisplayingImageEvent($image);
+                send_event($die);
+                $page->set_title(html_escape($die->title));
                 $iabbe = new ImageAdminBlockBuildingEvent($image, $user);
                 send_event($iabbe);
                 ksort($iabbe->parts);
@@ -169,6 +103,9 @@ class ViewImage extends Extension
         send_event($iibbe);
         ksort($iibbe->parts);
         $this->theme->display_meta_headers($event->get_image());
+
+        $event->title = "Image {$event->get_image()->id}: ".$event->get_image()->get_tag_list();
+
         $this->theme->display_page($event->get_image(), $iibbe->parts);
     }
 }
diff --git a/ext/view/theme.php b/ext/view/theme.php
index de1694ac..ba085159 100644
--- a/ext/view/theme.php
+++ b/ext/view/theme.php
@@ -20,8 +20,6 @@ class ViewImageTheme extends Themelet
     public function display_page(Image $image, $editor_parts)
     {
         global $page;
-
-        $page->set_title("Image {$image->id}: ".html_escape($image->get_tag_list()));
         $page->set_heading(html_escape($image->get_tag_list()));
         $page->add_block(new Block("Navigation", $this->build_navigation($image), "left", 0));
         $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "main", 20));
diff --git a/themes/danbooru/view.theme.php b/themes/danbooru/view.theme.php
index 3b448c2e..51adbe1b 100644
--- a/themes/danbooru/view.theme.php
+++ b/themes/danbooru/view.theme.php
@@ -5,7 +5,6 @@ class CustomViewImageTheme extends ViewImageTheme
     public function display_page(Image $image, $editor_parts)
     {
         global $page;
-        $page->set_title("Image {$image->id}: ".html_escape($image->get_tag_list()));
         $page->set_heading(html_escape($image->get_tag_list()));
         $page->add_block(new Block("Navigation", $this->build_navigation($image), "left", 0));
         $page->add_block(new Block("Statistics", $this->build_stats($image), "left", 15));
diff --git a/themes/danbooru2/view.theme.php b/themes/danbooru2/view.theme.php
index 3e588307..7f3439b5 100644
--- a/themes/danbooru2/view.theme.php
+++ b/themes/danbooru2/view.theme.php
@@ -5,7 +5,6 @@ class CustomViewImageTheme extends ViewImageTheme
     public function display_page(Image $image, $editor_parts)
     {
         global $page;
-        $page->set_title("Image {$image->id}: ".html_escape($image->get_tag_list()));
         $page->set_heading(html_escape($image->get_tag_list()));
         $page->add_block(new Block("Search", $this->build_navigation($image), "left", 0));
         $page->add_block(new Block("Information", $this->build_information($image), "left", 15));
diff --git a/themes/futaba/view.theme.php b/themes/futaba/view.theme.php
index feb06743..4bbba64a 100644
--- a/themes/futaba/view.theme.php
+++ b/themes/futaba/view.theme.php
@@ -5,7 +5,6 @@ class CustomViewImageTheme extends ViewImageTheme
     public function display_page(Image $image, $editor_parts)
     {
         global $page;
-        $page->set_title("Image {$image->id}: ".html_escape($image->get_tag_list()));
         $page->set_heading(html_escape($image->get_tag_list()));
         $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "main", 10));
     }
diff --git a/themes/lite/view.theme.php b/themes/lite/view.theme.php
index aea9b028..f9ce414f 100644
--- a/themes/lite/view.theme.php
+++ b/themes/lite/view.theme.php
@@ -5,7 +5,6 @@ class CustomViewImageTheme extends ViewImageTheme
     public function display_page(Image $image, $editor_parts)
     {
         global $page;
-        $page->set_title("Image {$image->id}: ".html_escape($image->get_tag_list()));
         $page->set_heading(html_escape($image->get_tag_list()));
         $page->add_block(new Block("Navigation", $this->build_navigation($image), "left", 0));
         $page->add_block(new Block("Statistics", $this->build_stats($image), "left", 15));
diff --git a/themes/material/view.theme.php b/themes/material/view.theme.php
index 71edf7c6..601faf94 100644
--- a/themes/material/view.theme.php
+++ b/themes/material/view.theme.php
@@ -8,7 +8,6 @@ class CustomViewImageTheme extends ViewImageTheme
     public function display_page(Image $image, $editor_parts)
     {
         global $page;
-        $page->set_title("Image {$image->id}: ".html_escape($image->get_tag_list()));
         $page->set_heading(html_escape($image->get_tag_list()));
         $page->add_block(new Block(null, $this->build_pin($image), "subtoolbar", 0));
         $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "left", 20));

From 7d110f11b6d9c1af0e81d873ddb314d3c743b689 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Sat, 13 Jul 2019 17:19:22 -0500
Subject: [PATCH 05/10] TagCategories config constant

---
 ext/tag_categories/config.php | 8 ++++++++
 ext/tag_categories/main.php   | 8 +++++---
 ext/tag_list/main.php         | 2 +-
 3 files changed, 14 insertions(+), 4 deletions(-)
 create mode 100644 ext/tag_categories/config.php

diff --git a/ext/tag_categories/config.php b/ext/tag_categories/config.php
new file mode 100644
index 00000000..ee6b6637
--- /dev/null
+++ b/ext/tag_categories/config.php
@@ -0,0 +1,8 @@
+<?php
+
+abstract class TagCategoriesConfig
+{
+    public const VERSION = "ext_tag_categories_version";
+
+    public const SPLIT_ON_VIEW = "tag_categories_split_on_view";
+}
\ No newline at end of file
diff --git a/ext/tag_categories/main.php b/ext/tag_categories/main.php
index 682455af..3002278b 100644
--- a/ext/tag_categories/main.php
+++ b/ext/tag_categories/main.php
@@ -6,6 +6,8 @@
  * Description: Let tags be split into 'categories', like Danbooru's tagging
  */
 
+require_once "config.php";
+
 class TagCategories extends Extension
 {
     public function onInitExt(InitExtEvent $event)
@@ -14,9 +16,9 @@ class TagCategories extends Extension
         
         // whether we split out separate categories on post view by default
         //  note: only takes effect if /post/view shows the image's exact tags
-        $config->set_default_bool("tag_categories_split_on_view", true);
+        $config->set_default_bool(TagCategoriesConfig::SPLIT_ON_VIEW, true);
 
-        if ($config->get_int("ext_tag_categories_version") < 1) {
+        if ($config->get_int(TagCategoriesConfig::VERSION) < 1) {
             // primary extension database, holds all our stuff!
             $database->create_table(
                 'image_tag_categories',
@@ -26,7 +28,7 @@ class TagCategories extends Extension
 				color VARCHAR(7)'
             );
 
-            $config->set_int("ext_tag_categories_version", 1);
+            $config->set_int(TagCategoriesConfig::VERSION, 1);
 
             log_info("tag_categories", "extension installed");
         }
diff --git a/ext/tag_list/main.php b/ext/tag_list/main.php
index ac5484a8..ec2a4032 100644
--- a/ext/tag_list/main.php
+++ b/ext/tag_list/main.php
@@ -100,7 +100,7 @@ class TagList extends Extension
             if ($config->get_string('tag_list_image_type') == 'related') {
                 $this->add_related_block($page, $event->image);
             } else {
-                if (class_exists("TagCategories") and $config->get_bool('tag_categories_split_on_view')) {
+                if (class_exists("TagCategories") and $config->get_bool(TagCategoriesConfig::SPLIT_ON_VIEW)) {
                     $this->add_split_tags_block($page, $event->image);
                 } else {
                     $this->add_tags_block($page, $event->image);

From fc294bfb3ca23b12d5d381db5650aff9f46fac18 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Wed, 31 Jul 2019 08:52:17 -0500
Subject: [PATCH 06/10] add tracer_enabled check to the database class to
 prevent unnecessary memory build-up when tracer isn't outputting. Globalized
 tracer_enabled to make it easier to access

---
 core/_bootstrap.php | 5 +++++
 core/database.php   | 8 +++++---
 core/send_event.php | 4 ++--
 3 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/core/_bootstrap.php b/core/_bootstrap.php
index 6dffbf14..a003ed6e 100644
--- a/core/_bootstrap.php
+++ b/core/_bootstrap.php
@@ -15,6 +15,11 @@ require_once "vendor/autoload.php";
 _version_check();
 _sanitise_environment();
 
+// The trace system has a certain amount of memory consumption every time it is used,
+// so to prevent running out of memory during complex operations code that uses it should
+// check if tracer output is enabled before making use of it.
+$tracer_enabled = constant('TRACE_FILE')!==null;
+
 // load base files
 $_tracer->begin("Bootstrap");
 $_tracer->begin("Opening files");
diff --git a/core/database.php b/core/database.php
index 003816d4..2cbf077f 100644
--- a/core/database.php
+++ b/core/database.php
@@ -190,10 +190,12 @@ class Database
 
     private function count_time(string $method, float $start, string $query, ?array $args): void
     {
-		global $_tracer;
+		global $_tracer, $tracer_enabled;
 		$dur = microtime(true) - $start;
-		$query = trim(preg_replace('/^[\t ]+/m', '', $query));  // trim leading whitespace
-		$_tracer->complete($start * 1000000, $dur * 1000000, "DB Query", ["query"=>$query, "args"=>$args, "method"=>$method]);
+        if($tracer_enabled) {
+            $query = trim(preg_replace('/^[\t ]+/m', '', $query));  // trim leading whitespace
+            $_tracer->complete($start * 1000000, $dur * 1000000, "DB Query", ["query"=>$query, "args"=>$args, "method"=>$method]);
+        }
 		$this->query_count++;
         $this->dbtime += $dur;
     }
diff --git a/core/send_event.php b/core/send_event.php
index ae2c42f3..24897658 100644
--- a/core/send_event.php
+++ b/core/send_event.php
@@ -108,6 +108,8 @@ $_shm_event_count = 0;
  */
 function send_event(Event $event): void
 {
+    global $tracer_enabled;
+    
     global $_shm_event_listeners, $_shm_event_count, $_tracer;
     if (!isset($_shm_event_listeners[get_class($event)])) {
         return;
@@ -116,8 +118,6 @@ function send_event(Event $event): void
 
     // send_event() is performance sensitive, and with the number
     // of times tracer gets called the time starts to add up
-    $tracer_enabled = constant('TRACE_FILE');
-
     if ($tracer_enabled) $_tracer->begin(get_class($event));
     // SHIT: http://bugs.php.net/bug.php?id=35106
     $my_event_listeners = $_shm_event_listeners[get_class($event)];

From 5ceb6f419311e27409bb8908e2fc33583eed287e Mon Sep 17 00:00:00 2001
From: Matthew Barbour <sanmadjack@users.noreply.github.com>
Date: Thu, 1 Aug 2019 08:40:15 -0500
Subject: [PATCH 07/10] Update main.php

---
 ext/alias_editor/main.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/ext/alias_editor/main.php b/ext/alias_editor/main.php
index 5e8e4c11..4321c78a 100644
--- a/ext/alias_editor/main.php
+++ b/ext/alias_editor/main.php
@@ -36,7 +36,7 @@ class AliasEditor extends Extension
 
         if ($event->page_matches("alias")) {
             if ($event->get_arg(0) == "add") {
-                if ($user->can(Permissions::MANAGE_ALIAS_LIST))) {
+                if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
                     if (isset($_POST['oldtag']) && isset($_POST['newtag'])) {
                         try {
                             $aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']);
@@ -49,7 +49,7 @@ class AliasEditor extends Extension
                     }
                 }
             } elseif ($event->get_arg(0) == "remove") {
-                if ($user->can(Permissions::MANAGE_ALIAS_LIST))) {
+                if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
                     if (isset($_POST['oldtag'])) {
                         $database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", ["oldtag" => $_POST['oldtag']]);
                         log_info("alias_editor", "Deleted alias for ".$_POST['oldtag'], "Deleted alias");

From 972b68bdd3d0b18a137105b3b1ce54e2052bd539 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Fri, 2 Aug 2019 14:40:03 -0500
Subject: [PATCH 08/10] Setup constants

---
 core/event.php                    |  2 +-
 core/imageboard/image.php         |  2 +-
 core/page.php                     |  2 +-
 core/urls.php                     |  2 +-
 core/util.php                     |  2 +-
 ext/browser_search/main.php       |  4 ++--
 ext/custom_html_headers/main.php  |  2 +-
 ext/downtime/theme.php            |  2 +-
 ext/et/main.php                   |  4 ++--
 ext/handle_static/main.php        |  2 +-
 ext/home/main.php                 |  6 +++---
 ext/index/theme.php               |  2 +-
 ext/numeric_score/theme.php       |  2 +-
 ext/rss_comments/main.php         |  4 ++--
 ext/rss_images/main.php           |  4 ++--
 ext/setup/config.php              | 12 ++++++++++++
 ext/setup/main.php                | 21 +++++++++++----------
 ext/sitemap/main.php              |  2 +-
 ext/upload/theme.php              |  4 ++--
 themes/danbooru/index.theme.php   |  2 +-
 themes/danbooru/layout.class.php  |  7 +++----
 themes/danbooru2/index.theme.php  |  2 +-
 themes/danbooru2/layout.class.php |  7 +++----
 themes/default/layout.class.php   |  2 +-
 themes/futaba/comment.theme.php   |  2 +-
 themes/futaba/layout.class.php    |  2 +-
 themes/lite/layout.class.php      |  4 ++--
 themes/material/layout.class.php  |  6 +++---
 themes/warm/layout.class.php      |  6 +++---
 29 files changed, 66 insertions(+), 55 deletions(-)
 create mode 100644 ext/setup/config.php

diff --git a/core/event.php b/core/event.php
index 349a6ce0..c9064c3e 100644
--- a/core/event.php
+++ b/core/event.php
@@ -60,7 +60,7 @@ class PageRequestEvent extends Event
 
         // if path is not specified, use the default front page
         if (empty($path)) {   /* empty is faster than strlen */
-            $path = $config->get_string('front_page');
+            $path = $config->get_string(SetupConfig::FRONT_PAGE);
         }
 
         // break the path into parts
diff --git a/core/imageboard/image.php b/core/imageboard/image.php
index 3e2529b8..598a2ab7 100644
--- a/core/imageboard/image.php
+++ b/core/imageboard/image.php
@@ -813,7 +813,7 @@ class Image
         $tmpl = str_replace('$size', "{$this->width}x{$this->height}", $tmpl);
         $tmpl = str_replace('$filesize', to_shorthand_int($this->filesize), $tmpl);
         $tmpl = str_replace('$filename', $_escape($base_fname), $tmpl);
-        $tmpl = str_replace('$title', $_escape($config->get_string("title")), $tmpl);
+        $tmpl = str_replace('$title', $_escape($config->get_string(SetupConfig::TITLE)), $tmpl);
         $tmpl = str_replace('$date', $_escape(autodate($this->posted, false)), $tmpl);
 
         // nothing seems to use this, sending the event out to 50 exts is a lot of overhead
diff --git a/core/page.php b/core/page.php
index e31ed7d3..e779290a 100644
--- a/core/page.php
+++ b/core/page.php
@@ -405,7 +405,7 @@ class Page
         global $config;
 
         $data_href = get_base_href();
-        $theme_name = $config->get_string('theme', 'default');
+        $theme_name = $config->get_string(SetupConfig::THEME, 'default');
 
         $this->add_html_header("<script type='text/javascript'>base_href = '$data_href';</script>", 40);
 
diff --git a/core/urls.php b/core/urls.php
index e4fc0977..3bdc9c71 100644
--- a/core/urls.php
+++ b/core/urls.php
@@ -14,7 +14,7 @@ function make_link(?string $page=null, ?string $query=null): string
     global $config;
 
     if (is_null($page)) {
-        $page = $config->get_string('main_page');
+        $page = $config->get_string(SetupConfig::MAIN_PAGE);
     }
 
     if (!is_null(BASE_URL)) {
diff --git a/core/util.php b/core/util.php
index aa310c45..7017078d 100644
--- a/core/util.php
+++ b/core/util.php
@@ -16,7 +16,7 @@ function mtimefile(string $file): string
 function get_theme(): string
 {
     global $config;
-    $theme = $config->get_string("theme", "default");
+    $theme = $config->get_string(SetupConfig::THEME, "default");
     if (!file_exists("themes/$theme")) {
         $theme = "default";
     }
diff --git a/ext/browser_search/main.php b/ext/browser_search/main.php
index 7c1fcc82..301ca0db 100644
--- a/ext/browser_search/main.php
+++ b/ext/browser_search/main.php
@@ -27,14 +27,14 @@ class BrowserSearch extends Extension
 
         // Add in header code to let the browser know that the search plugin exists
         // We need to build the data for the header
-        $search_title = $config->get_string('title');
+        $search_title = $config->get_string(SetupConfig::TITLE);
         $search_file_url = make_link('browser_search/please_dont_use_this_tag_as_it_would_break_stuff__search.xml');
         $page->add_html_header("<link rel='search' type='application/opensearchdescription+xml' title='$search_title' href='$search_file_url'>");
 
         // The search.xml file that is generated on the fly
         if ($event->page_matches("browser_search/please_dont_use_this_tag_as_it_would_break_stuff__search.xml")) {
             // First, we need to build all the variables we'll need
-            $search_title = $config->get_string('title');
+            $search_title = $config->get_string(SetupConfig::TITLE);
             $search_form_url =  make_link('post/list/{searchTerms}');
             $suggenton_url = make_link('browser_search/')."{searchTerms}";
             $icon_b64 = base64_encode(file_get_contents("ext/handle_static/static/favicon.ico"));
diff --git a/ext/custom_html_headers/main.php b/ext/custom_html_headers/main.php
index 3125f3b1..58715e9b 100644
--- a/ext/custom_html_headers/main.php
+++ b/ext/custom_html_headers/main.php
@@ -65,7 +65,7 @@ class custom_html_headers extends Extension
         global $config, $page;
             
         // get config values
-        $site_title = $config->get_string("title");
+        $site_title = $config->get_string(SetupConfig::TITLE);
         $sitename_in_title = $config->get_int("sitename_in_title");
             
         // if feature is enabled & sitename isn't already in title
diff --git a/ext/downtime/theme.php b/ext/downtime/theme.php
index caedcfda..99c4cffc 100644
--- a/ext/downtime/theme.php
+++ b/ext/downtime/theme.php
@@ -21,7 +21,7 @@ class DowntimeTheme extends Themelet
     public function display_message(string $message)
     {
         global $config, $user, $page;
-        $theme_name = $config->get_string('theme');
+        $theme_name = $config->get_string(SetupConfig::THEME);
         $data_href = get_base_href();
         $login_link = make_link("user_admin/login");
         $auth = $user->get_auth_html();
diff --git a/ext/et/main.php b/ext/et/main.php
index e344710d..f342a39f 100644
--- a/ext/et/main.php
+++ b/ext/et/main.php
@@ -40,8 +40,8 @@ class ET extends Extension
         global $config, $database;
 
         $info = [];
-        $info['site_title'] = $config->get_string("title");
-        $info['site_theme'] = $config->get_string("theme");
+        $info['site_title'] = $config->get_string(SetupConfig::TITLE);
+        $info['site_theme'] = $config->get_string(SetupConfig::THEME);
         $info['site_url']   = "http://" . $_SERVER["HTTP_HOST"] . get_base_href();
 
         $info['sys_shimmie'] = VERSION;
diff --git a/ext/handle_static/main.php b/ext/handle_static/main.php
index fb20dd59..69918964 100644
--- a/ext/handle_static/main.php
+++ b/ext/handle_static/main.php
@@ -17,7 +17,7 @@ class HandleStatic extends Extension
         if ($page->mode == PageMode::PAGE && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) {
             $h_pagename = html_escape(implode('/', $event->args));
             $f_pagename = preg_replace("/[^a-z_\-\.]+/", "_", $h_pagename);
-            $theme_name = $config->get_string("theme", "default");
+            $theme_name = $config->get_string(SetupConfig::THEME, "default");
 
             $theme_file = "themes/$theme_name/static/$f_pagename";
             $static_file = "ext/handle_static/static/$f_pagename";
diff --git a/ext/home/main.php b/ext/home/main.php
index 34b9422f..156035c6 100644
--- a/ext/home/main.php
+++ b/ext/home/main.php
@@ -22,8 +22,8 @@ class Home extends Extension
         global $config, $page;
         if ($event->page_matches("home")) {
             $base_href = get_base_href();
-            $sitename = $config->get_string('title');
-            $theme_name = $config->get_string('theme');
+            $sitename = $config->get_string(SetupConfig::TITLE);
+            $theme_name = $config->get_string(SetupConfig::THEME);
 
             $body = $this->get_body();
 
@@ -52,7 +52,7 @@ class Home extends Extension
         // returns just the contents of the body
         global $config;
         $base_href = get_base_href();
-        $sitename = $config->get_string('title');
+        $sitename = $config->get_string(SetupConfig::TITLE);
         $contact_link = contact_link();
         if (is_null($contact_link)) {
             $contact_link = "";
diff --git a/ext/index/theme.php b/ext/index/theme.php
index 5b58d627..f04e124c 100644
--- a/ext/index/theme.php
+++ b/ext/index/theme.php
@@ -110,7 +110,7 @@ and of course start organising your images :-)
         global $config;
 
         if (count($this->search_terms) == 0) {
-            $page_title = $config->get_string('title');
+            $page_title = $config->get_string(SetupConfig::TITLE);
         } else {
             $search_string = implode(' ', $this->search_terms);
             $page_title = html_escape($search_string);
diff --git a/ext/numeric_score/theme.php b/ext/numeric_score/theme.php
index 1852548c..05199b11 100644
--- a/ext/numeric_score/theme.php
+++ b/ext/numeric_score/theme.php
@@ -87,7 +87,7 @@ class NumericScoreTheme extends Themelet
 
         $nav_html = "<a href=".make_link().">Index</a>";
 
-        $page->set_heading($config->get_string('title'));
+        $page->set_heading($config->get_string(SetupConfig::TITLE));
         $page->add_block(new Block("Navigation", $nav_html, "left", 10));
         $page->add_block(new Block(null, $html, "main", 30));
     }
diff --git a/ext/rss_comments/main.php b/ext/rss_comments/main.php
index b00f92aa..f882ca40 100644
--- a/ext/rss_comments/main.php
+++ b/ext/rss_comments/main.php
@@ -14,7 +14,7 @@ class RSS_Comments extends Extension
     public function onPostListBuilding(PostListBuildingEvent $event)
     {
         global $config, $page;
-        $title = $config->get_string('title');
+        $title = $config->get_string(SetupConfig::TITLE);
 
         $page->add_html_header("<link rel=\"alternate\" type=\"application/rss+xml\" ".
             "title=\"$title - Comments\" href=\"".make_link("rss/comments")."\" />");
@@ -60,7 +60,7 @@ class RSS_Comments extends Extension
 				";
             }
 
-            $title = $config->get_string('title');
+            $title = $config->get_string(SetupConfig::TITLE);
             $base_href = make_http(get_base_href());
             $version = $config->get_string('version');
             $xml = <<<EOD
diff --git a/ext/rss_images/main.php b/ext/rss_images/main.php
index b11fad92..516c470f 100644
--- a/ext/rss_images/main.php
+++ b/ext/rss_images/main.php
@@ -12,7 +12,7 @@ class RSS_Images extends Extension
     public function onPostListBuilding(PostListBuildingEvent $event)
     {
         global $config, $page;
-        $title = $config->get_string('title');
+        $title = $config->get_string(SetupConfig::TITLE);
 
         if (count($event->search_terms) > 0) {
             $search = html_escape(implode(' ', $event->search_terms));
@@ -47,7 +47,7 @@ class RSS_Images extends Extension
             $data .= $this->thumb($image);
         }
 
-        $title = $config->get_string('title');
+        $title = $config->get_string(SetupConfig::TITLE);
         $base_href = make_http(get_base_href());
         $search = "";
         if (count($search_terms) > 0) {
diff --git a/ext/setup/config.php b/ext/setup/config.php
new file mode 100644
index 00000000..2b5be64e
--- /dev/null
+++ b/ext/setup/config.php
@@ -0,0 +1,12 @@
+<?php
+
+
+class SetupConfig
+{
+    public const TITLE = "title";
+    public const FRONT_PAGE = "front_page";
+    public const MAIN_PAGE = "main_page";
+    public const THEME = "theme";
+    public const WORD_WRAP = "word_wrap";
+    public const COMMENT_CAPTCHA = "comment_captcha";
+}
\ No newline at end of file
diff --git a/ext/setup/main.php b/ext/setup/main.php
index 9b8fa060..6259f259 100644
--- a/ext/setup/main.php
+++ b/ext/setup/main.php
@@ -6,6 +6,8 @@
  * Description: Allows the site admin to configure the board to his or her taste
  */
 
+include_once "config.php";
+
 /* ConfigSaveEvent {{{
  *
  * Sent when the setup screen's 'set' button has been
@@ -275,12 +277,11 @@ class Setup extends Extension
     public function onInitExt(InitExtEvent $event)
     {
         global $config;
-        $config->set_default_string("title", "Shimmie");
-        $config->set_default_string("front_page", "post/list");
-        $config->set_default_string("main_page", "post/list");
-        $config->set_default_string("theme", "default");
-        $config->set_default_bool("word_wrap", true);
-        $config->set_default_bool("comment_captcha", false);
+        $config->set_default_string(SetupConfig::TITLE, "Shimmie");
+        $config->set_default_string(SetupConfig::FRONT_PAGE, "post/list");
+        $config->set_default_string(SetupConfig::MAIN_PAGE, "post/list");
+        $config->set_default_string(SetupConfig::THEME, "default");
+        $config->set_default_bool(SetupConfig::WORD_WRAP, true);
     }
 
     public function onPageRequest(PageRequestEvent $event)
@@ -368,11 +369,11 @@ class Setup extends Extension
 		</script>";
         $sb = new SetupBlock("General");
         $sb->position = 0;
-        $sb->add_text_option("title", "Site title: ");
-        $sb->add_text_option("front_page", "<br>Front page: ");
-        $sb->add_text_option("main_page", "<br>Main page: ");
+        $sb->add_text_option(SetupConfig::TITLE, "Site title: ");
+        $sb->add_text_option(SetupConfig::FRONT_PAGE, "<br>Front page: ");
+        $sb->add_text_option(SetupConfig::MAIN_PAGE, "<br>Main page: ");
         $sb->add_text_option("contact_link", "<br>Contact URL: ");
-        $sb->add_choice_option("theme", $themes, "<br>Theme: ");
+        $sb->add_choice_option(SetupConfig::THEME, $themes, "<br>Theme: ");
         //$sb->add_multichoice_option("testarray", array("a" => "b", "c" => "d"), "<br>Test Array: ");
         $sb->add_bool_option("nice_urls", "<br>Nice URLs: ");
         $sb->add_label("<span id='nicetest'>(Javascript inactive, can't test!)</span>$nicescript");
diff --git a/ext/sitemap/main.php b/ext/sitemap/main.php
index 4ff80f88..6bf8b4a3 100644
--- a/ext/sitemap/main.php
+++ b/ext/sitemap/main.php
@@ -74,7 +74,7 @@ class XMLSitemap extends Extension
 
         // add index
         $index = [];
-        $index[0] = $config->get_string("front_page");
+        $index[0] = $config->get_string(SetupConfig::FRONT_PAGE);
         $this->add_sitemap_queue($index, "weekly", "1");
 
         /* --- Add 20 most used tags --- */
diff --git a/ext/upload/theme.php b/ext/upload/theme.php
index 6b3b503a..efa407c8 100644
--- a/ext/upload/theme.php
+++ b/ext/upload/theme.php
@@ -189,7 +189,7 @@ class UploadTheme extends Themelet
         global $config;
         $link = make_http(make_link("upload"));
         $main_page = make_http(make_link());
-        $title = $config->get_string('title');
+        $title = $config->get_string(SetupConfig::TITLE);
         $max_size = $config->get_int('upload_size');
         $max_kb = to_shorthand_int($max_size);
         $delimiter = $config->get_bool('nice_urls') ? '?' : '&amp;';
@@ -235,7 +235,7 @@ class UploadTheme extends Themelet
         if (class_exists("VideoFileHandler")) {
             $supported_ext .= " flv mp4 ogv webm m4v";
         }
-        $title = "Booru to " . $config->get_string('title');
+        $title = "Booru to " . $config->get_string(SetupConfig::TITLE);
         // CA=0: Ask to use current or new tags | CA=1: Always use current tags | CA=2: Always use new tags
         $html .= '<p><a href="javascript:
 			var ste=&quot;'. $link . $delimiter .'url=&quot;;
diff --git a/themes/danbooru/index.theme.php b/themes/danbooru/index.theme.php
index ef59ef69..9d55abd6 100644
--- a/themes/danbooru/index.theme.php
+++ b/themes/danbooru/index.theme.php
@@ -11,7 +11,7 @@ class CustomIndexTheme extends IndexTheme
 
         if (count($this->search_terms) == 0) {
             $query = null;
-            $page_title = $config->get_string('title');
+            $page_title = $config->get_string(SetupConfig::TITLE);
         } else {
             $search_string = implode(' ', $this->search_terms);
             $query = url_escape($search_string);
diff --git a/themes/danbooru/layout.class.php b/themes/danbooru/layout.class.php
index 9a22681a..42efe2a1 100644
--- a/themes/danbooru/layout.class.php
+++ b/themes/danbooru/layout.class.php
@@ -48,7 +48,7 @@ class Layout
     {
         global $config, $user;
 
-        $theme_name = $config->get_string('theme');
+        $theme_name = $config->get_string(SetupConfig::THEME);
         //$base_href = $config->get_string('base_href');
         $data_href = get_base_href();
         $contact_link = contact_link();
@@ -92,10 +92,9 @@ class Layout
             $subheading = "<div id='subtitle'>{$this->subheading}</div>";
         }
 
-        $site_name = $config->get_string('title'); // bzchan: change from normal default to get title for top of page
-        $main_page = $config->get_string('main_page'); // bzchan: change from normal default to get main page for top of page
+        $site_name = $config->get_string(SetupConfig::TITLE); // bzchan: change from normal default to get title for top of page
+        $main_page = $config->get_string(SetupConfig::MAIN_PAGE); // bzchan: change from normal default to get main page for top of page
 
-        // bzchan: CUSTOM LINKS are prepared here, change these to whatever you like
         $custom_links = "";
         if ($user->is_anonymous()) {
             $custom_links .= $this->navlinks(make_link('user_admin/login'), "My Account", ["user", "user_admin", "setup", "admin"]);
diff --git a/themes/danbooru2/index.theme.php b/themes/danbooru2/index.theme.php
index 51455d43..a62fdf8c 100644
--- a/themes/danbooru2/index.theme.php
+++ b/themes/danbooru2/index.theme.php
@@ -11,7 +11,7 @@ class CustomIndexTheme extends IndexTheme
 
         if (count($this->search_terms) == 0) {
             $query = null;
-            $page_title = $config->get_string('title');
+            $page_title = $config->get_string(SetupConfig::TITLE);
         } else {
             $search_string = implode(' ', $this->search_terms);
             $query = url_escape($search_string);
diff --git a/themes/danbooru2/layout.class.php b/themes/danbooru2/layout.class.php
index 6c4514f2..e3d0116a 100644
--- a/themes/danbooru2/layout.class.php
+++ b/themes/danbooru2/layout.class.php
@@ -48,7 +48,7 @@ class Layout
     {
         global $config, $user;
 
-        //$theme_name = $config->get_string('theme');
+        //$theme_name = $config->get_string(SetupConfig::THEME);
         //$base_href = $config->get_string('base_href');
         //$data_href = get_base_href();
         $contact_link = contact_link();
@@ -92,10 +92,9 @@ class Layout
             $subheading = "<div id='subtitle'>{$this->subheading}</div>";
         }
 
-        $site_name = $config->get_string('title'); // bzchan: change from normal default to get title for top of page
-        $main_page = $config->get_string('main_page'); // bzchan: change from normal default to get main page for top of page
+        $site_name = $config->get_string(SetupConfig::TITLE); // bzchan: change from normal default to get title for top of page
+        $main_page = $config->get_string(SetupConfig::MAIN_PAGE); // bzchan: change from normal default to get main page for top of page
 
-        // bzchan: CUSTOM LINKS are prepared here, change these to whatever you like
         $custom_links = "";
         if ($user->is_anonymous()) {
             $custom_links .= $this->navlinks(make_link('user_admin/login'), "Sign in", ["user", "user_admin", "setup", "admin"]);
diff --git a/themes/default/layout.class.php b/themes/default/layout.class.php
index e67309bb..973c0bb0 100644
--- a/themes/default/layout.class.php
+++ b/themes/default/layout.class.php
@@ -11,7 +11,7 @@ class Layout
     {
         global $config;
 
-        //$theme_name = $config->get_string('theme', 'default');
+        //$theme_name = $config->get_string(SetupConfig::THEME, 'default');
         //$data_href = get_base_href();
         $contact_link = contact_link();
         $header_html = $page->get_all_html_headers();
diff --git a/themes/futaba/comment.theme.php b/themes/futaba/comment.theme.php
index 7647c322..5ae42be1 100644
--- a/themes/futaba/comment.theme.php
+++ b/themes/futaba/comment.theme.php
@@ -11,7 +11,7 @@ class CustomCommentListTheme extends CommentListTheme
         //$prev = $page_number - 1;
         //$next = $page_number + 1;
 
-        $page_title = $config->get_string('title');
+        $page_title = $config->get_string(SetupConfig::TITLE);
         $page->set_title($page_title);
         $page->set_heading($page_title);
         $page->disable_left();
diff --git a/themes/futaba/layout.class.php b/themes/futaba/layout.class.php
index faccb090..98076afb 100644
--- a/themes/futaba/layout.class.php
+++ b/themes/futaba/layout.class.php
@@ -6,7 +6,7 @@ class Layout
     {
         global $config;
 
-        $theme_name = $config->get_string('theme', 'default');
+        $theme_name = $config->get_string(SetupConfig::THEME, 'default');
         $data_href = get_base_href();
         $contact_link = contact_link();
         $header_html = $page->get_all_html_headers();
diff --git a/themes/lite/layout.class.php b/themes/lite/layout.class.php
index 747b0086..3c1e1b2f 100644
--- a/themes/lite/layout.class.php
+++ b/themes/lite/layout.class.php
@@ -13,8 +13,8 @@ class Layout
     {
         global $config, $user;
 
-        $theme_name = $config->get_string('theme', 'lite');
-        $site_name = $config->get_string('title');
+        $theme_name = $config->get_string(SetupConfig::THEME, 'lite');
+        $site_name = $config->get_string(SetupConfig::TITLE);
         $data_href = get_base_href();
         $contact_link = contact_link();
         $header_html = $page->get_all_html_headers();
diff --git a/themes/material/layout.class.php b/themes/material/layout.class.php
index 4c923456..ada06030 100644
--- a/themes/material/layout.class.php
+++ b/themes/material/layout.class.php
@@ -11,10 +11,10 @@ class Layout
     {
         global $config;
 
-        $theme_name = $config->get_string('theme', 'material');
-        $site_name = $config->get_string('title');
+        $theme_name = $config->get_string(SetupConfig::THEME, 'material');
+        $site_name = $config->get_string(SetupConfig::TITLE);
         $data_href = get_base_href();
-        $main_page = $config->get_string('main_page');
+        $main_page = $config->get_string(SetupConfig::MAIN_PAGE);
         $contact_link = contact_link();
         $site_link = make_link();
         $header_html = $page->get_all_html_headers();
diff --git a/themes/warm/layout.class.php b/themes/warm/layout.class.php
index 20259570..1d3017fb 100644
--- a/themes/warm/layout.class.php
+++ b/themes/warm/layout.class.php
@@ -11,10 +11,10 @@ class Layout
     {
         global $config;
 
-        //$theme_name = $config->get_string('theme', 'default');
-        $site_name = $config->get_string('title');
+        //$theme_name = $config->get_string(SetupConfig::THEME, 'default');
+        $site_name = $config->get_string(SetupConfig::TITLE);
         $data_href = get_base_href();
-        $main_page = $config->get_string('main_page');
+        $main_page = $config->get_string(SetupConfig::MAIN_PAGE);
         $contact_link = contact_link();
         $header_html = $page->get_all_html_headers();
 

From 00464d25797fc4932b868e00259b20f94b854d84 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Fri, 2 Aug 2019 14:54:48 -0500
Subject: [PATCH 09/10] Implemented a nav link generating system so that
 extension power what shows up in the menus rather than being hard-coded in
 the themes.

---
 core/page.php                     | 144 +++++++++++++++++++++++++++++-
 core/sys_config.php               |   2 +-
 core/urls.php                     |  17 ++++
 ext/admin/main.php                |  10 +++
 ext/alias_editor/main.php         |   7 ++
 ext/blocks/main.php               |  10 +++
 ext/blotter/main.php              |  11 +++
 ext/comment/main.php              |  15 ++++
 ext/et/main.php                   |  12 +++
 ext/ext_manager/main.php          |  11 +++
 ext/favorites/main.php            |  14 +++
 ext/image_hash_ban/main.php       |  11 +++
 ext/index/main.php                |  12 +++
 ext/ipban/main.php                |  10 +++
 ext/log_db/main.php               |  10 +++
 ext/not_a_tag/main.php            |  10 +++
 ext/numeric_score/main.php        |  10 +++
 ext/pm/main.php                   |  13 +++
 ext/pools/main.php                |  17 ++++
 ext/random_image/main.php         |   7 ++
 ext/random_list/main.php          |   7 ++
 ext/report_image/main.php         |  14 +++
 ext/rss_comments/main.php         |   8 ++
 ext/rss_images/main.php           |   7 ++
 ext/setup/main.php                |  10 +++
 ext/source_history/main.php       |  10 +++
 ext/system/main.php               |  31 +++++++
 ext/tag_edit/main.php             |   9 ++
 ext/tag_history/main.php          |  11 +++
 ext/tag_list/main.php             |  15 ++++
 ext/tips/main.php                 |  10 +++
 ext/upload/main.php               |  15 ++++
 ext/user/main.php                 |  21 +++++
 ext/wiki/main.php                 |  15 ++++
 themes/danbooru/layout.class.php  | 114 ++++-------------------
 themes/danbooru2/layout.class.php | 138 ++++------------------------
 themes/default/layout.class.php   |   2 +-
 themes/lite/layout.class.php      | 137 ++++------------------------
 38 files changed, 583 insertions(+), 344 deletions(-)
 create mode 100644 ext/system/main.php

diff --git a/core/page.php b/core/page.php
index e779290a..7efa2833 100644
--- a/core/page.php
+++ b/core/page.php
@@ -299,9 +299,55 @@ class Page
                     $this->add_cookie("flash_message", "", -1, "/");
                 }
                 usort($this->blocks, "blockcmp");
+                $pnbe = new PageNavBuildingEvent();
+                send_event($pnbe);
+
+                $nav_links = $pnbe->links;
+
+                $active_link = null;
+                // To save on event calls, we check if one of the top-level links has already been marked as active
+                foreach ($nav_links as $link) {
+                    if($link->active===true) {
+                        $active_link = $link;
+                        break;
+                    }
+                }
+                $sub_links = null;
+                // If one is, we just query for sub-menu options under that one tab
+                if($active_link!==null) {
+                    $psnbe = new PageSubNavBuildingEvent($active_link->name);
+                    send_event($psnbe);
+                    $sub_links = $psnbe->links;
+                } else {
+                    // Otherwise we query for the sub-items under each of the tabs
+                    foreach ($nav_links as $link) {
+                        $psnbe = new PageSubNavBuildingEvent($link->name);
+                        send_event($psnbe);
+
+                        // Now we check for a current link so we can identify the sub-links to show
+                        foreach ($psnbe->links as $sub_link) {
+                            if($sub_link->active===true) {
+                                $sub_links = $psnbe->links;
+                                break;
+                            }
+                        }
+                        // If the active link has been detected, we break out
+                        if($sub_links!==null) {
+                            $link->active = true;
+                            break;
+                        }
+                    }
+                }
+
+
+
+                $sub_links = $sub_links??[];
+                usort($nav_links, "sort_nav_links");
+                usort($sub_links, "sort_nav_links");
+
                 $this->add_auto_html_headers();
                 $layout = new Layout();
-                $layout->display_page($page);
+                $layout->display_page($page, $nav_links, $sub_links);
                 break;
             case PageMode::DATA:
                 header("Content-Length: " . strlen($this->data));
@@ -471,3 +517,99 @@ class Page
         $this->add_html_header("<script src='$data_href/$js_cache_file' type='text/javascript'></script>", 44);
     }
 }
+
+class PageNavBuildingEvent extends Event
+{
+    public $links = [];
+
+    public function add_nav_link(string $name, Link $link, string $desc, ?bool $active = null, int $order = 50)
+    {
+        $this->links[]  = new NavLink($name, $link, $desc, $active, $order);
+    }
+}
+
+class PageSubNavBuildingEvent extends Event
+{
+    public $parent;
+
+    public $links = [];
+
+    public function __construct(string $parent)
+    {
+        $this->parent= $parent;
+    }
+
+    public function add_nav_link(string $name, Link $link, string $desc, ?bool $active = null, int $order = 50)
+    {
+        $this->links[]  = new NavLink($name, $link, $desc, $active,$order);
+    }
+}
+
+class NavLink
+{
+    public $name;
+    public $link;
+    public $description;
+    public $order;
+    public $active = false;
+
+    public function  __construct(String $name, Link $link, String $description, ?bool $active = null, int $order = 50)
+    {
+        global $config;
+
+        $this->name = $name;
+        $this->link = $link;
+        $this->description = $description;
+        $this->order = $order;
+        if($active==null) {
+            $query = ltrim(_get_query(), "/");
+            if ($query === "") {
+                // This indicates the front page, so we check what's set as the front page
+                $front_page = trim($config->get_string(SetupConfig::FRONT_PAGE), "/");
+
+                if ($front_page === $link->page) {
+                    $this->active = true;
+                } else {
+                    $this->active = self::is_active([$link->page], $front_page);
+                }
+            } elseif($query===$link->page) {
+                $this->active = true;
+            }else {
+                $this->active = self::is_active([$link->page]);
+            }
+        } else {
+            $this->active = $active;
+        }
+
+    }
+
+    public static function is_active(array $pages_matched, string $url = null): bool
+    {
+        /**
+         * Woo! We can actually SEE THE CURRENT PAGE!! (well... see it highlighted in the menu.)
+         */
+        $url = $url??ltrim(_get_query(), "/");
+
+        $re1='.*?';
+        $re2='((?:[a-z][a-z_]+))';
+
+        if (preg_match_all("/".$re1.$re2."/is", $url, $matches)) {
+            $url=$matches[1][0];
+        }
+
+        $count_pages_matched = count($pages_matched);
+
+        for ($i=0; $i < $count_pages_matched; $i++) {
+            if ($url == $pages_matched[$i]) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
+
+function sort_nav_links(NavLink $a, NavLink $b)
+{
+    return $a->order - $b->order;
+}
diff --git a/core/sys_config.php b/core/sys_config.php
index 9ce0d8d0..9eac5f59 100644
--- a/core/sys_config.php
+++ b/core/sys_config.php
@@ -40,7 +40,7 @@ _d("SEARCH_ACCEL", false);   // boolean  use search accelerator
 _d("WH_SPLITS", 1);          // int      how many levels of subfolders to put in the warehouse
 _d("VERSION", '2.7-beta');   // string   shimmie version
 _d("TIMEZONE", null);        // string   timezone
-_d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,handle_static,comment,tag_list,index,tag_edit,alias_editor,media"); // extensions to always enable
+_d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,handle_static,comment,tag_list,index,tag_edit,alias_editor,media,system"); // extensions to always enable
 _d("EXTRA_EXTS", "");        // string   optional extra extensions
 _d("BASE_URL", null);        // string   force a specific base URL (default is auto-detect)
 _d("MIN_PHP_VERSION", '7.1');// string   minimum supported PHP version
diff --git a/core/urls.php b/core/urls.php
index 3bdc9c71..457bfe1b 100644
--- a/core/urls.php
+++ b/core/urls.php
@@ -3,6 +3,23 @@
 * HTML Generation                                                           *
 \* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
 
+class Link
+{
+    public $page;
+    public $query;
+
+    public function __construct(?string $page=null, ?string $query=null)
+    {
+        $this->page = $page;
+        $this->query = $query;
+    }
+
+    public function make_link(): string
+    {
+        return make_link($this->page, $this->query);
+    }
+}
+
 /**
  * Figure out the correct way to link to a page, taking into account
  * things like the nice URLs setting.
diff --git a/ext/admin/main.php b/ext/admin/main.php
index eef919d7..423460c8 100644
--- a/ext/admin/main.php
+++ b/ext/admin/main.php
@@ -108,6 +108,16 @@ class AdminPage extends Extension
         $this->theme->display_form();
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="system") {
+            if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
+                $event->add_nav_link("admin", new Link('admin'), "Board Admin");
+            }
+        }
+    }
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/alias_editor/main.php b/ext/alias_editor/main.php
index 4321c78a..36edbfbb 100644
--- a/ext/alias_editor/main.php
+++ b/ext/alias_editor/main.php
@@ -117,6 +117,13 @@ class AliasEditor extends Extension
         }
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        if($event->parent=="tags") {
+            $event->add_nav_link("aliases", new Link('alias/list'), "Aliases", NavLink::is_active(["alias"]));
+        }
+    }
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/blocks/main.php b/ext/blocks/main.php
index b444b03a..197b5d9f 100644
--- a/ext/blocks/main.php
+++ b/ext/blocks/main.php
@@ -26,6 +26,16 @@ class Blocks extends Extension
         }
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="system") {
+            if ($user->can(Permissions::MANAGE_BLOCKS)) {
+                $event->add_nav_link("blocks", new Link('blocks/list'), "Blocks Editor");
+            }
+        }
+    }
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/blotter/main.php b/ext/blotter/main.php
index cb88490b..3e18b1a9 100644
--- a/ext/blotter/main.php
+++ b/ext/blotter/main.php
@@ -56,6 +56,17 @@ class Blotter extends Extension
         $event->panel->add_block($sb);
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="system") {
+            if ($user->is_admin()) {
+                $event->add_nav_link("blotter", new Link('blotter/editor'), "Blotter Editor");
+            }
+        }
+    }
+
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/comment/main.php b/ext/comment/main.php
index 2540ef4a..7ea23d37 100644
--- a/ext/comment/main.php
+++ b/ext/comment/main.php
@@ -157,6 +157,21 @@ class CommentList extends Extension
         }
     }
 
+
+    public function onPageNavBuilding(PageNavBuildingEvent $event)
+    {
+        $event->add_nav_link("comment", new Link('comment/list'), "Comments");
+    }
+
+
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        if($event->parent=="comment") {
+            $event->add_nav_link("comment_list", new Link('comment/list'), "All");
+            $event->add_nav_link("comment_help", new Link('ext_doc/comment'), "Help");
+        }
+    }
+
     public function onPageRequest(PageRequestEvent $event)
     {
         if ($event->page_matches("comment")) {
diff --git a/ext/et/main.php b/ext/et/main.php
index f342a39f..576765dd 100644
--- a/ext/et/main.php
+++ b/ext/et/main.php
@@ -24,6 +24,18 @@ class ET extends Extension
         }
     }
 
+
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="system") {
+            if ($user->can(Permissions::VIEW_SYSINTO)) {
+                $event->add_nav_link("system_info", new Link('system_info'), "System Info", null, 10);
+            }
+        }
+    }
+
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/ext_manager/main.php b/ext/ext_manager/main.php
index e024f286..39c7f572 100644
--- a/ext/ext_manager/main.php
+++ b/ext/ext_manager/main.php
@@ -162,6 +162,17 @@ class ExtManager extends Extension
         }
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="system") {
+            if ($user->can(Permissions::MANAGE_EXTENSION_LIST)) {
+                $event->add_nav_link("ext_manager", new Link('ext_manager'), "Extension Manager");
+            } else {
+                $event->add_nav_link("ext_doc", new Link('ext_doc'), "Board Help");
+            }
+        }
+    }
 
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
diff --git a/ext/favorites/main.php b/ext/favorites/main.php
index e8b7f6fe..91ac1c2d 100644
--- a/ext/favorites/main.php
+++ b/ext/favorites/main.php
@@ -155,6 +155,20 @@ class Favorites extends Extension
         }
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent=="posts") {
+            $event->add_nav_link("posts_favorites", new Link("post/list/favorited_by={$user->name}/1"), "My Favorites");
+        }
+
+        if($event->parent==="user") {
+            if ($user->can(Permissions::MANAGE_ADMINTOOLS)) {
+                $username = url_escape($user->name);
+                $event->add_nav_link("favorites", new Link("post/list/favorited_by=$username/1"), "My Favorites");
+            }
+        }
+    }
 
     private function install()
     {
diff --git a/ext/image_hash_ban/main.php b/ext/image_hash_ban/main.php
index 67298a89..cc3a7ca1 100644
--- a/ext/image_hash_ban/main.php
+++ b/ext/image_hash_ban/main.php
@@ -103,6 +103,17 @@ class ImageBan extends Extension
         }
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        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'), "Image Bans", NavLink::is_active(["image_hash_ban"]));
+            }
+        }
+    }
+
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/index/main.php b/ext/index/main.php
index eab9d7b3..80c4b610 100644
--- a/ext/index/main.php
+++ b/ext/index/main.php
@@ -332,6 +332,18 @@ class Index extends Extension
         }
     }
 
+    public function onPageNavBuilding(PageNavBuildingEvent $event)
+    {
+        $event->add_nav_link("posts", new Link('post/list'), "Posts", NavLink::is_active(["post","view"]),20);
+    }
+
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        if($event->parent=="posts") {
+            $event->add_nav_link("posts_all", new Link('post/list'), "All");
+        }
+    }
+
     public function onSearchTermParse(SearchTermParseEvent $event)
     {
         $matches = [];
diff --git a/ext/ipban/main.php b/ext/ipban/main.php
index bfefb813..90eb198a 100644
--- a/ext/ipban/main.php
+++ b/ext/ipban/main.php
@@ -105,6 +105,16 @@ class IPBan extends Extension
         $event->panel->add_block($sb);
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="system") {
+            if ($user->can(Permissions::BAN_IP)) {
+                $event->add_nav_link("ip_bans", new Link('ip_ban/list'), "IP Bans", NavLink::is_active(["ip_ban"]));
+            }
+        }
+    }
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/log_db/main.php b/ext/log_db/main.php
index a5dd1d7f..4002b68c 100644
--- a/ext/log_db/main.php
+++ b/ext/log_db/main.php
@@ -120,6 +120,16 @@ class LogDatabase extends Extension
         }
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="system") {
+            if ($user->can(Permissions::VIEW_EVENTLOG)) {
+                $event->add_nav_link("event_log", new Link('log/view'), "Event Log");
+            }
+        }
+    }
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/not_a_tag/main.php b/ext/not_a_tag/main.php
index 29f31e75..369ab0ff 100644
--- a/ext/not_a_tag/main.php
+++ b/ext/not_a_tag/main.php
@@ -58,6 +58,16 @@ class NotATag extends Extension
         }
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="tags") {
+            if ($user->can(Permissions::BAN_IMAGE)) {
+                $event->add_nav_link("untags", new Link('untag/list/1'), "UnTags");
+            }
+        }
+    }
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/numeric_score/main.php b/ext/numeric_score/main.php
index 446c5553..84179f7c 100644
--- a/ext/numeric_score/main.php
+++ b/ext/numeric_score/main.php
@@ -294,6 +294,16 @@ class NumericScore extends Extension
         }
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        if($event->parent=="posts") {
+            $event->add_nav_link("numeric_score_day", new Link('popular_by_day'), "Popular by Day");
+            $event->add_nav_link("numeric_score_month", new Link('popular_by_month'), "Popular by Month");
+            $event->add_nav_link("numeric_score_year", new Link('popular_by_year'), "Popular by Year");
+
+        }
+    }
+
     private function install()
     {
         global $database;
diff --git a/ext/pm/main.php b/ext/pm/main.php
index d4ef16da..d2c7f44d 100644
--- a/ext/pm/main.php
+++ b/ext/pm/main.php
@@ -93,6 +93,19 @@ class PrivMsg extends Extension
         }
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="user") {
+            if (!$user->is_anonymous()) {
+                $count = $this->count_pms($user);
+                $h_count = $count > 0 ? " <span class='unread'>($count)</span>" : "";
+                $event->add_nav_link("pm", new Link('user#private-messages'), "Private Messages$h_count");
+            }
+        }
+    }
+
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/pools/main.php b/ext/pools/main.php
index 37a1cc01..cbdf5164 100644
--- a/ext/pools/main.php
+++ b/ext/pools/main.php
@@ -145,6 +145,23 @@ class Pools extends Extension
         $event->panel->add_block($sb);
     }
 
+    public function onPageNavBuilding(PageNavBuildingEvent $event)
+    {
+        $event->add_nav_link("pool", new Link('pool/list'), "Pools");
+    }
+
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        if($event->parent=="pool") {
+            $event->add_nav_link("pool_list", new Link('pool/list'), "List");
+            $event->add_nav_link("pool_new", new Link('pool/new'), "Create");
+            $event->add_nav_link("pool_updated", new Link('pool/updated'), "Changes");
+            $event->add_nav_link("pool_help", new Link('ext_doc/pools'), "Help");
+        }
+    }
+
+
+
     public function onPageRequest(PageRequestEvent $event)
     {
         global $page, $user, $database;
diff --git a/ext/random_image/main.php b/ext/random_image/main.php
index fc0424f7..b03d21e4 100644
--- a/ext/random_image/main.php
+++ b/ext/random_image/main.php
@@ -75,4 +75,11 @@ class RandomImage extends Extension
             }
         }
     }
+
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        if($event->parent=="posts") {
+            $event->add_nav_link("posts_random", new Link('random_image/view'), "Random Image");
+        }
+    }
 }
diff --git a/ext/random_list/main.php b/ext/random_list/main.php
index 7333905d..47c55b9c 100644
--- a/ext/random_list/main.php
+++ b/ext/random_list/main.php
@@ -74,4 +74,11 @@ class RandomList extends Extension
 
         $event->panel->add_block($sb);
     }
+
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        if($event->parent=="posts") {
+            $event->add_nav_link("posts_random", new Link('random'), "Shuffle");
+        }
+    }
 }
diff --git a/ext/report_image/main.php b/ext/report_image/main.php
index 970d9466..d5436e6b 100644
--- a/ext/report_image/main.php
+++ b/ext/report_image/main.php
@@ -132,6 +132,20 @@ class ReportImage extends Extension
         }
     }
 
+
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="system") {
+            if ($user->can(Permissions::VIEW_IMAGE_REPORT)) {
+                $count = $this->count_reported_images();
+                $h_count = $count > 0 ? " ($count)" : "";
+
+                $event->add_nav_link("image_report", new Link('image_report/list'), "Reported Images$h_count");
+            }
+        }
+    }
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/rss_comments/main.php b/ext/rss_comments/main.php
index f882ca40..f64869e1 100644
--- a/ext/rss_comments/main.php
+++ b/ext/rss_comments/main.php
@@ -79,4 +79,12 @@ EOD;
             $page->set_data($xml);
         }
     }
+
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        if($event->parent=="comment") {
+            $event->add_nav_link("comment_rss", new Link('rss/comments'), "Feed");
+        }
+    }
+
 }
diff --git a/ext/rss_images/main.php b/ext/rss_images/main.php
index 516c470f..6e5f9026 100644
--- a/ext/rss_images/main.php
+++ b/ext/rss_images/main.php
@@ -116,4 +116,11 @@ class RSS_Images extends Extension
 
         return $data;
     }
+
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        if($event->parent=="posts") {
+            $event->add_nav_link("posts_rss", new Link('rss/images'), "Feed");
+        }
+    }
 }
diff --git a/ext/setup/main.php b/ext/setup/main.php
index 6259f259..b7bf6181 100644
--- a/ext/setup/main.php
+++ b/ext/setup/main.php
@@ -411,6 +411,16 @@ class Setup extends Extension
         log_warning("setup", "Cache cleared");
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="system") {
+            if ($user->can(Permissions::CHANGE_SETTING)) {
+                $event->add_nav_link("setup", new Link('setup'), "Board Config", null, 0);
+            }
+        }
+    }
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/source_history/main.php b/ext/source_history/main.php
index ac5f7ae7..3fd47cab 100644
--- a/ext/source_history/main.php
+++ b/ext/source_history/main.php
@@ -82,6 +82,16 @@ class Source_History extends Extension
         $this->add_source_history($event->image, $event->source);
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="system") {
+            if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) {
+                $event->add_nav_link("source_history", new Link('source_history/all/1'), "Source Changes", NavLink::is_active(["source_history"]));
+            }
+        }
+    }
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/system/main.php b/ext/system/main.php
new file mode 100644
index 00000000..2cce82c1
--- /dev/null
+++ b/ext/system/main.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Name: System
+ * Author: Matthew Barbour <matthew@darkholme.net>
+ * License: MIT
+ * Description: Provides system screen
+ */
+
+class System extends Extension
+{
+    public function onPageRequest(PageRequestEvent $event)
+    {
+        global $page, $user;
+
+        if ($event->page_matches("system")) {
+            $e = new PageSubNavBuildingEvent("system");
+            send_event($e);
+            usort($e->links, "sort_nav_links");
+            $link = $e->links[0]->link;
+
+            $page->set_redirect($link->make_link());
+            $page->set_mode(PageMode::REDIRECT);
+        }
+    }
+    public function onPageNavBuilding(PageNavBuildingEvent $event)
+    {
+        $event->add_nav_link("system", new Link('system'), "System");
+    }
+
+
+}
diff --git a/ext/tag_edit/main.php b/ext/tag_edit/main.php
index 7d20b7d0..df6d8b43 100644
--- a/ext/tag_edit/main.php
+++ b/ext/tag_edit/main.php
@@ -255,6 +255,15 @@ class TagEdit extends Extension
         $this->theme->display_mass_editor();
     }
 
+
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        if($event->parent=="tags") {
+            $event->add_nav_link("tags_help", new Link('ext_doc/tag_edit'), "Help");
+        }
+    }
+
+
     /**
      * When an alias is added, oldtag becomes inaccessible.
      */
diff --git a/ext/tag_history/main.php b/ext/tag_history/main.php
index 6e241c3e..b317abd3 100644
--- a/ext/tag_history/main.php
+++ b/ext/tag_history/main.php
@@ -82,6 +82,17 @@ class Tag_History extends Extension
         $this->add_tag_history($event->image, $event->tags);
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="system") {
+            if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) {
+                $event->add_nav_link("tag_history", new Link('tag_history/all/1'), "Tag Changes", NavLink::is_active(["tag_history"]));
+            }
+        }
+    }
+
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/tag_list/main.php b/ext/tag_list/main.php
index ec2a4032..9d0e8a53 100644
--- a/ext/tag_list/main.php
+++ b/ext/tag_list/main.php
@@ -93,6 +93,21 @@ class TagList extends Extension
         }
     }
 
+    public function onPageNavBuilding(PageNavBuildingEvent $event)
+    {
+        $event->add_nav_link("tags", new Link('tags/map'), "Tags");
+    }
+
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        if($event->parent=="tags") {
+            $event->add_nav_link("tags_map", new Link('tags/map'), "Map");
+            $event->add_nav_link("tags_alphabetic", new Link('tags/alphabetic'), "Alphabetic");
+            $event->add_nav_link("tags_popularity", new Link('tags/popularity'), "Popularity");
+            $event->add_nav_link("tags_categories", new Link('tags/categories'), "Categories");
+        }
+    }
+
     public function onDisplayingImage(DisplayingImageEvent $event)
     {
         global $config, $page;
diff --git a/ext/tips/main.php b/ext/tips/main.php
index 7e5610a6..60564028 100644
--- a/ext/tips/main.php
+++ b/ext/tips/main.php
@@ -73,6 +73,16 @@ class Tips extends Extension
         }
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="system") {
+            if ($user->is_admin()) {
+                $event->add_nav_link("tips", new Link('tips/list'), "Tips Editor");
+            }
+        }
+    }
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/upload/main.php b/ext/upload/main.php
index ed13c96b..8c9d313f 100644
--- a/ext/upload/main.php
+++ b/ext/upload/main.php
@@ -138,6 +138,21 @@ class Upload extends Extension
         $event->panel->add_block($sb);
     }
 
+
+    public function onPageNavBuilding(PageNavBuildingEvent $event)
+    {
+        $event->add_nav_link("upload",new Link('upload'), "Upload");
+    }
+
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        if($event->parent=="upload") {
+            if (class_exists("Wiki")) {
+                $event->add_nav_link("upload_guidelines", new Link('wiki/upload_guidelines'), "Guidelines");
+            }
+        }
+    }
+
     public function onDataUpload(DataUploadEvent $event)
     {
         global $config;
diff --git a/ext/user/main.php b/ext/user/main.php
index f7d202af..65280272 100644
--- a/ext/user/main.php
+++ b/ext/user/main.php
@@ -237,6 +237,17 @@ class UserPage extends Extension
         }
     }
 
+    public function onPageNavBuilding(PageNavBuildingEvent $event)
+    {
+        global $user;
+        if ($user->is_anonymous()) {
+            $event->add_nav_link("user", new Link('user_admin/login'), "Account", null, 10);
+        } else {
+            $event->add_nav_link("user", new Link('user'), "Account", null, 10);
+        }
+    }
+
+
     private function display_stats(UserPageBuildingEvent $event)
     {
         global $user, $page, $config;
@@ -305,6 +316,16 @@ class UserPage extends Extension
         $event->panel->add_block($sb);
     }
 
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        global $user;
+        if($event->parent==="system") {
+            if ($user->can(Permissions::EDIT_USER_CLASS)) {
+                $event->add_nav_link("user_admin", new Link('user_admin/list'), "User List", NavLink::is_active(["user_admin"]));
+            }
+        }
+    }
+
     public function onUserBlockBuilding(UserBlockBuildingEvent $event)
     {
         global $user;
diff --git a/ext/wiki/main.php b/ext/wiki/main.php
index 65d29ae0..2bb04b0a 100644
--- a/ext/wiki/main.php
+++ b/ext/wiki/main.php
@@ -176,6 +176,21 @@ class Wiki extends Extension
         }
     }
 
+
+    public function onPageNavBuilding(PageNavBuildingEvent $event)
+    {
+        $event->add_nav_link("wiki",new Link('wiki'), "Wiki");
+    }
+
+
+    public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+    {
+        if($event->parent=="wiki") {
+            $event->add_nav_link("wiki_rules", new Link('wiki/rules'), "Rules");
+            $event->add_nav_link("wiki_help", new Link('ext_doc/wiki'), "Help");
+        }
+    }
+
     public function onWikiUpdate(WikiUpdateEvent $event)
     {
         global $database;
diff --git a/themes/danbooru/layout.class.php b/themes/danbooru/layout.class.php
index 42efe2a1..6be406a4 100644
--- a/themes/danbooru/layout.class.php
+++ b/themes/danbooru/layout.class.php
@@ -44,7 +44,7 @@ Tips
 
 class Layout
 {
-    public function display_page(Page $page)
+    public function display_page(Page $page, array $nav_links, array $sub_links)
     {
         global $config, $user;
 
@@ -96,84 +96,17 @@ class Layout
         $main_page = $config->get_string(SetupConfig::MAIN_PAGE); // bzchan: change from normal default to get main page for top of page
 
         $custom_links = "";
-        if ($user->is_anonymous()) {
-            $custom_links .= $this->navlinks(make_link('user_admin/login'), "My Account", ["user", "user_admin", "setup", "admin"]);
-        } else {
-            $custom_links .= $this->navlinks(make_link('user'), "My Account", ["user", "user_admin", "setup", "admin"]);
-        }
-        $custom_links .= $this->navlinks(make_link('post/list'), "Posts", ["post"]);
-        $custom_links .= $this->navlinks(make_link('comment/list'), "Comments", ["comment"]);
-        $custom_links .= $this->navlinks(make_link('tags'), "Tags", ["tags"]);
-        if (class_exists("Pools")) {
-            $custom_links .= $this->navlinks(make_link('pool/list'), "Pools", ["pool"]);
-        }
-        $custom_links .= $this->navlinks(make_link('upload'), "Upload", ["upload"]);
-        if (class_exists("Wiki")) {
-            $custom_links .= $this->navlinks(make_link('wiki'), "Wiki", ["wiki"]);
-            $custom_links .= $this->navlinks(make_link('wiki/more'), "More &raquo;", ["wiki/more"]);
+        foreach ($nav_links as $nav_link) {
+            $custom_links .=  "<li>".$this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)."</li>";
         }
 
         $custom_sublinks = "";
-        // hack
-        $username = url_escape($user->name);
-        // hack
-        $qp = explode("/", ltrim(_get_query(), "/"));
-        // php sucks
-        switch ($qp[0]) {
-            default:
-                $custom_sublinks .= $user_block_html;
-                break;
-            case "":
-                # FIXME: this assumes that the front page is
-                # post/list; in 99% of case it will either be
-                # post/list or home, and in the latter case
-                # the subnav links aren't shown, but it would
-                # be nice to be correct
-            case "post":
-            case "upload":
-                if (class_exists("NumericScore")) {
-                    $custom_sublinks .= "<li><b>Popular by </b><a href='".make_link('popular_by_day')."'>Day</a>/<a href='".make_link('popular_by_month')."'>Month</a>/<a href='".make_link('popular_by_year')."'>Year</a></li>";
-                }
-                $custom_sublinks .= "<li><a href='".make_link('post/list')."'>All</a></li>";
-                if (class_exists("Favorites")) {
-                    $custom_sublinks .= "<li><a href='".make_link("post/list/favorited_by={$username}/1")."'>My Favorites</a></li>";
-                }
-                if (class_exists("RSS_Images")) {
-                    $custom_sublinks .= "<li><a href='".make_link('rss/images')."'>Feed</a></li>";
-                }
-                if (class_exists("RandomImage")) {
-                    $custom_sublinks .= "<li><a href='".make_link("random_image/view")."'>Random Image</a></li>";
-                }
-                if (class_exists("Wiki")) {
-                    $custom_sublinks .= "<li><a href='".make_link("wiki/posts")."'>Help</a></li>";
-                } else {
-                    $custom_sublinks .= "<li><a href='".make_link("ext_doc/index")."'>Help</a></li>";
-                }
-                break;
-            case "comment":
-                $custom_sublinks .= "<li><a href='".make_link('comment/list')."'>All</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("ext_doc/comment")."'>Help</a></li>";
-                break;
-            case "pool":
-                $custom_sublinks .= "<li><a href='".make_link('pool/list')."'>List</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("pool/new")."'>Create</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("pool/updated")."'>Changes</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("ext_doc/pools")."'>Help</a></li>";
-                break;
-            case "wiki":
-                $custom_sublinks .= "<li><a href='".make_link('wiki')."'>Index</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("wiki/rules")."'>Rules</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("ext_doc/wiki")."'>Help</a></li>";
-                break;
-            case "tags":
-            case "alias":
-                $custom_sublinks .= "<li><a href='".make_link('tags/map')."'>Map</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link('tags/alphabetic')."'>Alphabetic</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link('tags/popularity')."'>Popularity</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link('tags/categories')."'>Categories</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link('alias/list')."'>Aliases</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("ext_doc/tag_edit")."'>Help</a></li>";
-                break;
+        if(!empty($sub_links)) {
+            $custom_sublinks = "<div class='sbar'>";
+            foreach ($sub_links as $nav_link) {
+                $custom_sublinks .= "<li>".$this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)."</li>";
+            }
+            $custom_sublinks .= "</div>";
         }
 
 
@@ -245,31 +178,16 @@ EOD;
     /**
      * #param string[] $pages_matched
      */
-    private function navlinks(string $link, string $desc, array $pages_matched): string
+
+    public function navlinks(Link $link, string $desc, bool $active): ?string
     {
-        /**
-         * Woo! We can actually SEE THE CURRENT PAGE!! (well... see it highlighted in the menu.)
-         */
         $html = null;
-        $url = ltrim(_get_query(), "/");
+        if ($active) {
+            $html = "<a class='current-page' href='{$link->make_link()}'>{$desc}</a>";
+        } else {
+            $html = "<a class='tab' href='{$link->make_link()}'>{$desc}</a>";
+        }
 
-        $re1='.*?';
-        $re2='((?:[a-z][a-z_]+))';
-
-        if (preg_match_all("/".$re1.$re2."/is", $url, $matches)) {
-            $url=$matches[1][0];
-        }
-        
-        $count_pages_matched = count($pages_matched);
-        
-        for ($i=0; $i < $count_pages_matched; $i++) {
-            if ($url == $pages_matched[$i]) {
-                $html = "<li class='current-page'><a href='$link'>$desc</a></li>";
-            }
-        }
-        if (is_null($html)) {
-            $html = "<li><a class='tab' href='$link'>$desc</a></li>";
-        }
         return $html;
     }
 }
diff --git a/themes/danbooru2/layout.class.php b/themes/danbooru2/layout.class.php
index e3d0116a..440d0c88 100644
--- a/themes/danbooru2/layout.class.php
+++ b/themes/danbooru2/layout.class.php
@@ -44,7 +44,7 @@ Tips
 
 class Layout
 {
-    public function display_page($page)
+    public function display_page($page, array $nav_links, array $sub_links)
     {
         global $config, $user;
 
@@ -96,110 +96,17 @@ class Layout
         $main_page = $config->get_string(SetupConfig::MAIN_PAGE); // bzchan: change from normal default to get main page for top of page
 
         $custom_links = "";
-        if ($user->is_anonymous()) {
-            $custom_links .= $this->navlinks(make_link('user_admin/login'), "Sign in", ["user", "user_admin", "setup", "admin"]);
-        } else {
-            $custom_links .= $this->navlinks(make_link('user'), "My Account", ["user", "user_admin"]);
-        }
-        if ($user->is_admin()) {
-            $custom_links .= $this->navlinks(make_link('admin'), "Admin", ["admin", "ext_manager", "setup"]);
-        }
-        $custom_links .= $this->navlinks(make_link('post/list'), "Posts", ["post", "upload", "", "random_image"]);
-        $custom_links .= $this->navlinks(make_link('comment/list'), "Comments", ["comment"]);
-        $custom_links .= $this->navlinks(make_link('tags'), "Tags", ["tags", "alias"]);
-        if (class_exists("Pools")) {
-            $custom_links .= $this->navlinks(make_link('pool/list'), "Pools", ["pool"]);
-        }
-        if (class_exists("Wiki")) {
-            $custom_links .= $this->navlinks(make_link('wiki'), "Wiki", ["wiki"]);
-            $custom_links .= $this->navlinks(make_link('wiki/more'), "More &raquo;", ["wiki/more"]);
+        foreach ($nav_links as $nav_link) {
+            $custom_links .=  "<li>".$this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)."</li>";
         }
 
         $custom_sublinks = "";
-        // hack
-        $username = url_escape($user->name);
-        // hack
-        $qp = explode("/", ltrim(_get_query(), "/"));
-        // php sucks
-        switch ($qp[0]) {
-            default:
-            case "ext_doc":
-                $custom_sublinks .= $user_block_html;
-                break;
-            case "user":
-            case "user_admin":
-                if ($user->is_anonymous()) {
-                    $custom_sublinks .= "<li><a href='".make_link('user_admin/create')."'>Sign up</a></li>";
-                // $custom_sublinks .= "<li><a href='".make_link('')."'>Reset Password</a></li>";
-                    // $custom_sublinks .= "<li><a href='".make_link('')."'>Login Reminder</a></li>";
-                } else {
-                    $custom_sublinks .= "<li><a href='".make_link('user_admin/logout')."'>Sign out</a></li>";
-                }
-                break;
-            case "":
-                # FIXME: this assumes that the front page is
-                # post/list; in 99% of case it will either be
-                # post/list or home, and in the latter case
-                # the subnav links aren't shown, but it would
-                # be nice to be correct
-            case "random_image":
-            case "post":
-            case "upload":
-                if (class_exists("NumericScore")) {
-                    $custom_sublinks .= "<li><b>Popular by </b><a href='".make_link('popular_by_day')."'>Day</a>/<a href='".make_link('popular_by_month')."'>Month</a>/<a href='".make_link('popular_by_year')."'>Year</a></li>";
-                }
-                $custom_sublinks .= "<li><a href='".make_link('post/list')."'>Listing</a></li>";
-                if (class_exists("Favorites")) {
-                    $custom_sublinks .= "<li><a href='".make_link("post/list/favorited_by={$username}/1")."'>My Favorites</a></li>";
-                }
-                if (class_exists("RSS_Images")) {
-                    $custom_sublinks .= "<li><a href='".make_link('rss/images')."'>Feed</a></li>";
-                }
-                if (class_exists("RandomImage")) {
-                    $custom_sublinks .= "<li><a href='".make_link("random_image/view")."'>Random</a></li>";
-                }
-                $custom_sublinks .= "<li><a href='".make_link('upload')."'>Upload</a></li>";
-                if (class_exists("Wiki")) {
-                    $custom_sublinks .= "<li><a href='".make_link("wiki/posts")."'>Help</a></li>";
-                } else {
-                    $custom_sublinks .= "<li><a href='".make_link("ext_doc/index")."'>Help</a></li>";
-                }
-                break;
-            case "comment":
-                $custom_sublinks .= "<li><a href='".make_link('comment/list')."'>All</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("ext_doc/comment")."'>Help</a></li>";
-                break;
-            case "pool":
-                $custom_sublinks .= "<li><a href='".make_link('pool/list')."'>List</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("pool/new")."'>Create</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("pool/updated")."'>Changes</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("ext_doc/pools")."'>Help</a></li>";
-                break;
-            case "wiki":
-                $custom_sublinks .= "<li><a href='".make_link('wiki')."'>Index</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("wiki/rules")."'>Rules</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("ext_doc/wiki")."'>Help</a></li>";
-                break;
-            case "tags":
-            case "alias":
-                $custom_sublinks .= "<li><a href='".make_link('tags/map')."'>Map</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link('tags/alphabetic')."'>Alphabetic</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link('tags/popularity')."'>Popularity</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link('tags/categories')."'>Categories</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link('alias/list')."'>Aliases</a></li>";
-                $custom_sublinks .= "<li><a href='".make_link("ext_doc/tag_edit")."'>Help</a></li>";
-                break;
-            case "admin":
-            case "ext_manager":
-            case "setup":
-                if ($user->is_admin()) {
-                    $custom_sublinks .= "<li><a href='".make_link('ext_manager')."'>Extension Manager</a></li>";
-                    $custom_sublinks .= "<li><a href='".make_link('setup')."'>Board Config</a></li>";
-                    $custom_sublinks .= "<li><a href='".make_link('alias/list')."'>Alias Editor</a></li>";
-                } else {
-                    $custom_sublinks .= "<li>I think you might be lost</li>";
-                }
-                break;
+        if(!empty($sub_links)) {
+            $custom_sublinks = "<div class='sbar'>";
+            foreach ($sub_links as $nav_link) {
+                $custom_sublinks .= "<li>".$this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)."</li>";
+            }
+            $custom_sublinks .= "</div>";
         }
 
 
@@ -268,28 +175,15 @@ $header_html
 EOD;
     }
 
-    private function navlinks(string $link, string $desc, array $pages_matched): string
+    public function navlinks(Link $link, string $desc, bool $active): ?string
     {
-        $html = "";
-        $url = _get_query();
+        $html = null;
+        if ($active) {
+            $html = "<a class='current-page' href='{$link->make_link()}'>{$desc}</a>";
+        } else {
+            $html = "<a class='tab' href='{$link->make_link()}'>{$desc}</a>";
+        }
 
-        $re1='.*?';
-        $re2='((?:[a-z][a-z_]+))';
-
-        if (preg_match_all("/".$re1.$re2."/is", $url, $matches)) {
-            $url=$matches[1][0];
-        }
-        
-        $count_pages_matched = count($pages_matched);
-        
-        for ($i=0; $i < $count_pages_matched; $i++) {
-            if ($url == $pages_matched[$i]) {
-                $html = "<li class='current-page'><a href='$link'>$desc</a></li>";
-            }
-        }
-        if (empty($html)) {
-            $html = "<li><a class='tab' href='$link'>$desc</a></li>";
-        }
         return $html;
     }
 }
diff --git a/themes/default/layout.class.php b/themes/default/layout.class.php
index 973c0bb0..c1468e7f 100644
--- a/themes/default/layout.class.php
+++ b/themes/default/layout.class.php
@@ -7,7 +7,7 @@ class Layout
     /**
      * turns the Page into HTML
      */
-    public function display_page(Page $page)
+    public function display_page(Page $page, array $nav_links)
     {
         global $config;
 
diff --git a/themes/lite/layout.class.php b/themes/lite/layout.class.php
index 3c1e1b2f..f11cbe70 100644
--- a/themes/lite/layout.class.php
+++ b/themes/lite/layout.class.php
@@ -9,7 +9,7 @@
 */
 class Layout
 {
-    public function display_page(Page $page)
+    public function display_page(Page $page, array $nav_links, array $sub_links)
     {
         global $config, $user;
 
@@ -23,24 +23,11 @@ class Layout
 			<script type='text/javascript' src='{$data_href}/themes/{$theme_name}/wz_tooltip.js'></script>
 			<a href='".make_link()."' onmouseover='Tip(&#39;Home&#39;, BGCOLOR, &#39;#C3D2E0&#39;, FADEIN, 100)' onmouseout='UnTip()'><img src='{$data_href}/favicon.ico' style='position: relative; top: 3px;'></a>
 			<b>{$site_name}</b> ";
-        
+
         // Custom links: These appear on the menu.
         $custom_links = "";
-        if ($user->is_anonymous()) {
-            $custom_links .= $this->navlinks(make_link('user_admin/login'), "Account", ["user", "user_admin", "setup", "admin", "profile"]);
-        } else {
-            $custom_links .= $this->navlinks(make_link('user'), "Account", ["user", "setup", "user_admin", "admin", "profile"]);
-        }
-        $custom_links .= $this->navlinks(make_link('post/list'), "Posts", ["post", "view"]);
-        $custom_links .= $this->navlinks(make_link('comment/list'), "Comments", ["comment"]);
-        $custom_links .= $this->navlinks(make_link('tags'), "Tags", ["tags"]);
-        if (class_exists("Pools")) {
-            $custom_links .= $this->navlinks(make_link('pool/list'), "Pools", ["pool"]);
-        }
-        $custom_links .= $this->navlinks(make_link('upload'), "Upload", ["upload"]);
-        if (class_exists("Wiki")) {
-            $custom_links .= $this->navlinks(make_link('wiki/rules'), "Rules", ["wiki/rules"]);
-            $custom_links .= $this->navlinks(make_link('wiki'), "Wiki", ["wiki"]);
+        foreach ($nav_links as $nav_link) {
+            $custom_links .= $this->navlinks($nav_link->link, $nav_link->description, $nav_link->active);
         }
         $menu .= "{$custom_links}</div>";
         
@@ -69,87 +56,13 @@ class Layout
             }
         }
 
-        $custom_sublinks = "<div class='sbar'>";
-        // hack
-        $username = url_escape($user->name);
-        // hack
-        $qp = explode("/", ltrim(_get_query(), "/"));
-        $cs = "";
-
-        // php sucks
-        switch ($qp[0]) {
-            default:
-                $cs = $user_block_html;
-                break;
-            case "":
-                # FIXME: this assumes that the front page is
-                # post/list; in 99% of case it will either be
-                # post/list or home, and in the latter case
-                # the subnav links aren't shown, but it would
-                # be nice to be correct
-            case "post":
-                if (class_exists("NumericScore")) {
-                    $cs .= "<b>Popular by </b><a href='".make_link('popular_by_day')."'>Day</a><b>/</b><a href='".make_link('popular_by_month')."'>Month</a><b>/</b><a href='".make_link('popular_by_year')."'>Year</a> ";
-                }
-                $cs .= "<a class='tab' href='".make_link('post/list')."'>All</a>";
-                if (class_exists("Favorites")) {
-                    $cs .= "<a class='tab' href='".make_link("post/list/favorited_by={$username}/1")."'>My Favorites</a>";
-                }
-                if (class_exists("RSS_Images")) {
-                    $cs .= "<a class='tab' href='".make_link('rss/images')."'>Feed</a>";
-                }
-                if (class_exists("Random_Image")) {
-                    $cs .= "<a class='tab' href='".make_link("random_image/view")."'>Random Image</a>";
-                }
-                if (class_exists("Wiki")) {
-                    $cs .= "<a class='tab' href='".make_link("wiki/posts")."'>Help</a>";
-                } else {
-                    $cs .= "<a class='tab' href='".make_link("ext_doc/index")."'>Help</a>";
-                }
-                break;
-            case "comment":
-                $cs .= "<a class='tab' href='".make_link('comment/list')."'>All</a>";
-                $cs .= "<a class='tab' href='".make_link('rss/comments')."'>Feed</a>";
-                $cs .= "<a class='tab' href='".make_link("ext_doc/comment")."'>Help</a>";
-                break;
-            case "pool":
-                $cs .= "<a class='tab' href='".make_link('pool/list')."'>List</a>";
-                $cs .= "<a class='tab' href='".make_link("pool/new")."'>Create</a>";
-                $cs .= "<a class='tab' href='".make_link("pool/updated")."'>Changes</a>";
-                $cs .= "<a class='tab' href='".make_link("ext_doc/pools")."'>Help</a>";
-                break;
-            case "wiki":
-                $cs .= "<a class='tab' href='".make_link('wiki')."'>Index</a>";
-                $cs .= "<a class='tab' href='".make_link("wiki/rules")."'>Rules</a>";
-                $cs .= "<a class='tab' href='".make_link("ext_doc/wiki")."'>Help</a>";
-                break;
-            case "tags":
-            case "alias":
-                $cs .= "<a class='tab' href='".make_link('tags/map')."'>Map</a>";
-                $cs .= "<a class='tab' href='".make_link('tags/alphabetic')."'>Alphabetic</a>";
-                $cs .= "<a class='tab' href='".make_link('tags/popularity')."'>Popularity</a>";
-                $cs .= "<a class='tab' href='".make_link('tags/categories')."'>Categories</a>";
-                $cs .= "<a class='tab' href='".make_link('alias/list')."'>Aliases</a>";
-                $cs .= "<a class='tab' href='".make_link("ext_doc/tag_edit")."'>Help</a>";
-                break;
-            case "upload":
-                if (class_exists("Wiki")) {
-                    $cs .= "<a class='tab' href='".make_link("wiki/upload_guidelines")."'>Guidelines</a>";
-                }
-                break;
-            case "random":
-                $cs .= "<a class='tab' href='".make_link('random/view')."'>Shuffle</a>";
-                $cs .= "<a class='tab' href='".make_link('random/download')."'>Download</a>";
-                break;
-            case "featured":
-                $cs .= "<a class='tab' href='".make_link('featured/download')."'>Download</a>";
-                break;
-        }
-
-        if ($cs == "") {
-            $custom_sublinks = "";
-        } else {
-            $custom_sublinks .= "$cs</div>";
+        $custom_sublinks = "";
+        if(!empty($sub_links)) {
+            $custom_sublinks = "<div class='sbar'>";
+            foreach ($sub_links as $nav_link) {
+                $custom_sublinks .= $this->navlinks($nav_link->link, $nav_link->description, $nav_link->active);
+            }
+            $custom_sublinks .= "</div>";
         }
 
         $debug = get_debug_info();
@@ -240,31 +153,13 @@ EOD;
     /**
      * #param string[] $pages_matched
      */
-    public function navlinks(string $link, string $desc, array $pages_matched): ?string
+    public function navlinks(Link $link, string $desc, bool $active): ?string
     {
-        /**
-         * Woo! We can actually SEE THE CURRENT PAGE!! (well... see it highlighted in the menu.)
-         */
         $html = null;
-        $url = ltrim(_get_query(), "/");
-
-        $re1='.*?';
-        $re2='((?:[a-z][a-z_]+))';
-
-        if (preg_match_all("/".$re1.$re2."/is", $url, $matches)) {
-            $url=$matches[1][0];
-        }
-
-        $count_pages_matched = count($pages_matched);
-
-        for ($i=0; $i < $count_pages_matched; $i++) {
-            if ($url == $pages_matched[$i]) {
-                $html = "<a class='tab-selected' href='{$link}'>{$desc}</a>";
-            }
-        }
-
-        if (is_null($html)) {
-            $html = "<a class='tab' href='{$link}'>{$desc}</a>";
+        if ($active) {
+            $html = "<a class='tab-selected' href='{$link->make_link()}'>{$desc}</a>";
+        } else {
+            $html = "<a class='tab' href='{$link->make_link()}'>{$desc}</a>";
         }
 
         return $html;

From a18589ee0a9d2f8809b6280af688621e7bd343b7 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Fri, 2 Aug 2019 15:05:49 -0500
Subject: [PATCH 10/10] Help extension

Provides foundation for help pages that are generated from loaded extensions, starting with comprehensive search documentation. Addresses #522
---
 core/sys_config.php                           |   2 +-
 ext/artists/main.php                          |  13 +-
 ext/artists/theme.php                         |  10 +
 ext/comment/main.php                          |  10 +
 ext/comment/theme.php                         |  24 +++
 ext/favorites/main.php                        |  10 +
 ext/favorites/theme.php                       |  24 +++
 .../baseline_help_outline_black_18dp.png      | Bin 0 -> 290 bytes
 ext/help_pages/main.php                       |  94 +++++++++
 ext/help_pages/style.css                      |  13 ++
 ext/help_pages/theme.php                      |  31 +++
 ext/index/main.php                            |  12 ++
 ext/index/theme.php                           | 197 ++++++++++++++++++
 ext/media/main.php                            |  11 +
 ext/media/theme.php                           |  16 ++
 ext/notes/main.php                            |  14 +-
 ext/notes/theme.php                           |  25 +++
 ext/numeric_score/main.php                    |  10 +
 ext/numeric_score/theme.php                   |  43 ++++
 ext/pools/main.php                            |  11 +
 ext/pools/theme.php                           |  28 +++
 ext/rating/main.php                           |  15 ++
 ext/rating/theme.php                          |  28 +++
 ext/relatationships/main.php                  |  11 +
 ext/relatationships/theme.php                 |  28 +++
 ext/tag_categories/main.php                   |  15 +-
 ext/tag_categories/theme.php                  |  17 ++
 ext/trash/main.php                            |  14 ++
 ext/trash/theme.php                           |  12 ++
 ext/user/main.php                             |  11 +
 ext/user/theme.php                            |  26 +++
 31 files changed, 769 insertions(+), 6 deletions(-)
 create mode 100644 ext/help_pages/baseline_help_outline_black_18dp.png
 create mode 100644 ext/help_pages/main.php
 create mode 100644 ext/help_pages/style.css
 create mode 100644 ext/help_pages/theme.php

diff --git a/core/sys_config.php b/core/sys_config.php
index 9eac5f59..5dc80459 100644
--- a/core/sys_config.php
+++ b/core/sys_config.php
@@ -40,7 +40,7 @@ _d("SEARCH_ACCEL", false);   // boolean  use search accelerator
 _d("WH_SPLITS", 1);          // int      how many levels of subfolders to put in the warehouse
 _d("VERSION", '2.7-beta');   // string   shimmie version
 _d("TIMEZONE", null);        // string   timezone
-_d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,handle_static,comment,tag_list,index,tag_edit,alias_editor,media,system"); // extensions to always enable
+_d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,handle_static,comment,tag_list,index,tag_edit,alias_editor,media,help_pages,system"); // extensions to always enable
 _d("EXTRA_EXTS", "");        // string   optional extra extensions
 _d("BASE_URL", null);        // string   force a specific base URL (default is auto-detect)
 _d("MIN_PHP_VERSION", '7.1');// string   minimum supported PHP version
diff --git a/ext/artists/main.php b/ext/artists/main.php
index 568933de..b1f6efcc 100644
--- a/ext/artists/main.php
+++ b/ext/artists/main.php
@@ -47,12 +47,23 @@ class Artists extends Extension
     public function onSearchTermParse(SearchTermParseEvent $event)
     {
         $matches = [];
-        if (preg_match("/^author[=|:](.*)$/i", $event->term, $matches)) {
+        if (preg_match("/^(author|artist)[=|:](.*)$/i", $event->term, $matches)) {
             $char = $matches[1];
             $event->add_querylet(new Querylet("Author = :author_char", ["author_char"=>$char]));
         }
     }
 
+    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+    {
+        if($event->key===HelpPages::SEARCH) {
+            $block = new Block();
+            $block->header = "Artist";
+            $block->body = $this->theme->get_help_html();
+            $event->add_block($block);
+        }
+    }
+
+
     public function onInitExt(InitExtEvent $event)
     {
         global $config, $database;
diff --git a/ext/artists/theme.php b/ext/artists/theme.php
index aebd2757..1e5e5afb 100644
--- a/ext/artists/theme.php
+++ b/ext/artists/theme.php
@@ -545,4 +545,14 @@ class ArtistsTheme extends Themelet
         }
         return $html;
     }
+
+    public function get_help_html()
+    {
+        return '<p>Search for images with a particular artist.</p>
+        <div class="command_example">
+        <pre>artist=leonardo</pre>
+        <p>Returns images with the artist "leonardo".</p>
+        </div> 
+        ';
+    }
 }
diff --git a/ext/comment/main.php b/ext/comment/main.php
index 7ea23d37..f58a3076 100644
--- a/ext/comment/main.php
+++ b/ext/comment/main.php
@@ -366,6 +366,16 @@ class CommentList extends Extension
         }
     }
 
+    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+    {
+        if($event->key===HelpPages::SEARCH) {
+            $block = new Block();
+            $block->header = "Comments";
+            $block->body = $this->theme->get_help_html();
+            $event->add_block($block);
+        }
+    }
+
     // page building {{{
     private function build_page(int $current_page)
     {
diff --git a/ext/comment/theme.php b/ext/comment/theme.php
index 1bdc70cb..7e62da6b 100644
--- a/ext/comment/theme.php
+++ b/ext/comment/theme.php
@@ -290,4 +290,28 @@ class CommentListTheme extends Themelet
 		</div>
 		';
     }
+
+    public function get_help_html()
+    {
+        return '<p>Search for images containing a certain number of comments, or comments by a particular individual.</p>
+        <div class="command_example">
+        <pre>comments=1</pre>
+        <p>Returns images with exactly 1 comment.</p>
+        </div> 
+        <div class="command_example">
+        <pre>comments>0</pre>
+        <p>Returns images with 1 or more comments. </p>
+        </div>
+        <p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
+        <div class="command_example">
+        <pre>commented_by:username</pre>
+        <p>Returns images that have been commented on by "username". </p>
+        </div>
+        <div class="command_example">
+        <pre>commented_by_userno:123</pre>
+        <p>Returns images that have been commented on by user 123. </p>
+        </div>
+        ';
+
+    }
 }
diff --git a/ext/favorites/main.php b/ext/favorites/main.php
index 91ac1c2d..950d6178 100644
--- a/ext/favorites/main.php
+++ b/ext/favorites/main.php
@@ -155,6 +155,16 @@ class Favorites extends Extension
         }
     }
 
+    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+    {
+        if($event->key===HelpPages::SEARCH) {
+            $block = new Block();
+            $block->header = "Favorites";
+            $block->body = $this->theme->get_help_html();
+            $event->add_block($block);
+        }
+    }
+
     public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
     {
         global $user;
diff --git a/ext/favorites/theme.php b/ext/favorites/theme.php
index 89509ce2..22367347 100644
--- a/ext/favorites/theme.php
+++ b/ext/favorites/theme.php
@@ -34,4 +34,28 @@ class FavoritesTheme extends Themelet
 
         $page->add_block(new Block("Favorited By", $html, "left", 25));
     }
+
+    public function get_help_html()
+    {
+        return '<p>Search for images that have been favorited a certain number of times, or favorited by a particular individual.</p>
+        <div class="command_example">
+        <pre>favorites=1</pre>
+        <p>Returns images that have been favorited once.</p> 
+        </div>
+        <div class="command_example">
+        <pre>favorites>0</pre>
+        <p>Returns images that have been favorited 1 or more times</p>
+        </div>
+        <p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
+        <div class="command_example">
+        <pre>favorited_by:username</pre>
+        <p>Returns images that have been favorited by "username". </p>
+        </div>
+        <div class="command_example">
+        <pre>favorited_by_userno:123</pre>
+        <p>Returns images that have been favorited by user 123. </p>
+        </div>
+        ';
+
+    }
 }
diff --git a/ext/help_pages/baseline_help_outline_black_18dp.png b/ext/help_pages/baseline_help_outline_black_18dp.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6126844b84d7153d5561010b0e0a77c353434c8
GIT binary patch
literal 290
zcmV+-0p0$IP)<h;3K|Lk000e1NJLTq000sI000sQ1ONa4{KrPz0002#Nkl<Zcmb5r
zB|;zo5J1t_*L^9tySqgy)@f*i$`$a6Yv%%F>;g1gBJS?apEPbh&UeKcG${7nv-~&e
z^q!3Q=MfPW?dcK<LUq}*DB@w2B}-!QM~xbP5PNA00ASFud?Z-CY_%!@d5#SNd~>1~
zOj-2TUkj!|j}za3GN-aZvl7s13uHT02{bzO)g3|RjDxREH3CgeeFhL~!>k|Rvr|n#
z#cez2x8o-u%c+XTgF})u0KIpEGvGunV8R3-$B6*|{#mjt8dRwQ(U-@3@S3n^-xBT-
o|BSu$;Iko;N-hwBk_xE+087+T346!Kga7~l07*qoM6N<$f-#MEa{vGU

literal 0
HcmV?d00001

diff --git a/ext/help_pages/main.php b/ext/help_pages/main.php
new file mode 100644
index 00000000..d2821b00
--- /dev/null
+++ b/ext/help_pages/main.php
@@ -0,0 +1,94 @@
+<?php
+/**
+ * Name: Help Pages
+ * Author: Matthew Barbour <matthew@darkholme.net>
+ * License: MIT
+ * Description: Provides documentation screens
+ */
+
+class HelpPageListBuildingEvent extends Event
+{
+    public $pages = [];
+
+    public function add_page(string $key, string $name)
+    {
+        $this->pages[$key] = $name;
+    }
+
+}
+
+class HelpPageBuildingEvent extends Event
+{
+    public $key;
+    public $blocks = [];
+
+    public function __construct(string $key)
+    {
+        $this->key = $key;
+    }
+
+    function add_block(Block $block, int $position = 50)
+    {
+        if(!array_key_exists("$position",$this->blocks))
+        {
+            $this->blocks["$position"] = [];
+        }
+        $this->blocks["$position"][] = $block;
+    }
+}
+
+class HelpPages extends Extension
+{
+    public const SEARCH = "search";
+
+    public function onPageRequest(PageRequestEvent $event)
+    {
+        global $page, $user;
+
+        if ($event->page_matches("help")) {
+            $e = new HelpPageListBuildingEvent();
+            send_event($e);
+            $page->set_mode(PageMode::PAGE);
+
+            if ($event->count_args() == 0) {
+                $this->theme->display_list_page($e->pages);
+            } else {
+                $name = $event->get_arg(0);
+                $title = $name;
+                if(array_key_exists($name, $e->pages)) {
+                    $title = $e->pages[$name];
+                }
+
+                $this->theme->display_help_page($title);
+
+                $hpbe = new HelpPageBuildingEvent($name);
+                send_event($hpbe);
+                asort($hpbe->blocks);
+
+                foreach ($hpbe->blocks as $key=>$value) {
+                    foreach($value as $block) {
+                        $page->add_block($block);
+                    }
+                }
+            }
+        }
+    }
+
+    public function onHelpPageListBuilding(HelpPageListBuildingEvent $event)
+    {
+        $event->add_page("search", "Searching");
+    }
+
+    public function onPageNavBuilding(PageNavBuildingEvent $event)
+    {
+        $event->add_nav_link("help", new Link('help'), "Help");
+    }
+
+    public function onUserBlockBuilding(UserBlockBuildingEvent $event)
+    {
+        global $user;
+        $event->add_link("Help", make_link("help"));
+    }
+
+
+}
diff --git a/ext/help_pages/style.css b/ext/help_pages/style.css
new file mode 100644
index 00000000..85a40f5b
--- /dev/null
+++ b/ext/help_pages/style.css
@@ -0,0 +1,13 @@
+.command_example {
+	margin: 12pt;
+	padding-left: 16pt;
+}
+
+.command_example pre {
+	padding:4pt;
+	border: dashed 2px black;
+}
+
+.command_example p {
+	padding-left: 16pt;
+}
\ No newline at end of file
diff --git a/ext/help_pages/theme.php b/ext/help_pages/theme.php
new file mode 100644
index 00000000..2b3b7818
--- /dev/null
+++ b/ext/help_pages/theme.php
@@ -0,0 +1,31 @@
+<?php
+
+class HelpPagesTheme extends Themelet
+{
+
+    public function display_list_page(array $pages)
+    {
+        global $page;
+
+        $page->set_title("Help Pages");
+        $page->set_heading("Help Pages");
+
+        $nav_block = new Block("Help", "", "left", 0);
+        foreach ($pages as $link=>$desc) {
+            $link = make_link("help/{$link}");
+            $nav_block->body .= "<a href='{$link}'>".html_escape($desc)."</a><br/>";
+        }
+
+        $page->add_block($nav_block);
+        $page->add_block(new Block("Help Pages", "See list of pages to left"));
+    }
+
+    public function display_help_page(String $title)
+    {
+        global $page;
+
+        $page->set_title("Help - $title");
+        $page->set_heading("Help - $title");
+    }
+
+}
diff --git a/ext/index/main.php b/ext/index/main.php
index 80c4b610..9bf22091 100644
--- a/ext/index/main.php
+++ b/ext/index/main.php
@@ -344,6 +344,17 @@ class Index extends Extension
         }
     }
 
+    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+    {
+        if($event->key===HelpPages::SEARCH) {
+            $block = new Block();
+            $block->header = "General";
+            $block->body = $this->theme->get_help_html();
+            $event->add_block($block, 0);
+        }
+    }
+
+
     public function onSearchTermParse(SearchTermParseEvent $event)
     {
         $matches = [];
@@ -392,6 +403,7 @@ class Index extends Extension
                 $event->add_querylet(new Querylet('images.source LIKE :src', ["src"=>"%$source%"]));
             }
         } elseif (preg_match("/^posted([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])([0-9-]*)$/i", $event->term, $matches)) {
+            // TODO Make this able to search = without needing a time component.
             $cmp = ltrim($matches[1], ":") ?: "=";
             $val = $matches[2];
             $event->add_querylet(new Querylet("images.posted $cmp :posted{$this->stpen}", ["posted{$this->stpen}"=>$val]));
diff --git a/ext/index/theme.php b/ext/index/theme.php
index f04e124c..e216ea06 100644
--- a/ext/index/theme.php
+++ b/ext/index/theme.php
@@ -144,4 +144,201 @@ and of course start organising your images :-)
             $this->display_paginator($page, "post/list", null, $this->page_number, $this->total_pages, true);
         }
     }
+
+    public function get_help_html()
+    {
+        return '<p>Searching is largely based on tags, with a number of special keywords available that allow searching based on properties of the images.</p>
+
+        <div class="command_example">
+        <pre>tagname</pre>
+        <p>Returns images that are tagged with "tagname".</p>
+        </div>
+         
+        <div class="command_example">
+        <pre>tagname othertagname</pre>
+        <p>Returns images that are tagged with "tagname" and "othertagname".</p> 
+        </div>
+
+        <p>Most tags and keywords can be prefaced with a negative sign (-) to indicate that you want to search for images that do not match something.</p>
+
+        <div class="command_example">
+        <pre>-tagname</pre>
+        <p>Returns images that are not tagged with "tagname".</p> 
+        </div>
+
+        <div class="command_example">
+        <pre>-tagname -othertagname</pre>
+        <p>Returns images that are not tagged with "tagname" and "othertagname". This is different than without the negative sign, as images with "tagname" or "othertagname" can still be returned as long as the other one is not present.</p> 
+        </div>
+
+        <div class="command_example">
+        <pre>tagname -othertagname</pre>
+        <p>Returns images that are tagged with "tagname", but are not tagged with "othertagname".</p> 
+        </div>
+
+        <p>Wildcard searches are possible as well using * for "any one, more, or none" and ? for "any one".</p>
+
+        <div class="command_example">
+        <pre>tagn*</pre>
+        <p>Returns images that are tagged with "tagname", "tagnot", or anything else that starts with "tagn".</p> 
+        </div>
+
+        <div class="command_example">
+        <pre>tagn?me</pre>
+        <p>Returns images that are tagged with "tagname", "tagnome", or anything else that starts with "tagn", has one character, and ends with "me".</p> 
+        </div>
+
+        <div class="command_example">
+        <pre>tags=1</pre>
+        <p>Returns images with exactly 1 tag.</p> 
+        </div>
+
+        <div class="command_example">
+        <pre>tags>0</pre>
+        <p>Returns images with 1 or more tags. </p>
+        </div>
+
+        <p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
+
+        <hr/>
+        
+        <p>Search for images by aspect ratio</p>
+        
+        <div class="command_example">
+        <pre>ratio=4:3</pre>
+        <p>Returns images with an aspect ratio of 4:3.</p> 
+        </div>
+
+        <div class="command_example">
+        <pre>ratio>16:9</pre>
+        <p>Returns images with an aspect ratio greater than 16:9. </p>
+        </div>
+
+        <p>Can use &lt;, &lt;=, &gt;, &gt;=, or =. The relation is calculated by dividing width by height.</p>
+        
+        <hr/>
+
+        <p>Search for images by file size</p>
+        
+        <div class="command_example">
+        <pre>filesize=1</pre>
+        <p>Returns images exactly 1 byte in size.</p> 
+        </div>
+
+        <div class="command_example">
+        <pre>filesize>100mb</pre>
+        <p>Returns images greater than 100 megabytes in size. </p>
+        </div>
+
+        <p>Can use &lt;, &lt;=, &gt;, &gt;=, or =. Supported suffixes are kb, mb, and gb. Uses multiples of 1024.</p>
+        
+        <hr/>
+
+        <p>Search for images by MD5 hash</p>
+        
+        <div class="command_example">
+        <pre>hash=0D3512CAA964B2BA5D7851AF5951F33B</pre>
+        <p>Returns image with an MD5 hash 0D3512CAA964B2BA5D7851AF5951F33B.</p> 
+        </div>
+
+        <hr/>
+
+        <p>Search for images by file type</p>
+        
+        <div class="command_example">
+        <pre>filetype=jpg</pre>
+        <p>Returns images that are of type "jpg".</p> 
+        </div>
+
+        <hr/>
+
+        <p>Search for images by file name</p>
+        
+        <div class="command_example">
+        <pre>filename=picasso.jpg</pre>
+        <p>Returns images that are named "picasso.jpg".</p> 
+        </div>
+
+        <hr/>
+
+        <p>Search for images by source</p>
+        
+        <div class="command_example">
+        <pre>source=http://google.com/</pre>
+        <p>Returns images with a source of "http://google.com/".</p> 
+        </div>
+
+        <div class="command_example">
+        <pre>source=any</pre>
+        <p>Returns images with a source set.</p> 
+        </div>
+
+        <div class="command_example">
+        <pre>source=none</pre>
+        <p>Returns images without a source set.</p> 
+        </div>
+        
+        <hr/>
+
+        <p>Search for images by date posted.</p>
+
+        <div class="command_example">
+        <pre>posted>=07-19-2019</pre>
+        <p>Returns images posted on or after 07-19-2019.</p> 
+        </div>
+
+        <p>Can use &lt;, &lt;=, &gt;, &gt;=, or =. Date format is mm-dd-yyyy. Date posted includes time component, so = will not work unless the time is exact.</p>
+        
+        <hr/>
+
+        <p>Search for images by image dimensions</p>
+
+        <div class="command_example">
+        <pre>size=640x480</pre>
+        <p>Returns images exactly 640 pixels wide by 480 pixels high.</p> 
+        </div>
+
+        <div class="command_example">
+        <pre>size>1920x1080</pre>
+        <p>Returns images with a width larger than 1920 and a height larger than 1080.</p>
+        </div>
+
+        <div class="command_example">
+        <pre>width=1000</pre>
+        <p>Returns images exactly 1000 pixels wide.</p>
+        </div>
+
+        <div class="command_example">
+        <pre>height=1000</pre>
+        <p>Returns images exactly 1000 pixels high.</p>
+        </div>
+
+        <p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
+        
+        <hr/>
+        
+        <p>Sorting search results can be done using the pattern order:field_direction. _direction can be either _asc or _desc, indicating ascending (123) or descending (321) order.</p>
+        
+        <div class="command_example">
+        <pre>order:id_asc</pre>
+        <p>Returns images sorted by ID, smallest first.</p> 
+        </div>
+
+        <div class="command_example">
+        <pre>order:width_desc</pre>
+        <p>Returns images sorted by width, largest first.</p>
+        </div>
+        
+        <p>These fields are supported:
+            <ul>
+            <li>id</li>
+            <li>width</li>
+            <li>height</li>
+            <li>filesize</li>
+            <li>filename</li>
+            </ul>
+        </p>
+        ';
+
+    }
 }
diff --git a/ext/media/main.php b/ext/media/main.php
index acce17ab..012507b3 100644
--- a/ext/media/main.php
+++ b/ext/media/main.php
@@ -419,6 +419,17 @@ class Media extends Extension
         }
     }
 
+    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+    {
+        if($event->key===HelpPages::SEARCH) {
+            $block = new Block();
+            $block->header = "Media";
+            $block->body = $this->theme->get_help_html();
+            $event->add_block($block);
+        }
+    }
+
+
     public function onTagTermParse(TagTermParseEvent $event)
     {
         $matches = [];
diff --git a/ext/media/theme.php b/ext/media/theme.php
index 12d77f32..7871ee69 100644
--- a/ext/media/theme.php
+++ b/ext/media/theme.php
@@ -28,4 +28,20 @@ class MediaTheme extends Themelet
 			</form>
 		";
     }
+
+    public function get_help_html()
+    {
+        return '<p>Search for items based on the type of media.</p>
+        <div class="command_example">
+        <pre>content:audio</pre>
+        <p>Returns items that contain audio, including videos and audio files.</p>
+        </div> 
+        <div class="command_example">
+        <pre>content:video</pre>
+        <p>Returns items that contain video, including animated GIFs.</p>
+        </div>
+        <p>These search terms depend on the items being scanned for media content. Automatic scanning was implemented in mid-2019, so items uploaded before, or items uploaded on a system without ffmpeg, will require additional scanning before this will work.</p> 
+        ';
+
+    }
 }
diff --git a/ext/notes/main.php b/ext/notes/main.php
index aafa928f..18c45f82 100644
--- a/ext/notes/main.php
+++ b/ext/notes/main.php
@@ -210,12 +210,22 @@ class Notes extends Extension
             }
 
             $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM notes WHERE user_id = $user_id)"));
-        } elseif (preg_match("/^notes_by_userno[=|:](\d+)$/i", $event->term, $matches)) {
-            $user_id = int_escape($matches[1]);
+        } elseif (preg_match("/^(notes_by_userno|notes_by_user_id)[=|:](\d+)$/i", $event->term, $matches)) {
+            $user_id = int_escape($matches[2]);
             $event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM notes WHERE user_id = $user_id)"));
         }
     }
 
+    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+    {
+        if($event->key===HelpPages::SEARCH) {
+            $block = new Block();
+            $block->header = "Notes";
+            $block->body = $this->theme->get_help_html();
+            $event->add_block($block);
+        }
+    }
+
 
     /**
      * HERE WE GET ALL NOTES FOR DISPLAYED IMAGE.
diff --git a/ext/notes/theme.php b/ext/notes/theme.php
index 912ec615..6878c9c2 100644
--- a/ext/notes/theme.php
+++ b/ext/notes/theme.php
@@ -247,4 +247,29 @@ class NotesTheme extends Themelet
 
         $this->display_paginator($page, "note/updated", null, $pageNumber, $totalPages);
     }
+
+    public function get_help_html()
+    {
+        return '<p>Search for images with notes.</p>
+        <div class="command_example">
+        <pre>note=noted</pre>
+        <p>Returns images with a note matching "noted".</p>
+        </div> 
+        <div class="command_example">
+        <pre>notes>0</pre>
+        <p>Returns images with 1 or more notes.</p>
+        </div>
+        <p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
+        <div class="command_example">
+        <pre>notes_by=username</pre>
+        <p>Returns images with note(s) by "username".</p>
+        </div>
+        <div class="command_example">
+        <pre>notes_by_user_id=123</pre>
+        <p>Returns images with note(s) by user 123.</p>
+        </div>
+        ';
+
+    }
+
 }
diff --git a/ext/numeric_score/main.php b/ext/numeric_score/main.php
index 84179f7c..0b93c369 100644
--- a/ext/numeric_score/main.php
+++ b/ext/numeric_score/main.php
@@ -228,6 +228,16 @@ class NumericScore extends Extension
         $event->replace('$score', $event->image->numeric_score);
     }
 
+    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+    {
+        if($event->key===HelpPages::SEARCH) {
+            $block = new Block();
+            $block->header = "Numeric Score";
+            $block->body = $this->theme->get_help_html();
+            $event->add_block($block);
+        }
+    }
+
     public function onSearchTermParse(SearchTermParseEvent $event)
     {
         $matches = [];
diff --git a/ext/numeric_score/theme.php b/ext/numeric_score/theme.php
index 05199b11..e6dbe7fa 100644
--- a/ext/numeric_score/theme.php
+++ b/ext/numeric_score/theme.php
@@ -91,4 +91,47 @@ class NumericScoreTheme extends Themelet
         $page->add_block(new Block("Navigation", $nav_html, "left", 10));
         $page->add_block(new Block(null, $html, "main", 30));
     }
+
+
+    public function get_help_html()
+    {
+        return '<p>Search for images that have received numeric scores by the score or by the scorer.</p>
+        <div class="command_example">
+        <pre>score=1</pre>
+        <p>Returns images with a score of 1.</p>
+        </div> 
+        <div class="command_example">
+        <pre>score>0</pre>
+        <p>Returns images with a score of 1 or more.</p>
+        </div>
+        <p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
+
+        <div class="command_example">
+        <pre>upvoted_by=username</pre>
+        <p>Returns images upvoted by "username".</p>
+        </div>
+        <div class="command_example">
+        <pre>upvoted_by_id=123</pre>
+        <p>Returns images upvoted by user 123.</p>
+        </div>
+        <div class="command_example">
+        <pre>downvoted_by=username</pre>
+        <p>Returns images downvoted by "username".</p>
+        </div>
+        <div class="command_example">
+        <pre>downvoted_by_id=123</pre>
+        <p>Returns images downvoted by user 123.</p>
+        </div>
+
+        <div class="command_example">
+        <pre>order:score_desc</pre>
+        <p>Sorts the search results by score, descending.</p>
+        </div>
+        <div class="command_example">
+        <pre>order:score_asc</pre>
+        <p>Sorts the search results by score, ascending.</p>
+        </div>
+        ';
+
+    }
 }
diff --git a/ext/pools/main.php b/ext/pools/main.php
index cbdf5164..71d3f492 100644
--- a/ext/pools/main.php
+++ b/ext/pools/main.php
@@ -372,6 +372,17 @@ class Pools extends Extension
         }
     }
 
+    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+    {
+        if($event->key===HelpPages::SEARCH) {
+            $block = new Block();
+            $block->header = "Pools";
+            $block->body = $this->theme->get_help_html();
+            $event->add_block($block);
+        }
+    }
+
+
     public function onSearchTermParse(SearchTermParseEvent $event)
     {
         $matches = [];
diff --git a/ext/pools/theme.php b/ext/pools/theme.php
index bb798c8d..86a2406c 100644
--- a/ext/pools/theme.php
+++ b/ext/pools/theme.php
@@ -403,4 +403,32 @@ class PoolsTheme extends Themelet
     {
         return "<input type='text' name='bulk_pool_new' placeholder='New pool' required='required' value='".(implode(" ",$search_terms))."' />";
     }
+
+
+    public function get_help_html()
+    {
+        return '<p>Search for images that are in a pool.</p>
+        <div class="command_example">
+        <pre>pool=1</pre>
+        <p>Returns images in pool #1.</p> 
+        </div>
+        <div class="command_example">
+        <pre>pool=any</pre>
+        <p>Returns images in any pool.</p>
+        </div> 
+        <div class="command_example">
+        <pre>pool=none</pre>
+        <p>Returns images not in any pool.</p>
+        </div>
+        <div class="command_example">
+        <pre>pool_by_name=swimming</pre>
+        <p>Returns images in the "swimming" pool.</p>
+        </div>
+        <div class="command_example">
+        <pre>pool_by_name=swimming_pool</pre>
+        <p>Returns images in the "swimming pool" pool. Note that the underscore becomes a space</p>
+        </div>
+        ';
+
+    }
 }
diff --git a/ext/rating/main.php b/ext/rating/main.php
index c62fd03e..e4dc0180 100644
--- a/ext/rating/main.php
+++ b/ext/rating/main.php
@@ -126,6 +126,21 @@ class Ratings extends Extension
         $event->replace('$rating', $this->rating_to_human($event->image->rating));
     }
 
+    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+    {
+        global $user;
+
+        if($event->key===HelpPages::SEARCH) {
+            $block = new Block();
+            $block->header = "Ratings";
+
+            $ratings = self::get_sorted_ratings();
+
+            $block->body = $this->theme->get_help_html($ratings);
+            $event->add_block($block);
+        }
+    }
+
     public function onSearchTermParse(SearchTermParseEvent $event)
     {
         global $user;
diff --git a/ext/rating/theme.php b/ext/rating/theme.php
index d414e3f6..faec02dc 100644
--- a/ext/rating/theme.php
+++ b/ext/rating/theme.php
@@ -55,4 +55,32 @@ class RatingsTheme extends Themelet
 					<option value='u'>Unrated</option>
 				</select>";
     }
+
+    public function get_help_html(array $ratings)
+    {
+        $output =  '<p>Search for images with one or more possible ratings.</p>
+        <div class="command_example">
+        <pre>rating:'.$ratings[0]->search_term.'</pre>
+        <p>Returns images with the '.$ratings[0]->name.' rating.</p>
+        </div> 
+        <p>Ratings can be abbreviated to a single letter as well</p>
+        <div class="command_example">
+        <pre>rating:'.$ratings[0]->code.'</pre>
+        <p>Returns images with the '.$ratings[0]->name.' rating.</p>
+        </div> 
+        <p>If abbreviations are used, multiple ratings can be searched for.</p>
+        <div class="command_example">
+        <pre>rating:'.$ratings[0]->code.$ratings[1]->code.'</pre>
+        <p>Returns images with the '.$ratings[0]->name.' or '.$ratings[1]->name.' rating.</p>
+        </div> 
+        <p>Available ratings:</p>
+        <table>
+        <tr><th>Name</th><th>Search Term</th><th>Abbreviation</th></tr>
+        ';
+        foreach ($ratings as $rating) {
+            $output .= "<tr><td>{$rating->name}</td><td>{$rating->search_term}</td><td>{$rating->code}</td></tr>";
+        }
+        $output .= "</table>";
+        return $output;
+    }
 }
diff --git a/ext/relatationships/main.php b/ext/relatationships/main.php
index eaae08b9..905bc1e8 100644
--- a/ext/relatationships/main.php
+++ b/ext/relatationships/main.php
@@ -81,6 +81,17 @@ class Relationships extends Extension
         }
     }
 
+    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+    {
+        if($event->key===HelpPages::SEARCH) {
+            $block = new Block();
+            $block->header = "Relationships";
+            $block->body = $this->theme->get_help_html();
+            $event->add_block($block);
+        }
+    }
+
+
     public function onTagTermParse(TagTermParseEvent $event)
     {
         $matches = [];
diff --git a/ext/relatationships/theme.php b/ext/relatationships/theme.php
index 15c92310..40c0fe8c 100644
--- a/ext/relatationships/theme.php
+++ b/ext/relatationships/theme.php
@@ -46,4 +46,32 @@ class RelationshipsTheme extends Themelet
                 "</tr>\n";
         return $html;
     }
+
+
+    public function get_help_html()
+    {
+        return '<p>Search for images that have parent/child relationships.</p>
+        <div class="command_example">
+        <pre>parent=any</pre>
+        <p>Returns images that have a parent.</p>
+        </div> 
+        <div class="command_example">
+        <pre>parent=none</pre>
+        <p>Returns images that have no parent.</p>
+        </div> 
+        <div class="command_example">
+        <pre>parent=123</pre>
+        <p>Returns images that have image 123 set as parent.</p>
+        </div> 
+        <div class="command_example">
+        <pre>child=any</pre>
+        <p>Returns images that have at least 1 child.</p>
+        </div> 
+        <div class="command_example">
+        <pre>child=none</pre>
+        <p>Returns images that have no children.</p>
+        </div> 
+        ';
+
+    }
 }
diff --git a/ext/tag_categories/main.php b/ext/tag_categories/main.php
index 3002278b..95e1caeb 100644
--- a/ext/tag_categories/main.php
+++ b/ext/tag_categories/main.php
@@ -70,11 +70,12 @@ class TagCategories extends Extension
 
         if (preg_match("/^(.+)tags([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])([0-9]+)$/i", $event->term, $matches)) {
             global $database;
-            $type = $matches[1];
+            $type = strtolower($matches[1]);
             $cmp = ltrim($matches[2], ":") ?: "=";
             $count = $matches[3];
 
-            $types = $database->get_col('SELECT category FROM image_tag_categories');
+            $types = $database->get_col(
+                $database->scoreql_to_sql('SELECT SCORE_STRNORM(category) FROM image_tag_categories'));
             if (in_array($type, $types)) {
                 $event->add_querylet(
                     new Querylet($database->scoreql_to_sql("EXISTS (
@@ -90,6 +91,16 @@ class TagCategories extends Extension
         }
     }
 
+    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+    {
+        if($event->key===HelpPages::SEARCH) {
+            $block = new Block();
+            $block->header = "Tag Categories";
+            $block->body = $this->theme->get_help_html();
+            $event->add_block($block);
+        }
+    }
+
     public function getDict()
     {
         global $database;
diff --git a/ext/tag_categories/theme.php b/ext/tag_categories/theme.php
index df09beaf..c1894a27 100644
--- a/ext/tag_categories/theme.php
+++ b/ext/tag_categories/theme.php
@@ -98,4 +98,21 @@ class TagCategoriesTheme extends Themelet
         // add html to stuffs
         $page->add_block(new Block("Editing", $html, "main", 10));
     }
+
+    public function get_help_html()
+    {
+        return '<p>Search for images containing a certain number of tags with the specified tag category.</p>
+        <div class="command_example">
+        <pre>persontags=1</pre>
+        <p>Returns images with exactly 1 tag with the tag category "person".</p>
+        </div> 
+        <div class="command_example">
+        <pre>cattags>0</pre>
+        <p>Returns images with 1 or more tags with the tag category "cat". </p>
+        </div>
+        <p>Can use &lt;, &lt;=, &gt;, &gt;=, or =.</p>
+        <p>Category name is not case sensitive, category must exist for search to work.</p>
+        ';
+
+    }
 }
diff --git a/ext/trash/main.php b/ext/trash/main.php
index bb1b766f..e8732c64 100644
--- a/ext/trash/main.php
+++ b/ext/trash/main.php
@@ -93,6 +93,20 @@ class Trash extends Extension
         }
     }
 
+    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+    {
+        global $user;
+        if($event->key===HelpPages::SEARCH) {
+            if($user->can(Permissions::VIEW_TRASH)) {
+                $block = new Block();
+                $block->header = "Trash";
+                $block->body = $this->theme->get_help_html();
+                $event->add_block($block);
+            }
+        }
+    }
+
+
     private function no_trash_query(array $context): bool
     {
         foreach ($context as $term) {
diff --git a/ext/trash/theme.php b/ext/trash/theme.php
index 5e4e2e15..3211aa1f 100644
--- a/ext/trash/theme.php
+++ b/ext/trash/theme.php
@@ -11,4 +11,16 @@ class TrashTheme extends Themelet
 		";
 
         return $html;    }
+
+
+    public function get_help_html()
+    {
+        return '<p>Search for images in the trash.</p>
+        <div class="command_example">
+        <pre>in:trash</pre>
+        <p>Returns images that are in the trash.</p>
+        </div> 
+        ';
+
+    }
 }
diff --git a/ext/user/main.php b/ext/user/main.php
index 65280272..4bb726d0 100644
--- a/ext/user/main.php
+++ b/ext/user/main.php
@@ -364,6 +364,17 @@ class UserPage extends Extension
         }
     }
 
+    public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+    {
+        if($event->key===HelpPages::SEARCH) {
+            $block = new Block();
+            $block->header = "Users";
+            $block->body = $this->theme->get_help_html();
+            $event->add_block($block);
+        }
+    }
+
+
     private function show_user_info()
     {
         global $user, $page;
diff --git a/ext/user/theme.php b/ext/user/theme.php
index c773dd0e..dab26e8c 100644
--- a/ext/user/theme.php
+++ b/ext/user/theme.php
@@ -343,4 +343,30 @@ class UserPageTheme extends Themelet
         return $html;
     }
     // }}}
+
+    public function get_help_html()
+    {
+        global $user;
+        $output = '<p>Search for images posted by particular individuals.</p>
+        <div class="command_example">
+        <pre>poster=username</pre>
+        <p>Returns images posted by "username".</p>
+        </div> 
+        <div class="command_example">
+        <pre>poster_id=123</pre>
+        <p>Returns images posted by user 123.</p>
+        </div> 
+        ';
+
+
+        if ($user->can(Permissions::VIEW_IP)) {
+            $output .="
+        <div class=\"command_example\">
+                <pre>poster_ip=127.0.0.1</pre>
+                <p>Returns images posted from IP 127.0.0.1.</p>
+                </div> 
+                ";
+        }
+        return $output;
+    }
 }