From 1b76366dd9fff566374fe9831d8ec6a3041fd432 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Fri, 14 Jun 2019 09:34:37 -0500
Subject: [PATCH 01/23] Cleaned up some of the new image processing code, added
 documentation

---
 core/imageboard/misc.php  | 148 ++++++++++++++++++++++----------------
 ext/handle_pixel/main.php |   2 +-
 2 files changed, 89 insertions(+), 61 deletions(-)

diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php
index dbdf25f5..6336f8bb 100644
--- a/core/imageboard/misc.php
+++ b/core/imageboard/misc.php
@@ -7,6 +7,7 @@
  * Move a file from PHP's temporary area into shimmie's image storage
  * hierarchy, or throw an exception trying.
  *
+ * @param DataUploadEvent $event
  * @throws UploadException
  */
 function move_upload_to_archive(DataUploadEvent $event): void
@@ -24,7 +25,8 @@ function move_upload_to_archive(DataUploadEvent $event): void
 /**
  * Add a directory full of images
  *
- * #return string[]
+ * @param string $base
+ * @return array
  */
 function add_dir(string $base): array
 {
@@ -48,6 +50,14 @@ function add_dir(string $base): array
     return $results;
 }
 
+/**
+ * Sends a DataUploadEvent for a file.
+ *
+ * @param string $tmpname
+ * @param string $filename
+ * @param string $tags
+ * @throws UploadException
+ */
 function add_image(string $tmpname, string $filename, string $tags): void
 {
     assert(file_exists($tmpname));
@@ -65,10 +75,15 @@ function add_image(string $tmpname, string $filename, string $tags): void
     send_event($event);
 }
 
-
-function get_extension_from_mime(String $file_path): ?String
+/**
+ * Gets an the extension defined in MIME_TYPE_MAP for a file.
+ *
+ * @param String $file_path
+ * @return String The extension that was found.
+ * @throws UploadException if the mimetype could not be determined, or if an extension for hte mimetype could not be found.
+ */
+function get_extension_from_mime(String $file_path): String
 {
-    global $config;
     $mime = mime_content_type($file_path);
     if (!empty($mime)) {
         $ext = get_extension($mime);
@@ -83,11 +98,15 @@ function get_extension_from_mime(String $file_path): ?String
 
 /**
  * Given a full size pair of dimensions, return a pair scaled down to fit
- * into the configured thumbnail square, with ratio intact
+ * into the configured thumbnail square, with ratio intact.
+ * Optionally uses the High-DPI scaling setting to adjust the final resolution.
  *
- * #return int[]
+ * @param int $orig_width
+ * @param int $orig_height
+ * @param bool $use_dpi_scaling Enables the High-DPI scaling.
+ * @return array
  */
-function get_thumbnail_size(int $orig_width, int $orig_height): array
+function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_scaling = false): array
 {
     global $config;
 
@@ -105,8 +124,15 @@ function get_thumbnail_size(int $orig_width, int $orig_height): array
         $orig_height = $orig_width * 5;
     }
 
-    $max_width  = $config->get_int('thumb_width');
-    $max_height = $config->get_int('thumb_height');
+
+    if($use_dpi_scaling) {
+        $max_size = get_thumbnail_max_size_scaled();
+        $max_width  = $max_size[0];
+        $max_height = $max_size[1];
+    } else {
+        $max_width = $config->get_int('thumb_width');
+        $max_height = $config->get_int('thumb_height');
+    }
 
     $xscale = ($max_height / $orig_height);
     $yscale = ($max_width / $orig_width);
@@ -120,44 +146,10 @@ function get_thumbnail_size(int $orig_width, int $orig_height): array
 }
 
 /**
- * Given a full size pair of dimensions, return a pair scaled down to fit
- * into the configured thumbnail square, with ratio intact, using thumb_scaling
+ * Fetches the thumbnails height and width settings and applies the High-DPI scaling setting before returning the dimensions.
  *
- * #return int[]
+ * @return array [width, height]
  */
-function get_thumbnail_size_scaled(int $orig_width, int $orig_height): array
-{
-    global $config;
-
-    if ($orig_width === 0) {
-        $orig_width = 192;
-    }
-    if ($orig_height === 0) {
-        $orig_height = 192;
-    }
-
-    if ($orig_width > $orig_height * 5) {
-        $orig_width = $orig_height * 5;
-    }
-    if ($orig_height > $orig_width * 5) {
-        $orig_height = $orig_width * 5;
-    }
-
-    $max_size = get_thumbnail_max_size_scaled();
-    $max_width  = $max_size[0];
-    $max_height = $max_size[1];
-
-    $xscale = ($max_height / $orig_height);
-    $yscale = ($max_width / $orig_width);
-    $scale = ($xscale < $yscale) ? $xscale : $yscale;
-
-    if ($scale > 1 && $config->get_bool('thumb_upscale')) {
-        return [(int)$orig_width, (int)$orig_height];
-    } else {
-        return [(int)($orig_width*$scale), (int)($orig_height*$scale)];
-    }
-}
-
 function get_thumbnail_max_size_scaled(): array
 {
     global $config;
@@ -168,7 +160,13 @@ function get_thumbnail_max_size_scaled(): array
     return [$max_width, $max_height];
 }
 
-function create_thumbnail_convert($hash): bool
+/**
+ * Creates a thumbnail file using ImageMagick's convert command.
+ *
+ * @param $hash
+ * @return bool true is successful, false if not.
+ */
+function create_thumbnail_convert($hash): bool 
 {
     global $config;
 
@@ -187,9 +185,7 @@ function create_thumbnail_convert($hash): bool
     //$cmd = sprintf($format, $convert, $inname);
     //$size = shell_exec($cmd);
     //$size = explode(" ", trim($size));
-    $tsize = get_thumbnail_max_size_scaled();
-    $w = $tsize[0];
-    $h = $tsize[1];
+    list($w, $h) = get_thumbnail_max_size_scaled();
 
 
     // running the call with cmd.exe requires quoting for our paths
@@ -205,7 +201,6 @@ function create_thumbnail_convert($hash): bool
         $bg = "none";
     }
     $format = '"%s" -flatten -strip -thumbnail %ux%u%s -quality %u -background %s "%s[0]"  %s:"%s"';
-
     $cmd = sprintf($format, $convert, $w, $h, $options, $q, $bg, $inname, $type, $outname);
     $cmd = str_replace("\"convert\"", "convert", $cmd); // quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
     exec($cmd, $output, $ret);
@@ -219,6 +214,12 @@ function create_thumbnail_convert($hash): bool
     return true;
 }
 
+/**
+ * Creates a thumbnail using ffmpeg.
+ *
+ * @param $hash
+ * @return bool true if successful, false if not.
+ */
 function create_thumbnail_ffmpeg($hash): bool
 {
     global $config;
@@ -232,7 +233,7 @@ function create_thumbnail_ffmpeg($hash): bool
     $outname = warehouse_path("thumbs", $hash);
 
     $orig_size = video_size($inname);
-    $scaled_size = get_thumbnail_size_scaled($orig_size[0], $orig_size[1]);
+    $scaled_size = get_thumbnail_size($orig_size[0], $orig_size[1], true);
     
     $codec = "mjpeg";
     $quality = $config->get_int("thumb_quality");
@@ -270,6 +271,12 @@ function create_thumbnail_ffmpeg($hash): bool
     }
 }
 
+/**
+ * Determines the dimensions of a video file using ffmpeg.
+ *
+ * @param string $filename
+ * @return array [width, height]
+ */
 function video_size(string $filename): array
 {
     global $config;
@@ -307,6 +314,9 @@ function video_size(string $filename): array
  *
  * The factor of 2.5 is simply a rough guideline.
  * http://stackoverflow.com/questions/527532/reasonable-php-memory-limit-for-image-resize
+ *
+ * @param array $info The output of getimagesize() for the source file in question.
+ * @return int The number of bytes an image resize operation is estimated to use.
  */
 function calc_memory_use(array $info): int
 {
@@ -320,12 +330,25 @@ function calc_memory_use(array $info): int
     return (int)$memory_use;
 }
 
+/**
+ * Performs a resize operation on an image file using GD.
+ *
+ * @param String $image_filename The source file to be resized.
+ * @param array $info The output of getimagesize() for the source file.
+ * @param int $new_width
+ * @param int $new_height
+ * @param string $output_filename
+ * @param string|null $output_type If set to null, the output file type will be automatically determined via the $info parameter. Otherwise an exception will be thrown.
+ * @param int $output_quality Defaults to 80.
+ * @throws ImageResizeException
+ * @throws InsufficientMemoryException if the estimated memory usage exceeds the memory limit.
+ */
 function image_resize_gd(
     String $image_filename,
     array $info,
     int $new_width,
     int $new_height,
-    string $output_filename=null,
+    string $output_filename,
     string $output_type=null,
     int $output_quality = 80
 ) {
@@ -423,7 +446,7 @@ function image_resize_gd(
             throw new ImageResizeException("Unable to copy resized image data to new image");
         }
 
-        $result = false;
+
         switch ($output_type) {
             case "bmp":
                 $result = imagebmp($image_resized, $output_filename, true);
@@ -453,15 +476,20 @@ function image_resize_gd(
     }
 }
 
-function is_animated_gif(String $image_filename)
-{
-    $isanigif = 0;
+/**
+ * Determines if a file is an animated gif.
+ *
+ * @param String $image_filename The path of the file to check.
+ * @return bool true if the file is an animated gif, false if it is not.
+ */
+function is_animated_gif(String $image_filename) {
+    $is_anim_gif = 0;
     if (($fh = @fopen($image_filename, 'rb'))) {
         //check if gif is animated (via http://www.php.net/manual/en/function.imagecreatefromgif.php#104473)
-        while (!feof($fh) && $isanigif < 2) {
+        while (!feof($fh) && $is_anim_gif < 2) {
             $chunk = fread($fh, 1024 * 100);
-            $isanigif += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
+            $is_anim_gif += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
         }
     }
-    return ($isanigif == 0);
-}
+    return ($is_anim_gif == 0);
+}
\ No newline at end of file
diff --git a/ext/handle_pixel/main.php b/ext/handle_pixel/main.php
index e10d8f75..daef5fe2 100644
--- a/ext/handle_pixel/main.php
+++ b/ext/handle_pixel/main.php
@@ -96,7 +96,7 @@ class PixelFileHandler extends DataHandlerExtension
 
         try {
             $info = getimagesize($inname);
-            $tsize = get_thumbnail_size_scaled($info[0], $info[1]);
+            $tsize = get_thumbnail_size($info[0], $info[1], true);
             $image = image_resize_gd(
                 $inname,
                 $info,

From ed4b6bc4a0e6f5391f5204c1f48d6ec5e5564956 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Fri, 14 Jun 2019 12:34:53 -0500
Subject: [PATCH 02/23] Updated handle_ico to use new common image thumbnailing
 and to inherit DataHandlerExtension

---
 core/extension.php        |  6 +--
 core/imageboard/misc.php  | 10 +++--
 ext/handle_flash/main.php |  2 +-
 ext/handle_ico/main.php   | 83 +++++++++++----------------------------
 ext/handle_mp3/main.php   |  2 +-
 ext/handle_pixel/main.php |  7 ++--
 ext/handle_svg/main.php   |  2 +-
 ext/handle_video/main.php |  2 +-
 8 files changed, 40 insertions(+), 74 deletions(-)

diff --git a/core/extension.php b/core/extension.php
index b7472583..af7bb6ad 100644
--- a/core/extension.php
+++ b/core/extension.php
@@ -222,13 +222,13 @@ abstract class DataHandlerExtension extends Extension
         $result = false;
         if ($this->supported_ext($event->type)) {
             if ($event->force) {
-                $result = $this->create_thumb($event->hash);
+                $result = $this->create_thumb($event->hash, $event->type);
             } else {
                 $outname = warehouse_path("thumbs", $event->hash);
                 if (file_exists($outname)) {
                     return;
                 }
-                $result = $this->create_thumb($event->hash);
+                $result = $this->create_thumb($event->hash, $event->type);
             }
         }
         if ($result) {
@@ -256,5 +256,5 @@ abstract class DataHandlerExtension extends Extension
     abstract protected function supported_ext(string $ext): bool;
     abstract protected function check_contents(string $tmpname): bool;
     abstract protected function create_image_from_data(string $filename, array $metadata);
-    abstract protected function create_thumb(string $hash): bool;
+    abstract protected function create_thumb(string $hash, string $type): bool;
 }
diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php
index 6336f8bb..e3e31576 100644
--- a/core/imageboard/misc.php
+++ b/core/imageboard/misc.php
@@ -164,9 +164,10 @@ function get_thumbnail_max_size_scaled(): array
  * Creates a thumbnail file using ImageMagick's convert command.
  *
  * @param $hash
+ * @param string $input_type Optional, allows specifying the input format. Usually not necessary.
  * @return bool true is successful, false if not.
  */
-function create_thumbnail_convert($hash): bool 
+function create_thumbnail_convert($hash, $input_type = ""): bool
 {
     global $config;
 
@@ -200,8 +201,11 @@ function create_thumbnail_convert($hash): bool
     if ($type=="webp") {
         $bg = "none";
     }
-    $format = '"%s" -flatten -strip -thumbnail %ux%u%s -quality %u -background %s "%s[0]"  %s:"%s"';
-    $cmd = sprintf($format, $convert, $w, $h, $options, $q, $bg, $inname, $type, $outname);
+    if(!empty($input_type)) {
+        $input_type = $input_type.":";
+    }
+    $format = '"%s" -flatten -strip -thumbnail %ux%u%s -quality %u -background %s %s"%s[0]"  %s:"%s" 2>&1';
+    $cmd = sprintf($format, $convert, $w, $h, $options, $q, $bg,$input_type, $inname, $type, $outname);
     $cmd = str_replace("\"convert\"", "convert", $cmd); // quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
     exec($cmd, $output, $ret);
 
diff --git a/ext/handle_flash/main.php b/ext/handle_flash/main.php
index 8da00583..9476499e 100644
--- a/ext/handle_flash/main.php
+++ b/ext/handle_flash/main.php
@@ -8,7 +8,7 @@
 
 class FlashFileHandler extends DataHandlerExtension
 {
-    protected function create_thumb(string $hash): bool
+    protected function create_thumb(string $hash, string $type): bool
     {
         global $config;
 
diff --git a/ext/handle_ico/main.php b/ext/handle_ico/main.php
index 56e3f373..ab005c96 100644
--- a/ext/handle_ico/main.php
+++ b/ext/handle_ico/main.php
@@ -5,65 +5,45 @@
  * Description: Handle windows icons
  */
 
-class IcoFileHandler extends Extension
+class IcoFileHandler extends DataHandlerExtension
 {
-    public function onDataUpload(DataUploadEvent $event)
+    const SUPPORTED_EXTENSIONS = ["ico", "ani", "cur"];
+
+
+    protected function supported_ext(string $ext): bool
     {
-        if ($this->supported_ext($event->type) && $this->check_contents($event->tmpname)) {
-            $hash = $event->hash;
-            $ha = substr($hash, 0, 2);
-            move_upload_to_archive($event);
-            send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
-            $image = $this->create_image_from_data("images/$ha/$hash", $event->metadata);
-            if (is_null($image)) {
-                throw new UploadException("Icon handler failed to create image object from data");
-            }
-            $iae = new ImageAdditionEvent($image);
-            send_event($iae);
-            $event->image_id = $iae->image->id;
-        }
+        return in_array(strtolower($ext), self::SUPPORTED_EXTENSIONS);
     }
 
-    public function onDisplayingImage(DisplayingImageEvent $event)
-    {
-        global $page;
-        if ($this->supported_ext($event->image->ext)) {
-            $this->theme->display_image($page, $event->image);
-        }
-    }
-
-    private function supported_ext(string $ext): bool
-    {
-        $exts = ["ico", "ani", "cur"];
-        return in_array(strtolower($ext), $exts);
-    }
-
-    private function create_image_from_data(string $filename, array $metadata)
+    protected function create_image_from_data(string $filename, array $metadata)
     {
         $image = new Image();
 
-        $fp = fopen($filename, "r");
-        $header = unpack("Snull/Stype/Scount", fread($fp, 6));
 
-        $subheader = unpack("Cwidth/Cheight/Ccolours/Cnull/Splanes/Sbpp/Lsize/loffset", fread($fp, 16));
-        fclose($fp);
+        $fp = fopen($filename, "r");
+        try {
+            unpack("Snull/Stype/Scount", fread($fp, 6));
+            $subheader = unpack("Cwidth/Cheight/Ccolours/Cnull/Splanes/Sbpp/Lsize/loffset", fread($fp, 16));
+        } finally {
+            fclose($fp);
+        }
 
         $width = $subheader['width'];
         $height = $subheader['height'];
         $image->width = $width == 0 ? 256 : $width;
         $image->height = $height == 0 ? 256 : $height;
 
-        $image->filesize  = $metadata['size'];
-        $image->hash      = $metadata['hash'];
-        $image->filename  = $metadata['filename'];
-        $image->ext       = $metadata['extension'];
+        $image->filesize = $metadata['size'];
+        $image->hash = $metadata['hash'];
+        $image->filename = $metadata['filename'];
+        $image->ext = $metadata['extension'];
         $image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
-        $image->source    = $metadata['source'];
+        $image->source = $metadata['source'];
 
         return $image;
     }
 
-    private function check_contents(string $file): bool
+    protected function check_contents(string $file): bool
     {
         if (!file_exists($file)) {
             return false;
@@ -74,27 +54,8 @@ class IcoFileHandler extends Extension
         return ($header['null'] == 0 && ($header['type'] == 0 || $header['type'] == 1));
     }
 
-    private function create_thumb(string $hash): bool
+    protected function create_thumb(string $hash, string $type): bool
     {
-        global $config;
-
-        $inname  = warehouse_path("images", $hash);
-        $outname = warehouse_path("thumbs", $hash);
-
-        $tsize = get_thumbnail_size_scaled($width, $height);
-        $w = $tsize[0];
-        $h = $tsise[1];
-        
-        $q = $config->get_int("thumb_quality");
-        $mem = $config->get_int("thumb_mem_limit") / 1024 / 1024; // IM takes memory in MB
-
-        if ($config->get_bool("ico_convert")) {
-            // "-limit memory $mem" broken?
-            exec("convert {$inname}[0] -geometry {$w}x{$h} -quality {$q} jpg:$outname");
-        } else {
-            copy($inname, $outname);
-        }
-
-        return true;
+        return create_thumbnail_convert($hash, $type);
     }
 }
diff --git a/ext/handle_mp3/main.php b/ext/handle_mp3/main.php
index a3e3dc9c..13e2bab4 100644
--- a/ext/handle_mp3/main.php
+++ b/ext/handle_mp3/main.php
@@ -7,7 +7,7 @@
 
 class MP3FileHandler extends DataHandlerExtension
 {
-    protected function create_thumb(string $hash): bool
+    protected function create_thumb(string $hash, string $type): bool
     {
         copy("ext/handle_mp3/thumb.jpg", warehouse_path("thumbs", $hash));
         return true;
diff --git a/ext/handle_pixel/main.php b/ext/handle_pixel/main.php
index daef5fe2..a3bc3bd2 100644
--- a/ext/handle_pixel/main.php
+++ b/ext/handle_pixel/main.php
@@ -8,11 +8,12 @@
 
 class PixelFileHandler extends DataHandlerExtension
 {
+    const SUPPORTED_EXTENSIONS = ["jpg", "jpeg", "gif", "png", "webp"];
+
     protected function supported_ext(string $ext): bool
     {
-        $exts = ["jpg", "jpeg", "gif", "png", "webp"];
         $ext = (($pos = strpos($ext, '?')) !== false) ? substr($ext, 0, $pos) : $ext;
-        return in_array(strtolower($ext), $exts);
+        return in_array(strtolower($ext), self::SUPPORTED_EXTENSIONS);
     }
 
     protected function create_image_from_data(string $filename, array $metadata)
@@ -53,7 +54,7 @@ class PixelFileHandler extends DataHandlerExtension
         return false;
     }
 
-    protected function create_thumb(string $hash): bool
+    protected function create_thumb(string $hash, string $type): bool
     {
         global $config;
 
diff --git a/ext/handle_svg/main.php b/ext/handle_svg/main.php
index 5676d24f..f2151c06 100644
--- a/ext/handle_svg/main.php
+++ b/ext/handle_svg/main.php
@@ -32,7 +32,7 @@ class SVGFileHandler extends DataHandlerExtension
         }
     }
 
-    protected function create_thumb(string $hash): bool
+    protected function create_thumb(string $hash, string $type): bool
     {
         if (!create_thumbnail_convert($hash)) {
             copy("ext/handle_svg/thumb.jpg", warehouse_path("thumbs", $hash));
diff --git a/ext/handle_video/main.php b/ext/handle_video/main.php
index 316139c8..f4f50320 100644
--- a/ext/handle_video/main.php
+++ b/ext/handle_video/main.php
@@ -53,7 +53,7 @@ class VideoFileHandler extends DataHandlerExtension
     /**
      * Generate the Thumbnail image for particular file.
      */
-    protected function create_thumb(string $hash): bool
+    protected function create_thumb(string $hash, string $type): bool
     {
         return create_thumbnail_ffmpeg($hash);
     }

From 070429402bd279455618de35c7f1d698ea0e253a Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Fri, 14 Jun 2019 12:45:15 -0500
Subject: [PATCH 03/23] readme corrections

---
 README.markdown | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/README.markdown b/README.markdown
index bd71aa06..23dec0ee 100644
--- a/README.markdown
+++ b/README.markdown
@@ -99,21 +99,21 @@ For example, one can override the default anonymous "allow nothing"
 permissions like so:
 
 ```php
-new UserClass("anonymous", "base", array(
+new UserClass("anonymous", "base", [
 	"create_comment" => True,
 	"edit_image_tag" => True,
 	"edit_image_source" => True,
 	"create_image_report" => True,
-));
+]);
 ```
 
 For a moderator class, being a regular user who can delete images and comments:
 
 ```php
-new UserClass("moderator", "user", array(
+new UserClass("moderator", "user", [
 	"delete_image" => True,
 	"delete_comment" => True,
-));
+]);
 ```
 
 For a list of permissions, see `core/userclass.php`

From 58acb7128278c1523d9279ac8b81a8dfedb00a98 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Fri, 14 Jun 2019 12:59:12 -0500
Subject: [PATCH 04/23] Change imagemagick commands to return the error output
 Added ico to transcode extension

---
 core/imageboard/misc.php |  7 +++++--
 ext/transcode/main.php   | 14 +++++++++++---
 2 files changed, 16 insertions(+), 5 deletions(-)

diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php
index e3e31576..e9bd93c6 100644
--- a/core/imageboard/misc.php
+++ b/core/imageboard/misc.php
@@ -208,8 +208,11 @@ function create_thumbnail_convert($hash, $input_type = ""): bool
     $cmd = sprintf($format, $convert, $w, $h, $options, $q, $bg,$input_type, $inname, $type, $outname);
     $cmd = str_replace("\"convert\"", "convert", $cmd); // quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
     exec($cmd, $output, $ret);
-
-    log_debug('handle_pixel', "Generating thumbnail with command `$cmd`, returns $ret");
+    if ($ret!=0) {
+        log_warning('imageboard/misc', "Generating thumbnail with command `$cmd`, returns $ret, outputting ".implode("\r\n",$output));
+    } else {
+        log_debug('imageboard/misc', "Generating thumbnail with command `$cmd`, returns $ret");
+    }
 
     if ($config->get_bool("thumb_optim", false)) {
         exec("jpegoptim $outname", $output, $ret);
diff --git a/ext/transcode/main.php b/ext/transcode/main.php
index 85c554d8..aa471d0a 100644
--- a/ext/transcode/main.php
+++ b/ext/transcode/main.php
@@ -43,6 +43,7 @@ class TranscodeImage extends Extension
             "psd",
             "tiff",
             "webp",
+            "ico",
         ]
     ];
 
@@ -68,6 +69,7 @@ class TranscodeImage extends Extension
     const INPUT_FORMATS = [
         "BMP" => "bmp",
         "GIF" => "gif",
+        "ICO" => "ico",
         "JPG" => "jpg",
         "PNG" => "png",
         "PSD" => "psd",
@@ -440,15 +442,21 @@ class TranscodeImage extends Extension
         }
         $tmp_name = tempnam("/tmp", "shimmie_transcode");
 
-        $format = '"%s" %s -quality %u -background %s "%s"  %s:"%s"';
-        $cmd = sprintf($format, $convert, $args, $q, $bg, $source_name, $ext, $tmp_name);
+        $source_type = "";
+        switch ($source_format) {
+            case "ico":
+                $source_type = "ico:";
+        }
+
+        $format = '"%s" %s -quality %u -background %s %s"%s"  %s:"%s" 2>&1';
+        $cmd = sprintf($format, $convert, $args, $q, $bg, $source_type, $source_name, $ext, $tmp_name);
         $cmd = str_replace("\"convert\"", "convert", $cmd); // quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
         exec($cmd, $output, $ret);
 
         log_debug('transcode', "Transcoding with command `$cmd`, returns $ret");
 
         if ($ret!==0) {
-            throw new ImageTranscodeException("Transcoding failed with command ".$cmd);
+            throw new ImageTranscodeException("Transcoding failed with command ".$cmd.", returning ".implode("\r\n", $output));
         }
 
         return $tmp_name;

From 8950d27d642a41a1a9819fad5bd113d2ff540c7e Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Fri, 14 Jun 2019 12:59:58 -0500
Subject: [PATCH 05/23] Changed upload to detect unrecognized files so that it
 doesn't just blankly refresh when the type isn't handled

---
 ext/upload/main.php | 55 ++++++++++++++++++++++++---------------------
 1 file changed, 30 insertions(+), 25 deletions(-)

diff --git a/ext/upload/main.php b/ext/upload/main.php
index f42f1360..e0274346 100644
--- a/ext/upload/main.php
+++ b/ext/upload/main.php
@@ -27,7 +27,6 @@ class DataUploadEvent extends Event
     public $merged = false;
 
 
-
     /**
      * Some data is being uploaded.
      * This should be caught by a file handler.
@@ -49,10 +48,10 @@ class DataUploadEvent extends Event
         if ($config->get_bool("upload_use_mime")) {
             $this->set_type(get_extension_from_mime($tmpname));
         } else {
-            if (array_key_exists('extension', $metadata)&&!empty($metadata['extension'])) {
+            if (array_key_exists('extension', $metadata) && !empty($metadata['extension'])) {
                 $this->type = strtolower($metadata['extension']);
             } else {
-                throw new UploadException("Could not determine extension for file ".$metadata["filename"]);
+                throw new UploadException("Could not determine extension for file " . $metadata["filename"]);
             }
         }
     }
@@ -130,9 +129,9 @@ class Upload extends Extension
         $sb->position = 10;
         // Output the limits from PHP so the user has an idea of what they can set.
         $sb->add_int_option("upload_count", "Max uploads: ");
-        $sb->add_label("<i>PHP Limit = ".ini_get('max_file_uploads')."</i>");
+        $sb->add_label("<i>PHP Limit = " . ini_get('max_file_uploads') . "</i>");
         $sb->add_shorthand_int_option("upload_size", "<br/>Max size per file: ");
-        $sb->add_label("<i>PHP Limit = ".ini_get('upload_max_filesize')."</i>");
+        $sb->add_label("<i>PHP Limit = " . ini_get('upload_max_filesize') . "</i>");
         $sb->add_choice_option("transload_engine", $tes, "<br/>Transload: ");
         $sb->add_bool_option("upload_tlsource", "<br/>Use transloaded URL as source if none is provided: ");
         $sb->add_bool_option("upload_use_mime", "<br/>Use mime type to determine file types: ");
@@ -190,10 +189,10 @@ class Upload extends Extension
                     if (count($_FILES) > 1) {
                         throw new UploadException("Can not upload more than one image for replacing.");
                     }
-                    
+
                     $source = isset($_POST['source']) ? $_POST['source'] : null;
                     $tags = []; // Tags aren't changed when replacing. Set to empty to stop PHP warnings.
-                    
+
                     $ok = false;
                     if (count($_FILES)) {
                         foreach ($_FILES as $file) {
@@ -249,7 +248,7 @@ class Upload extends Extension
                     if (!empty($_GET['tags']) && $_GET['tags'] != "null") {
                         $tags = Tag::explode($_GET['tags']);
                     }
-                            
+
                     $ok = $this->try_transload($url, $tags, $source);
                     $this->theme->display_upload_status($page, $ok);
                 } else {
@@ -314,7 +313,7 @@ class Upload extends Extension
      * #param string[] $file
      * #param string[] $tags
      */
-    private function try_upload(array $file, array $tags, ?string $source=null, int $replace=-1): bool
+    private function try_upload(array $file, array $tags, ?string $source = null, int $replace = -1): bool
     {
         global $page;
 
@@ -331,7 +330,7 @@ class Upload extends Extension
                 if ($file['error'] !== UPLOAD_ERR_OK) {
                     throw new UploadException($this->upload_error_message($file['error']));
                 }
-                
+
                 $pathinfo = pathinfo($file['name']);
                 $metadata = [];
                 $metadata['filename'] = $pathinfo['basename'];
@@ -340,19 +339,22 @@ class Upload extends Extension
                 }
                 $metadata['tags'] = $tags;
                 $metadata['source'] = $source;
-                
+
                 /* check if we have been given an image ID to replace */
                 if ($replace >= 0) {
                     $metadata['replace'] = $replace;
                 }
-                
+
                 $event = new DataUploadEvent($file['tmp_name'], $metadata);
                 send_event($event);
-                $page->add_http_header("X-Shimmie-Image-ID: ".int_escape($event->image_id));
+                if ($event->image_id == -1) {
+                    throw new UploadException("File type not supported: " . $metadata['extension']);
+                }
+                $page->add_http_header("X-Shimmie-Image-ID: " . int_escape($event->image_id));
             } catch (UploadException $ex) {
                 $this->theme->display_upload_error(
                     $page,
-                    "Error with ".html_escape($file['name']),
+                    "Error with " . html_escape($file['name']),
                     $ex->getMessage()
                 );
                 $ok = false;
@@ -362,7 +364,7 @@ class Upload extends Extension
         return $ok;
     }
 
-    private function try_transload(string $url, array $tags, string $source=null, int $replace=-1): bool
+    private function try_transload(string $url, array $tags, string $source = null, int $replace = -1): bool
     {
         global $page, $config, $user;
 
@@ -372,7 +374,7 @@ class Upload extends Extension
         if ($user->can("edit_image_lock") && !empty($_GET['locked'])) {
             $locked = bool_escape($_GET['locked']);
         }
-        
+
         // Checks if url contains rating, also checks if the rating extension is enabled.
         if ($config->get_string("transload_engine", "none") != "none" && ext_is_live("Ratings") && !empty($_GET['rating'])) {
             // Rating event will validate that this is s/q/e/u
@@ -386,7 +388,7 @@ class Upload extends Extension
 
         // transload() returns Array or Bool, depending on the transload_engine.
         $headers = transload($url, $tmp_filename);
-        
+
         $s_filename = is_array($headers) ? findHeader($headers, 'Content-Disposition') : null;
         $h_filename = ($s_filename ? preg_replace('/^.*filename="([^ ]+)"/i', '$1', $s_filename) : null);
         $filename = $h_filename ?: basename($url);
@@ -394,8 +396,8 @@ class Upload extends Extension
         if (!$headers) {
             $this->theme->display_upload_error(
                 $page,
-                "Error with ".html_escape($filename),
-                "Error reading from ".html_escape($url)
+                "Error with " . html_escape($filename),
+                "Error reading from " . html_escape($url)
             );
             return false;
         }
@@ -403,7 +405,7 @@ class Upload extends Extension
         if (filesize($tmp_filename) == 0) {
             $this->theme->display_upload_error(
                 $page,
-                "Error with ".html_escape($filename),
+                "Error with " . html_escape($filename),
                 "No data found -- perhaps the site has hotlink protection?"
             );
             $ok = false;
@@ -413,7 +415,7 @@ class Upload extends Extension
             $metadata['filename'] = $filename;
             $metadata['tags'] = $tags;
             $metadata['source'] = (($url == $source) && !$config->get_bool('upload_tlsource') ? "" : $source);
-            
+
             $ext = false;
             if (is_array($headers)) {
                 $ext = get_extension(findHeader($headers, 'Content-Type'));
@@ -422,7 +424,7 @@ class Upload extends Extension
                 $ext = $pathinfo['extension'];
             }
             $metadata['extension'] = $ext;
-            
+
             /* check for locked > adds to metadata if it has */
             if (!empty($locked)) {
                 $metadata['locked'] = $locked ? "on" : "";
@@ -432,19 +434,22 @@ class Upload extends Extension
             if (!empty($rating)) {
                 $metadata['rating'] = $rating;
             }
-            
+
             /* check if we have been given an image ID to replace */
             if ($replace >= 0) {
                 $metadata['replace'] = $replace;
             }
-            
+
             try {
                 $event = new DataUploadEvent($tmp_filename, $metadata);
                 send_event($event);
+                if ($event->image_id == -1) {
+                    throw new UploadException("File type not supported: " . $metadata['extension']);
+                }
             } catch (UploadException $ex) {
                 $this->theme->display_upload_error(
                     $page,
-                    "Error with ".html_escape($url),
+                    "Error with " . html_escape($url),
                     $ex->getMessage()
                 );
                 $ok = false;

From 444de26ce3ea32e91aae8cd5407b84fe546894bb Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Fri, 14 Jun 2019 13:33:47 -0500
Subject: [PATCH 06/23] Added warning for webp thumbnails

---
 ext/image/main.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ext/image/main.php b/ext/image/main.php
index b2fe5722..56405d65 100644
--- a/ext/image/main.php
+++ b/ext/image/main.php
@@ -141,7 +141,7 @@ class ImageIO extends Extension
 
         $thumb_types = [];
         $thumb_types['JPEG'] = "jpg";
-        $thumb_types['WEBP'] = "webp";
+        $thumb_types['WEBP (Not IE/Safari compatible)'] = "webp";
 
 
         $sb = new SetupBlock("Thumbnailing");

From 6f501a6e74bb7e3cf52da8c315de2daed9d6362d Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Fri, 14 Jun 2019 13:17:03 -0500
Subject: [PATCH 07/23] Database driver constants

---
 core/_install.php            | 14 +++++++-------
 core/database.php            | 22 +++++++++++++---------
 core/dbengine.php            |  6 +++---
 core/imageboard/image.php    |  4 ++--
 core/user.php                |  2 +-
 ext/admin/main.php           | 12 ++++++------
 ext/admin/theme.php          |  2 +-
 ext/comment/main.php         |  4 ++--
 ext/index/test.php           |  2 +-
 ext/ipban/main.php           |  2 +-
 ext/ipban/theme.php          |  2 +-
 ext/log_db/main.php          |  2 +-
 ext/rating/main.php          |  6 +++---
 ext/relatationships/main.php |  2 +-
 ext/rss_comments/main.php    |  2 +-
 ext/rule34/main.php          |  2 +-
 ext/rule34/theme.php         |  2 +-
 ext/tips/main.php            |  2 +-
 ext/upgrade/main.php         | 12 ++++++------
 19 files changed, 53 insertions(+), 49 deletions(-)

diff --git a/core/_install.php b/core/_install.php
index dcc622be..99fa864d 100644
--- a/core/_install.php
+++ b/core/_install.php
@@ -110,7 +110,7 @@ function do_install()
 { // {{{
     if (file_exists("data/config/auto_install.conf.php")) {
         require_once "data/config/auto_install.conf.php";
-    } elseif (@$_POST["database_type"] == "sqlite") {
+    } elseif (@$_POST["database_type"] == Database::SQLITE_DRIVER) {
         $id = bin2hex(random_bytes(5));
         define('DATABASE_DSN', "sqlite:data/shimmie.{$id}.sqlite");
     } elseif (isset($_POST['database_type']) && isset($_POST['database_host']) && isset($_POST['database_user']) && isset($_POST['database_name'])) {
@@ -153,9 +153,9 @@ function ask_questions()
 
     $drivers = PDO::getAvailableDrivers();
     if (
-        !in_array("mysql", $drivers) &&
-        !in_array("pgsql", $drivers) &&
-        !in_array("sqlite", $drivers)
+        !in_array(Database::MYSQL_DRIVER, $drivers) &&
+        !in_array(Database::PGSQL_DRIVER, $drivers) &&
+        !in_array(Database::SQLITE_DRIVER, $drivers)
     ) {
         $errors[] = "
 			No database connection library could be found; shimmie needs
@@ -163,9 +163,9 @@ function ask_questions()
 		";
     }
 
-    $db_m = in_array("mysql", $drivers)  ? '<option value="mysql">MySQL</option>' : "";
-    $db_p = in_array("pgsql", $drivers)  ? '<option value="pgsql">PostgreSQL</option>' : "";
-    $db_s = in_array("sqlite", $drivers) ? '<option value="sqlite">SQLite</option>' : "";
+    $db_m = in_array(Database::MYSQL_DRIVER, $drivers)  ? '<option value="'.Database::MYSQL_DRIVER.'">MySQL</option>' : "";
+    $db_p = in_array(Database::PGSQL_DRIVER, $drivers)  ? '<option value="'.Database::PGSQL_DRIVER.'">PostgreSQL</option>' : "";
+    $db_s = in_array(Database::SQLITE_DRIVER, $drivers) ? '<option value="'.Database::SQLITE_DRIVER.'">SQLite</option>' : "";
 
     $warn_msg = $warnings ? "<h3>Warnings</h3>".implode("\n<p>", $warnings) : "";
     $err_msg = $errors ? "<h3>Errors</h3>".implode("\n<p>", $errors) : "";
diff --git a/core/database.php b/core/database.php
index c6135878..29381209 100644
--- a/core/database.php
+++ b/core/database.php
@@ -4,6 +4,10 @@
  */
 class Database
 {
+    const MYSQL_DRIVER = "mysql";
+    const PGSQL_DRIVER = "pgsql";
+    const SQLITE_DRIVER = "sqlite";
+
     /**
      * The PDO database connection object, for anyone who wants direct access.
      * @var null|PDO
@@ -72,7 +76,7 @@ class Database
 
         // https://bugs.php.net/bug.php?id=70221
         $ka = DATABASE_KA;
-        if (version_compare(PHP_VERSION, "6.9.9") == 1 && $this->get_driver_name() == "sqlite") {
+        if (version_compare(PHP_VERSION, "6.9.9") == 1 && $this->get_driver_name() == self::SQLITE_DRIVER) {
             $ka = false;
         }
 
@@ -96,11 +100,11 @@ class Database
             throw new SCoreException("Can't figure out database engine");
         }
 
-        if ($db_proto === "mysql") {
+        if ($db_proto === self::MYSQL_DRIVER) {
             $this->engine = new MySQL();
-        } elseif ($db_proto === "pgsql") {
+        } elseif ($db_proto === self::PGSQL_DRIVER) {
             $this->engine = new PostgreSQL();
-        } elseif ($db_proto === "sqlite") {
+        } elseif ($db_proto === self::SQLITE_DRIVER) {
             $this->engine = new SQLite();
         } else {
             die('Unknown PDO driver: '.$db_proto);
@@ -224,7 +228,7 @@ class Database
             }
             return $stmt;
         } catch (PDOException $pdoe) {
-            throw new SCoreException($pdoe->getMessage()."<p><b>Query:</b> ".$query);
+            throw new SCoreException($pdoe->getMessage()."<p><b>Query:</b> ".$query, $pdoe->getCode(), $pdoe);
         }
     }
 
@@ -296,7 +300,7 @@ class Database
      */
     public function get_last_insert_id(string $seq): int
     {
-        if ($this->engine->name == "pgsql") {
+        if ($this->engine->name == self::PGSQL_DRIVER) {
             return $this->db->lastInsertId($seq);
         } else {
             return $this->db->lastInsertId();
@@ -326,15 +330,15 @@ class Database
             $this->connect_db();
         }
 
-        if ($this->engine->name === "mysql") {
+        if ($this->engine->name === self::MYSQL_DRIVER) {
             return count(
                 $this->get_all("SHOW TABLES")
             );
-        } elseif ($this->engine->name === "pgsql") {
+        } elseif ($this->engine->name === self::PGSQL_DRIVER) {
             return count(
                 $this->get_all("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
             );
-        } elseif ($this->engine->name === "sqlite") {
+        } elseif ($this->engine->name === self::SQLITE_DRIVER) {
             return count(
                 $this->get_all("SELECT name FROM sqlite_master WHERE type = 'table'")
             );
diff --git a/core/dbengine.php b/core/dbengine.php
index bb7c674b..d76a1a43 100644
--- a/core/dbengine.php
+++ b/core/dbengine.php
@@ -22,7 +22,7 @@ class DBEngine
 class MySQL extends DBEngine
 {
     /** @var string */
-    public $name = "mysql";
+    public $name = Database::MYSQL_DRIVER;
 
     public function init(PDO $db)
     {
@@ -54,7 +54,7 @@ class MySQL extends DBEngine
 class PostgreSQL extends DBEngine
 {
     /** @var string */
-    public $name = "pgsql";
+    public $name = Database::PGSQL_DRIVER;
 
     public function init(PDO $db)
     {
@@ -136,7 +136,7 @@ function _ln($n)
 class SQLite extends DBEngine
 {
     /** @var string  */
-    public $name = "sqlite";
+    public $name = Database::SQLITE_DRIVER;
 
     public function init(PDO $db)
     {
diff --git a/core/imageboard/image.php b/core/imageboard/image.php
index 928d4914..af6a15d8 100644
--- a/core/imageboard/image.php
+++ b/core/imageboard/image.php
@@ -590,7 +590,7 @@ class Image
     public function delete_tags_from_image(): void
     {
         global $database;
-        if ($database->get_driver_name() == "mysql") {
+        if ($database->get_driver_name() == Database::MYSQL_DRIVER) {
             //mysql < 5.6 has terrible subquery optimization, using EXISTS / JOIN fixes this
             $database->execute(
                 "
@@ -907,7 +907,7 @@ class Image
 
         // more than one positive tag, or more than zero negative tags
         else {
-            if ($database->get_driver_name() === "mysql") {
+            if ($database->get_driver_name() === Database::MYSQL_DRIVER) {
                 $query = Image::build_ugly_search_querylet($tag_querylets);
             } else {
                 $query = Image::build_accurate_search_querylet($tag_querylets);
diff --git a/core/user.php b/core/user.php
index 098c7723..a2a4d537 100644
--- a/core/user.php
+++ b/core/user.php
@@ -69,7 +69,7 @@ class User
         global $config, $database;
         $row = $database->cache->get("user-session:$name-$session");
         if (!$row) {
-            if ($database->get_driver_name() === "mysql") {
+            if ($database->get_driver_name() === Database::MYSQL_DRIVER) {
                 $query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess";
             } else {
                 $query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess";
diff --git a/ext/admin/main.php b/ext/admin/main.php
index 212b07fb..2b484bc1 100644
--- a/ext/admin/main.php
+++ b/ext/admin/main.php
@@ -201,14 +201,14 @@ class AdminPage extends Extension
         $database = $matches['dbname'];
 
         switch ($software) {
-            case 'mysql':
+            case Database::MYSQL_DRIVER:
                 $cmd = "mysqldump -h$hostname -u$username -p$password $database";
                 break;
-            case 'pgsql':
+            case Database::PGSQL_DRIVER:
                 putenv("PGPASSWORD=$password");
                 $cmd = "pg_dump -h $hostname -U $username $database";
                 break;
-            case 'sqlite':
+            case Database::SQLITE_DRIVER:
                 $cmd = "sqlite3 $database .dump";
                 break;
             default:
@@ -257,7 +257,7 @@ class AdminPage extends Extension
         //TODO: Update score_log (Having an optional ID column for score_log would be nice..)
         preg_match("#^(?P<proto>\w+)\:(?:user=(?P<user>\w+)(?:;|$)|password=(?P<password>\w*)(?:;|$)|host=(?P<host>[\w\.\-]+)(?:;|$)|dbname=(?P<dbname>[\w_]+)(?:;|$))+#", DATABASE_DSN, $matches);
 
-        if ($matches['proto'] == "mysql") {
+        if ($matches['proto'] == Database::MYSQL_DRIVER) {
             $tables = $database->get_col("SELECT TABLE_NAME
 			                              FROM information_schema.KEY_COLUMN_USAGE
 			                              WHERE TABLE_SCHEMA = :db
@@ -280,9 +280,9 @@ class AdminPage extends Extension
                 $i++;
             }
             $database->execute("ALTER TABLE images AUTO_INCREMENT=".(count($ids) + 1));
-        } elseif ($matches['proto'] == "pgsql") {
+        } elseif ($matches['proto'] == Database::PGSQL_DRIVER) {
             //TODO: Make this work with PostgreSQL
-        } elseif ($matches['proto'] == "sqlite") {
+        } elseif ($matches['proto'] == Database::SQLITE_DRIVER) {
             //TODO: Make this work with SQLite
         }
         return true;
diff --git a/ext/admin/theme.php b/ext/admin/theme.php
index 3e60d224..5d3de30f 100644
--- a/ext/admin/theme.php
+++ b/ext/admin/theme.php
@@ -45,7 +45,7 @@ class AdminPageTheme extends Themelet
             $html .= $this->button("Download all images", "download_all_images", false);
         }
         $html .= $this->button("Download database contents", "database_dump", false);
-        if ($database->get_driver_name() == "mysql") {
+        if ($database->get_driver_name() == Database::MYSQL_DRIVER) {
             $html .= $this->button("Reset image IDs", "reset_image_ids", true);
         }
         $page->add_block(new Block("Misc Admin Tools", $html));
diff --git a/ext/comment/main.php b/ext/comment/main.php
index 85a9672b..75b171d4 100644
--- a/ext/comment/main.php
+++ b/ext/comment/main.php
@@ -480,14 +480,14 @@ class CommentList extends Extension
         global $config, $database;
 
         // sqlite fails at intervals
-        if ($database->get_driver_name() === "sqlite") {
+        if ($database->get_driver_name() === Database::SQLITE_DRIVER) {
             return false;
         }
 
         $window = int_escape($config->get_int('comment_window'));
         $max = int_escape($config->get_int('comment_limit'));
 
-        if ($database->get_driver_name() == "mysql") {
+        if ($database->get_driver_name() == Database::MYSQL_DRIVER) {
             $window_sql = "interval $window minute";
         } else {
             $window_sql = "interval '$window minute'";
diff --git a/ext/index/test.php b/ext/index/test.php
index e9debe74..8f57bdb2 100644
--- a/ext/index/test.php
+++ b/ext/index/test.php
@@ -157,7 +157,7 @@ class IndexTest extends ShimmiePHPUnitTestCase
 
         global $database;
         $db = $database->get_driver_name();
-        if ($db == "pgsql" || $db == "sqlite") {
+        if ($db == Database::PGSQL_DRIVER || $db == Database::SQLITE_DRIVER) {
             $this->markTestIncomplete();
         }
 
diff --git a/ext/ipban/main.php b/ext/ipban/main.php
index 659246f9..541c853b 100644
--- a/ext/ipban/main.php
+++ b/ext/ipban/main.php
@@ -235,7 +235,7 @@ class IPBan extends Extension
     {
         global $config, $database;
 
-        $prefix = ($database->get_driver_name() == "sqlite" ? "bans." : "");
+        $prefix = ($database->get_driver_name() == Database::SQLITE_DRIVER ? "bans." : "");
 
         $bans = $this->get_active_bans();
 
diff --git a/ext/ipban/theme.php b/ext/ipban/theme.php
index 6529c51f..67979128 100644
--- a/ext/ipban/theme.php
+++ b/ext/ipban/theme.php
@@ -16,7 +16,7 @@ class IPBanTheme extends Themelet
     {
         global $database, $user;
         $h_bans = "";
-        $prefix = ($database->get_driver_name() == "sqlite" ? "bans." : "");
+        $prefix = ($database->get_driver_name() == Database::SQLITE_DRIVER ? "bans." : "");
         foreach ($bans as $ban) {
             $end_human = date('Y-m-d', $ban[$prefix.'end_timestamp']);
             $h_bans .= "
diff --git a/ext/log_db/main.php b/ext/log_db/main.php
index b185c783..5b400b12 100644
--- a/ext/log_db/main.php
+++ b/ext/log_db/main.php
@@ -68,7 +68,7 @@ class LogDatabase extends Extension
                     $args["module"] = $_GET["module"];
                 }
                 if (!empty($_GET["user"])) {
-                    if ($database->get_driver_name() == "pgsql") {
+                    if ($database->get_driver_name() == Database::PGSQL_DRIVER) {
                         if (preg_match("#\d+\.\d+\.\d+\.\d+(/\d+)?#", $_GET["user"])) {
                             $wheres[] = "(username = :user1 OR text(address) = :user2)";
                             $args["user1"] = $_GET["user"];
diff --git a/ext/rating/main.php b/ext/rating/main.php
index 18b40823..9f32280d 100644
--- a/ext/rating/main.php
+++ b/ext/rating/main.php
@@ -37,7 +37,7 @@ class RatingSetEvent extends Event
 
 class Ratings extends Extension
 {
-    protected $db_support = ['mysql','pgsql'];  // ?
+    protected $db_support = [Database::MYSQL_DRIVER,Database::PGSQL_DRIVER];
 
     public function get_priority(): int
     {
@@ -331,10 +331,10 @@ class Ratings extends Extension
         if ($config->get_int("ext_ratings2_version") < 3) {
             $database->Execute("UPDATE images SET rating = 'u' WHERE rating is null");
             switch ($database->get_driver_name()) {
-                case "mysql":
+                case Database::MYSQL_DRIVER:
                     $database->Execute("ALTER TABLE images CHANGE rating rating CHAR(1) NOT NULL DEFAULT 'u'");
                     break;
-                case "pgsql":
+                case Database::PGSQL_DRIVER:
                     $database->Execute("ALTER TABLE images ALTER COLUMN rating SET DEFAULT 'u'");
                     $database->Execute("ALTER TABLE images ALTER COLUMN rating SET NOT NULL");
                     break;
diff --git a/ext/relatationships/main.php b/ext/relatationships/main.php
index 28e785d1..402f4fd0 100644
--- a/ext/relatationships/main.php
+++ b/ext/relatationships/main.php
@@ -8,7 +8,7 @@
 
 class Relationships extends Extension
 {
-    protected $db_support = ['mysql', 'pgsql'];
+    protected $db_support = [Database::MYSQL_DRIVER, Database::PGSQL_DRIVER];
 
     public function onInitExt(InitExtEvent $event)
     {
diff --git a/ext/rss_comments/main.php b/ext/rss_comments/main.php
index 4f3b5ed5..dfc37717 100644
--- a/ext/rss_comments/main.php
+++ b/ext/rss_comments/main.php
@@ -9,7 +9,7 @@
 
 class RSS_Comments extends Extension
 {
-    protected $db_support = ['mysql', 'sqlite'];  // pgsql has no UNIX_TIMESTAMP
+    protected $db_support = [Database::MYSQL_DRIVER, Database::SQLITE_DRIVER];  // pgsql has no UNIX_TIMESTAMP
 
     public function onPostListBuilding(PostListBuildingEvent $event)
     {
diff --git a/ext/rule34/main.php b/ext/rule34/main.php
index a39b6949..577ca5af 100644
--- a/ext/rule34/main.php
+++ b/ext/rule34/main.php
@@ -19,7 +19,7 @@ if ( // kill these glitched requests immediately
 
 class Rule34 extends Extension
 {
-    protected $db_support = ['pgsql'];  # Only PG has the NOTIFY pubsub system
+    protected $db_support = [Database::PGSQL_DRIVER];  # Only PG has the NOTIFY pubsub system
 
     public function onImageDeletion(ImageDeletionEvent $event)
     {
diff --git a/ext/rule34/theme.php b/ext/rule34/theme.php
index 09712b3d..d4dd29e1 100644
--- a/ext/rule34/theme.php
+++ b/ext/rule34/theme.php
@@ -19,7 +19,7 @@ class Rule34Theme extends Themelet
     {
         global $database, $user;
         $h_bans = "";
-        $prefix = ($database->get_driver_name() == "sqlite" ? "bans." : "");
+        $prefix = ($database->get_driver_name() == Database::SQLITE_DRIVER ? "bans." : "");
         foreach ($bans as $ban) {
             $h_bans .= "
 				<tr>
diff --git a/ext/tips/main.php b/ext/tips/main.php
index f4f7d619..04fb5124 100644
--- a/ext/tips/main.php
+++ b/ext/tips/main.php
@@ -10,7 +10,7 @@
 
 class Tips extends Extension
 {
-    protected $db_support = ['mysql', 'sqlite'];  // rand() ?
+    protected $db_support = [Database::MYSQL_DRIVER, Database::SQLITE_DRIVER];  // rand() ?
 
     public function onInitExt(InitExtEvent $event)
     {
diff --git a/ext/upgrade/main.php b/ext/upgrade/main.php
index 3321b409..0aef4530 100644
--- a/ext/upgrade/main.php
+++ b/ext/upgrade/main.php
@@ -44,7 +44,7 @@ class Upgrade extends Extension
             $config->set_bool("in_upgrade", true);
             $config->set_int("db_version", 9);
 
-            if ($database->get_driver_name() == 'mysql') {
+            if ($database->get_driver_name() == Database::MYSQL_DRIVER) {
                 $tables = $database->get_col("SHOW TABLES");
                 foreach ($tables as $table) {
                     log_info("upgrade", "converting $table to innodb");
@@ -84,7 +84,7 @@ class Upgrade extends Extension
             $config->set_bool("in_upgrade", true);
             $config->set_int("db_version", 12);
 
-            if ($database->get_driver_name() == 'pgsql') {
+            if ($database->get_driver_name() == Database::PGSQL_DRIVER) {
                 log_info("upgrade", "Changing ext column to VARCHAR");
                 $database->execute("ALTER TABLE images ALTER COLUMN ext SET DATA TYPE VARCHAR(4)");
             }
@@ -101,9 +101,9 @@ class Upgrade extends Extension
             $config->set_int("db_version", 13);
 
             log_info("upgrade", "Changing password column to VARCHAR(250)");
-            if ($database->get_driver_name() == 'pgsql') {
+            if ($database->get_driver_name() == Database::PGSQL_DRIVER) {
                 $database->execute("ALTER TABLE users ALTER COLUMN pass SET DATA TYPE VARCHAR(250)");
-            } elseif ($database->get_driver_name() == 'mysql') {
+            } elseif ($database->get_driver_name() == Database::MYSQL_DRIVER) {
                 $database->execute("ALTER TABLE users CHANGE pass pass VARCHAR(250)");
             }
 
@@ -116,11 +116,11 @@ class Upgrade extends Extension
             $config->set_int("db_version", 14);
 
             log_info("upgrade", "Changing tag column to VARCHAR(255)");
-            if ($database->get_driver_name() == 'pgsql') {
+            if ($database->get_driver_name() == Database::PGSQL_DRIVER) {
                 $database->execute('ALTER TABLE tags ALTER COLUMN tag SET DATA TYPE VARCHAR(255)');
                 $database->execute('ALTER TABLE aliases ALTER COLUMN oldtag SET DATA TYPE VARCHAR(255)');
                 $database->execute('ALTER TABLE aliases ALTER COLUMN newtag SET DATA TYPE VARCHAR(255)');
-            } elseif ($database->get_driver_name() == 'mysql') {
+            } elseif ($database->get_driver_name() == Database::MYSQL_DRIVER) {
                 $database->execute('ALTER TABLE tags MODIFY COLUMN tag VARCHAR(255) NOT NULL');
                 $database->execute('ALTER TABLE aliases MODIFY COLUMN oldtag VARCHAR(255) NOT NULL');
                 $database->execute('ALTER TABLE aliases MODIFY COLUMN newtag VARCHAR(255) NOT NULL');

From e940d87c229daab775dfc7ad4876d91c54319c67 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Sat, 15 Jun 2019 10:02:08 -0500
Subject: [PATCH 08/23] Added image_id null check to resize's data upload
 event, to prevent an error when merging is enabled

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

diff --git a/ext/resize/main.php b/ext/resize/main.php
index 41eabde0..35fd537f 100644
--- a/ext/resize/main.php
+++ b/ext/resize/main.php
@@ -62,6 +62,10 @@ class ResizeImage extends Extension
     {
         global $config, $page;
 
+        if($event->image_id==null) {
+            return;
+        }
+
         $image_obj = Image::by_id($event->image_id);
 
         if ($config->get_bool("resize_upload") == true && ($image_obj->ext == "jpg" || $image_obj->ext == "png" || $image_obj->ext == "gif" || $image_obj->ext == "webp")) {

From 0202597f88b602d294117b32f06eacad9429f2b9 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Sat, 15 Jun 2019 11:01:13 -0500
Subject: [PATCH 09/23] Added lock file usage to cron uploader to prevent
 concurrent runs. Changed extension manager to allow author to be a
 comma-separated list.

---
 ext/cron_uploader/main.php | 187 ++++++++++++++++++++-----------------
 ext/ext_manager/main.php   |  54 +++++++----
 ext/ext_manager/theme.php  |  41 ++++----
 3 files changed, 162 insertions(+), 120 deletions(-)

diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php
index f4bd3c51..fe1bbfbf 100644
--- a/ext/cron_uploader/main.php
+++ b/ext/cron_uploader/main.php
@@ -1,42 +1,44 @@
 <?php
+
 /*
  * Name: Cron Uploader
- * Author: YaoiFox <admin@yaoifox.com>
+ * Authors: YaoiFox <admin@yaoifox.com>, Matthew Barbour <matthew@darkholme.net>
  * Link: http://www.yaoifox.com/
  * License: GPLv2
  * Description: Uploads images automatically using Cron Jobs
  * Documentation: Installation guide: activate this extension and navigate to www.yoursite.com/cron_upload
  */
+
 class CronUploader extends Extension
 {
     // TODO: Checkbox option to only allow localhost + a list of additional IP adresses that can be set in /cron_upload
     // TODO: Change logging to MySQL + display log at /cron_upload
     // TODO: Move stuff to theme.php
-    
+
     /**
      * Lists all log events this session
      * @var string
      */
     private $upload_info = "";
-    
+
     /**
      * Lists all files & info required to upload.
      * @var array
      */
     private $image_queue = [];
-    
+
     /**
      * Cron Uploader root directory
      * @var string
      */
     private $root_dir = "";
-    
+
     /**
      * Key used to identify uploader
      * @var string
      */
     private $upload_key = "";
-    
+
     /**
      * Checks if the cron upload page has been accessed
      * and initializes the upload.
@@ -44,39 +46,50 @@ class CronUploader extends Extension
     public function onPageRequest(PageRequestEvent $event)
     {
         global $config, $user;
-        
+
         if ($event->page_matches("cron_upload")) {
             $this->upload_key = $config->get_string("cron_uploader_key", "");
-            
+
             // If the key is in the url, upload
             if ($this->upload_key != "" && $event->get_arg(0) == $this->upload_key) {
                 // log in as admin
-                $this->process_upload(); // Start upload
+                $this->set_dir();
+
+                $lockfile = fopen($this->root_dir . "/.lock", "w");
+                try {
+                    if (!flock($lockfile, LOCK_EX | LOCK_NB)) {
+                        throw new Exception("Cron upload process is already running");
+                    }
+                    $this->process_upload(); // Start upload
+                } finally {
+                    flock($lockfile, LOCK_UN);
+                    fclose($lockfile);
+                }
             } elseif ($user->is_admin()) {
                 $this->set_dir();
                 $this->display_documentation();
             }
         }
     }
-    
+
     private function display_documentation()
     {
         global $page;
         $this->set_dir(); // Determines path to cron_uploader_dir
-        
-        
+
+
         $queue_dir = $this->root_dir . "/queue";
         $uploaded_dir = $this->root_dir . "/uploaded";
         $failed_dir = $this->root_dir . "/failed_to_upload";
-        
+
         $queue_dirinfo = $this->scan_dir($queue_dir);
         $uploaded_dirinfo = $this->scan_dir($uploaded_dir);
         $failed_dirinfo = $this->scan_dir($failed_dir);
-        
+
         $cron_url = make_http(make_link("/cron_upload/" . $this->upload_key));
         $cron_cmd = "curl --silent $cron_url";
         $log_path = $this->root_dir . "/uploads.log";
-        
+
         $info_html = "<b>Information</b>
 			<br>
 			<table style='width:470px;'>
@@ -105,7 +118,7 @@ class CronUploader extends Extension
 			<br>Cron Command: <input type='text' size='60' value='$cron_cmd'><br>
 			Create a cron job with the command above.<br/>
 				Read the documentation if you're not sure what to do.<br>";
-        
+
         $install_html = "
 			This cron uploader is fairly easy to use but has to be configured first.
 			<br />1. Install & activate this plugin.
@@ -130,8 +143,10 @@ class CronUploader extends Extension
 			<br />So when you want to manually upload an image, all you have to do is open the link once.
 			<br />This link can be found under 'Cron Command' in the board config, just remove the 'wget ' part and only the url remains.
 			<br />(<b>$cron_url</b>)";
-        
-        
+
+        $page->set_title("Cron Uploader");
+        $page->set_heading("Cron Uploader");
+
         $block = new Block("Cron Uploader", $info_html, "main", 10);
         $block_install = new Block("Installation Guide", $install_html, "main", 20);
         $page->add_block($block);
@@ -143,35 +158,35 @@ class CronUploader extends Extension
         global $config;
         // Set default values
         $this->upload_key = $config->get_string("cron_uploader_key", "");
-        if (strlen($this->upload_key)<=0) {
+        if (strlen($this->upload_key) <= 0) {
             $this->upload_key = $this->generate_key();
-    
+
             $config->set_default_int('cron_uploader_count', 1);
             $config->set_default_string('cron_uploader_key', $this->upload_key);
             $this->set_dir();
         }
     }
-    
+
     public function onSetupBuilding(SetupBuildingEvent $event)
     {
         $this->set_dir();
-        
+
         $cron_url = make_http(make_link("/cron_upload/" . $this->upload_key));
         $cron_cmd = "curl --silent $cron_url";
         $documentation_link = make_http(make_link("cron_upload"));
-        
+
         $sb = new SetupBlock("Cron Uploader");
         $sb->add_label("<b>Settings</b><br>");
         $sb->add_int_option("cron_uploader_count", "How many to upload each time");
         $sb->add_text_option("cron_uploader_dir", "<br>Set Cron Uploader root directory<br>");
-        
+
         $sb->add_label("<br>Cron Command: <input type='text' size='60' value='$cron_cmd'><br>
 		Create a cron job with the command above.<br/>
 		<a href='$documentation_link'>Read the documentation</a> if you're not sure what to do.");
 
         $event->panel->add_block($sb);
     }
-    
+
     /*
      * Generates a unique key for the website to prevent unauthorized access.
      */
@@ -180,14 +195,14 @@ class CronUploader extends Extension
         $length = 20;
         $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
         $randomString = '';
-        
-        for ($i = 0; $i < $length; $i ++) {
+
+        for ($i = 0; $i < $length; $i++) {
             $randomString .= $characters [rand(0, strlen($characters) - 1)];
         }
-        
+
         return $randomString;
     }
-    
+
     /*
      * Set the directory for the image queue. If no directory was given, set it to the default directory.
      */
@@ -195,15 +210,15 @@ class CronUploader extends Extension
     {
         global $config;
         // Determine directory (none = default)
-        
+
         $dir = $config->get_string("cron_uploader_dir", "");
-        
+
         // Sets new default dir if not in config yet/anymore
         if ($dir == "") {
             $dir = data_path("cron_uploader");
             $config->set_string('cron_uploader_dir', $dir);
         }
-            
+
         // Make the directory if it doesn't exist yet
         if (!is_dir($dir . "/queue/")) {
             mkdir($dir . "/queue/", 0775, true);
@@ -214,31 +229,31 @@ class CronUploader extends Extension
         if (!is_dir($dir . "/failed_to_upload/")) {
             mkdir($dir . "/failed_to_upload/", 0775, true);
         }
-        
+
         $this->root_dir = $dir;
         return $dir;
     }
-    
+
     /**
      * Returns amount of files & total size of dir.
      */
     public function scan_dir(string $path): array
     {
-        $bytestotal=0;
-        $nbfiles=0;
+        $bytestotal = 0;
+        $nbfiles = 0;
 
-        $ite=new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
-        foreach (new RecursiveIteratorIterator($ite) as $filename=>$cur) {
+        $ite = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
+        foreach (new RecursiveIteratorIterator($ite) as $filename => $cur) {
             $filesize = $cur->getSize();
             $bytestotal += $filesize;
             $nbfiles++;
         }
-    
+
         $size_mb = $bytestotal / 1048576; // to mb
         $size_mb = number_format($size_mb, 2, '.', '');
-        return ['total_files'=>$nbfiles,'total_mb'=>$size_mb];
+        return ['total_files' => $nbfiles, 'total_mb' => $size_mb];
     }
-    
+
     /**
      * Uploads the image & handles everything
      */
@@ -246,24 +261,24 @@ class CronUploader extends Extension
     {
         global $config, $database;
 
-        set_time_limit(0);
+        //set_time_limit(0);
 
-        $output_subdir = date('Ymd-His', time())."/";
-        $this->set_dir();
+
+        $output_subdir = date('Ymd-His', time()) . "/";
         $this->generate_image_queue();
-        
+
         // Gets amount of imgs to upload
         if ($upload_count == 0) {
             $upload_count = $config->get_int("cron_uploader_count", 1);
         }
-        
+
         // Throw exception if there's nothing in the queue
         if (count($this->image_queue) == 0) {
             $this->add_upload_info("Your queue is empty so nothing could be uploaded.");
             $this->handle_log();
             return false;
         }
-        
+
         // Randomize Images
         //shuffle($this->image_queue);
 
@@ -274,15 +289,15 @@ class CronUploader extends Extension
         $failedItems = [];
 
         // Upload the file(s)
-        for ($i = 0; $i < $upload_count && sizeof($this->image_queue)>0; $i++) {
+        for ($i = 0; $i < $upload_count && sizeof($this->image_queue) > 0; $i++) {
             $img = array_pop($this->image_queue);
-            
+
             try {
                 $database->beginTransaction();
                 $result = $this->add_image($img[0], $img[1], $img[2]);
                 $database->commit();
                 $this->move_uploaded($img[0], $img[1], $output_subdir, false);
-                if ($result==null) {
+                if ($result == null) {
                     $merged++;
                 } else {
                     $added++;
@@ -290,7 +305,7 @@ class CronUploader extends Extension
             } catch (Exception $e) {
                 $failed++;
                 $this->move_uploaded($img[0], $img[1], $output_subdir, true);
-                $msgNumber = $this->add_upload_info("(".gettype($e).") ".$e->getMessage());
+                $msgNumber = $this->add_upload_info("(" . gettype($e) . ") " . $e->getMessage());
                 $msgNumber = $this->add_upload_info($e->getTraceAsString());
                 if (strpos($e->getMessage(), 'SQLSTATE') !== false) {
                     // Postgres invalidates the transaction if there is an SQL error,
@@ -310,40 +325,40 @@ class CronUploader extends Extension
         $msgNumber = $this->add_upload_info("Items failed: $failed");
 
 
-        
         // Display & save upload log
         $this->handle_log();
-        
+
         return true;
+
     }
-    
+
     private function move_uploaded($path, $filename, $output_subdir, $corrupt = false)
     {
         global $config;
-        
+
         // Create
         $newDir = $this->root_dir;
-        
+
         $relativeDir = dirname(substr($path, strlen($this->root_dir) + 7));
 
         // Determine which dir to move to
         if ($corrupt) {
             // Move to corrupt dir
-            $newDir .= "/failed_to_upload/".$output_subdir.$relativeDir;
+            $newDir .= "/failed_to_upload/" . $output_subdir . $relativeDir;
             $info = "ERROR: Image was not uploaded.";
         } else {
-            $newDir .= "/uploaded/".$output_subdir.$relativeDir;
+            $newDir .= "/uploaded/" . $output_subdir . $relativeDir;
             $info = "Image successfully uploaded. ";
         }
-        $newDir = str_replace("//", "/", $newDir."/");
+        $newDir = str_replace("//", "/", $newDir . "/");
 
         if (!is_dir($newDir)) {
             mkdir($newDir, 0775, true);
         }
 
         // move file to correct dir
-        rename($path, $newDir.$filename);
-        
+        rename($path, $newDir . $filename);
+
         $this->add_upload_info($info . "Image \"$filename\" moved from queue to \"$newDir\".");
     }
 
@@ -353,7 +368,7 @@ class CronUploader extends Extension
     private function add_image(string $tmpname, string $filename, string $tags)
     {
         assert(file_exists($tmpname));
-        
+
         $pathinfo = pathinfo($filename);
         $metadata = [];
         $metadata ['filename'] = $pathinfo ['basename'];
@@ -364,7 +379,7 @@ class CronUploader extends Extension
         $metadata ['source'] = null;
         $event = new DataUploadEvent($tmpname, $metadata);
         send_event($event);
-        
+
         // Generate info message
         $infomsg = ""; // Will contain info message
         if ($event->image_id == -1) {
@@ -377,18 +392,18 @@ class CronUploader extends Extension
         $msgNumber = $this->add_upload_info($infomsg);
         return $event->image_id;
     }
-    
+
     private function generate_image_queue(): void
     {
         $base = $this->root_dir . "/queue";
-        
-        if (! is_dir($base)) {
+
+        if (!is_dir($base)) {
             $this->add_upload_info("Image Queue Directory could not be found at \"$base\".");
             return;
         }
-        
-        $ite=new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS);
-        foreach (new RecursiveIteratorIterator($ite) as $fullpath=>$cur) {
+
+        $ite = new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS);
+        foreach (new RecursiveIteratorIterator($ite) as $fullpath => $cur) {
             if (!is_link($fullpath) && !is_dir($fullpath)) {
                 $pathinfo = pathinfo($fullpath);
 
@@ -396,62 +411,62 @@ class CronUploader extends Extension
                 $tags = path_to_tags($relativePath);
 
                 $img = [
-                        0 => $fullpath,
-                        1 => $pathinfo ["basename"],
-                        2 => $tags
+                    0 => $fullpath,
+                    1 => $pathinfo ["basename"],
+                    2 => $tags
                 ];
                 array_push($this->image_queue, $img);
             }
         }
     }
-    
+
     /**
      * Adds a message to the info being published at the end
      */
     private function add_upload_info(string $text, int $addon = 0): int
     {
         $info = $this->upload_info;
-        $time = "[" .date('Y-m-d H:i:s'). "]";
-        
+        $time = "[" . date('Y-m-d H:i:s') . "]";
+
         // If addon function is not used
         if ($addon == 0) {
-            $this->upload_info .=  "$time $text\r\n";
-            
+            $this->upload_info .= "$time $text\r\n";
+
             // Returns the number of the current line
-            $currentLine = substr_count($this->upload_info, "\n") -1;
+            $currentLine = substr_count($this->upload_info, "\n") - 1;
             return $currentLine;
         }
-        
+
         // else if addon function is used, select the line & modify it
         $lines = substr($info, "\n"); // Seperate the string to array in lines
         $lines[$addon] = "$lines[$addon] $text"; // Add the content to the line
         $this->upload_info = implode("\n", $lines); // Put string back together & update
-        
+
         return $addon; // Return line number
     }
-    
+
     /**
      * This is run at the end to display & save the log.
      */
     private function handle_log()
     {
         global $page;
-        
+
         // Display message
         $page->set_mode("data");
         $page->set_type("text/plain");
         $page->set_data($this->upload_info);
-        
+
         // Save log
         $log_path = $this->root_dir . "/uploads.log";
-        
+
         if (file_exists($log_path)) {
             $prev_content = file_get_contents($log_path);
         } else {
             $prev_content = "";
         }
-         
-        $content = $prev_content ."\r\n".$this->upload_info;
+
+        $content = $prev_content . "\r\n" . $this->upload_info;
         file_put_contents($log_path, $content);
     }
 }
diff --git a/ext/ext_manager/main.php b/ext/ext_manager/main.php
index f9ef6bba..dddefc70 100644
--- a/ext/ext_manager/main.php
+++ b/ext/ext_manager/main.php
@@ -22,8 +22,7 @@ class ExtensionInfo
     public $ext_name;
     public $name;
     public $link;
-    public $author;
-    public $email;
+    public $authors;
     public $description;
     public $documentation;
     public $version;
@@ -39,8 +38,9 @@ class ExtensionInfo
         $this->ext_name = $matches[1];
         $this->name = $this->ext_name;
         $this->enabled = $this->is_enabled($this->ext_name);
+        $this->authors = [];
 
-        for ($i=0; $i<$number_of_lines; $i++) {
+        for ($i = 0; $i < $number_of_lines; $i++) {
             $line = $lines[$i];
             if (preg_match("/Name: (.*)/", $line, $matches)) {
                 $this->name = $matches[1];
@@ -53,25 +53,31 @@ class ExtensionInfo
                 }
             } elseif (preg_match("/Version: (.*)/", $line, $matches)) {
                 $this->version = $matches[1];
-            } elseif (preg_match("/Author: (.*) [<\(](.*@.*)[>\)]/", $line, $matches)) {
-                $this->author = $matches[1];
-                $this->email = $matches[2];
-            } elseif (preg_match("/Author: (.*)/", $line, $matches)) {
-                $this->author = $matches[1];
+            } elseif (preg_match("/Authors?: (.*)/", $line, $matches)) {
+                $author_list = explode(',', $matches[1]);
+                foreach ($author_list as $author) {
+                    if (preg_match("/(.*) [<\(](.*@.*)[>\)]/", $author, $matches)) {
+                        $this->authors[] = new ExtensionAuthor($matches[1], $matches[2]);
+                    } else {
+                        $this->authors[] = new ExtensionAuthor($author, null);
+                    }
+                }
+
+
             } elseif (preg_match("/(.*)Description: ?(.*)/", $line, $matches)) {
                 $this->description = $matches[2];
-                $start = $matches[1]." ";
+                $start = $matches[1] . " ";
                 $start_len = strlen($start);
-                while (substr($lines[$i+1], 0, $start_len) == $start) {
-                    $this->description .= " ".substr($lines[$i+1], $start_len);
+                while (substr($lines[$i + 1], 0, $start_len) == $start) {
+                    $this->description .= " " . substr($lines[$i + 1], $start_len);
                     $i++;
                 }
             } elseif (preg_match("/(.*)Documentation: ?(.*)/", $line, $matches)) {
                 $this->documentation = $matches[2];
-                $start = $matches[1]." ";
+                $start = $matches[1] . " ";
                 $start_len = strlen($start);
-                while (substr($lines[$i+1], 0, $start_len) == $start) {
-                    $this->documentation .= " ".substr($lines[$i+1], $start_len);
+                while (substr($lines[$i + 1], 0, $start_len) == $start) {
+                    $this->documentation .= " " . substr($lines[$i + 1], $start_len);
                     $i++;
                 }
                 $this->documentation = str_replace('$site', make_http(get_base_href()), $this->documentation);
@@ -96,6 +102,18 @@ class ExtensionInfo
     }
 }
 
+class ExtensionAuthor
+{
+    public $name;
+    public $email;
+
+    public function __construct(string $name, string $email)
+    {
+        $this->name = $name;
+        $this->email = $email;
+    }
+}
+
 class ExtManager extends Extension
 {
     public function onPageRequest(PageRequestEvent $event)
@@ -166,7 +184,7 @@ class ExtManager extends Extension
         if ($all) {
             $exts = zglob("ext/*/main.php");
         } else {
-            $exts = zglob("ext/{".ENABLED_EXTS."}/main.php");
+            $exts = zglob("ext/{" . ENABLED_EXTS . "}/main.php");
         }
         foreach ($exts as $main) {
             $extensions[] = new ExtensionInfo($main);
@@ -200,9 +218,9 @@ class ExtManager extends Extension
     {
         file_put_contents(
             "data/config/extensions.conf.php",
-            '<'.'?php'."\n".
-            'define("EXTRA_EXTS", "'.implode(",", $extras).'");'."\n".
-            '?'.">"
+            '<' . '?php' . "\n" .
+            'define("EXTRA_EXTS", "' . implode(",", $extras) . '");' . "\n" .
+            '?' . ">"
         );
 
         // when the list of active extensions changes, we can be
diff --git a/ext/ext_manager/theme.php b/ext/ext_manager/theme.php
index 3cc6d871..58bd79ab 100644
--- a/ext/ext_manager/theme.php
+++ b/ext/ext_manager/theme.php
@@ -9,7 +9,7 @@ class ExtManagerTheme extends Themelet
     {
         $h_en = $editable ? "<th>Enabled</th>" : "";
         $html = "
-			".make_form(make_link("ext_manager/set"))."
+			" . make_form(make_link("ext_manager/set")) . "
 				<table id='extensions' class='zebra sortable'>
 					<thead>
 						<tr>
@@ -26,17 +26,17 @@ class ExtManagerTheme extends Themelet
                 continue;
             }
 
-            $h_name        = html_escape(empty($extension->name) ? $extension->ext_name : $extension->name);
+            $h_name = html_escape(empty($extension->name) ? $extension->ext_name : $extension->name);
             $h_description = html_escape($extension->description);
-            $h_link        = make_link("ext_doc/".url_escape($extension->ext_name));
-            $h_enabled     = ($extension->enabled === true ? " checked='checked'" : ($extension->enabled === false ? "" : " disabled checked='checked'"));
-            $h_enabled_box = $editable ? "<td><input type='checkbox' name='ext_".html_escape($extension->ext_name)."' id='ext_".html_escape($extension->ext_name)."'$h_enabled></td>" : "";
-            $h_docs        = ($extension->documentation ? "<a href='$h_link'>â– </a>" : ""); //TODO: A proper "docs" symbol would be preferred here.
+            $h_link = make_link("ext_doc/" . url_escape($extension->ext_name));
+            $h_enabled = ($extension->enabled === true ? " checked='checked'" : ($extension->enabled === false ? "" : " disabled checked='checked'"));
+            $h_enabled_box = $editable ? "<td><input type='checkbox' name='ext_" . html_escape($extension->ext_name) . "' id='ext_" . html_escape($extension->ext_name) . "'$h_enabled></td>" : "";
+            $h_docs = ($extension->documentation ? "<a href='$h_link'>â– </a>" : ""); //TODO: A proper "docs" symbol would be preferred here.
 
             $html .= "
 				<tr data-ext='{$extension->ext_name}'>
 					{$h_enabled_box}
-					<td><label for='ext_".html_escape($extension->ext_name)."'>{$h_name}</label></td>
+					<td><label for='ext_" . html_escape($extension->ext_name) . "'>{$h_name}</label></td>
 					<td>{$h_docs}</td>
 					<td style='text-align: left;'>{$h_description}</td>
 				</tr>";
@@ -116,15 +116,24 @@ class ExtManagerTheme extends Themelet
     public function display_doc(Page $page, ExtensionInfo $info)
     {
         $author = "";
-        if ($info->author) {
-            if ($info->email) {
-                $author = "<br><b>Author:</b> <a href=\"mailto:".html_escape($info->email)."\">".html_escape($info->author)."</a>";
-            } else {
-                $author = "<br><b>Author:</b> ".html_escape($info->author);
+        if (count($info->authors) > 0) {
+            $author = "<br /><b>Author";
+            if (count($info->authors) > 1) {
+                $author .= "s";
             }
+            $author .= ":</b>";
+            foreach ($info->authors as $auth) {
+                if (!empty($auth->email)) {
+                    $author .= "<a href=\"mailto:" . html_escape($auth->email) . "\">" . html_escape($auth->name) . "</a>";
+                } else {
+                    $author .= html_escape($auth->name);
+                }
+            }
+
         }
-        $version = ($info->version) ? "<br><b>Version:</b> ".html_escape($info->version) : "";
-        $link = ($info->link) ? "<br><b>Home Page:</b> <a href=\"".html_escape($info->link)."\">Link</a>" : "";
+
+        $version = ($info->version) ? "<br><b>Version:</b> " . html_escape($info->version) : "";
+        $link = ($info->link) ? "<br><b>Home Page:</b> <a href=\"" . html_escape($info->link) . "\">Link</a>" : "";
         $doc = $info->documentation;
         $html = "
 			<div style='margin: auto; text-align: left; width: 512px;'>
@@ -133,10 +142,10 @@ class ExtManagerTheme extends Themelet
 				$link
 				<p>$doc
 				<hr>
-				<p><a href='".make_link("ext_manager")."'>Back to the list</a>
+				<p><a href='" . make_link("ext_manager") . "'>Back to the list</a>
 			</div>";
 
-        $page->set_title("Documentation for ".html_escape($info->name));
+        $page->set_title("Documentation for " . html_escape($info->name));
         $page->set_heading(html_escape($info->name));
         $page->add_block(new NavBlock());
         $page->add_block(new Block("Documentation", $html));

From 4ade0090ccd924e3c7254054a1a10ed7ec51f6f8 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Sat, 15 Jun 2019 11:03:09 -0500
Subject: [PATCH 10/23] Added float support to config

---
 core/config.php | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/core/config.php b/core/config.php
index 695fa8f1..c9fb225b 100644
--- a/core/config.php
+++ b/core/config.php
@@ -144,6 +144,13 @@ abstract class BaseConfig implements Config
         }
     }
 
+    public function set_default_float(string $name, float $value): void
+    {
+        if (is_null($this->get($name))) {
+            $this->values[$name] = $value;
+        }
+    }
+
     public function set_default_string(string $name, string $value): void
     {
         if (is_null($this->get($name))) {
@@ -170,6 +177,11 @@ abstract class BaseConfig implements Config
         return (int)($this->get($name, $default));
     }
 
+    public function get_float(string $name, ?float $default=null): ?float
+    {
+        return (float)($this->get($name, $default));
+    }
+
     public function get_string(string $name, ?string $default=null): ?string
     {
         return $this->get($name, $default);

From 37fe743f6565d8b3e663fad0e08c8173d293c311 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Sat, 15 Jun 2019 11:18:52 -0500
Subject: [PATCH 11/23] Changed "images" and "thumbs" usages to constants

---
 core/extension.php        |  6 +++---
 core/imageboard/image.php |  8 ++++++--
 core/imageboard/misc.php  | 10 +++++-----
 core/util.php             |  7 +++++--
 ext/admin/main.php        |  2 +-
 ext/bulk_add_csv/main.php |  2 +-
 ext/handle_flash/main.php |  2 +-
 ext/handle_mp3/main.php   |  2 +-
 ext/handle_pixel/main.php |  4 ++--
 ext/handle_svg/main.php   |  8 ++++----
 ext/regen_thumb/main.php  |  4 ++--
 ext/resize/main.php       |  6 +++---
 ext/rotate/main.php       |  4 ++--
 ext/rule34/main.php       |  4 ++--
 ext/transcode/main.php    |  4 ++--
 15 files changed, 40 insertions(+), 33 deletions(-)

diff --git a/core/extension.php b/core/extension.php
index af7bb6ad..d691bd68 100644
--- a/core/extension.php
+++ b/core/extension.php
@@ -182,7 +182,7 @@ abstract class DataHandlerExtension extends Extension
 
                 // even more hax..
                 $event->metadata['tags'] = $existing->get_tag_list();
-                $image = $this->create_image_from_data(warehouse_path("images", $event->metadata['hash']), $event->metadata);
+                $image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->metadata['hash']), $event->metadata);
 
                 if (is_null($image)) {
                     throw new UploadException("Data handler failed to create image object from data");
@@ -192,7 +192,7 @@ abstract class DataHandlerExtension extends Extension
                 send_event($ire);
                 $event->image_id = $image_id;
             } else {
-                $image = $this->create_image_from_data(warehouse_path("images", $event->hash), $event->metadata);
+                $image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
                 if (is_null($image)) {
                     throw new UploadException("Data handler failed to create image object from data");
                 }
@@ -224,7 +224,7 @@ abstract class DataHandlerExtension extends Extension
             if ($event->force) {
                 $result = $this->create_thumb($event->hash, $event->type);
             } else {
-                $outname = warehouse_path("thumbs", $event->hash);
+                $outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash);
                 if (file_exists($outname)) {
                     return;
                 }
diff --git a/core/imageboard/image.php b/core/imageboard/image.php
index af6a15d8..717abf7f 100644
--- a/core/imageboard/image.php
+++ b/core/imageboard/image.php
@@ -10,6 +10,10 @@
  */
 class Image
 {
+    public const DATA_DIR = "data";
+    public const IMAGE_DIR = "images";
+    public const THUMBNAIL_DIR = "thumbs";
+
     private static $tag_n = 0; // temp hack
     public static $order_sql = null; // this feels ugly
 
@@ -502,7 +506,7 @@ class Image
      */
     public function get_image_filename(): string
     {
-        return warehouse_path("images", $this->hash);
+        return warehouse_path(self::IMAGE_DIR, $this->hash);
     }
 
     /**
@@ -510,7 +514,7 @@ class Image
      */
     public function get_thumb_filename(): string
     {
-        return warehouse_path("thumbs", $this->hash);
+        return warehouse_path(self::THUMBNAIL_DIR, $this->hash);
     }
 
     /**
diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php
index e9bd93c6..d08daf2b 100644
--- a/core/imageboard/misc.php
+++ b/core/imageboard/misc.php
@@ -12,7 +12,7 @@
  */
 function move_upload_to_archive(DataUploadEvent $event): void
 {
-    $target = warehouse_path("images", $event->hash);
+    $target = warehouse_path(Image::IMAGE_DIR, $event->hash);
     if (!@copy($event->tmpname, $target)) {
         $errors = error_get_last();
         throw new UploadException(
@@ -171,8 +171,8 @@ function create_thumbnail_convert($hash, $input_type = ""): bool
 {
     global $config;
 
-    $inname  = warehouse_path("images", $hash);
-    $outname = warehouse_path("thumbs", $hash);
+    $inname  = warehouse_path(Image::IMAGE_DIR, $hash);
+    $outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
 
     $q = $config->get_int("thumb_quality");
     $convert = $config->get_string("thumb_convert_path");
@@ -236,8 +236,8 @@ function create_thumbnail_ffmpeg($hash): bool
         return false;
     }
 
-    $inname  = warehouse_path("images", $hash);
-    $outname = warehouse_path("thumbs", $hash);
+    $inname  = warehouse_path(Image::IMAGE_DIR, $hash);
+    $outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
 
     $orig_size = video_size($inname);
     $scaled_size = get_thumbnail_size($orig_size[0], $orig_size[1], true);
diff --git a/core/util.php b/core/util.php
index 299463d8..6dd74b26 100644
--- a/core/util.php
+++ b/core/util.php
@@ -163,10 +163,13 @@ function warehouse_path(string $base, string $hash, bool $create=true): string
 {
     $ab = substr($hash, 0, 2);
     $cd = substr($hash, 2, 2);
+
+    $pa = Image::DATA_DIR.'/'.$base.'/';
+
     if (WH_SPLITS == 2) {
-        $pa = 'data/'.$base.'/'.$ab.'/'.$cd.'/'.$hash;
+        $pa .= $ab.'/'.$cd.'/'.$hash;
     } else {
-        $pa = 'data/'.$base.'/'.$ab.'/'.$hash;
+        $pa .= $ab.'/'.$hash;
     }
     if ($create && !file_exists(dirname($pa))) {
         mkdir(dirname($pa), 0755, true);
diff --git a/ext/admin/main.php b/ext/admin/main.php
index 2b484bc1..22845575 100644
--- a/ext/admin/main.php
+++ b/ext/admin/main.php
@@ -237,7 +237,7 @@ class AdminPage extends Extension
         $zip = new ZipArchive;
         if ($zip->open($filename, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE) === true) {
             foreach ($images as $img) {
-                $img_loc = warehouse_path("images", $img["hash"], false);
+                $img_loc = warehouse_path(Image::IMAGE_DIR, $img["hash"], false);
                 $zip->addFile($img_loc, $img["hash"].".".$img["ext"]);
             }
             $zip->close();
diff --git a/ext/bulk_add_csv/main.php b/ext/bulk_add_csv/main.php
index 14db3591..d1648a7d 100644
--- a/ext/bulk_add_csv/main.php
+++ b/ext/bulk_add_csv/main.php
@@ -81,7 +81,7 @@ class BulkAddCSV extends Extension
                 send_event($ratingevent);
             }
             if (file_exists($thumbfile)) {
-                copy($thumbfile, warehouse_path("thumbs", $event->hash));
+                copy($thumbfile, warehouse_path(Image::THUMBNAIL_DIR, $event->hash));
             }
         }
     }
diff --git a/ext/handle_flash/main.php b/ext/handle_flash/main.php
index 9476499e..e47b193b 100644
--- a/ext/handle_flash/main.php
+++ b/ext/handle_flash/main.php
@@ -13,7 +13,7 @@ class FlashFileHandler extends DataHandlerExtension
         global $config;
 
         if (!create_thumbnail_ffmpeg($hash)) {
-            copy("ext/handle_flash/thumb.jpg", warehouse_path("thumbs", $hash));
+            copy("ext/handle_flash/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash));
         }
         return true;
     }
diff --git a/ext/handle_mp3/main.php b/ext/handle_mp3/main.php
index 13e2bab4..e0fcd5a7 100644
--- a/ext/handle_mp3/main.php
+++ b/ext/handle_mp3/main.php
@@ -9,7 +9,7 @@ class MP3FileHandler extends DataHandlerExtension
 {
     protected function create_thumb(string $hash, string $type): bool
     {
-        copy("ext/handle_mp3/thumb.jpg", warehouse_path("thumbs", $hash));
+        copy("ext/handle_mp3/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash));
         return true;
     }
 
diff --git a/ext/handle_pixel/main.php b/ext/handle_pixel/main.php
index a3bc3bd2..ac72bcc1 100644
--- a/ext/handle_pixel/main.php
+++ b/ext/handle_pixel/main.php
@@ -58,8 +58,8 @@ class PixelFileHandler extends DataHandlerExtension
     {
         global $config;
 
-        $inname  = warehouse_path("images", $hash);
-        $outname = warehouse_path("thumbs", $hash);
+        $inname  = warehouse_path(Image::IMAGE_DIR, $hash);
+        $outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
 
         $ok = false;
 
diff --git a/ext/handle_svg/main.php b/ext/handle_svg/main.php
index f2151c06..a15942b1 100644
--- a/ext/handle_svg/main.php
+++ b/ext/handle_svg/main.php
@@ -19,10 +19,10 @@ class SVGFileHandler extends DataHandlerExtension
             $sanitizer->removeRemoteReferences(true);
             $dirtySVG = file_get_contents($event->tmpname);
             $cleanSVG = $sanitizer->sanitize($dirtySVG);
-            file_put_contents(warehouse_path("images", $hash), $cleanSVG);
+            file_put_contents(warehouse_path(Image::IMAGE_DIR, $hash), $cleanSVG);
 
             send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
-            $image = $this->create_image_from_data(warehouse_path("images", $hash), $event->metadata);
+            $image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $hash), $event->metadata);
             if (is_null($image)) {
                 throw new UploadException("SVG handler failed to create image object from data");
             }
@@ -35,7 +35,7 @@ class SVGFileHandler extends DataHandlerExtension
     protected function create_thumb(string $hash, string $type): bool
     {
         if (!create_thumbnail_convert($hash)) {
-            copy("ext/handle_svg/thumb.jpg", warehouse_path("thumbs", $hash));
+            copy("ext/handle_svg/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash));
         }
         return true;
     }
@@ -61,7 +61,7 @@ class SVGFileHandler extends DataHandlerExtension
 
             $sanitizer = new Sanitizer();
             $sanitizer->removeRemoteReferences(true);
-            $dirtySVG = file_get_contents(warehouse_path("images", $hash));
+            $dirtySVG = file_get_contents(warehouse_path(Image::IMAGE_DIR, $hash));
             $cleanSVG = $sanitizer->sanitize($dirtySVG);
             $page->set_data($cleanSVG);
         }
diff --git a/ext/regen_thumb/main.php b/ext/regen_thumb/main.php
index c7115b34..a653e902 100644
--- a/ext/regen_thumb/main.php
+++ b/ext/regen_thumb/main.php
@@ -133,7 +133,7 @@ class RegenThumb extends Extension
                 $i = 0;
                 foreach ($images as $image) {
                     if (!$force) {
-                        $path = warehouse_path("thumbs", $image["hash"], false);
+                        $path = warehouse_path(Image::THUMBNAIL_DIR, $image["hash"], false);
                         if (file_exists($path)) {
                             continue;
                         }
@@ -157,7 +157,7 @@ class RegenThumb extends Extension
                 
                     $i = 0;
                     foreach ($images as $image) {
-                        $outname = warehouse_path("thumbs", $image["hash"]);
+                        $outname = warehouse_path(Image::THUMBNAIL_DIR, $image["hash"]);
                         if (file_exists($outname)) {
                             unlink($outname);
                             $i++;
diff --git a/ext/resize/main.php b/ext/resize/main.php
index 35fd537f..bad21f6b 100644
--- a/ext/resize/main.php
+++ b/ext/resize/main.php
@@ -79,7 +79,7 @@ class ResizeImage extends Extension
             }
             $isanigif = 0;
             if ($image_obj->ext == "gif") {
-                $image_filename = warehouse_path("images", $image_obj->hash);
+                $image_filename = warehouse_path(Image::IMAGE_DIR, $image_obj->hash);
                 if (($fh = @fopen($image_filename, 'rb'))) {
                     //check if gif is animated (via http://www.php.net/manual/en/function.imagecreatefromgif.php#104473)
                     while (!feof($fh) && $isanigif < 2) {
@@ -167,7 +167,7 @@ class ResizeImage extends Extension
         }
         
         $hash = $image_obj->hash;
-        $image_filename  = warehouse_path("images", $hash);
+        $image_filename  = warehouse_path(Image::IMAGE_DIR, $hash);
 
         $info = getimagesize($image_filename);
         if (($image_obj->width != $info[0]) || ($image_obj->height != $info[1])) {
@@ -193,7 +193,7 @@ class ResizeImage extends Extension
         $new_image->ext = $image_obj->ext;
 
         /* Move the new image into the main storage location */
-        $target = warehouse_path("images", $new_image->hash);
+        $target = warehouse_path(Image::IMAGE_DIR, $new_image->hash);
         if (!@copy($tmp_filename, $target)) {
             throw new ImageResizeException("Failed to copy new image file from temporary location ({$tmp_filename}) to archive ($target)");
         }
diff --git a/ext/rotate/main.php b/ext/rotate/main.php
index 397639a3..b2b1df3d 100644
--- a/ext/rotate/main.php
+++ b/ext/rotate/main.php
@@ -120,7 +120,7 @@ class RotateImage extends Extension
             throw new ImageRotateException("Image does not have a hash associated with it.");
         }
         
-        $image_filename  = warehouse_path("images", $hash);
+        $image_filename  = warehouse_path(Image::IMAGE_DIR, $hash);
         if (file_exists($image_filename)==false) {
             throw new ImageRotateException("$image_filename does not exist.");
         }
@@ -212,7 +212,7 @@ class RotateImage extends Extension
         $new_image->ext = $image_obj->ext;
 
         /* Move the new image into the main storage location */
-        $target = warehouse_path("images", $new_image->hash);
+        $target = warehouse_path(Image::IMAGE_DIR, $new_image->hash);
         if (!@copy($tmp_filename, $target)) {
             throw new ImageRotateException("Failed to copy new image file from temporary location ({$tmp_filename}) to archive ($target)");
         }
diff --git a/ext/rule34/main.php b/ext/rule34/main.php
index 577ca5af..775fe0ec 100644
--- a/ext/rule34/main.php
+++ b/ext/rule34/main.php
@@ -116,8 +116,8 @@ class Rule34 extends Extension
                                 continue;
                             }
                             log_info("admin", "Cleaning {$hash}");
-                            @unlink(warehouse_path('images', $hash));
-                            @unlink(warehouse_path('thumbs', $hash));
+                            @unlink(warehouse_path(Image::IMAGE_DIR, $hash));
+                            @unlink(warehouse_path(Image::THUMBNAIL_DIR, $hash));
                             $database->execute("NOTIFY shm_image_bans, '{$hash}';");
                         }
                     }
diff --git a/ext/transcode/main.php b/ext/transcode/main.php
index aa471d0a..ba7e0947 100644
--- a/ext/transcode/main.php
+++ b/ext/transcode/main.php
@@ -308,7 +308,7 @@ class TranscodeImage extends Extension
     private function transcode_and_replace_image(Image $image_obj, String $target_format)
     {
         $target_format = $this->clean_format($target_format);
-        $original_file = warehouse_path("images", $image_obj->hash);
+        $original_file = warehouse_path(Image::IMAGE_DIR, $image_obj->hash);
 
         $tmp_filename = $this->transcode_image($original_file, $image_obj->ext, $target_format);
         
@@ -321,7 +321,7 @@ class TranscodeImage extends Extension
         $new_image->ext = $this->determine_ext($target_format);
 
         /* Move the new image into the main storage location */
-        $target = warehouse_path("images", $new_image->hash);
+        $target = warehouse_path(Image::IMAGE_DIR, $new_image->hash);
         if (!@copy($tmp_filename, $target)) {
             throw new ImageTranscodeException("Failed to copy new image file from temporary location ({$tmp_filename}) to archive ($target)");
         }

From ed9bd5e78839b16cbf2adf1bec112ce9a00bc3ae Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Sat, 15 Jun 2019 11:29:13 -0500
Subject: [PATCH 12/23] Fix in ExtensionAuthor

---
 ext/ext_manager/main.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ext/ext_manager/main.php b/ext/ext_manager/main.php
index dddefc70..4739fb9d 100644
--- a/ext/ext_manager/main.php
+++ b/ext/ext_manager/main.php
@@ -107,7 +107,7 @@ class ExtensionAuthor
     public $name;
     public $email;
 
-    public function __construct(string $name, string $email)
+    public function __construct(string $name, ?string $email)
     {
         $this->name = $name;
         $this->email = $email;

From ab9389007f872488e8620d97949a4692d2e67f0a Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Sat, 15 Jun 2019 11:35:36 -0500
Subject: [PATCH 13/23] Changed key-generation process for cron upload so it
 doesn't endlessly generate new keys before the user first hits the same
 buttons in settings.

---
 ext/cron_uploader/main.php | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php
index fe1bbfbf..1c12b05f 100644
--- a/ext/cron_uploader/main.php
+++ b/ext/cron_uploader/main.php
@@ -157,13 +157,14 @@ class CronUploader extends Extension
     {
         global $config;
         // Set default values
+        $config->set_default_int('cron_uploader_count', 1);
+        $this->set_dir();
+
         $this->upload_key = $config->get_string("cron_uploader_key", "");
-        if (strlen($this->upload_key) <= 0) {
+        if (empty($this->upload_key)) {
             $this->upload_key = $this->generate_key();
 
-            $config->set_default_int('cron_uploader_count', 1);
-            $config->set_default_string('cron_uploader_key', $this->upload_key);
-            $this->set_dir();
+            $config->set_string('cron_uploader_key', $this->upload_key);
         }
     }
 
@@ -180,7 +181,7 @@ class CronUploader extends Extension
         $sb->add_int_option("cron_uploader_count", "How many to upload each time");
         $sb->add_text_option("cron_uploader_dir", "<br>Set Cron Uploader root directory<br>");
 
-        $sb->add_label("<br>Cron Command: <input type='text' size='60' value='$cron_cmd'><br>
+        $sb->add_label("<br>Cron Command: <input type='text' size='60' readonly='readonly' value='".html_escape($cron_cmd)."'><br>
 		Create a cron job with the command above.<br/>
 		<a href='$documentation_link'>Read the documentation</a> if you're not sure what to do.");
 

From 8b531c04a2e51e588079788e7f49f76ade39664d Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Sat, 15 Jun 2019 12:15:47 -0500
Subject: [PATCH 14/23] removed SQLERROR escape from cron uploader, not
 necessary now that it is individualizing transactions. Change cron uploader
 to use constants for dir and config names

---
 ext/cron_uploader/main.php | 58 ++++++++++++++++++++------------------
 1 file changed, 30 insertions(+), 28 deletions(-)

diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php
index 1c12b05f..7b5f7061 100644
--- a/ext/cron_uploader/main.php
+++ b/ext/cron_uploader/main.php
@@ -15,6 +15,14 @@ class CronUploader extends Extension
     // TODO: Change logging to MySQL + display log at /cron_upload
     // TODO: Move stuff to theme.php
 
+    const QUEUE_DIR = "queue";
+    const UPLOADED_DIR = "uploaded";
+    const FAILED_DIR = "failed_to_upload";
+
+    const CONFIG_KEY = "cron_uploader_key";
+    const CONFIG_COUNT = "cron_uploader_count";
+    const CONFIG_DIR = "cron_uploader_dir";
+
     /**
      * Lists all log events this session
      * @var string
@@ -78,9 +86,9 @@ class CronUploader extends Extension
         $this->set_dir(); // Determines path to cron_uploader_dir
 
 
-        $queue_dir = $this->root_dir . "/queue";
-        $uploaded_dir = $this->root_dir . "/uploaded";
-        $failed_dir = $this->root_dir . "/failed_to_upload";
+        $queue_dir = $this->root_dir . "/" . self::QUEUE_DIR;
+        $uploaded_dir = $this->root_dir . "/" . self::UPLOADED_DIR;
+        $failed_dir = $this->root_dir . "/" . self::FAILED_DIR;
 
         $queue_dirinfo = $this->scan_dir($queue_dir);
         $uploaded_dirinfo = $this->scan_dir($uploaded_dir);
@@ -157,14 +165,14 @@ class CronUploader extends Extension
     {
         global $config;
         // Set default values
-        $config->set_default_int('cron_uploader_count', 1);
+        $config->set_default_int(self::CONFIG_COUNT, 1);
         $this->set_dir();
 
-        $this->upload_key = $config->get_string("cron_uploader_key", "");
+        $this->upload_key = $config->get_string(self::CONFIG_KEY, "");
         if (empty($this->upload_key)) {
             $this->upload_key = $this->generate_key();
 
-            $config->set_string('cron_uploader_key', $this->upload_key);
+            $config->set_string(self::CONFIG_KEY, $this->upload_key);
         }
     }
 
@@ -178,10 +186,10 @@ class CronUploader extends Extension
 
         $sb = new SetupBlock("Cron Uploader");
         $sb->add_label("<b>Settings</b><br>");
-        $sb->add_int_option("cron_uploader_count", "How many to upload each time");
-        $sb->add_text_option("cron_uploader_dir", "<br>Set Cron Uploader root directory<br>");
+        $sb->add_int_option(self::CONFIG_COUNT, "How many to upload each time");
+        $sb->add_text_option(self::CONFIG_DIR, "<br>Set Cron Uploader root directory<br>");
 
-        $sb->add_label("<br>Cron Command: <input type='text' size='60' readonly='readonly' value='".html_escape($cron_cmd)."'><br>
+        $sb->add_label("<br>Cron Command: <input type='text' size='60' readonly='readonly' value='" . html_escape($cron_cmd) . "'><br>
 		Create a cron job with the command above.<br/>
 		<a href='$documentation_link'>Read the documentation</a> if you're not sure what to do.");
 
@@ -212,23 +220,23 @@ class CronUploader extends Extension
         global $config;
         // Determine directory (none = default)
 
-        $dir = $config->get_string("cron_uploader_dir", "");
+        $dir = $config->get_string(self::CONFIG_DIR, "");
 
         // Sets new default dir if not in config yet/anymore
         if ($dir == "") {
             $dir = data_path("cron_uploader");
-            $config->set_string('cron_uploader_dir', $dir);
+            $config->set_string(self::CONFIG_DIR, $dir);
         }
 
         // Make the directory if it doesn't exist yet
-        if (!is_dir($dir . "/queue/")) {
-            mkdir($dir . "/queue/", 0775, true);
+        if (!is_dir($dir . "/" . self::QUEUE_DIR . "/")) {
+            mkdir($dir . "/" . self::QUEUE_DIR . "/", 0775, true);
         }
-        if (!is_dir($dir . "/uploaded/")) {
-            mkdir($dir . "/uploaded/", 0775, true);
+        if (!is_dir($dir . "/" . self::UPLOADED_DIR . "/")) {
+            mkdir($dir . "/" . self::UPLOADED_DIR . "/", 0775, true);
         }
-        if (!is_dir($dir . "/failed_to_upload/")) {
-            mkdir($dir . "/failed_to_upload/", 0775, true);
+        if (!is_dir($dir . "/" . self::FAILED_DIR . "/")) {
+            mkdir($dir . "/" . self::FAILED_DIR . "/", 0775, true);
         }
 
         $this->root_dir = $dir;
@@ -270,7 +278,7 @@ class CronUploader extends Extension
 
         // Gets amount of imgs to upload
         if ($upload_count == 0) {
-            $upload_count = $config->get_int("cron_uploader_count", 1);
+            $upload_count = $config->get_int(self::CONFIG_COUNT, 1);
         }
 
         // Throw exception if there's nothing in the queue
@@ -287,8 +295,6 @@ class CronUploader extends Extension
         $added = 0;
         $failed = 0;
 
-        $failedItems = [];
-
         // Upload the file(s)
         for ($i = 0; $i < $upload_count && sizeof($this->image_queue) > 0; $i++) {
             $img = array_pop($this->image_queue);
@@ -308,11 +314,7 @@ class CronUploader extends Extension
                 $this->move_uploaded($img[0], $img[1], $output_subdir, true);
                 $msgNumber = $this->add_upload_info("(" . gettype($e) . ") " . $e->getMessage());
                 $msgNumber = $this->add_upload_info($e->getTraceAsString());
-                if (strpos($e->getMessage(), 'SQLSTATE') !== false) {
-                    // Postgres invalidates the transaction if there is an SQL error,
-                    // so all subsequence transactions will fail.
-                    break;
-                }
+
                 try {
                     $database->rollback();
                 } catch (Exception $e) {
@@ -345,10 +347,10 @@ class CronUploader extends Extension
         // Determine which dir to move to
         if ($corrupt) {
             // Move to corrupt dir
-            $newDir .= "/failed_to_upload/" . $output_subdir . $relativeDir;
+            $newDir .= "/" . self::FAILED_DIR . "/" . $output_subdir . $relativeDir;
             $info = "ERROR: Image was not uploaded.";
         } else {
-            $newDir .= "/uploaded/" . $output_subdir . $relativeDir;
+            $newDir .= "/" . self::UPLOADED_DIR . "/" . $output_subdir . $relativeDir;
             $info = "Image successfully uploaded. ";
         }
         $newDir = str_replace("//", "/", $newDir . "/");
@@ -396,7 +398,7 @@ class CronUploader extends Extension
 
     private function generate_image_queue(): void
     {
-        $base = $this->root_dir . "/queue";
+        $base = $this->root_dir . "/" . self::QUEUE_DIR;
 
         if (!is_dir($base)) {
             $this->add_upload_info("Image Queue Directory could not be found at \"$base\".");

From 1fe18e757302d74b1789cd6e80c0c9b22c581ddc Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Sat, 15 Jun 2019 12:51:59 -0500
Subject: [PATCH 15/23] Missed a dir name

---
 ext/cron_uploader/main.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php
index 7b5f7061..87947b96 100644
--- a/ext/cron_uploader/main.php
+++ b/ext/cron_uploader/main.php
@@ -56,7 +56,7 @@ class CronUploader extends Extension
         global $config, $user;
 
         if ($event->page_matches("cron_upload")) {
-            $this->upload_key = $config->get_string("cron_uploader_key", "");
+            $this->upload_key = $config->get_string(self::CONFIG_KEY, "");
 
             // If the key is in the url, upload
             if ($this->upload_key != "" && $event->get_arg(0) == $this->upload_key) {

From 014a4c2cd2bbb94d67c0280c47a6c29932f0d4ce Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Tue, 18 Jun 2019 08:06:05 -0500
Subject: [PATCH 16/23] Added extension constant lists to resize and rotate
 extensions so that they weren't rendering their controls ont he wrong image
 types

---
 ext/resize/main.php | 8 ++++++--
 ext/rotate/main.php | 5 ++++-
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/ext/resize/main.php b/ext/resize/main.php
index bad21f6b..81db9007 100644
--- a/ext/resize/main.php
+++ b/ext/resize/main.php
@@ -16,6 +16,8 @@
  */
 class ResizeImage extends Extension
 {
+    const SUPPORTED_EXT = ["jpg","jpeg","png","gif","webp"];
+
     /**
      * Needs to be after the data processing extensions
      */
@@ -37,7 +39,8 @@ class ResizeImage extends Extension
     public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
     {
         global $user, $config;
-        if ($user->is_admin() && $config->get_bool("resize_enabled")) {
+        if ($user->is_admin() && $config->get_bool("resize_enabled")
+            && in_array($event->image->ext, self::SUPPORTED_EXT)) {
             /* Add a link to resize the image */
             $event->add_part($this->theme->get_resize_html($event->image));
         }
@@ -68,7 +71,8 @@ class ResizeImage extends Extension
 
         $image_obj = Image::by_id($event->image_id);
 
-        if ($config->get_bool("resize_upload") == true && ($image_obj->ext == "jpg" || $image_obj->ext == "png" || $image_obj->ext == "gif" || $image_obj->ext == "webp")) {
+        if ($config->get_bool("resize_upload") == true
+                && in_array($event->type, self::SUPPORTED_EXT)) {
             $width = $height = 0;
 
             if ($config->get_int("resize_default_width") !== 0) {
diff --git a/ext/rotate/main.php b/ext/rotate/main.php
index b2b1df3d..d612de15 100644
--- a/ext/rotate/main.php
+++ b/ext/rotate/main.php
@@ -31,6 +31,8 @@ class ImageRotateException extends SCoreException
  */
 class RotateImage extends Extension
 {
+    const SUPPORTED_EXT = ["jpg","jpeg","png","gif","webp"];
+
     public function onInitExt(InitExtEvent $event)
     {
         global $config;
@@ -41,7 +43,8 @@ class RotateImage extends Extension
     public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
     {
         global $user, $config;
-        if ($user->is_admin() && $config->get_bool("rotate_enabled")) {
+        if ($user->is_admin() && $config->get_bool("rotate_enabled")
+                && in_array($event->image->ext, self::SUPPORTED_EXT)) {
             /* Add a link to rotate the image */
             $event->add_part($this->theme->get_rotate_html($event->image->id));
         }

From 826c623538e3bea63b6b867eaf09b6924109abec Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Tue, 18 Jun 2019 20:58:28 -0500
Subject: [PATCH 17/23] PageMode constants

---
 core/page.php                  | 13 +++++++++----
 ext/admin/main.php             |  8 ++++----
 ext/alias_editor/main.php      |  8 ++++----
 ext/artists/main.php           | 30 +++++++++++++++---------------
 ext/autocomplete/main.php      |  2 +-
 ext/blocks/main.php            |  4 ++--
 ext/blotter/main.php           |  4 ++--
 ext/browser_search/main.php    |  4 ++--
 ext/bulk_actions/main.php      |  2 +-
 ext/comment/main.php           |  6 +++---
 ext/cron_uploader/main.php     |  2 +-
 ext/danbooru_api/main.php      |  4 ++--
 ext/downtime/theme.php         |  2 +-
 ext/emoticons/theme.php        |  2 +-
 ext/ext_manager/main.php       |  2 +-
 ext/favorites/main.php         |  2 +-
 ext/featured/main.php          |  4 ++--
 ext/forum/main.php             | 10 +++++-----
 ext/handle_404/main.php        |  2 +-
 ext/handle_static/main.php     |  4 ++--
 ext/handle_svg/main.php        |  2 +-
 ext/home/theme.php             |  2 +-
 ext/image/main.php             |  6 +++---
 ext/image_hash_ban/main.php    |  4 ++--
 ext/index/main.php             |  6 +++---
 ext/ipban/main.php             |  4 ++--
 ext/mail/main.php              |  2 +-
 ext/mass_tagger/main.php       |  2 +-
 ext/not_a_tag/main.php         |  4 ++--
 ext/notes/main.php             | 16 ++++++++--------
 ext/numeric_score/main.php     |  6 +++---
 ext/oekaki/main.php            |  2 +-
 ext/ouroboros_api/main.php     |  4 ++--
 ext/pm/main.php                |  4 ++--
 ext/pools/main.php             | 20 ++++++++++----------
 ext/random_image/main.php      |  4 ++--
 ext/random_list/main.php       |  4 ++--
 ext/rating/main.php            |  4 ++--
 ext/regen_thumb/main.php       |  2 +-
 ext/report_image/main.php      |  6 +++---
 ext/resize/main.php            |  2 +-
 ext/rotate/main.php            |  2 +-
 ext/rss_comments/main.php      |  2 +-
 ext/rss_images/main.php        |  2 +-
 ext/rule34/main.php            |  6 +++---
 ext/setup/main.php             |  4 ++--
 ext/shimmie_api/main.php       |  4 ++--
 ext/sitemap/main.php           |  4 ++--
 ext/source_history/main.php    |  4 ++--
 ext/tag_edit/main.php          |  4 ++--
 ext/tag_history/main.php       |  4 ++--
 ext/tag_list/main.php          |  2 +-
 ext/tagger/main.php            |  2 +-
 ext/tips/main.php              |  6 +++---
 ext/transcode/main.php         |  2 +-
 ext/update/main.php            |  4 ++--
 ext/upload/theme.php           |  2 +-
 ext/user/main.php              | 12 ++++++------
 ext/view/main.php              |  4 ++--
 ext/wiki/main.php              | 22 +++++++++-------------
 tests/bootstrap.php            |  4 ++--
 themes/material/home.theme.php |  2 +-
 62 files changed, 160 insertions(+), 159 deletions(-)

diff --git a/core/page.php b/core/page.php
index e9b3384b..998adc7d 100644
--- a/core/page.php
+++ b/core/page.php
@@ -26,6 +26,11 @@
  * Various other common functions are available as part of the Themelet class.
  */
 
+abstract class PageMode {
+    const REDIRECT = 'redirect';
+    const DATA = 'data';
+    const PAGE = 'page';
+}
 
 /**
  * Class Page
@@ -40,7 +45,7 @@ class Page
     /** @name Overall */
     //@{
     /** @var string */
-    public $mode = "page";
+    public $mode = PageMode::PAGE;
     /** @var string */
     public $type = "text/html; charset=utf-8";
 
@@ -261,7 +266,7 @@ class Page
         }
 
         switch ($this->mode) {
-            case "page":
+            case PageMode::PAGE:
                 if (CACHE_HTTP) {
                     header("Vary: Cookie, Accept-Encoding");
                     if ($user->is_anonymous() && $_SERVER["REQUEST_METHOD"] == "GET") {
@@ -285,14 +290,14 @@ class Page
                 $layout = new Layout();
                 $layout->display_page($page);
                 break;
-            case "data":
+            case PageMode::DATA:
                 header("Content-Length: ".strlen($this->data));
                 if (!is_null($this->filename)) {
                     header('Content-Disposition: attachment; filename='.$this->filename);
                 }
                 print $this->data;
                 break;
-            case "redirect":
+            case PageMode::REDIRECT:
                 header('Location: '.$this->redirect);
                 print 'You should be redirected to <a href="'.$this->redirect.'">'.$this->redirect.'</a>';
                 break;
diff --git a/ext/admin/main.php b/ext/admin/main.php
index 22845575..c9d2feff 100644
--- a/ext/admin/main.php
+++ b/ext/admin/main.php
@@ -70,7 +70,7 @@ class AdminPage extends Extension
                     }
 
                     if ($aae->redirect) {
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("admin"));
                     }
                 }
@@ -149,7 +149,7 @@ class AdminPage extends Extension
             send_event(new ImageDeletionEvent($image));
         }
 
-        $page->set_mode("redirect");
+        $page->set_mode(PageMode::REDIRECT);
         $page->set_redirect(make_link("post/list"));
         return false;
     }
@@ -218,7 +218,7 @@ class AdminPage extends Extension
         //FIXME: .SQL dump is empty if cmd doesn't exist
 
         if ($cmd) {
-            $page->set_mode("data");
+            $page->set_mode(PageMode::DATA);
             $page->set_type("application/x-unknown");
             $page->set_filename('shimmie-'.date('Ymd').'.sql');
             $page->set_data(shell_exec($cmd));
@@ -243,7 +243,7 @@ class AdminPage extends Extension
             $zip->close();
         }
 
-        $page->set_mode("redirect");
+        $page->set_mode(PageMode::REDIRECT);
         $page->set_redirect(make_link($filename)); //TODO: Delete file after downloaded?
 
         return false;  // we do want a redirect, but a manual one
diff --git a/ext/alias_editor/main.php b/ext/alias_editor/main.php
index 9e7139cf..07f9f289 100644
--- a/ext/alias_editor/main.php
+++ b/ext/alias_editor/main.php
@@ -41,7 +41,7 @@ class AliasEditor extends Extension
                         try {
                             $aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']);
                             send_event($aae);
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("alias/list"));
                         } catch (AddAliasException $ex) {
                             $this->theme->display_error(500, "Error adding alias", $ex->getMessage());
@@ -54,7 +54,7 @@ class AliasEditor extends Extension
                         $database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", ["oldtag" => $_POST['oldtag']]);
                         log_info("alias_editor", "Deleted alias for ".$_POST['oldtag'], "Deleted alias");
 
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("alias/list"));
                     }
                 }
@@ -80,7 +80,7 @@ class AliasEditor extends Extension
 
                 $this->theme->display_aliases($alias, $page_number + 1, $total_pages);
             } elseif ($event->get_arg(0) == "export") {
-                $page->set_mode("data");
+                $page->set_mode(PageMode::DATA);
                 $page->set_type("text/csv");
                 $page->set_filename("aliases.csv");
                 $page->set_data($this->get_alias_csv($database));
@@ -91,7 +91,7 @@ class AliasEditor extends Extension
                         $contents = file_get_contents($tmp);
                         $this->add_alias_csv($database, $contents);
                         log_info("alias_editor", "Imported aliases from file", "Imported aliases"); # FIXME: how many?
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("alias/list"));
                     } else {
                         $this->theme->display_error(400, "No File Specified", "You have to upload a file");
diff --git a/ext/artists/main.php b/ext/artists/main.php
index 7a3f3377..568933de 100644
--- a/ext/artists/main.php
+++ b/ext/artists/main.php
@@ -172,7 +172,7 @@ class Artists extends Extension
                 }
                 case "new_artist":
                 {
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("artist/new"));
                     break;
                 }
@@ -183,7 +183,7 @@ class Artists extends Extension
                         if ($newArtistID == -1) {
                             $this->theme->display_error(400, "Error", "Error when entering artist data.");
                         } else {
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("artist/view/".$newArtistID));
                         }
                     } else {
@@ -238,7 +238,7 @@ class Artists extends Extension
                 case "edit_artist":
                 {
                     $artistID = $_POST['artist_id'];
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("artist/edit/".$artistID));
                     break;
                 }
@@ -246,14 +246,14 @@ class Artists extends Extension
                 {
                     $artistID = int_escape($_POST['id']);
                     $this->update_artist();
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("artist/view/".$artistID));
                     break;
                 }
                 case "nuke_artist":
                 {
                     $artistID = $_POST['artist_id'];
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("artist/nuke/".$artistID));
                     break;
                 }
@@ -261,7 +261,7 @@ class Artists extends Extension
                 {
                     $artistID = $event->get_arg(1);
                     $this->delete_artist($artistID); // this will delete the artist, its alias, its urls and its members
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("artist/list"));
                     break;
                 }
@@ -291,7 +291,7 @@ class Artists extends Extension
                         {
                             $artistID = $_POST['artistID'];
                             $this->add_alias();
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("artist/view/".$artistID));
                             break;
                         }
@@ -300,7 +300,7 @@ class Artists extends Extension
                             $aliasID = $event->get_arg(2);
                             $artistID = $this->get_artistID_by_aliasID($aliasID);
                             $this->delete_alias($aliasID);
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("artist/view/".$artistID));
                             break;
                         }
@@ -316,7 +316,7 @@ class Artists extends Extension
                             $this->update_alias();
                             $aliasID = int_escape($_POST['aliasID']);
                             $artistID = $this->get_artistID_by_aliasID($aliasID);
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("artist/view/".$artistID));
                             break;
                         }
@@ -332,7 +332,7 @@ class Artists extends Extension
                         {
                             $artistID = $_POST['artistID'];
                             $this->add_urls();
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("artist/view/".$artistID));
                             break;
                         }
@@ -341,7 +341,7 @@ class Artists extends Extension
                             $urlID = $event->get_arg(2);
                             $artistID = $this->get_artistID_by_urlID($urlID);
                             $this->delete_url($urlID);
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("artist/view/".$artistID));
                             break;
                         }
@@ -357,7 +357,7 @@ class Artists extends Extension
                             $this->update_url();
                             $urlID = int_escape($_POST['urlID']);
                             $artistID = $this->get_artistID_by_urlID($urlID);
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("artist/view/".$artistID));
                             break;
                         }
@@ -372,7 +372,7 @@ class Artists extends Extension
                         {
                             $artistID = $_POST['artistID'];
                             $this->add_members();
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("artist/view/".$artistID));
                             break;
                         }
@@ -381,7 +381,7 @@ class Artists extends Extension
                             $memberID = int_escape($event->get_arg(2));
                             $artistID = $this->get_artistID_by_memberID($memberID);
                             $this->delete_member($memberID);
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("artist/view/".$artistID));
                             break;
                         }
@@ -397,7 +397,7 @@ class Artists extends Extension
                             $this->update_member();
                             $memberID = int_escape($_POST['memberID']);
                             $artistID = $this->get_artistID_by_memberID($memberID);
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("artist/view/".$artistID));
                             break;
                         }
diff --git a/ext/autocomplete/main.php b/ext/autocomplete/main.php
index eb71227b..a7c85050 100644
--- a/ext/autocomplete/main.php
+++ b/ext/autocomplete/main.php
@@ -21,7 +21,7 @@ class AutoComplete extends Extension
                 return;
             }
 
-            $page->set_mode("data");
+            $page->set_mode(PageMode::DATA);
             $page->set_type("application/json");
 
             $s = strtolower($_GET["s"]);
diff --git a/ext/blocks/main.php b/ext/blocks/main.php
index 86e0a1c5..cb9c375c 100644
--- a/ext/blocks/main.php
+++ b/ext/blocks/main.php
@@ -61,7 +61,7 @@ class Blocks extends Extension
 					", [$_POST['pages'], $_POST['title'], $_POST['area'], (int)$_POST['priority'], $_POST['content']]);
                     log_info("blocks", "Added Block #".($database->get_last_insert_id('blocks_id_seq'))." (".$_POST['title'].")");
                     $database->cache->delete("blocks");
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("blocks/list"));
                 }
             }
@@ -81,7 +81,7 @@ class Blocks extends Extension
                         log_info("blocks", "Updated Block #".$_POST['id']." (".$_POST['title'].")");
                     }
                     $database->cache->delete("blocks");
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("blocks/list"));
                 }
             } elseif ($event->get_arg(0) == "list") {
diff --git a/ext/blotter/main.php b/ext/blotter/main.php
index 8f54576e..cb88490b 100644
--- a/ext/blotter/main.php
+++ b/ext/blotter/main.php
@@ -102,7 +102,7 @@ class Blotter extends Extension
                             [$entry_text, $important]
                         );
                         log_info("blotter", "Added Message: $entry_text");
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("blotter/editor"));
                     }
                     break;
@@ -119,7 +119,7 @@ class Blotter extends Extension
                         }
                         $database->Execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]);
                         log_info("blotter", "Removed Entry #$id");
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("blotter/editor"));
                     }
                     break;
diff --git a/ext/browser_search/main.php b/ext/browser_search/main.php
index 10950000..7c1fcc82 100644
--- a/ext/browser_search/main.php
+++ b/ext/browser_search/main.php
@@ -54,7 +54,7 @@ class BrowserSearch extends Extension
 			";
 
             // And now to send it to the browser
-            $page->set_mode("data");
+            $page->set_mode(PageMode::DATA);
             $page->set_type("text/xml");
             $page->set_data($xml);
         } elseif (
@@ -85,7 +85,7 @@ class BrowserSearch extends Extension
 
             // And now for the final output
             $json_string = "[\"$tag_search\",[\"$json_tag_list\"],[],[]]";
-            $page->set_mode("data");
+            $page->set_mode(PageMode::DATA);
             $page->set_data($json_string);
         }
     }
diff --git a/ext/bulk_actions/main.php b/ext/bulk_actions/main.php
index 2c93de4e..073a85f1 100644
--- a/ext/bulk_actions/main.php
+++ b/ext/bulk_actions/main.php
@@ -171,7 +171,7 @@ class BulkActions extends Extension
             }
 
 
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             if (!isset($_SERVER['HTTP_REFERER'])) {
                 $_SERVER['HTTP_REFERER'] = make_link();
             }
diff --git a/ext/comment/main.php b/ext/comment/main.php
index 75b171d4..d4cef828 100644
--- a/ext/comment/main.php
+++ b/ext/comment/main.php
@@ -178,7 +178,7 @@ class CommentList extends Extension
                 $i_iid = int_escape($_POST['image_id']);
                 $cpe = new CommentPostingEvent($_POST['image_id'], $user, $_POST['comment']);
                 send_event($cpe);
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 $page->set_redirect(make_link("post/view/$i_iid#comment_on_$i_iid"));
             } catch (CommentPostingException $ex) {
                 $this->theme->display_error(403, "Comment Blocked", $ex->getMessage());
@@ -194,7 +194,7 @@ class CommentList extends Extension
             if ($event->count_args() === 3) {
                 send_event(new CommentDeletionEvent($event->get_arg(1)));
                 flash_message("Deleted comment");
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 if (!empty($_SERVER['HTTP_REFERER'])) {
                     $page->set_redirect($_SERVER['HTTP_REFERER']);
                 } else {
@@ -224,7 +224,7 @@ class CommentList extends Extension
             }
             flash_message("Deleted $num comments");
 
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link("admin"));
         } else {
             $this->theme->display_permission_denied();
diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php
index 87947b96..977a4f7a 100644
--- a/ext/cron_uploader/main.php
+++ b/ext/cron_uploader/main.php
@@ -456,7 +456,7 @@ class CronUploader extends Extension
         global $page;
 
         // Display message
-        $page->set_mode("data");
+        $page->set_mode(PageMode::DATA);
         $page->set_type("text/plain");
         $page->set_data($this->upload_info);
 
diff --git a/ext/danbooru_api/main.php b/ext/danbooru_api/main.php
index a831f366..ce13295b 100644
--- a/ext/danbooru_api/main.php
+++ b/ext/danbooru_api/main.php
@@ -60,7 +60,7 @@ class DanbooruApi extends Extension
     private function api_danbooru(PageRequestEvent $event)
     {
         global $page;
-        $page->set_mode("data");
+        $page->set_mode(PageMode::DATA);
 
         if (($event->get_arg(1) == 'add_post') || (($event->get_arg(1) == 'post') && ($event->get_arg(2) == 'create.xml'))) {
             // No XML data is returned from this function
@@ -80,7 +80,7 @@ class DanbooruApi extends Extension
         // This redirects that to http://shimmie/post/view/123
         elseif (($event->get_arg(1) == 'post') && ($event->get_arg(2) == 'show')) {
             $fixedlocation = make_link("post/view/" . $event->get_arg(3));
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect($fixedlocation);
         }
     }
diff --git a/ext/downtime/theme.php b/ext/downtime/theme.php
index feb7e4ea..caedcfda 100644
--- a/ext/downtime/theme.php
+++ b/ext/downtime/theme.php
@@ -26,7 +26,7 @@ class DowntimeTheme extends Themelet
         $login_link = make_link("user_admin/login");
         $auth = $user->get_auth_html();
 
-        $page->set_mode('data');
+        $page->set_mode(PageMode::DATA);
         $page->set_code(503);
         $page->set_data(
             <<<EOD
diff --git a/ext/emoticons/theme.php b/ext/emoticons/theme.php
index a673c308..212ebd20 100644
--- a/ext/emoticons/theme.php
+++ b/ext/emoticons/theme.php
@@ -18,7 +18,7 @@ class EmoticonListTheme extends Themelet
         }
         $html .= "</tr></table>";
         $html .= "</body></html>";
-        $page->set_mode("data");
+        $page->set_mode(PageMode::DATA);
         $page->set_data($html);
     }
 }
diff --git a/ext/ext_manager/main.php b/ext/ext_manager/main.php
index 4739fb9d..b41b53dd 100644
--- a/ext/ext_manager/main.php
+++ b/ext/ext_manager/main.php
@@ -125,7 +125,7 @@ class ExtManager extends Extension
                     if (is_writable("data/config")) {
                         $this->set_things($_POST);
                         log_warning("ext_manager", "Active extensions changed", "Active extensions changed");
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("ext_manager"));
                     } else {
                         $this->theme->display_error(
diff --git a/ext/favorites/main.php b/ext/favorites/main.php
index 1ccf2031..e8b7f6fe 100644
--- a/ext/favorites/main.php
+++ b/ext/favorites/main.php
@@ -81,7 +81,7 @@ class Favorites extends Extension
                     log_debug("favourite", "Favourite removed for $image_id", "Favourite removed");
                 }
             }
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link("post/view/$image_id"));
         }
     }
diff --git a/ext/featured/main.php b/ext/featured/main.php
index c58be8b4..4b713424 100644
--- a/ext/featured/main.php
+++ b/ext/featured/main.php
@@ -37,7 +37,7 @@ class Featured extends Extension
                     if ($id > 0) {
                         $config->set_int("featured_id", $id);
                         log_info("featured", "Featured image set to $id", "Featured image set");
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("post/view/$id"));
                     }
                 }
@@ -45,7 +45,7 @@ class Featured extends Extension
             if ($event->get_arg(0) == "download") {
                 $image = Image::by_id($config->get_int("featured_id"));
                 if (!is_null($image)) {
-                    $page->set_mode("data");
+                    $page->set_mode(PageMode::DATA);
                     $page->set_type($image->get_mime_type());
                     $page->set_data(file_get_contents($image->get_image_filename()));
                 }
diff --git a/ext/forum/main.php b/ext/forum/main.php
index 95d79908..db2ff1a8 100644
--- a/ext/forum/main.php
+++ b/ext/forum/main.php
@@ -139,7 +139,7 @@ class Forum extends Extension
                         $redirectTo = "forum/view/".$newThreadID."/1";
                     }
 
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link($redirectTo));
 
                     break;
@@ -151,7 +151,7 @@ class Forum extends Extension
                         $this->delete_post($postID);
                     }
 
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("forum/view/".$threadID));
                     break;
                 case "nuke":
@@ -161,7 +161,7 @@ class Forum extends Extension
                         $this->delete_thread($threadID);
                     }
 
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("forum/index"));
                     break;
                 case "answer":
@@ -176,11 +176,11 @@ class Forum extends Extension
                         }
                         $this->save_new_post($threadID, $user);
                     }
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("forum/view/".$threadID."/".$total_pages));
                     break;
                 default:
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("forum/index"));
                     //$this->theme->display_error(400, "Invalid action", "You should check forum/index.");
                     break;
diff --git a/ext/handle_404/main.php b/ext/handle_404/main.php
index 4bc31dfd..a45ea7fb 100644
--- a/ext/handle_404/main.php
+++ b/ext/handle_404/main.php
@@ -14,7 +14,7 @@ class Handle404 extends Extension
     {
         global $config, $page;
         // hax.
-        if ($page->mode == "page" && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) {
+        if ($page->mode == PageMode::PAGE && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) {
             $h_pagename = html_escape(implode('/', $event->args));
             log_debug("handle_404", "Hit 404: $h_pagename");
             $page->set_code(404);
diff --git a/ext/handle_static/main.php b/ext/handle_static/main.php
index e2dd7de1..fb20dd59 100644
--- a/ext/handle_static/main.php
+++ b/ext/handle_static/main.php
@@ -14,7 +14,7 @@ class HandleStatic extends Extension
     {
         global $config, $page;
         // hax.
-        if ($page->mode == "page" && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) {
+        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");
@@ -27,7 +27,7 @@ class HandleStatic extends Extension
 
                 $page->add_http_header("Cache-control: public, max-age=600");
                 $page->add_http_header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 600) . ' GMT');
-                $page->set_mode("data");
+                $page->set_mode(PageMode::DATA);
                 $page->set_data(file_get_contents($filename));
                 if (endsWith($filename, ".ico")) {
                     $page->set_type("image/x-icon");
diff --git a/ext/handle_svg/main.php b/ext/handle_svg/main.php
index a15942b1..96dea29a 100644
--- a/ext/handle_svg/main.php
+++ b/ext/handle_svg/main.php
@@ -57,7 +57,7 @@ class SVGFileHandler extends DataHandlerExtension
             $hash = $image->hash;
 
             $page->set_type("image/svg+xml");
-            $page->set_mode("data");
+            $page->set_mode(PageMode::DATA);
 
             $sanitizer = new Sanitizer();
             $sanitizer->removeRemoteReferences(true);
diff --git a/ext/home/theme.php b/ext/home/theme.php
index e8f4086d..8dfc666d 100644
--- a/ext/home/theme.php
+++ b/ext/home/theme.php
@@ -4,7 +4,7 @@ class HomeTheme extends Themelet
 {
     public function display_page(Page $page, $sitename, $base_href, $theme_name, $body)
     {
-        $page->set_mode("data");
+        $page->set_mode(PageMode::DATA);
         $page->add_auto_html_headers();
         $hh = $page->get_all_html_headers();
         $page->set_data(
diff --git a/ext/image/main.php b/ext/image/main.php
index 56405d65..f9b31180 100644
--- a/ext/image/main.php
+++ b/ext/image/main.php
@@ -43,7 +43,7 @@ class ImageIO extends Extension
                 $image = Image::by_id($_POST['image_id']);
                 if ($image) {
                     send_event(new ImageDeletionEvent($image));
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     if (isset($_SERVER['HTTP_REFERER']) && !strstr($_SERVER['HTTP_REFERER'], 'post/view')) {
                         $page->set_redirect($_SERVER['HTTP_REFERER']);
                     } else {
@@ -56,7 +56,7 @@ class ImageIO extends Extension
             if ($user->can("replace_image") && isset($_POST['image_id']) && $user->check_auth_token()) {
                 $image = Image::by_id($_POST['image_id']);
                 if ($image) {
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link('upload/replace/'.$image->id));
                 } else {
                     /* Invalid image ID */
@@ -251,7 +251,7 @@ class ImageIO extends Extension
 
         global $page;
         if (!is_null($image)) {
-            $page->set_mode("data");
+            $page->set_mode(PageMode::DATA);
             if ($type == "thumb") {
                 $ext = $config->get_string("thumb_type");
                 if (array_key_exists($ext, MIME_TYPE_MAP)) {
diff --git a/ext/image_hash_ban/main.php b/ext/image_hash_ban/main.php
index 6589ee16..c2e3ec3a 100644
--- a/ext/image_hash_ban/main.php
+++ b/ext/image_hash_ban/main.php
@@ -79,7 +79,7 @@ class ImageBan extends Extension
                             flash_message("Image deleted");
                         }
 
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect($_SERVER['HTTP_REFERER']);
                     }
                 } elseif ($event->get_arg(0) == "remove") {
@@ -87,7 +87,7 @@ class ImageBan extends Extension
                         send_event(new RemoveImageHashBanEvent($_POST['hash']));
 
                         flash_message("Image ban removed");
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect($_SERVER['HTTP_REFERER']);
                     }
                 } elseif ($event->get_arg(0) == "list") {
diff --git a/ext/index/main.php b/ext/index/main.php
index f8d77843..7e37f7ea 100644
--- a/ext/index/main.php
+++ b/ext/index/main.php
@@ -239,10 +239,10 @@ class Index extends Extension
                 // implode(explode()) to resolve aliases and sanitise
                 $search = url_escape(Tag::implode(Tag::explode($_GET['search'], false)));
                 if (empty($search)) {
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("post/list/1"));
                 } else {
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link('post/list/'.$search.'/1'));
                 }
                 return;
@@ -278,7 +278,7 @@ class Index extends Extension
                 $this->theme->display_intro($page);
                 send_event(new PostListBuildingEvent($search_terms));
             } elseif ($count_search_terms > 0 && $count_images === 1 && $page_number === 1) {
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 $page->set_redirect(make_link('post/view/'.$images[0]->id));
             } else {
                 $plbe = new PostListBuildingEvent($search_terms);
diff --git a/ext/ipban/main.php b/ext/ipban/main.php
index 541c853b..f86c10ff 100644
--- a/ext/ipban/main.php
+++ b/ext/ipban/main.php
@@ -77,7 +77,7 @@ class IPBan extends Extension
                         send_event(new AddIPBanEvent($_POST['ip'], $_POST['reason'], $end));
 
                         flash_message("Ban for {$_POST['ip']} added");
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("ip_ban/list"));
                     }
                 } elseif ($event->get_arg(0) == "remove" && $user->check_auth_token()) {
@@ -85,7 +85,7 @@ class IPBan extends Extension
                         send_event(new RemoveIPBanEvent($_POST['id']));
 
                         flash_message("Ban removed");
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("ip_ban/list"));
                     }
                 } elseif ($event->get_arg(0) == "list") {
diff --git a/ext/mail/main.php b/ext/mail/main.php
index 3e51bcb4..35fb2061 100644
--- a/ext/mail/main.php
+++ b/ext/mail/main.php
@@ -35,7 +35,7 @@ class MailTest extends Extension
     {
         if ($event->page_matches("mail/test")) {
             global $page;
-            $page->set_mode("data");
+            $page->set_mode(PageMode::DATA);
             echo "Alert: uncomment this page's code on /ext/mail/main.php starting on line 33, and change the email address. Make sure you're using a server with a domain, not localhost.";
             /*
             echo "Preparing to send message:<br>";
diff --git a/ext/mass_tagger/main.php b/ext/mass_tagger/main.php
index 648d7b79..fe91b366 100644
--- a/ext/mass_tagger/main.php
+++ b/ext/mass_tagger/main.php
@@ -64,7 +64,7 @@ class MassTagger extends Extension
                 }
             }
 
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             if (!isset($_SERVER['HTTP_REFERER'])) {
                 $_SERVER['HTTP_REFERER'] = make_link();
             }
diff --git a/ext/not_a_tag/main.php b/ext/not_a_tag/main.php
index c03b0e81..18486e9c 100644
--- a/ext/not_a_tag/main.php
+++ b/ext/not_a_tag/main.php
@@ -81,14 +81,14 @@ class NotATag extends Extension
                         [$tag, $redirect]
                     );
 
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect($_SERVER['HTTP_REFERER']);
                 } elseif ($event->get_arg(0) == "remove") {
                     if (isset($_POST['tag'])) {
                         $database->Execute("DELETE FROM untags WHERE tag = ?", [$_POST['tag']]);
 
                         flash_message("Image ban removed");
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect($_SERVER['HTTP_REFERER']);
                     }
                 } elseif ($event->get_arg(0) == "list") {
diff --git a/ext/notes/main.php b/ext/notes/main.php
index 14285df2..aafa928f 100644
--- a/ext/notes/main.php
+++ b/ext/notes/main.php
@@ -100,7 +100,7 @@ class Notes extends Extension
                         $this->revert_history($noteID, $reviewID);
                     }
 
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("note/updated"));
                     break;
                 case "add_note":
@@ -108,7 +108,7 @@ class Notes extends Extension
                         $this->add_new_note();
                     }
 
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("post/view/".$_POST["image_id"]));
                     break;
                 case "add_request":
@@ -116,7 +116,7 @@ class Notes extends Extension
                         $this->add_note_request();
                     }
 
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("post/view/".$_POST["image_id"]));
                     break;
                 case "nuke_notes":
@@ -124,7 +124,7 @@ class Notes extends Extension
                         $this->nuke_notes();
                     }
 
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("post/view/".$_POST["image_id"]));
                     break;
                 case "nuke_requests":
@@ -132,25 +132,25 @@ class Notes extends Extension
                         $this->nuke_requests();
                     }
 
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("post/view/".$_POST["image_id"]));
                     break;
                 case "edit_note":
                     if (!$user->is_anonymous()) {
                         $this->update_note();
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("post/view/" . $_POST["image_id"]));
                     }
                     break;
                 case "delete_note":
                     if ($user->is_admin()) {
                         $this->delete_note();
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("post/view/".$_POST["image_id"]));
                     }
                     break;
                 default:
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("note/list"));
                     break;
             }
diff --git a/ext/numeric_score/main.php b/ext/numeric_score/main.php
index f15a9d37..5275dfa7 100644
--- a/ext/numeric_score/main.php
+++ b/ext/numeric_score/main.php
@@ -94,7 +94,7 @@ class NumericScore extends Extension
                 if (!is_null($score) && $image_id>0) {
                     send_event(new NumericScoreSetEvent($image_id, $user, $score));
                 }
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 $page->set_redirect(make_link("post/view/$image_id"));
             }
         } elseif ($event->page_matches("numeric_score/remove_votes_on") && $user->check_auth_token()) {
@@ -108,13 +108,13 @@ class NumericScore extends Extension
                     "UPDATE images SET numeric_score=0 WHERE id=?",
                     [$image_id]
                 );
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 $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")) {
                 $this->delete_votes_by(int_escape($_POST['user_id']));
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 $page->set_redirect(make_link());
             }
         } elseif ($event->page_matches("popular_by_day") || $event->page_matches("popular_by_month") || $event->page_matches("popular_by_year")) {
diff --git a/ext/oekaki/main.php b/ext/oekaki/main.php
index f18383e9..1da9cc90 100644
--- a/ext/oekaki/main.php
+++ b/ext/oekaki/main.php
@@ -41,7 +41,7 @@ class Oekaki extends Extension
                             throw new UploadException("File type not recognised");
                         } else {
                             unlink($tmpname);
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("post/view/".$duev->image_id));
                         }
                     }
diff --git a/ext/ouroboros_api/main.php b/ext/ouroboros_api/main.php
index 9434f6aa..91fd85ef 100644
--- a/ext/ouroboros_api/main.php
+++ b/ext/ouroboros_api/main.php
@@ -404,7 +404,7 @@ class OuroborosAPI extends Extension
             } elseif ($this->type == 'xml') {
                 $page->set_type('text/xml; charset=utf-8');
             }
-            $page->set_mode('data');
+            $page->set_mode(PageMode::DATA);
             $this->tryAuth();
 
             if ($event->page_matches('post')) {
@@ -464,7 +464,7 @@ class OuroborosAPI extends Extension
                 }
             }
         } elseif ($event->page_matches('post/show')) {
-            $page->set_mode('redirect');
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link(str_replace('post/show', 'post/view', implode('/', $event->args))));
             $page->display();
             die();
diff --git a/ext/pm/main.php b/ext/pm/main.php
index 2cf8d0a7..123d6368 100644
--- a/ext/pm/main.php
+++ b/ext/pm/main.php
@@ -149,7 +149,7 @@ class PrivMsg extends Extension
                                 $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");
-                                $page->set_mode("redirect");
+                                $page->set_mode(PageMode::REDIRECT);
                                 $page->set_redirect($_SERVER["HTTP_REFERER"]);
                             }
                         }
@@ -162,7 +162,7 @@ class PrivMsg extends Extension
                             $message = $_POST["message"];
                             send_event(new SendPMEvent(new PM($from_id, $_SERVER["REMOTE_ADDR"], $to_id, $subject, $message)));
                             flash_message("PM sent");
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect($_SERVER["HTTP_REFERER"]);
                         }
                         break;
diff --git a/ext/pools/main.php b/ext/pools/main.php
index 6fd35b9e..bdf8ce87 100644
--- a/ext/pools/main.php
+++ b/ext/pools/main.php
@@ -130,7 +130,7 @@ class Pools extends Extension
                 case "create": // ADD _POST
                     try {
                         $newPoolID = $this->add_pool();
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("pool/view/".$newPoolID));
                     } catch (PoolCreationException $e) {
                         $this->theme->display_error(400, "Error", $e->error);
@@ -150,7 +150,7 @@ class Pools extends Extension
                     if (!$user->is_anonymous()) {
                         $historyID = int_escape($event->get_arg(1));
                         $this->revert_history($historyID);
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("pool/updated"));
                     }
                     break;
@@ -159,7 +159,7 @@ class Pools extends Extension
                     if ($this->have_permission($user, $pool)) {
                         $this->theme->edit_pool($page, $this->get_pool($pool_id), $this->edit_posts($pool_id));
                     } else {
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("pool/view/".$pool_id));
                     }
                     break;
@@ -169,13 +169,13 @@ class Pools extends Extension
                         if ($this->have_permission($user, $pool)) {
                             $this->theme->edit_order($page, $this->get_pool($pool_id), $this->edit_order($pool_id));
                         } else {
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("pool/view/".$pool_id));
                         }
                     } else {
                         if ($this->have_permission($user, $pool)) {
                             $this->order_posts();
-                            $page->set_mode("redirect");
+                            $page->set_mode(PageMode::REDIRECT);
                             $page->set_redirect(make_link("pool/view/".$pool_id));
                         } else {
                             $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
@@ -194,7 +194,7 @@ class Pools extends Extension
                 case "add_posts":
                     if ($this->have_permission($user, $pool)) {
                         $this->add_posts();
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("pool/view/".$pool_id));
                     } else {
                         $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
@@ -204,7 +204,7 @@ class Pools extends Extension
                 case "remove_posts":
                     if ($this->have_permission($user, $pool)) {
                         $this->remove_posts();
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("pool/view/".$pool_id));
                     } else {
                         $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
@@ -215,7 +215,7 @@ class Pools extends Extension
                 case "edit_description":
                     if ($this->have_permission($user, $pool)) {
                         $this->edit_description();
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("pool/view/".$pool_id));
                     } else {
                         $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
@@ -228,7 +228,7 @@ class Pools extends Extension
                     //  -> Only admins and owners may do this
                     if ($user->is_admin() || $user->id == $pool['user_id']) {
                         $this->nuke_pool($pool_id);
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("pool/list"));
                     } else {
                         $this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
@@ -236,7 +236,7 @@ class Pools extends Extension
                     break;
 
                 default:
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("pool/list"));
                     break;
             }
diff --git a/ext/random_image/main.php b/ext/random_image/main.php
index 6630b8cb..fc0424f7 100644
--- a/ext/random_image/main.php
+++ b/ext/random_image/main.php
@@ -40,7 +40,7 @@ class RandomImage extends Extension
 
             if ($action === "download") {
                 if (!is_null($image)) {
-                    $page->set_mode("data");
+                    $page->set_mode(PageMode::DATA);
                     $page->set_type($image->get_mime_type());
                     $page->set_data(file_get_contents($image->get_image_filename()));
                 }
@@ -50,7 +50,7 @@ class RandomImage extends Extension
                 }
             } elseif ($action === "widget") {
                 if (!is_null($image)) {
-                    $page->set_mode("data");
+                    $page->set_mode(PageMode::DATA);
                     $page->set_type("text/html");
                     $page->set_data($this->theme->build_thumb_html($image));
                 }
diff --git a/ext/random_list/main.php b/ext/random_list/main.php
index a4da5604..7333905d 100644
--- a/ext/random_list/main.php
+++ b/ext/random_list/main.php
@@ -21,10 +21,10 @@ class RandomList extends Extension
                 // implode(explode()) to resolve aliases and sanitise
                 $search = url_escape(Tag::implode(Tag::explode($_GET['search'], false)));
                 if (empty($search)) {
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("random"));
                 } else {
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link('random/'.$search));
                 }
                 return;
diff --git a/ext/rating/main.php b/ext/rating/main.php
index 9f32280d..794129e4 100644
--- a/ext/rating/main.php
+++ b/ext/rating/main.php
@@ -91,7 +91,7 @@ class Ratings extends Extension
         $user_view_level = Ratings::get_user_privs($user);
         $user_view_level = preg_split('//', $user_view_level, -1);
         if (!in_array($event->image->rating, $user_view_level)) {
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link("post/list"));
         }
     }
@@ -228,7 +228,7 @@ class Ratings extends Extension
                 #		select image_id from image_tags join tags
                 #		on image_tags.tag_id = tags.id where tags.tag = ?);
                 #	", array($_POST["rating"], $_POST["tag"]));
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 $page->set_redirect(make_link("post/list"));
             }
         }
diff --git a/ext/regen_thumb/main.php b/ext/regen_thumb/main.php
index a653e902..b402281f 100644
--- a/ext/regen_thumb/main.php
+++ b/ext/regen_thumb/main.php
@@ -43,7 +43,7 @@ class RegenThumb extends Extension
                 $this->regenerate_thumbnail($image);
             }
 
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link("post/list"));
         }
     }
diff --git a/ext/report_image/main.php b/ext/report_image/main.php
index 0e716fec..d3ee2c81 100644
--- a/ext/report_image/main.php
+++ b/ext/report_image/main.php
@@ -67,7 +67,7 @@ class ReportImage extends Extension
                 if (!empty($_POST['image_id']) && !empty($_POST['reason'])) {
                     $image_id = int_escape($_POST['image_id']);
                     send_event(new AddReportedImageEvent(new ImageReport($image_id, $user->id, $_POST['reason'])));
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("post/view/$image_id"));
                 } else {
                     $this->theme->display_error(500, "Missing input", "Missing image ID or report reason");
@@ -76,7 +76,7 @@ class ReportImage extends Extension
                 if (!empty($_POST['id'])) {
                     if ($user->can("view_image_report")) {
                         send_event(new RemoveReportedImageEvent($_POST['id']));
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("image_report/list"));
                     }
                 } else {
@@ -85,7 +85,7 @@ class ReportImage extends Extension
             } elseif ($event->get_arg(0) == "remove_reports_by" && $user->check_auth_token()) {
                 if ($user->can("view_image_report")) {
                     $this->delete_reports_by(int_escape($_POST['user_id']));
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link());
                 }
             } elseif ($event->get_arg(0) == "list") {
diff --git a/ext/resize/main.php b/ext/resize/main.php
index 81db9007..785fcc31 100644
--- a/ext/resize/main.php
+++ b/ext/resize/main.php
@@ -149,7 +149,7 @@ class ResizeImage extends Extension
                         
                         //$this->theme->display_resize_page($page, $image_id);
                         
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("post/view/".$image_id));
                     } catch (ImageResizeException $e) {
                         $this->theme->display_resize_error($page, "Error Resizing", $e->error);
diff --git a/ext/rotate/main.php b/ext/rotate/main.php
index d612de15..65194bbc 100644
--- a/ext/rotate/main.php
+++ b/ext/rotate/main.php
@@ -96,7 +96,7 @@ class RotateImage extends Extension
                         
                         //$this->theme->display_rotate_page($page, $image_id);
                         
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("post/view/".$image_id));
                     } catch (ImageRotateException $e) {
                         $this->theme->display_rotate_error($page, "Error Rotating", $e->error);
diff --git a/ext/rss_comments/main.php b/ext/rss_comments/main.php
index dfc37717..012bca93 100644
--- a/ext/rss_comments/main.php
+++ b/ext/rss_comments/main.php
@@ -24,7 +24,7 @@ class RSS_Comments extends Extension
     {
         global $config, $database, $page;
         if ($event->page_matches("rss/comments")) {
-            $page->set_mode("data");
+            $page->set_mode(PageMode::DATA);
             $page->set_type("application/rss+xml");
 
             $comments = $database->get_all("
diff --git a/ext/rss_images/main.php b/ext/rss_images/main.php
index 292ecf05..b11fad92 100644
--- a/ext/rss_images/main.php
+++ b/ext/rss_images/main.php
@@ -39,7 +39,7 @@ class RSS_Images extends Extension
     {
         global $page;
         global $config;
-        $page->set_mode("data");
+        $page->set_mode(PageMode::DATA);
         $page->set_type("application/rss+xml");
 
         $data = "";
diff --git a/ext/rule34/main.php b/ext/rule34/main.php
index 775fe0ec..4dc2a241 100644
--- a/ext/rule34/main.php
+++ b/ext/rule34/main.php
@@ -90,14 +90,14 @@ class Rule34 extends Extension
                     'UPDATE users SET comic_admin=? WHERE id=?',
                     [$input['is_admin'] ? 't' : 'f', $input['user_id']]
                 );
-                $page->set_mode('redirect');
+                $page->set_mode(PageMode::REDIRECT);
                 $page->set_redirect(@$_SERVER['HTTP_REFERER']);
             }
         }
 
         if ($event->page_matches("tnc_agreed")) {
             setcookie("ui-tnc-agreed", "true", 0, "/");
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(@$_SERVER['HTTP_REFERER'] ?? "/");
         }
 
@@ -123,7 +123,7 @@ class Rule34 extends Extension
                     }
                 }
 
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 $page->set_redirect(make_link("admin"));
             }
         }
diff --git a/ext/setup/main.php b/ext/setup/main.php
index b316c975..23999102 100644
--- a/ext/setup/main.php
+++ b/ext/setup/main.php
@@ -203,7 +203,7 @@ class Setup extends Extension
         global $config, $page, $user;
 
         if ($event->page_matches("nicetest")) {
-            $page->set_mode("data");
+            $page->set_mode(PageMode::DATA);
             $page->set_data("ok");
         }
 
@@ -216,7 +216,7 @@ class Setup extends Extension
                     $config->save();
                     flash_message("Config saved");
 
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("setup"));
                 } elseif ($event->get_arg(0) == "advanced") {
                     $this->theme->display_advanced($page, $config->values);
diff --git a/ext/shimmie_api/main.php b/ext/shimmie_api/main.php
index ed5a3e1c..130c4971 100644
--- a/ext/shimmie_api/main.php
+++ b/ext/shimmie_api/main.php
@@ -53,7 +53,7 @@ class ShimmieApi extends Extension
         global $page, $user;
 
         if ($event->page_matches("api/shimmie")) {
-            $page->set_mode("data");
+            $page->set_mode(PageMode::DATA);
             $page->set_type("text/plain");
 
             if ($event->page_matches("api/shimmie/get_tags")) {
@@ -100,7 +100,7 @@ class ShimmieApi extends Extension
                 $all = $this->api_get_user($type, $query);
                 $page->set_data(json_encode($all));
             } else {
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 $page->set_redirect(make_link("ext_doc/shimmie_api"));
             }
         }
diff --git a/ext/sitemap/main.php b/ext/sitemap/main.php
index d7a17217..4ff80f88 100644
--- a/ext/sitemap/main.php
+++ b/ext/sitemap/main.php
@@ -152,7 +152,7 @@ class XMLSitemap extends Extension
 
         // Generate new sitemap
         file_put_contents($this->sitemap_filepath, $xml);
-        $page->set_mode("data");
+        $page->set_mode(PageMode::DATA);
         $page->set_type("application/xml");
         $page->set_data($xml);
     }
@@ -188,7 +188,7 @@ class XMLSitemap extends Extension
 
         $xml = file_get_contents($this->sitemap_filepath);
 
-        $page->set_mode("data");
+        $page->set_mode(PageMode::DATA);
         $page->set_type("application/xml");
         $page->set_data($xml);
     }
diff --git a/ext/source_history/main.php b/ext/source_history/main.php
index d8de6b55..591fd217 100644
--- a/ext/source_history/main.php
+++ b/ext/source_history/main.php
@@ -132,7 +132,7 @@ class Source_History extends Extension
 
         // check for the nothing case
         if ($revert_id < 1) {
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link());
             return;
         }
@@ -165,7 +165,7 @@ class Source_History extends Extension
         send_event(new SourceSetEvent($image, $stored_source));
         
         // all should be done now so redirect the user back to the image
-        $page->set_mode("redirect");
+        $page->set_mode(PageMode::REDIRECT);
         $page->set_redirect(make_link('post/view/'.$stored_image_id));
     }
 
diff --git a/ext/tag_edit/main.php b/ext/tag_edit/main.php
index 19f0dfa0..aa443797 100644
--- a/ext/tag_edit/main.php
+++ b/ext/tag_edit/main.php
@@ -165,14 +165,14 @@ class TagEdit extends Extension
                     $search = $_POST['search'];
                     $replace = $_POST['replace'];
                     $this->mass_tag_edit($search, $replace);
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("admin"));
                 }
             }
             if ($event->get_arg(0) == "mass_source_set") {
                 if ($user->can("mass_tag_edit") && isset($_POST['tags']) && isset($_POST['source'])) {
                     $this->mass_source_edit($_POST['tags'], $_POST['source']);
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("post/list"));
                 }
             }
diff --git a/ext/tag_history/main.php b/ext/tag_history/main.php
index 125c36c7..618bd85a 100644
--- a/ext/tag_history/main.php
+++ b/ext/tag_history/main.php
@@ -132,7 +132,7 @@ class Tag_History extends Extension
 
         // check for the nothing case
         if ($revert_id < 1) {
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link());
             return;
         }
@@ -162,7 +162,7 @@ class Tag_History extends Extension
         send_event(new TagSetEvent($image, Tag::explode($stored_tags)));
         
         // all should be done now so redirect the user back to the image
-        $page->set_mode("redirect");
+        $page->set_mode(PageMode::REDIRECT);
         $page->set_redirect(make_link('post/view/'.$stored_image_id));
     }
 
diff --git a/ext/tag_list/main.php b/ext/tag_list/main.php
index c546794e..a415d153 100644
--- a/ext/tag_list/main.php
+++ b/ext/tag_list/main.php
@@ -75,7 +75,7 @@ class TagList extends Extension
                 $database->cache->set($cache_key, $res, 600);
             }
 
-            $page->set_mode("data");
+            $page->set_mode(PageMode::DATA);
             $page->set_type("text/plain");
             $page->set_data(implode("\n", $res));
         }
diff --git a/ext/tagger/main.php b/ext/tagger/main.php
index e7a9148e..631da7b4 100644
--- a/ext/tagger/main.php
+++ b/ext/tagger/main.php
@@ -59,7 +59,7 @@ class TaggerXML extends Extension
                 $tags.
             "</tags>";
 
-            $page->set_mode("data");
+            $page->set_mode(PageMode::DATA);
             $page->set_type("text/xml");
             $page->set_data($xml);
         }
diff --git a/ext/tips/main.php b/ext/tips/main.php
index 04fb5124..5fd54e0d 100644
--- a/ext/tips/main.php
+++ b/ext/tips/main.php
@@ -51,7 +51,7 @@ class Tips extends Extension
                 case "save":
                     if ($user->check_auth_token()) {
                         $this->saveTip();
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("tips/list"));
                     }
                     break;
@@ -59,14 +59,14 @@ class Tips extends Extension
                     // FIXME: HTTP GET CSRF
                     $tipID = int_escape($event->get_arg(1));
                     $this->setStatus($tipID);
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("tips/list"));
                     break;
                 case "delete":
                     // FIXME: HTTP GET CSRF
                     $tipID = int_escape($event->get_arg(1));
                     $this->deleteTip($tipID);
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("tips/list"));
                     break;
             }
diff --git a/ext/transcode/main.php b/ext/transcode/main.php
index ba7e0947..94520ba0 100644
--- a/ext/transcode/main.php
+++ b/ext/transcode/main.php
@@ -199,7 +199,7 @@ class TranscodeImage extends Extension
                 if (isset($_POST['transcode_format'])) {
                     try {
                         $this->transcode_and_replace_image($image_obj, $_POST['transcode_format']);
-                        $page->set_mode("redirect");
+                        $page->set_mode(PageMode::REDIRECT);
                         $page->set_redirect(make_link("post/view/".$image_id));
                     } catch (ImageTranscodeException $e) {
                         $this->theme->display_transcode_error($page, "Error Transcoding", $e->getMessage());
diff --git a/ext/update/main.php b/ext/update/main.php
index cf995c10..00762620 100644
--- a/ext/update/main.php
+++ b/ext/update/main.php
@@ -38,7 +38,7 @@ class Update extends Extension
             if ($event->page_matches("update/download")) {
                 $ok = $this->download_shimmie();
 
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 if ($ok) {
                     $page->set_redirect(make_link("update/update", "sha=".$_GET['sha']));
                 } else {
@@ -47,7 +47,7 @@ class Update extends Extension
             } elseif ($event->page_matches("update/update")) {
                 $ok = $this->update_shimmie();
 
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 if ($ok) {
                     $page->set_redirect(make_link("admin"));
                 } //TODO: Show success?
diff --git a/ext/upload/theme.php b/ext/upload/theme.php
index 71041030..6b3b503a 100644
--- a/ext/upload/theme.php
+++ b/ext/upload/theme.php
@@ -300,7 +300,7 @@ class UploadTheme extends Themelet
     public function display_upload_status(Page $page, bool $ok)
     {
         if ($ok) {
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link());
         } else {
             $page->set_title("Upload Status");
diff --git a/ext/user/main.php b/ext/user/main.php
index f9ecc54c..58c6ce38 100644
--- a/ext/user/main.php
+++ b/ext/user/main.php
@@ -372,7 +372,7 @@ class UserPage extends Extension
         if (!is_null($duser)) {
             $user = $duser;
             $this->set_login_cookie($duser->name, $pass);
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
 
             // Try returning to previous page
             if ($config->get_int("user_loginshowprofile", 0) == 0 &&
@@ -397,7 +397,7 @@ class UserPage extends Extension
             $page->add_cookie("user", "", time() + 60 * 60 * 24 * $config->get_int('login_memory'), "/");
         }
         log_info("user", "Logged out");
-        $page->set_mode("redirect");
+        $page->set_mode(PageMode::REDIRECT);
 
         // Try forwarding to same page on logout unless user comes from registration page
         if ($config->get_int("user_loginshowprofile", 0) == 0 &&
@@ -440,7 +440,7 @@ class UserPage extends Extension
                 $uce = new UserCreationEvent($_POST['name'], $_POST['pass1'], $_POST['email']);
                 send_event($uce);
                 $this->set_login_cookie($uce->username, $uce->password);
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 $page->set_redirect(make_link("user"));
             } catch (UserCreationException $ex) {
                 $this->theme->display_error(400, "User Creation Error", $ex->getMessage());
@@ -532,10 +532,10 @@ class UserPage extends Extension
         global $page, $user;
 
         if ($user->id == $duser->id) {
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link("user"));
         } else {
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link("user/{$duser->name}"));
         }
     }
@@ -698,7 +698,7 @@ class UserPage extends Extension
                 ["id" => $_POST['id']]
             );
         
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link("post/list"));
         }
     }
diff --git a/ext/view/main.php b/ext/view/main.php
index bfbe2fba..bb2ee8c6 100644
--- a/ext/view/main.php
+++ b/ext/view/main.php
@@ -123,7 +123,7 @@ class ViewImage extends Extension
                 return;
             }
 
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link("post/view/{$image->id}", $query));
         } elseif ($event->page_matches("post/view")) {
             if (!is_numeric($event->get_arg(0))) {
@@ -157,7 +157,7 @@ class ViewImage extends Extension
 
             send_event(new ImageInfoSetEvent(Image::by_id($image_id)));
 
-            $page->set_mode("redirect");
+            $page->set_mode(PageMode::REDIRECT);
             $page->set_redirect(make_link("post/view/$image_id", url_escape(@$_POST['query'])));
         }
     }
diff --git a/ext/wiki/main.php b/ext/wiki/main.php
index 360c686a..7279b630 100644
--- a/ext/wiki/main.php
+++ b/ext/wiki/main.php
@@ -137,7 +137,7 @@ class Wiki extends Extension
                     send_event(new WikiUpdateEvent($user, $wikipage));
 
                     $u_title = url_escape($title);
-                    $page->set_mode("redirect");
+                    $page->set_mode(PageMode::REDIRECT);
                     $page->set_redirect(make_link("wiki/$u_title"));
                 } catch (WikiUpdateException $e) {
                     $original = $this->get_page($title);
@@ -159,7 +159,7 @@ class Wiki extends Extension
                     ["title"=>$_POST["title"], "rev"=>$_POST["revision"]]
                 );
                 $u_title = url_escape($_POST["title"]);
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 $page->set_redirect(make_link("wiki/$u_title"));
             }
         } elseif ($event->page_matches("wiki_admin/delete_all")) {
@@ -170,7 +170,7 @@ class Wiki extends Extension
                     ["title"=>$_POST["title"]]
                 );
                 $u_title = url_escape($_POST["title"]);
-                $page->set_mode("redirect");
+                $page->set_mode(PageMode::REDIRECT);
                 $page->set_redirect(make_link("wiki/$u_title"));
             }
         }
@@ -213,7 +213,7 @@ class Wiki extends Extension
         return false;
     }
 
-    private function get_page(string $title): WikiPage
+    private function get_page(string $title, int $revision=-1): WikiPage
     {
         global $database;
         // first try and get the actual page
@@ -222,21 +222,17 @@ class Wiki extends Extension
 				SELECT *
 				FROM wiki_pages
 				WHERE SCORE_STRNORM(title) LIKE SCORE_STRNORM(:title)
-				ORDER BY revision DESC
-				LIMIT 1
-			"),
+				ORDER BY revision DESC"),
             ["title"=>$title]
         );
 
         // fall back to wiki:default
         if (empty($row)) {
             $row = $database->get_row("
-				SELECT *
-				FROM wiki_pages
-				WHERE title LIKE :title
-				ORDER BY revision DESC
-				LIMIT 1
-			", ["title"=>"wiki:default"]);
+					SELECT *
+					FROM wiki_pages
+					WHERE title LIKE :title
+					ORDER BY revision DESC", ["title"=>"wiki:default"]);
 
             // fall further back to manual
             if (empty($row)) {
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 28539364..e2bcb800 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -52,7 +52,7 @@ abstract class ShimmiePHPUnitTestCase extends \PHPUnit\Framework\TestCase
         $_POST = [];
         $page = class_exists("CustomPage") ? new CustomPage() : new Page();
         send_event(new PageRequestEvent($page_name));
-        if ($page->mode == "redirect") {
+        if ($page->mode == PageMode::REDIRECT) {
             $page->code = 302;
         }
     }
@@ -68,7 +68,7 @@ abstract class ShimmiePHPUnitTestCase extends \PHPUnit\Framework\TestCase
         $_POST = $args;
         $page = class_exists("CustomPage") ? new CustomPage() : new Page();
         send_event(new PageRequestEvent($page_name));
-        if ($page->mode == "redirect") {
+        if ($page->mode == PageMode::REDIRECT) {
             $page->code = 302;
         }
     }
diff --git a/themes/material/home.theme.php b/themes/material/home.theme.php
index 68b700e1..ada70b17 100644
--- a/themes/material/home.theme.php
+++ b/themes/material/home.theme.php
@@ -4,7 +4,7 @@ class CustomHomeTheme extends HomeTheme
 {
     public function display_page(Page $page, $sitename, $base_href, $theme_name, $body)
     {
-        $page->set_mode("data");
+        $page->set_mode(PageMode::DATA);
         $page->add_auto_html_headers();
         $hh = $page->get_all_html_headers();
         $page->set_data(

From 5a30ce1c83a14fd2354971672ce3d1e4751ad49d Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Wed, 19 Jun 2019 18:59:18 -0500
Subject: [PATCH 18/23] Reverted removal of latter tag write

---
 ext/cron_uploader/main.php | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php
index 977a4f7a..a6130e3b 100644
--- a/ext/cron_uploader/main.php
+++ b/ext/cron_uploader/main.php
@@ -64,10 +64,10 @@ class CronUploader extends Extension
                 $this->set_dir();
 
                 $lockfile = fopen($this->root_dir . "/.lock", "w");
+                if (!flock($lockfile, LOCK_EX | LOCK_NB)) {
+                    throw new Exception("Cron upload process is already running");
+                }
                 try {
-                    if (!flock($lockfile, LOCK_EX | LOCK_NB)) {
-                        throw new Exception("Cron upload process is already running");
-                    }
                     $this->process_upload(); // Start upload
                 } finally {
                     flock($lockfile, LOCK_UN);
@@ -301,6 +301,7 @@ class CronUploader extends Extension
 
             try {
                 $database->beginTransaction();
+                $this->add_upload_info("Adding file: {$img[1]} - tags: {$img[2]}");
                 $result = $this->add_image($img[0], $img[1], $img[2]);
                 $database->commit();
                 $this->move_uploaded($img[0], $img[1], $output_subdir, false);
@@ -378,7 +379,7 @@ class CronUploader extends Extension
         if (array_key_exists('extension', $pathinfo)) {
             $metadata ['extension'] = $pathinfo ['extension'];
         }
-        $metadata ['tags'] = Tag::explode($tags);
+        $metadata ['tags'] = Tag::explode($tags); // doesn't work when not logged in here, handled below
         $metadata ['source'] = null;
         $event = new DataUploadEvent($tmpname, $metadata);
         send_event($event);
@@ -393,6 +394,13 @@ class CronUploader extends Extension
             $infomsg = "Image uploaded. ID: {$event->image_id} - Filename: {$filename}";
         }
         $msgNumber = $this->add_upload_info($infomsg);
+
+        // Set tags
+        $img = Image::by_id($event->image_id);
+        if(count($img->get_tag_array())==0) {
+            $img->set_tags(Tag::explode($tags));
+        }
+
         return $event->image_id;
     }
 

From 5eb4a66ab7c19abb2fceb39d718f13aae89f6320 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Wed, 19 Jun 2019 19:40:25 -0500
Subject: [PATCH 19/23] Added merged indicator to DataUploadEvent and
 ImageAddEvent Changed merge process so that the ID of the merged image can
 make it back through the event chanin

---
 core/extension.php         |  1 +
 core/imageboard/event.php  |  2 ++
 ext/cron_uploader/main.php |  4 ++--
 ext/handle_svg/main.php    |  1 +
 ext/image/main.php         | 10 +++++++---
 ext/resize/main.php        |  4 ----
 6 files changed, 13 insertions(+), 9 deletions(-)

diff --git a/core/extension.php b/core/extension.php
index d691bd68..5c8c61fa 100644
--- a/core/extension.php
+++ b/core/extension.php
@@ -199,6 +199,7 @@ abstract class DataHandlerExtension extends Extension
                 $iae = new ImageAdditionEvent($image);
                 send_event($iae);
                 $event->image_id = $iae->image->id;
+                $event->merged = $iae->merged;
 
                 // Rating Stuff.
                 if (!empty($event->metadata['rating'])) {
diff --git a/core/imageboard/event.php b/core/imageboard/event.php
index 2fc40fef..ec663d6b 100644
--- a/core/imageboard/event.php
+++ b/core/imageboard/event.php
@@ -11,6 +11,8 @@ class ImageAdditionEvent extends Event
     /** @var Image */
     public $image;
 
+    public $merged = false;
+
     /**
      * Inserts a new image into the database with its associated
      * information. Also calls TagSetEvent to set the tags for
diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php
index a6130e3b..3a6d5e42 100644
--- a/ext/cron_uploader/main.php
+++ b/ext/cron_uploader/main.php
@@ -388,8 +388,8 @@ class CronUploader extends Extension
         $infomsg = ""; // Will contain info message
         if ($event->image_id == -1) {
             throw new Exception("File type not recognised. Filename: {$filename}");
-        } elseif ($event->image_id == null) {
-            $infomsg = "Image merged. Filename: {$filename}";
+        } elseif ($event->merged === true) {
+            $infomsg = "Image merged. ID: {$event->image_id} Filename: {$filename}";
         } else {
             $infomsg = "Image uploaded. ID: {$event->image_id} - Filename: {$filename}";
         }
diff --git a/ext/handle_svg/main.php b/ext/handle_svg/main.php
index 96dea29a..9998a245 100644
--- a/ext/handle_svg/main.php
+++ b/ext/handle_svg/main.php
@@ -29,6 +29,7 @@ class SVGFileHandler extends DataHandlerExtension
             $iae = new ImageAdditionEvent($image);
             send_event($iae);
             $event->image_id = $iae->image->id;
+            $event->merged = $iae->merged;
         }
     }
 
diff --git a/ext/image/main.php b/ext/image/main.php
index f9b31180..3fc63325 100644
--- a/ext/image/main.php
+++ b/ext/image/main.php
@@ -88,7 +88,7 @@ class ImageIO extends Extension
     public function onImageAddition(ImageAdditionEvent $event)
     {
         try {
-            $this->add_image($event->image);
+            $this->add_image($event);
         } catch (ImageAdditionException $e) {
             throw new UploadException($e->error);
         }
@@ -175,10 +175,12 @@ class ImageIO extends Extension
 
 
     // add image {{{
-    private function add_image(Image $image)
+    private function add_image(ImageAdditionEvent $event)
     {
         global $user, $database, $config;
 
+        $image = $event->image;
+
         /*
          * Validate things
          */
@@ -201,7 +203,9 @@ class ImageIO extends Extension
                 if (isset($_GET['source']) && isset($_GET['update'])) {
                     send_event(new SourceSetEvent($existing, $_GET['source']));
                 }
-                return null;
+                $event->merged = true;
+                $event->image = Image::by_id($existing->id);
+                return;
             } else {
                 $error = "Image <a href='".make_link("post/view/{$existing->id}")."'>{$existing->id}</a> ".
                         "already has hash {$image->hash}:<p>".$this->theme->build_thumb_html($existing);
diff --git a/ext/resize/main.php b/ext/resize/main.php
index 785fcc31..a998814e 100644
--- a/ext/resize/main.php
+++ b/ext/resize/main.php
@@ -65,10 +65,6 @@ class ResizeImage extends Extension
     {
         global $config, $page;
 
-        if($event->image_id==null) {
-            return;
-        }
-
         $image_obj = Image::by_id($event->image_id);
 
         if ($config->get_bool("resize_upload") == true

From 921ec9a7bbff82573613a9586f965ca7765e6ac0 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Wed, 19 Jun 2019 20:14:19 -0500
Subject: [PATCH 20/23] Adjusted cron upload for new merged flag, and to make
 sure tags merge properly

---
 ext/cron_uploader/main.php | 17 ++++++++++-------
 1 file changed, 10 insertions(+), 7 deletions(-)

diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php
index 3a6d5e42..560a15a3 100644
--- a/ext/cron_uploader/main.php
+++ b/ext/cron_uploader/main.php
@@ -305,7 +305,7 @@ class CronUploader extends Extension
                 $result = $this->add_image($img[0], $img[1], $img[2]);
                 $database->commit();
                 $this->move_uploaded($img[0], $img[1], $output_subdir, false);
-                if ($result == null) {
+                if ($result->merged) {
                     $merged++;
                 } else {
                     $added++;
@@ -369,17 +369,22 @@ class CronUploader extends Extension
     /**
      * Generate the necessary DataUploadEvent for a given image and tags.
      */
-    private function add_image(string $tmpname, string $filename, string $tags)
+    private function add_image(string $tmpname, string $filename, string $tags): DataUploadEvent
     {
         assert(file_exists($tmpname));
 
+        $tagArray = Tag::explode($tags);
+        if(count($tagArray)==0) {
+            $tagArray[] = "tagme";
+        }
+
         $pathinfo = pathinfo($filename);
         $metadata = [];
         $metadata ['filename'] = $pathinfo ['basename'];
         if (array_key_exists('extension', $pathinfo)) {
             $metadata ['extension'] = $pathinfo ['extension'];
         }
-        $metadata ['tags'] = Tag::explode($tags); // doesn't work when not logged in here, handled below
+        $metadata ['tags'] = $tagArray; // doesn't work when not logged in here, handled below
         $metadata ['source'] = null;
         $event = new DataUploadEvent($tmpname, $metadata);
         send_event($event);
@@ -397,11 +402,9 @@ class CronUploader extends Extension
 
         // Set tags
         $img = Image::by_id($event->image_id);
-        if(count($img->get_tag_array())==0) {
-            $img->set_tags(Tag::explode($tags));
-        }
+        $img->set_tags(array_merge($tagArray, $img->get_tag_array()));
 
-        return $event->image_id;
+        return $event;
     }
 
     private function generate_image_queue(): void

From a834d1f81459b7c3ed624efd0ba9339289886e35 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Wed, 19 Jun 2019 23:37:33 -0500
Subject: [PATCH 21/23] Resolved issue with bulk rater

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

diff --git a/ext/rating/main.php b/ext/rating/main.php
index 794129e4..99f2b218 100644
--- a/ext/rating/main.php
+++ b/ext/rating/main.php
@@ -170,7 +170,7 @@ class Ratings extends Extension
         global $user;
 
         if ($user->is_admin()) {
-            $event->add_action("bulk_rate", "Set Rating", "", $this->theme->get_selection_rater_html("bulk_rating"));
+            $event->add_action("bulk_rate","Set Rating","",$this->theme->get_selection_rater_html("u","bulk_rating"));
         }
     }
 
@@ -191,7 +191,7 @@ class Ratings extends Extension
                         if ($image==null) {
                             continue;
                         }
-        
+
                         send_event(new RatingSetEvent($image, $rating));
                         $total++;
                     }

From d128dfa78ec25d216e6d17d53e74da88e3ede9fe Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Thu, 20 Jun 2019 10:05:53 -0500
Subject: [PATCH 22/23] Added lower indexes for postgresql to tags.tag and
 users.name to speed up queries for them using lower()

---
 ext/upgrade/main.php | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/ext/upgrade/main.php b/ext/upgrade/main.php
index 0aef4530..42522ad9 100644
--- a/ext/upgrade/main.php
+++ b/ext/upgrade/main.php
@@ -129,6 +129,20 @@ class Upgrade extends Extension
             log_info("upgrade", "Database at version 14");
             $config->set_bool("in_upgrade", false);
         }
+
+        if ($config->get_int("db_version") < 15) {
+            $config->set_bool("in_upgrade", true);
+            $config->set_int("db_version", 15);
+
+            log_info("upgrade", "Adding lower indexes for postgresql use");
+            if ($database->get_driver_name() == Database::PGSQL_DRIVER) {
+                $database->execute('CREATE INDEX tags_lower_tag_idx ON tags ((lower(tag)))');
+                $database->execute('CREATE INDEX users_lower_name_idx ON users ((lower(name)))');
+            }
+
+            log_info("upgrade", "Database at version 15");
+            $config->set_bool("in_upgrade", false);
+        }
     }
 
     public function get_priority(): int

From 1370afec72c3418f9d024c13ebb0f78707f04776 Mon Sep 17 00:00:00 2001
From: Matthew Barbour <matthew@darkholme.net>
Date: Thu, 20 Jun 2019 10:42:32 -0500
Subject: [PATCH 23/23] Moved database driver constants to DatabaseDriver

---
 core/_install.php            | 14 +++++++-------
 core/database.php            | 28 ++++++++++++++++------------
 core/dbengine.php            |  6 +++---
 core/imageboard/image.php    |  4 ++--
 core/user.php                |  2 +-
 ext/admin/main.php           | 12 ++++++------
 ext/admin/theme.php          |  2 +-
 ext/comment/main.php         |  4 ++--
 ext/index/test.php           |  2 +-
 ext/ipban/main.php           |  2 +-
 ext/ipban/theme.php          |  2 +-
 ext/log_db/main.php          |  2 +-
 ext/rating/main.php          |  6 +++---
 ext/relatationships/main.php |  2 +-
 ext/rss_comments/main.php    |  2 +-
 ext/rule34/main.php          |  2 +-
 ext/rule34/theme.php         |  2 +-
 ext/tips/main.php            |  2 +-
 ext/upgrade/main.php         | 14 +++++++-------
 19 files changed, 57 insertions(+), 53 deletions(-)

diff --git a/core/_install.php b/core/_install.php
index 99fa864d..a695c237 100644
--- a/core/_install.php
+++ b/core/_install.php
@@ -110,7 +110,7 @@ function do_install()
 { // {{{
     if (file_exists("data/config/auto_install.conf.php")) {
         require_once "data/config/auto_install.conf.php";
-    } elseif (@$_POST["database_type"] == Database::SQLITE_DRIVER) {
+    } elseif (@$_POST["database_type"] == DatabaseDriver::SQLITE) {
         $id = bin2hex(random_bytes(5));
         define('DATABASE_DSN', "sqlite:data/shimmie.{$id}.sqlite");
     } elseif (isset($_POST['database_type']) && isset($_POST['database_host']) && isset($_POST['database_user']) && isset($_POST['database_name'])) {
@@ -153,9 +153,9 @@ function ask_questions()
 
     $drivers = PDO::getAvailableDrivers();
     if (
-        !in_array(Database::MYSQL_DRIVER, $drivers) &&
-        !in_array(Database::PGSQL_DRIVER, $drivers) &&
-        !in_array(Database::SQLITE_DRIVER, $drivers)
+        !in_array(DatabaseDriver::MYSQL, $drivers) &&
+        !in_array(DatabaseDriver::PGSQL, $drivers) &&
+        !in_array(DatabaseDriver::SQLITE, $drivers)
     ) {
         $errors[] = "
 			No database connection library could be found; shimmie needs
@@ -163,9 +163,9 @@ function ask_questions()
 		";
     }
 
-    $db_m = in_array(Database::MYSQL_DRIVER, $drivers)  ? '<option value="'.Database::MYSQL_DRIVER.'">MySQL</option>' : "";
-    $db_p = in_array(Database::PGSQL_DRIVER, $drivers)  ? '<option value="'.Database::PGSQL_DRIVER.'">PostgreSQL</option>' : "";
-    $db_s = in_array(Database::SQLITE_DRIVER, $drivers) ? '<option value="'.Database::SQLITE_DRIVER.'">SQLite</option>' : "";
+    $db_m = in_array(DatabaseDriver::MYSQL, $drivers)  ? '<option value="'. DatabaseDriver::MYSQL .'">MySQL</option>' : "";
+    $db_p = in_array(DatabaseDriver::PGSQL, $drivers)  ? '<option value="'. DatabaseDriver::PGSQL .'">PostgreSQL</option>' : "";
+    $db_s = in_array(DatabaseDriver::SQLITE, $drivers) ? '<option value="'. DatabaseDriver::SQLITE .'">SQLite</option>' : "";
 
     $warn_msg = $warnings ? "<h3>Warnings</h3>".implode("\n<p>", $warnings) : "";
     $err_msg = $errors ? "<h3>Errors</h3>".implode("\n<p>", $errors) : "";
diff --git a/core/database.php b/core/database.php
index 29381209..65405211 100644
--- a/core/database.php
+++ b/core/database.php
@@ -1,12 +1,16 @@
 <?php
+abstract class DatabaseDriver
+{
+    public const MYSQL = "mysql";
+    public const PGSQL = "pgsql";
+    public const SQLITE = "sqlite";
+}
+
 /**
  * A class for controlled database access
  */
 class Database
 {
-    const MYSQL_DRIVER = "mysql";
-    const PGSQL_DRIVER = "pgsql";
-    const SQLITE_DRIVER = "sqlite";
 
     /**
      * The PDO database connection object, for anyone who wants direct access.
@@ -76,7 +80,7 @@ class Database
 
         // https://bugs.php.net/bug.php?id=70221
         $ka = DATABASE_KA;
-        if (version_compare(PHP_VERSION, "6.9.9") == 1 && $this->get_driver_name() == self::SQLITE_DRIVER) {
+        if (version_compare(PHP_VERSION, "6.9.9") == 1 && $this->get_driver_name() == DatabaseDriver::SQLITE) {
             $ka = false;
         }
 
@@ -100,11 +104,11 @@ class Database
             throw new SCoreException("Can't figure out database engine");
         }
 
-        if ($db_proto === self::MYSQL_DRIVER) {
+        if ($db_proto === DatabaseDriver::MYSQL) {
             $this->engine = new MySQL();
-        } elseif ($db_proto === self::PGSQL_DRIVER) {
+        } elseif ($db_proto === DatabaseDriver::PGSQL) {
             $this->engine = new PostgreSQL();
-        } elseif ($db_proto === self::SQLITE_DRIVER) {
+        } elseif ($db_proto === DatabaseDriver::SQLITE) {
             $this->engine = new SQLite();
         } else {
             die('Unknown PDO driver: '.$db_proto);
@@ -228,7 +232,7 @@ class Database
             }
             return $stmt;
         } catch (PDOException $pdoe) {
-            throw new SCoreException($pdoe->getMessage()."<p><b>Query:</b> ".$query, $pdoe->getCode(), $pdoe);
+            throw new SCoreException($pdoe->getMessage()."<p><b>Query:</b> ".$query);
         }
     }
 
@@ -300,7 +304,7 @@ class Database
      */
     public function get_last_insert_id(string $seq): int
     {
-        if ($this->engine->name == self::PGSQL_DRIVER) {
+        if ($this->engine->name == DatabaseDriver::PGSQL) {
             return $this->db->lastInsertId($seq);
         } else {
             return $this->db->lastInsertId();
@@ -330,15 +334,15 @@ class Database
             $this->connect_db();
         }
 
-        if ($this->engine->name === self::MYSQL_DRIVER) {
+        if ($this->engine->name === DatabaseDriver::MYSQL) {
             return count(
                 $this->get_all("SHOW TABLES")
             );
-        } elseif ($this->engine->name === self::PGSQL_DRIVER) {
+        } elseif ($this->engine->name === DatabaseDriver::PGSQL) {
             return count(
                 $this->get_all("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
             );
-        } elseif ($this->engine->name === self::SQLITE_DRIVER) {
+        } elseif ($this->engine->name === DatabaseDriver::SQLITE) {
             return count(
                 $this->get_all("SELECT name FROM sqlite_master WHERE type = 'table'")
             );
diff --git a/core/dbengine.php b/core/dbengine.php
index d76a1a43..404adad9 100644
--- a/core/dbengine.php
+++ b/core/dbengine.php
@@ -22,7 +22,7 @@ class DBEngine
 class MySQL extends DBEngine
 {
     /** @var string */
-    public $name = Database::MYSQL_DRIVER;
+    public $name = DatabaseDriver::MYSQL;
 
     public function init(PDO $db)
     {
@@ -54,7 +54,7 @@ class MySQL extends DBEngine
 class PostgreSQL extends DBEngine
 {
     /** @var string */
-    public $name = Database::PGSQL_DRIVER;
+    public $name = DatabaseDriver::PGSQL;
 
     public function init(PDO $db)
     {
@@ -136,7 +136,7 @@ function _ln($n)
 class SQLite extends DBEngine
 {
     /** @var string  */
-    public $name = Database::SQLITE_DRIVER;
+    public $name = DatabaseDriver::SQLITE;
 
     public function init(PDO $db)
     {
diff --git a/core/imageboard/image.php b/core/imageboard/image.php
index 72364d3a..2f224c8b 100644
--- a/core/imageboard/image.php
+++ b/core/imageboard/image.php
@@ -644,7 +644,7 @@ class Image
     public function delete_tags_from_image(): void
     {
         global $database;
-        if ($database->get_driver_name() == Database::MYSQL_DRIVER) {
+        if ($database->get_driver_name() == DatabaseDriver::MYSQL) {
             //mysql < 5.6 has terrible subquery optimization, using EXISTS / JOIN fixes this
             $database->execute(
                 "
@@ -921,7 +921,7 @@ class Image
 
         // more than one positive tag, or more than zero negative tags
         else {
-            if ($database->get_driver_name() === Database::MYSQL_DRIVER) {
+            if ($database->get_driver_name() === DatabaseDriver::MYSQL) {
                 $query = Image::build_ugly_search_querylet($tag_conditions);
             } else {
                 $query = Image::build_accurate_search_querylet($tag_conditions);
diff --git a/core/user.php b/core/user.php
index a2a4d537..05f6e7f7 100644
--- a/core/user.php
+++ b/core/user.php
@@ -69,7 +69,7 @@ class User
         global $config, $database;
         $row = $database->cache->get("user-session:$name-$session");
         if (!$row) {
-            if ($database->get_driver_name() === Database::MYSQL_DRIVER) {
+            if ($database->get_driver_name() === DatabaseDriver::MYSQL) {
                 $query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess";
             } else {
                 $query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess";
diff --git a/ext/admin/main.php b/ext/admin/main.php
index c9d2feff..c6c7f33f 100644
--- a/ext/admin/main.php
+++ b/ext/admin/main.php
@@ -201,14 +201,14 @@ class AdminPage extends Extension
         $database = $matches['dbname'];
 
         switch ($software) {
-            case Database::MYSQL_DRIVER:
+            case DatabaseDriver::MYSQL:
                 $cmd = "mysqldump -h$hostname -u$username -p$password $database";
                 break;
-            case Database::PGSQL_DRIVER:
+            case DatabaseDriver::PGSQL:
                 putenv("PGPASSWORD=$password");
                 $cmd = "pg_dump -h $hostname -U $username $database";
                 break;
-            case Database::SQLITE_DRIVER:
+            case DatabaseDriver::SQLITE:
                 $cmd = "sqlite3 $database .dump";
                 break;
             default:
@@ -257,7 +257,7 @@ class AdminPage extends Extension
         //TODO: Update score_log (Having an optional ID column for score_log would be nice..)
         preg_match("#^(?P<proto>\w+)\:(?:user=(?P<user>\w+)(?:;|$)|password=(?P<password>\w*)(?:;|$)|host=(?P<host>[\w\.\-]+)(?:;|$)|dbname=(?P<dbname>[\w_]+)(?:;|$))+#", DATABASE_DSN, $matches);
 
-        if ($matches['proto'] == Database::MYSQL_DRIVER) {
+        if ($matches['proto'] == DatabaseDriver::MYSQL) {
             $tables = $database->get_col("SELECT TABLE_NAME
 			                              FROM information_schema.KEY_COLUMN_USAGE
 			                              WHERE TABLE_SCHEMA = :db
@@ -280,9 +280,9 @@ class AdminPage extends Extension
                 $i++;
             }
             $database->execute("ALTER TABLE images AUTO_INCREMENT=".(count($ids) + 1));
-        } elseif ($matches['proto'] == Database::PGSQL_DRIVER) {
+        } elseif ($matches['proto'] == DatabaseDriver::PGSQL) {
             //TODO: Make this work with PostgreSQL
-        } elseif ($matches['proto'] == Database::SQLITE_DRIVER) {
+        } elseif ($matches['proto'] == DatabaseDriver::SQLITE) {
             //TODO: Make this work with SQLite
         }
         return true;
diff --git a/ext/admin/theme.php b/ext/admin/theme.php
index 5d3de30f..64191067 100644
--- a/ext/admin/theme.php
+++ b/ext/admin/theme.php
@@ -45,7 +45,7 @@ class AdminPageTheme extends Themelet
             $html .= $this->button("Download all images", "download_all_images", false);
         }
         $html .= $this->button("Download database contents", "database_dump", false);
-        if ($database->get_driver_name() == Database::MYSQL_DRIVER) {
+        if ($database->get_driver_name() == DatabaseDriver::MYSQL) {
             $html .= $this->button("Reset image IDs", "reset_image_ids", true);
         }
         $page->add_block(new Block("Misc Admin Tools", $html));
diff --git a/ext/comment/main.php b/ext/comment/main.php
index d4cef828..1dfdc03b 100644
--- a/ext/comment/main.php
+++ b/ext/comment/main.php
@@ -480,14 +480,14 @@ class CommentList extends Extension
         global $config, $database;
 
         // sqlite fails at intervals
-        if ($database->get_driver_name() === Database::SQLITE_DRIVER) {
+        if ($database->get_driver_name() === DatabaseDriver::SQLITE) {
             return false;
         }
 
         $window = int_escape($config->get_int('comment_window'));
         $max = int_escape($config->get_int('comment_limit'));
 
-        if ($database->get_driver_name() == Database::MYSQL_DRIVER) {
+        if ($database->get_driver_name() == DatabaseDriver::MYSQL) {
             $window_sql = "interval $window minute";
         } else {
             $window_sql = "interval '$window minute'";
diff --git a/ext/index/test.php b/ext/index/test.php
index 8f57bdb2..5d54fd42 100644
--- a/ext/index/test.php
+++ b/ext/index/test.php
@@ -157,7 +157,7 @@ class IndexTest extends ShimmiePHPUnitTestCase
 
         global $database;
         $db = $database->get_driver_name();
-        if ($db == Database::PGSQL_DRIVER || $db == Database::SQLITE_DRIVER) {
+        if ($db == DatabaseDriver::PGSQL || $db == DatabaseDriver::SQLITE) {
             $this->markTestIncomplete();
         }
 
diff --git a/ext/ipban/main.php b/ext/ipban/main.php
index f86c10ff..d6feb092 100644
--- a/ext/ipban/main.php
+++ b/ext/ipban/main.php
@@ -235,7 +235,7 @@ class IPBan extends Extension
     {
         global $config, $database;
 
-        $prefix = ($database->get_driver_name() == Database::SQLITE_DRIVER ? "bans." : "");
+        $prefix = ($database->get_driver_name() == DatabaseDriver::SQLITE ? "bans." : "");
 
         $bans = $this->get_active_bans();
 
diff --git a/ext/ipban/theme.php b/ext/ipban/theme.php
index 67979128..0373c94d 100644
--- a/ext/ipban/theme.php
+++ b/ext/ipban/theme.php
@@ -16,7 +16,7 @@ class IPBanTheme extends Themelet
     {
         global $database, $user;
         $h_bans = "";
-        $prefix = ($database->get_driver_name() == Database::SQLITE_DRIVER ? "bans." : "");
+        $prefix = ($database->get_driver_name() == DatabaseDriver::SQLITE ? "bans." : "");
         foreach ($bans as $ban) {
             $end_human = date('Y-m-d', $ban[$prefix.'end_timestamp']);
             $h_bans .= "
diff --git a/ext/log_db/main.php b/ext/log_db/main.php
index 5b400b12..2f1d761a 100644
--- a/ext/log_db/main.php
+++ b/ext/log_db/main.php
@@ -68,7 +68,7 @@ class LogDatabase extends Extension
                     $args["module"] = $_GET["module"];
                 }
                 if (!empty($_GET["user"])) {
-                    if ($database->get_driver_name() == Database::PGSQL_DRIVER) {
+                    if ($database->get_driver_name() == DatabaseDriver::PGSQL) {
                         if (preg_match("#\d+\.\d+\.\d+\.\d+(/\d+)?#", $_GET["user"])) {
                             $wheres[] = "(username = :user1 OR text(address) = :user2)";
                             $args["user1"] = $_GET["user"];
diff --git a/ext/rating/main.php b/ext/rating/main.php
index 99f2b218..a5e173a1 100644
--- a/ext/rating/main.php
+++ b/ext/rating/main.php
@@ -37,7 +37,7 @@ class RatingSetEvent extends Event
 
 class Ratings extends Extension
 {
-    protected $db_support = [Database::MYSQL_DRIVER,Database::PGSQL_DRIVER];
+    protected $db_support = [DatabaseDriver::MYSQL, DatabaseDriver::PGSQL];
 
     public function get_priority(): int
     {
@@ -331,10 +331,10 @@ class Ratings extends Extension
         if ($config->get_int("ext_ratings2_version") < 3) {
             $database->Execute("UPDATE images SET rating = 'u' WHERE rating is null");
             switch ($database->get_driver_name()) {
-                case Database::MYSQL_DRIVER:
+                case DatabaseDriver::MYSQL:
                     $database->Execute("ALTER TABLE images CHANGE rating rating CHAR(1) NOT NULL DEFAULT 'u'");
                     break;
-                case Database::PGSQL_DRIVER:
+                case DatabaseDriver::PGSQL:
                     $database->Execute("ALTER TABLE images ALTER COLUMN rating SET DEFAULT 'u'");
                     $database->Execute("ALTER TABLE images ALTER COLUMN rating SET NOT NULL");
                     break;
diff --git a/ext/relatationships/main.php b/ext/relatationships/main.php
index 402f4fd0..5304e3a5 100644
--- a/ext/relatationships/main.php
+++ b/ext/relatationships/main.php
@@ -8,7 +8,7 @@
 
 class Relationships extends Extension
 {
-    protected $db_support = [Database::MYSQL_DRIVER, Database::PGSQL_DRIVER];
+    protected $db_support = [DatabaseDriver::MYSQL, DatabaseDriver::PGSQL];
 
     public function onInitExt(InitExtEvent $event)
     {
diff --git a/ext/rss_comments/main.php b/ext/rss_comments/main.php
index 012bca93..b00f92aa 100644
--- a/ext/rss_comments/main.php
+++ b/ext/rss_comments/main.php
@@ -9,7 +9,7 @@
 
 class RSS_Comments extends Extension
 {
-    protected $db_support = [Database::MYSQL_DRIVER, Database::SQLITE_DRIVER];  // pgsql has no UNIX_TIMESTAMP
+    protected $db_support = [DatabaseDriver::MYSQL, DatabaseDriver::SQLITE];  // pgsql has no UNIX_TIMESTAMP
 
     public function onPostListBuilding(PostListBuildingEvent $event)
     {
diff --git a/ext/rule34/main.php b/ext/rule34/main.php
index 4dc2a241..529e6201 100644
--- a/ext/rule34/main.php
+++ b/ext/rule34/main.php
@@ -19,7 +19,7 @@ if ( // kill these glitched requests immediately
 
 class Rule34 extends Extension
 {
-    protected $db_support = [Database::PGSQL_DRIVER];  # Only PG has the NOTIFY pubsub system
+    protected $db_support = [DatabaseDriver::PGSQL];  # Only PG has the NOTIFY pubsub system
 
     public function onImageDeletion(ImageDeletionEvent $event)
     {
diff --git a/ext/rule34/theme.php b/ext/rule34/theme.php
index d4dd29e1..f71d8c15 100644
--- a/ext/rule34/theme.php
+++ b/ext/rule34/theme.php
@@ -19,7 +19,7 @@ class Rule34Theme extends Themelet
     {
         global $database, $user;
         $h_bans = "";
-        $prefix = ($database->get_driver_name() == Database::SQLITE_DRIVER ? "bans." : "");
+        $prefix = ($database->get_driver_name() == DatabaseDriver::SQLITE ? "bans." : "");
         foreach ($bans as $ban) {
             $h_bans .= "
 				<tr>
diff --git a/ext/tips/main.php b/ext/tips/main.php
index 5fd54e0d..7e5610a6 100644
--- a/ext/tips/main.php
+++ b/ext/tips/main.php
@@ -10,7 +10,7 @@
 
 class Tips extends Extension
 {
-    protected $db_support = [Database::MYSQL_DRIVER, Database::SQLITE_DRIVER];  // rand() ?
+    protected $db_support = [DatabaseDriver::MYSQL, DatabaseDriver::SQLITE];  // rand() ?
 
     public function onInitExt(InitExtEvent $event)
     {
diff --git a/ext/upgrade/main.php b/ext/upgrade/main.php
index 42522ad9..160422b5 100644
--- a/ext/upgrade/main.php
+++ b/ext/upgrade/main.php
@@ -44,7 +44,7 @@ class Upgrade extends Extension
             $config->set_bool("in_upgrade", true);
             $config->set_int("db_version", 9);
 
-            if ($database->get_driver_name() == Database::MYSQL_DRIVER) {
+            if ($database->get_driver_name() == DatabaseDriver::MYSQL) {
                 $tables = $database->get_col("SHOW TABLES");
                 foreach ($tables as $table) {
                     log_info("upgrade", "converting $table to innodb");
@@ -84,7 +84,7 @@ class Upgrade extends Extension
             $config->set_bool("in_upgrade", true);
             $config->set_int("db_version", 12);
 
-            if ($database->get_driver_name() == Database::PGSQL_DRIVER) {
+            if ($database->get_driver_name() == DatabaseDriver::PGSQL) {
                 log_info("upgrade", "Changing ext column to VARCHAR");
                 $database->execute("ALTER TABLE images ALTER COLUMN ext SET DATA TYPE VARCHAR(4)");
             }
@@ -101,9 +101,9 @@ class Upgrade extends Extension
             $config->set_int("db_version", 13);
 
             log_info("upgrade", "Changing password column to VARCHAR(250)");
-            if ($database->get_driver_name() == Database::PGSQL_DRIVER) {
+            if ($database->get_driver_name() == DatabaseDriver::PGSQL) {
                 $database->execute("ALTER TABLE users ALTER COLUMN pass SET DATA TYPE VARCHAR(250)");
-            } elseif ($database->get_driver_name() == Database::MYSQL_DRIVER) {
+            } elseif ($database->get_driver_name() == DatabaseDriver::MYSQL) {
                 $database->execute("ALTER TABLE users CHANGE pass pass VARCHAR(250)");
             }
 
@@ -116,11 +116,11 @@ class Upgrade extends Extension
             $config->set_int("db_version", 14);
 
             log_info("upgrade", "Changing tag column to VARCHAR(255)");
-            if ($database->get_driver_name() == Database::PGSQL_DRIVER) {
+            if ($database->get_driver_name() == DatabaseDriver::PGSQL) {
                 $database->execute('ALTER TABLE tags ALTER COLUMN tag SET DATA TYPE VARCHAR(255)');
                 $database->execute('ALTER TABLE aliases ALTER COLUMN oldtag SET DATA TYPE VARCHAR(255)');
                 $database->execute('ALTER TABLE aliases ALTER COLUMN newtag SET DATA TYPE VARCHAR(255)');
-            } elseif ($database->get_driver_name() == Database::MYSQL_DRIVER) {
+            } elseif ($database->get_driver_name() == DatabaseDriver::MYSQL) {
                 $database->execute('ALTER TABLE tags MODIFY COLUMN tag VARCHAR(255) NOT NULL');
                 $database->execute('ALTER TABLE aliases MODIFY COLUMN oldtag VARCHAR(255) NOT NULL');
                 $database->execute('ALTER TABLE aliases MODIFY COLUMN newtag VARCHAR(255) NOT NULL');
@@ -135,7 +135,7 @@ class Upgrade extends Extension
             $config->set_int("db_version", 15);
 
             log_info("upgrade", "Adding lower indexes for postgresql use");
-            if ($database->get_driver_name() == Database::PGSQL_DRIVER) {
+            if ($database->get_driver_name() == DatabaseDriver::PGSQL) {
                 $database->execute('CREATE INDEX tags_lower_tag_idx ON tags ((lower(tag)))');
                 $database->execute('CREATE INDEX users_lower_name_idx ON users ((lower(name)))');
             }