From c208a3715c82ac4e6f5d56577c31ae5df1c9c7cd Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Fri, 10 Mar 2017 16:14:56 +0000
Subject: [PATCH 01/41] mark static functions as static

---
 core/imageboard.pack.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php
index 0827b411..5f0253fc 100644
--- a/core/imageboard.pack.php
+++ b/core/imageboard.pack.php
@@ -175,7 +175,7 @@ class Image {
 		return $images;
 	}
 
-	public function validate_accel($tags) {
+	public static function validate_accel($tags) {
 		$yays = 0;
 		$nays = 0;
 		foreach($tags as $tag) {
@@ -195,7 +195,7 @@ class Image {
 	 * @return null|PDOStatement
 	 * @throws SCoreException
 	 */
-	public function get_accelerated_result($tags, $offset, $limit) {
+	public static function get_accelerated_result($tags, $offset, $limit) {
 		global $database;
 
 		if(!Image::validate_accel($tags)) {

From 10863d4c4bb14eface0bb62adc6cfb8bd6bf7ee1 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Fri, 10 Mar 2017 16:15:31 +0000
Subject: [PATCH 02/41] import imageboard to avoid warnings

---
 core/_bootstrap.inc.php | 1 +
 1 file changed, 1 insertion(+)

diff --git a/core/_bootstrap.inc.php b/core/_bootstrap.inc.php
index df92da94..e8d6559a 100644
--- a/core/_bootstrap.inc.php
+++ b/core/_bootstrap.inc.php
@@ -10,6 +10,7 @@ require_once "core/sys_config.inc.php";
 require_once "core/util.inc.php";
 require_once "lib/context.php";
 require_once "vendor/autoload.php";
+require_once "core/imageboard.pack.php";
 
 // set up and purify the environment
 _version_check();

From 277d80c4df41918a4b664ecdd88fa1a0b4f0be35 Mon Sep 17 00:00:00 2001
From: jgen <jeffgenovy@gmail.com>
Date: Thu, 23 Mar 2017 00:35:18 -0700
Subject: [PATCH 03/41] Bump the version number

---
 core/sys_config.inc.php | 2 +-
 install.php             | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/core/sys_config.inc.php b/core/sys_config.inc.php
index abc00766..12d71c5e 100644
--- a/core/sys_config.inc.php
+++ b/core/sys_config.inc.php
@@ -36,7 +36,7 @@ _d("COMPILE_ELS", false);    // boolean  pre-build the list of event listeners
 _d("NICE_URLS", false);      // boolean  force niceurl mode
 _d("SEARCH_ACCEL", false);   // boolean  use search accelerator
 _d("WH_SPLITS", 1);          // int      how many levels of subfolders to put in the warehouse
-_d("VERSION", '2.5.5+');     // string   shimmie version
+_d("VERSION", '2.6.0+');     // string   shimmie version
 _d("TIMEZONE", null);        // string   timezone
 _d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,comment,tag_list,index,tag_edit,alias_editor"); // extensions to always enable
 _d("EXTRA_EXTS", "");        // string   optional extra extensions
diff --git a/install.php b/install.php
index 1efe82ac..ec2c5f1e 100644
--- a/install.php
+++ b/install.php
@@ -3,7 +3,7 @@
  * Shimmie Installer
  *
  * @package    Shimmie
- * @copyright  Copyright (c) 2007-2015, Shish et al.
+ * @copyright  Copyright (c) 2007-2017, Shish et al.
  * @author     Shish [webmaster at shishnet.org], jgen [jeffgenovy at gmail.com]
  * @link       http://code.shishnet.org/shimmie2/
  * @license    http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2

From 8ff24eb1c6ab7b12ea47e393dd9849702a0b7d2e Mon Sep 17 00:00:00 2001
From: Jeff <jeffgenovy@gmail.com>
Date: Fri, 24 Mar 2017 23:21:48 -0700
Subject: [PATCH 04/41] Revert "Bump the version number"

This reverts commit 277d80c4df41918a4b664ecdd88fa1a0b4f0be35.
---
 core/sys_config.inc.php | 2 +-
 install.php             | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/core/sys_config.inc.php b/core/sys_config.inc.php
index 12d71c5e..abc00766 100644
--- a/core/sys_config.inc.php
+++ b/core/sys_config.inc.php
@@ -36,7 +36,7 @@ _d("COMPILE_ELS", false);    // boolean  pre-build the list of event listeners
 _d("NICE_URLS", false);      // boolean  force niceurl mode
 _d("SEARCH_ACCEL", false);   // boolean  use search accelerator
 _d("WH_SPLITS", 1);          // int      how many levels of subfolders to put in the warehouse
-_d("VERSION", '2.6.0+');     // string   shimmie version
+_d("VERSION", '2.5.5+');     // string   shimmie version
 _d("TIMEZONE", null);        // string   timezone
 _d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,comment,tag_list,index,tag_edit,alias_editor"); // extensions to always enable
 _d("EXTRA_EXTS", "");        // string   optional extra extensions
diff --git a/install.php b/install.php
index ec2c5f1e..1efe82ac 100644
--- a/install.php
+++ b/install.php
@@ -3,7 +3,7 @@
  * Shimmie Installer
  *
  * @package    Shimmie
- * @copyright  Copyright (c) 2007-2017, Shish et al.
+ * @copyright  Copyright (c) 2007-2015, Shish et al.
  * @author     Shish [webmaster at shishnet.org], jgen [jeffgenovy at gmail.com]
  * @link       http://code.shishnet.org/shimmie2/
  * @license    http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2

From 8ef13db69cfa67956b3d66ad2748fc47386a5c5b Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Mon, 27 Mar 2017 18:15:16 +0100
Subject: [PATCH 05/41] bump develop

---
 core/sys_config.inc.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/sys_config.inc.php b/core/sys_config.inc.php
index a47051ef..12d71c5e 100644
--- a/core/sys_config.inc.php
+++ b/core/sys_config.inc.php
@@ -36,7 +36,7 @@ _d("COMPILE_ELS", false);    // boolean  pre-build the list of event listeners
 _d("NICE_URLS", false);      // boolean  force niceurl mode
 _d("SEARCH_ACCEL", false);   // boolean  use search accelerator
 _d("WH_SPLITS", 1);          // int      how many levels of subfolders to put in the warehouse
-_d("VERSION", '2.6.0');      // string   shimmie version
+_d("VERSION", '2.6.0+');     // string   shimmie version
 _d("TIMEZONE", null);        // string   timezone
 _d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,comment,tag_list,index,tag_edit,alias_editor"); // extensions to always enable
 _d("EXTRA_EXTS", "");        // string   optional extra extensions

From f934baa207fba32bcbd571db1e6e5c3013618b7f Mon Sep 17 00:00:00 2001
From: John Brooks <john@fastquake.com>
Date: Sun, 23 Apr 2017 02:17:49 +0000
Subject: [PATCH 06/41] bulk_add_csv: Run Tag::explode() on the tags before
 passing them to handlers

Fixes #575
---
 ext/bulk_add_csv/main.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ext/bulk_add_csv/main.php b/ext/bulk_add_csv/main.php
index 34645e91..2e7cf6d7 100644
--- a/ext/bulk_add_csv/main.php
+++ b/ext/bulk_add_csv/main.php
@@ -126,7 +126,7 @@ class BulkAddCSV extends Extension {
 			$list .= "<br>".html_escape("$shortpath (".str_replace(" ", ", ", $tags).")... ");
 			if (file_exists($csvdata[0]) && is_file($csvdata[0])) {
 				try{
-					$this->add_image($fullpath, $pathinfo["basename"], $tags, $source, $rating, $thumbfile);
+					$this->add_image($fullpath, $pathinfo["basename"], Tag::explode($tags), $source, $rating, $thumbfile);
 					$list .= "ok\n";
 				}
 				catch(Exception $ex) {

From 1625bd68e3773e25e9b34f40cc0d5b3d0b71df1f Mon Sep 17 00:00:00 2001
From: Thomas Hori <thomas.hori@liddicott.com>
Date: Tue, 25 Apr 2017 17:48:34 +0100
Subject: [PATCH 07/41] Fix error upon bulk add.

Fixes "PHP Fatal error:  Uncaught TypeError: Argument 2 passed to
TagSetEvent::__construct() must be of the type array, string given"
upon bulk add.
---
 core/imageboard.pack.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php
index 755de669..5a2dacab 100644
--- a/core/imageboard.pack.php
+++ b/core/imageboard.pack.php
@@ -1219,7 +1219,7 @@ function add_dir($base) {
         $short_path = str_replace($base, "", $full_path);
         $filename = basename($full_path);
 
-        $tags = path_to_tags($short_path);
+        $tags = Tag::explode(path_to_tags($short_path));
         $result = "$short_path (".str_replace(" ", ", ", $tags).")... ";
         try {
             add_image($full_path, $filename, $tags);

From d1306cfb2d0293af639f724e259866e4d613e394 Mon Sep 17 00:00:00 2001
From: Thomas Hori <thomas.hori@liddicott.com>
Date: Wed, 26 Apr 2017 14:08:06 +0100
Subject: [PATCH 08/41] Move Tag::explode call into add_image call so that
 $result is unaffected.

---
 core/imageboard.pack.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php
index 5a2dacab..9e20b33e 100644
--- a/core/imageboard.pack.php
+++ b/core/imageboard.pack.php
@@ -1219,10 +1219,10 @@ function add_dir($base) {
         $short_path = str_replace($base, "", $full_path);
         $filename = basename($full_path);
 
-        $tags = Tag::explode(path_to_tags($short_path));
+        $tags = path_to_tags($short_path);
         $result = "$short_path (".str_replace(" ", ", ", $tags).")... ";
         try {
-            add_image($full_path, $filename, $tags);
+            add_image($full_path, $filename, Tag::explode($tags));
             $result .= "ok";
         }
         catch(UploadException $ex) {

From c0e87ae2aec0c6a37187b73b3e2e2c39f7e042a5 Mon Sep 17 00:00:00 2001
From: jgen <jeffgenovy@gmail.com>
Date: Thu, 11 May 2017 23:43:10 -0700
Subject: [PATCH 09/41] Fix issue with archive handler if no add_status method.

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

diff --git a/ext/handle_archive/main.php b/ext/handle_archive/main.php
index 1fe56ca6..ad43c4ac 100644
--- a/ext/handle_archive/main.php
+++ b/ext/handle_archive/main.php
@@ -35,8 +35,10 @@ class ArchiveFileHandler extends Extension {
 			exec($cmd);
 			$results = add_dir($tmpdir);
 			if(count($results) > 0) {
-        // FIXME no theme?
-				$this->theme->add_status("Adding files", $results);
+				// Not all themes have the add_status() method, so need to check before calling.
+				if (method_exists($this->theme, "add_status")) {
+					$this->theme->add_status("Adding files", $results);
+				}
 			}
 			deltree($tmpdir);
 			$event->image_id = -2; // default -1 = upload wasn't handled

From f492c6c2c37e7efc8a7a7b6d57ce49580d14ac9e Mon Sep 17 00:00:00 2001
From: jgen <jeffgenovy@gmail.com>
Date: Fri, 12 May 2017 00:57:50 -0700
Subject: [PATCH 10/41] Ensure that the Image object tag_array and the metadata
 array tags are always arrays.

---
 core/imageboard.pack.php   | 6 +++---
 ext/bulk_add_csv/main.php  | 4 ++--
 ext/cron_uploader/main.php | 6 +++++-
 ext/handle_flash/main.php  | 2 +-
 ext/handle_ico/main.php    | 2 +-
 ext/handle_mp3/main.php    | 2 +-
 ext/handle_pixel/main.php  | 2 +-
 ext/handle_svg/main.php    | 2 +-
 ext/handle_video/main.php  | 2 +-
 ext/oekaki/main.php        | 2 +-
 ext/ouroboros_api/main.php | 2 +-
 tests/bootstrap.php        | 2 +-
 12 files changed, 19 insertions(+), 15 deletions(-)

diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php
index 9e20b33e..81c84c51 100644
--- a/core/imageboard.pack.php
+++ b/core/imageboard.pack.php
@@ -1210,7 +1210,7 @@ function move_upload_to_archive(DataUploadEvent $event) {
  * Add a directory full of images
  *
  * @param $base string
- * @return array
+ * @return array|string[]
  */
 function add_dir($base) {
     $results = array();
@@ -1222,7 +1222,7 @@ function add_dir($base) {
         $tags = path_to_tags($short_path);
         $result = "$short_path (".str_replace(" ", ", ", $tags).")... ";
         try {
-            add_image($full_path, $filename, Tag::explode($tags));
+            add_image($full_path, $filename, $tags);
             $result .= "ok";
         }
         catch(UploadException $ex) {
@@ -1250,7 +1250,7 @@ function add_image($tmpname, $filename, $tags) {
     $metadata = array();
     $metadata['filename'] = $pathinfo['basename'];
     $metadata['extension'] = $pathinfo['extension'];
-    $metadata['tags'] = $tags;
+    $metadata['tags'] = Tag::explode($tags);
     $metadata['source'] = null;
     $event = new DataUploadEvent($tmpname, $metadata);
     send_event($event);
diff --git a/ext/bulk_add_csv/main.php b/ext/bulk_add_csv/main.php
index 2e7cf6d7..a5b999e7 100644
--- a/ext/bulk_add_csv/main.php
+++ b/ext/bulk_add_csv/main.php
@@ -73,7 +73,7 @@ class BulkAddCSV extends Extension {
 		$metadata = array();
 		$metadata['filename'] = $pathinfo['basename'];
 		$metadata['extension'] = $pathinfo['extension'];
-		$metadata['tags'] = $tags;
+		$metadata['tags'] = Tag::explode($tags);
 		$metadata['source'] = $source;
 		$event = new DataUploadEvent($tmpname, $metadata);
 		send_event($event);
@@ -126,7 +126,7 @@ class BulkAddCSV extends Extension {
 			$list .= "<br>".html_escape("$shortpath (".str_replace(" ", ", ", $tags).")... ");
 			if (file_exists($csvdata[0]) && is_file($csvdata[0])) {
 				try{
-					$this->add_image($fullpath, $pathinfo["basename"], Tag::explode($tags), $source, $rating, $thumbfile);
+					$this->add_image($fullpath, $pathinfo["basename"], $tags, $source, $rating, $thumbfile);
 					$list .= "ok\n";
 				}
 				catch(Exception $ex) {
diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php
index 182a1848..202ee14c 100644
--- a/ext/cron_uploader/main.php
+++ b/ext/cron_uploader/main.php
@@ -304,6 +304,10 @@ class CronUploader extends Extension {
 
 	/**
 	 * Generate the necessary DataUploadEvent for a given image and tags.
+	 *
+	 * @param string $tmpname
+	 * @param string $filename
+	 * @param string $tags
 	 */
 	private function add_image($tmpname, $filename, $tags) {
 		assert ( file_exists ( $tmpname ) );
@@ -315,7 +319,7 @@ class CronUploader extends Extension {
 		$metadata = array();
 		$metadata ['filename'] = $pathinfo ['basename'];
 		$metadata ['extension'] = $pathinfo ['extension'];
-		$metadata ['tags'] = ""; // = $tags; doesn't work when not logged in here
+		$metadata ['tags'] = array(""); // = $tags; doesn't work when not logged in here
 		$metadata ['source'] = null;
 		$event = new DataUploadEvent ( $tmpname, $metadata );
 		send_event ( $event );
diff --git a/ext/handle_flash/main.php b/ext/handle_flash/main.php
index 6122cb46..719648d4 100644
--- a/ext/handle_flash/main.php
+++ b/ext/handle_flash/main.php
@@ -37,7 +37,7 @@ class FlashFileHandler extends DataHandlerExtension {
 		$image->hash	  = $metadata['hash'];
 		$image->filename  = $metadata['filename'];
 		$image->ext       = $metadata['extension'];
-		$image->tag_array = $metadata['tags'];
+		$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
 		$image->source    = $metadata['source'];
 
 		$info = getimagesize($filename);
diff --git a/ext/handle_ico/main.php b/ext/handle_ico/main.php
index beda9596..31d5e337 100644
--- a/ext/handle_ico/main.php
+++ b/ext/handle_ico/main.php
@@ -67,7 +67,7 @@ class IcoFileHandler extends Extension {
 		$image->hash      = $metadata['hash'];
 		$image->filename  = $metadata['filename'];
 		$image->ext       = $metadata['extension'];
-		$image->tag_array = $metadata['tags'];
+		$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
 		$image->source    = $metadata['source'];
 
 		return $image;
diff --git a/ext/handle_mp3/main.php b/ext/handle_mp3/main.php
index 9bbbe55a..4f0eae9e 100644
--- a/ext/handle_mp3/main.php
+++ b/ext/handle_mp3/main.php
@@ -43,7 +43,7 @@ class MP3FileHandler extends DataHandlerExtension {
 		$image->filename = $metadata['filename'];
 
 		$image->ext       = $metadata['extension'];
-		$image->tag_array = $metadata['tags'];
+		$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
 		$image->source    = $metadata['source'];
 
 		return $image;
diff --git a/ext/handle_pixel/main.php b/ext/handle_pixel/main.php
index 38ba6b3d..149677eb 100644
--- a/ext/handle_pixel/main.php
+++ b/ext/handle_pixel/main.php
@@ -35,7 +35,7 @@ class PixelFileHandler extends DataHandlerExtension {
 		$image->hash      = $metadata['hash'];
 		$image->filename  = (($pos = strpos($metadata['filename'],'?')) !== false) ? substr($metadata['filename'],0,$pos) : $metadata['filename'];
 		$image->ext       = (($pos = strpos($metadata['extension'],'?')) !== false) ? substr($metadata['extension'],0,$pos) : $metadata['extension'];
-		$image->tag_array = $metadata['tags'];
+		$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
 		$image->source    = $metadata['source'];
 
 		return $image;
diff --git a/ext/handle_svg/main.php b/ext/handle_svg/main.php
index c932f6ba..2e58dbd3 100644
--- a/ext/handle_svg/main.php
+++ b/ext/handle_svg/main.php
@@ -75,7 +75,7 @@ class SVGFileHandler extends Extension {
 		$image->hash      = $metadata['hash'];
 		$image->filename  = $metadata['filename'];
 		$image->ext       = $metadata['extension'];
-		$image->tag_array = $metadata['tags'];
+		$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
 		$image->source    = $metadata['source'];
 
 		return $image;
diff --git a/ext/handle_video/main.php b/ext/handle_video/main.php
index 4edbcc32..a31ac781 100644
--- a/ext/handle_video/main.php
+++ b/ext/handle_video/main.php
@@ -170,7 +170,7 @@ class VideoFileHandler extends DataHandlerExtension {
 		$image->filesize  = $metadata['size'];
 		$image->hash      = $metadata['hash'];
 		$image->filename  = $metadata['filename'];
-		$image->tag_array = $metadata['tags'];
+		$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
 		$image->source    = $metadata['source'];
 
 		return $image;
diff --git a/ext/oekaki/main.php b/ext/oekaki/main.php
index bf4c804d..c26c1019 100644
--- a/ext/oekaki/main.php
+++ b/ext/oekaki/main.php
@@ -31,7 +31,7 @@ class Oekaki extends Extension {
 						$metadata = array();
 						$metadata['filename'] = 'oekaki.png';
 						$metadata['extension'] = $pathinfo['extension'];
-						$metadata['tags'] = 'oekaki tagme';
+						$metadata['tags'] = Tag::explode('oekaki tagme');
 						$metadata['source'] = null;
 						$duev = new DataUploadEvent($tmpname, $metadata);
 						send_event($duev);
diff --git a/ext/ouroboros_api/main.php b/ext/ouroboros_api/main.php
index 95a69d97..966e8bc9 100644
--- a/ext/ouroboros_api/main.php
+++ b/ext/ouroboros_api/main.php
@@ -500,7 +500,7 @@ class OuroborosAPI extends Extension
             }
         }
         $meta = array();
-        $meta['tags'] = $post->tags;
+        $meta['tags'] = Tag::explode($post->tags);
         $meta['source'] = $post->source;
         if (defined('ENABLED_EXTS')) {
             if (strstr(ENABLED_EXTS, 'rating') !== false) {
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index d17b242a..961c0c0b 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -132,7 +132,7 @@ abstract class ShimmiePHPUnitTestCase extends \PHPUnit_Framework_TestCase {
 	// post things
 	/**
 	 * @param string $filename
-	 * @param string|string[] $tags
+	 * @param string $tags
 	 * @return int
 	 */
 	protected function post_image($filename, $tags) {

From 7ebe301ffd45f0ccf0cbaa6549976934909c7cd5 Mon Sep 17 00:00:00 2001
From: jgen <jeffgenovy@gmail.com>
Date: Sat, 13 May 2017 18:01:31 -0700
Subject: [PATCH 11/41] Check if already an array before exploding.

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

diff --git a/ext/ouroboros_api/main.php b/ext/ouroboros_api/main.php
index 966e8bc9..f58190b2 100644
--- a/ext/ouroboros_api/main.php
+++ b/ext/ouroboros_api/main.php
@@ -500,7 +500,7 @@ class OuroborosAPI extends Extension
             }
         }
         $meta = array();
-        $meta['tags'] = Tag::explode($post->tags);
+        $meta['tags'] = is_array($post->tags) ? $post->tags : Tag::explode($post->tags);
         $meta['source'] = $post->source;
         if (defined('ENABLED_EXTS')) {
             if (strstr(ENABLED_EXTS, 'rating') !== false) {

From 3ffb2da91cd1deef07ec92f62f639ce840df18cc Mon Sep 17 00:00:00 2001
From: jgen <jeffgenovy@gmail.com>
Date: Sat, 13 May 2017 23:18:47 -0700
Subject: [PATCH 12/41] More checking to ensure tags is an array.

---
 ext/ouroboros_api/main.php | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/ext/ouroboros_api/main.php b/ext/ouroboros_api/main.php
index f58190b2..963832a2 100644
--- a/ext/ouroboros_api/main.php
+++ b/ext/ouroboros_api/main.php
@@ -536,7 +536,8 @@ class OuroborosAPI extends Extension
             if (!is_null($img)) {
                 $handler = $config->get_string("upload_collision_handler");
                 if($handler == "merge") {
-                    $merged = array_merge(Tag::explode($post->tags), $img->get_tag_array());
+                    $postTags = is_array($post->tags) ? $post->tags : Tag::explode($post->tags);
+                    $merged = array_merge($postTags, $img->get_tag_array());
                     send_event(new TagSetEvent($img, $merged));
 
                     // This is really the only thing besides tags we should care

From cf5aacaddffd841754ed4c23b572e1b3bb3be913 Mon Sep 17 00:00:00 2001
From: jgen <jeffgenovy@gmail.com>
Date: Sun, 14 May 2017 14:00:20 -0700
Subject: [PATCH 13/41] Use empty array instead of array with empty string.
 (Thanks Shish!)

---
 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 202ee14c..046dd9b9 100644
--- a/ext/cron_uploader/main.php
+++ b/ext/cron_uploader/main.php
@@ -319,7 +319,7 @@ class CronUploader extends Extension {
 		$metadata = array();
 		$metadata ['filename'] = $pathinfo ['basename'];
 		$metadata ['extension'] = $pathinfo ['extension'];
-		$metadata ['tags'] = array(""); // = $tags; doesn't work when not logged in here
+		$metadata ['tags'] = array(); // = $tags; doesn't work when not logged in here
 		$metadata ['source'] = null;
 		$event = new DataUploadEvent ( $tmpname, $metadata );
 		send_event ( $event );

From fc7d96b530f176e7e93c4b3b01e39ab40089a2a1 Mon Sep 17 00:00:00 2001
From: jgen <jeffgenovy@gmail.com>
Date: Sun, 14 May 2017 22:18:44 -0700
Subject: [PATCH 14/41] Another location where $tags should be an array instead
 of a string.

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

diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php
index 046dd9b9..a9b05650 100644
--- a/ext/cron_uploader/main.php
+++ b/ext/cron_uploader/main.php
@@ -311,6 +311,7 @@ class CronUploader extends Extension {
 	 */
 	private function add_image($tmpname, $filename, $tags) {
 		assert ( file_exists ( $tmpname ) );
+		assert('is_string($tags)');
 		
 		$pathinfo = pathinfo ( $filename );
 		if (! array_key_exists ( 'extension', $pathinfo )) {
@@ -333,7 +334,7 @@ class CronUploader extends Extension {
 		
 		// Set tags
 		$img = Image::by_id($event->image_id);
-		$img->set_tags($tags);
+		$img->set_tags(Tag::explode($tags));
 	}
 	
 	private function generate_image_queue($base = "", $subdir = "") {

From 684efedcfdf654cf83853b4db65a316229579293 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Mon, 29 May 2017 10:16:32 +0100
Subject: [PATCH 15/41] Log what URL query caused invalid search queries

---
 core/imageboard.pack.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php
index 5f0253fc..46424643 100644
--- a/core/imageboard.pack.php
+++ b/core/imageboard.pack.php
@@ -953,7 +953,7 @@ class Image {
 			}
 		}
 
-		assert('$positive_tag_id_array || $negative_tag_id_array');
+		assert('$positive_tag_id_array || $negative_tag_id_array', @$_GET['q']);
 		$wheres = array();
 		if (!empty($positive_tag_id_array)) {
 			$positive_tag_id_list = join(', ', $positive_tag_id_array);

From 51e165aecfd09d3a5db5e13c6a59881ac18f4f6f Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Mon, 29 May 2017 10:18:11 +0100
Subject: [PATCH 16/41] Add separate memcached cache

---
 core/database.class.php | 114 ++++++++++++++++++++++++++++++++++++----
 1 file changed, 105 insertions(+), 9 deletions(-)

diff --git a/core/database.class.php b/core/database.class.php
index 27037ab1..f2995251 100644
--- a/core/database.class.php
+++ b/core/database.class.php
@@ -291,13 +291,8 @@ class MemcacheCache implements CacheEngine {
 	 */
 	public function __construct($args) {
 		$hp = explode(":", $args);
-		if(class_exists("Memcache")) {
-			$this->memcache = new Memcache;
-			@$this->memcache->pconnect($hp[0], $hp[1]);
-		}
-		else {
-			print "no memcache"; exit;
-		}
+		$this->memcache = new Memcache;
+		@$this->memcache->pconnect($hp[0], $hp[1]);
 	}
 
 	/**
@@ -355,6 +350,92 @@ class MemcacheCache implements CacheEngine {
 	 */
 	public function get_misses() {return $this->misses;}
 }
+class MemcachedCache implements CacheEngine {
+	/** @var \Memcached|null */
+	public $memcache=null;
+	/** @var int */
+	private $hits=0;
+	/** @var int */
+	private $misses=0;
+
+	/**
+	 * @param string $args
+	 */
+	public function __construct($args) {
+		$hp = explode(":", $args);
+		$this->memcache = new Memcached;
+		#$this->memcache->setOption(Memcached::OPT_COMPRESSION, False);
+		#$this->memcache->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP);
+		#$this->memcache->setOption(Memcached::OPT_PREFIX_KEY, phpversion());
+		$this->memcache->addServer($hp[0], $hp[1]);
+	}
+
+	/**
+	 * @param string $key
+	 * @return array|bool|string
+	 */
+	public function get($key) {
+		assert('!is_null($key)');
+		$key = "d-" . $key;
+
+		$val = $this->memcache->get($key);
+		$res = $this->memcache->getResultCode();
+
+		if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
+			$hit = $res == Memcached::RES_SUCCESS ? "miss" : "hit";
+			file_put_contents("data/cache.log", "Cache $hit: $key\n", FILE_APPEND);
+		}
+		if($res == Memcached::RES_SUCCESS) {
+			$this->hits++;
+			return $val;
+		}
+		else if($res == Memcached::RES_NOTFOUND) {
+			$this->misses++;
+			return false;
+		}
+		else {
+			error_log("Memcached error: $res");
+		}
+	}
+
+	/**
+	 * @param string $key
+	 * @param mixed $val
+	 * @param int $time
+	 */
+	public function set($key, $val, $time=0) {
+		assert('!is_null($key)');
+		$key = "d-" . $key;
+
+		$this->memcache->set($key, $val, $time);
+		if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
+			file_put_contents("data/cache.log", "Cache set: $key ($time)\n", FILE_APPEND);
+		}
+	}
+
+	/**
+	 * @param string $key
+	 */
+	public function delete($key) {
+		assert('!is_null($key)');
+		$key = "d-" . $key;
+
+		$this->memcache->delete($key);
+		if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
+			file_put_contents("data/cache.log", "Cache delete: $key\n", FILE_APPEND);
+		}
+	}
+
+	/**
+	 * @return int
+	 */
+	public function get_hits() {return $this->hits;}
+
+	/**
+	 * @return int
+	 */
+	public function get_misses() {return $this->misses;}
+}
 
 class APCCache implements CacheEngine {
 	public $hits=0, $misses=0;
@@ -439,9 +520,24 @@ class Database {
 
 	private function connect_cache() {
 		$matches = array();
-		if(defined("CACHE_DSN") && CACHE_DSN && preg_match("#(memcache|apc)://(.*)#", CACHE_DSN, $matches)) {
+		if(defined("CACHE_DSN") && CACHE_DSN && preg_match("#(memcache|memcached|apc)://(.*)#", CACHE_DSN, $matches)) {
 			if($matches[1] == "memcache") {
-				$this->cache = new MemcacheCache($matches[2]);
+				if(class_exists("Memcache")) {
+					$this->cache = new MemcacheCache($matches[2]);
+				}
+				else {
+					#error_log("no memcache module - caching disabled");
+					$this->cache = new NoCache();
+				}
+			}
+			else if($matches[1] == "memcached") {
+				if(class_exists("Memcached")) {
+					$this->cache = new MemcachedCache($matches[2]);
+				}
+				else {
+					#error_log("no memcached module - caching disabled");
+					$this->cache = new NoCache();
+				}
 			}
 			else if($matches[1] == "apc") {
 				$this->cache = new APCCache($matches[2]);

From 8828fdfd05aa43ef50b1ab6c64415880923f593c Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Mon, 29 May 2017 10:19:11 +0100
Subject: [PATCH 17/41] log SQL query times in DEBUG_SQL mode

---
 core/database.class.php | 21 ++++++++++++++++-----
 1 file changed, 16 insertions(+), 5 deletions(-)

diff --git a/core/database.class.php b/core/database.class.php
index f2995251..6f3325b6 100644
--- a/core/database.class.php
+++ b/core/database.class.php
@@ -682,6 +682,17 @@ class Database {
 		else $this->query_count++;
 	}
 
+	private function count_time($method, $start) {
+		if ((defined('DEBUG_SQL') && DEBUG_SQL === true) || (!defined('DEBUG_SQL') && @$_GET['DEBUG_SQL'])) {
+			$fp = @fopen("data/sql.log", "a");
+			if($fp) {
+				fwrite($fp, $method.":".(microtime(true) - $start)."\n");
+				fclose($fp);
+			}
+		}
+		$this->dbtime += microtime(true) - $start;
+	}
+
 	/**
 	 * Execute an SQL query and return an PDO result-set.
 	 *
@@ -726,7 +737,7 @@ class Database {
 	public function get_all($query, $args=array()) {
 		$_start = microtime(true);
 		$data = $this->execute($query, $args)->fetchAll();
-		$this->dbtime += microtime(true) - $_start;
+		$this->count_time("get_all", $_start);
 		return $data;
 	}
 
@@ -740,7 +751,7 @@ class Database {
 	public function get_row($query, $args=array()) {
 		$_start = microtime(true);
 		$row = $this->execute($query, $args)->fetch();
-		$this->dbtime += microtime(true) - $_start;
+		$this->count_time("get_row", $_start);
 		return $row ? $row : null;
 	}
 
@@ -758,7 +769,7 @@ class Database {
 		foreach($stmt as $row) {
 			$res[] = $row[0];
 		}
-		$this->dbtime += microtime(true) - $_start;
+		$this->count_time("get_col", $_start);
 		return $res;
 	}
 
@@ -776,7 +787,7 @@ class Database {
 		foreach($stmt as $row) {
 			$res[$row[0]] = $row[1];
 		}
-		$this->dbtime += microtime(true) - $_start;
+		$this->count_time("get_pairs", $_start);
 		return $res;
 	}
 
@@ -790,7 +801,7 @@ class Database {
 	public function get_one($query, $args=array()) {
 		$_start = microtime(true);
 		$row = $this->execute($query, $args)->fetch();
-		$this->dbtime += microtime(true) - $_start;
+		$this->count_time("get_one", $_start);
 		return $row[0];
 	}
 

From 4e5af700939b99e72a287fb65bc0bcee68ed3658 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Mon, 29 May 2017 11:09:28 +0100
Subject: [PATCH 18/41] re-stub mb_strlen (see #615)

---
 core/util.inc.php | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/core/util.inc.php b/core/util.inc.php
index 81582cdf..842918dd 100644
--- a/core/util.inc.php
+++ b/core/util.inc.php
@@ -958,6 +958,13 @@ function data_path($filename) {
 	return $filename;
 }
 
+if (!function_exists('mb_strlen')) {
+	// TODO: we should warn the admin that they are missing multibyte support
+	function mb_strlen($str, $encoding) {
+		return strlen($str);
+	}
+}
+
 /**
  * @param string $url
  * @param string $mfile

From 2f557326df1d904f2df9c2177dc3196f7a854477 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Tue, 30 May 2017 02:13:11 +0100
Subject: [PATCH 19/41] die if caching modules are missing, don't silently fall
 back to NoCache

---
 core/database.class.php | 43 +++++++++++------------------------------
 1 file changed, 11 insertions(+), 32 deletions(-)

diff --git a/core/database.class.php b/core/database.class.php
index 78b33ea8..2ab4230e 100644
--- a/core/database.class.php
+++ b/core/database.class.php
@@ -399,7 +399,6 @@ class MemcachedCache implements CacheEngine {
 	 */
 	public function get($key) {
 		assert('!is_null($key)');
-		$key = "d-" . $key;
 
 		$val = $this->memcache->get($key);
 		$res = $this->memcache->getResultCode();
@@ -428,7 +427,6 @@ class MemcachedCache implements CacheEngine {
 	 */
 	public function set($key, $val, $time=0) {
 		assert('!is_null($key)');
-		$key = "d-" . $key;
 
 		$this->memcache->set($key, $val, $time);
 		if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
@@ -441,7 +439,6 @@ class MemcachedCache implements CacheEngine {
 	 */
 	public function delete($key) {
 		assert('!is_null($key)');
-		$key = "d-" . $key;
 
 		$this->memcache->delete($key);
 		if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
@@ -549,22 +546,10 @@ class Database {
 		$matches = array();
 		if(defined("CACHE_DSN") && CACHE_DSN && preg_match("#(memcache|memcached|apc)://(.*)#", CACHE_DSN, $matches)) {
 			if($matches[1] == "memcache") {
-				if(class_exists("Memcache")) {
-					$this->cache = new MemcacheCache($matches[2]);
-				}
-				else {
-					#error_log("no memcache module - caching disabled");
-					$this->cache = new NoCache();
-				}
+				$this->cache = new MemcacheCache($matches[2]);
 			}
 			else if($matches[1] == "memcached") {
-				if(class_exists("Memcached")) {
-					$this->cache = new MemcachedCache($matches[2]);
-				}
-				else {
-					#error_log("no memcached module - caching disabled");
-					$this->cache = new NoCache();
-				}
+				$this->cache = new MemcachedCache($matches[2]);
 			}
 			else if($matches[1] == "apc") {
 				$this->cache = new APCCache($matches[2]);
@@ -695,17 +680,14 @@ class Database {
 	 */
 	private function count_execs($db, $sql, $inputarray) {
 		if ((defined('DEBUG_SQL') && DEBUG_SQL === true) || (!defined('DEBUG_SQL') && @$_GET['DEBUG_SQL'])) {
-			$fp = @fopen("data/sql.log", "a");
-			if($fp) {
-				$sql = trim(preg_replace('/\s+/msi', ' ', $sql));
-				if(isset($inputarray) && is_array($inputarray) && !empty($inputarray)) {
-					fwrite($fp, $sql." -- ".join(", ", $inputarray)."\n");
-				}
-				else {
-					fwrite($fp, $sql."\n");
-				}
-				fclose($fp);
+			$sql = trim(preg_replace('/\s+/msi', ' ', $sql));
+			if(isset($inputarray) && is_array($inputarray) && !empty($inputarray)) {
+				$text = $sql." -- ".join(", ", $inputarray)."\n";
 			}
+			else {
+				$text = $sql."\n";
+			}
+			file_put_contents("data/sql.log", $text, FILE_APPEND);
 		}
 		if(!is_array($inputarray)) $this->query_count++;
 		# handle 2-dimensional input arrays
@@ -715,11 +697,8 @@ class Database {
 
 	private function count_time($method, $start) {
 		if ((defined('DEBUG_SQL') && DEBUG_SQL === true) || (!defined('DEBUG_SQL') && @$_GET['DEBUG_SQL'])) {
-			$fp = @fopen("data/sql.log", "a");
-			if($fp) {
-				fwrite($fp, $method.":".(microtime(true) - $start)."\n");
-				fclose($fp);
-			}
+			$text = $method.":".(microtime(true) - $start)."\n";
+			file_put_contents("data/sql.log", $text, FILE_APPEND);
 		}
 		$this->dbtime += microtime(true) - $start;
 	}

From 2666d83579104f0b180c191de9e77e575a4a918f Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Thu, 1 Jun 2017 20:44:02 +0100
Subject: [PATCH 20/41] fix URL for video fallback flash player

---
 ext/handle_video/theme.php | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/ext/handle_video/theme.php b/ext/handle_video/theme.php
index 9cc28fa5..83d14104 100644
--- a/ext/handle_video/theme.php
+++ b/ext/handle_video/theme.php
@@ -9,6 +9,7 @@ class VideoFileHandlerTheme extends Themelet {
 		$full_url = make_http($ilink);
 		$autoplay = $config->get_bool("video_playback_autoplay");
 		$loop = $config->get_bool("video_playback_loop");
+		$player = make_link('lib/vendor/swf/flashmediaelement.swf');
 
 		$html = "Video not playing? <a href='" . $image->parse_link_template(make_link('image/$id/$id%20-%20$tags.$ext')) . "'>Click here</a> to download the file.<br/>";
 
@@ -22,8 +23,8 @@ class VideoFileHandlerTheme extends Themelet {
 			}
 
 			$html_fallback = "
-						<object width=\"100%\" height=\"480px\" type=\"application/x-shockwave-flash\" data=\"lib/vendor/swf/flashmediaelement.swf\">
-							<param name=\"movie\" value=\"lib/vendor/swf/flashmediaelement.swf\" />
+						<object width=\"100%\" height=\"480px\" type=\"application/x-shockwave-flash\" data=\"$player\">
+							<param name=\"movie\" value=\"$player\" />
 
 							<param name=\"allowFullScreen\" value=\"true\" />
 							<param name=\"wmode\" value=\"opaque\" />

From 53c6f6df3086ab77b5a9ecf8d861a9fd51790b5f Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Thu, 1 Jun 2017 20:44:17 +0100
Subject: [PATCH 21/41] bulk thumb regen

---
 ext/regen_thumb/main.php  | 22 ++++++++++++++++++++--
 ext/regen_thumb/theme.php | 12 +++++++++++-
 2 files changed, 31 insertions(+), 3 deletions(-)

diff --git a/ext/regen_thumb/main.php b/ext/regen_thumb/main.php
index ec48c649..16a00f7a 100644
--- a/ext/regen_thumb/main.php
+++ b/ext/regen_thumb/main.php
@@ -15,13 +15,24 @@
 
 class RegenThumb extends Extension {
 	public function onPageRequest(PageRequestEvent $event) {
-		global $page, $user;
+		global $database, $page, $user;
 
-		if($event->page_matches("regen_thumb") && $user->can("delete_image") && isset($_POST['image_id'])) {
+		if($event->page_matches("regen_thumb/one") && $user->can("delete_image") && isset($_POST['image_id'])) {
 			$image = Image::by_id(int_escape($_POST['image_id']));
 			send_event(new ThumbnailGenerationEvent($image->hash, $image->ext, true));
 			$this->theme->display_results($page, $image);
 		}
+		if($event->page_matches("regen_thumb/mass") && $user->can("delete_image") && isset($_POST['tags'])) {
+			$tags = Tag::explode(strtolower($_POST['tags']), false);
+			$images = Image::find_images(0, 10000, $tags);
+
+			foreach($images as $image) {
+				send_event(new ThumbnailGenerationEvent($image->hash, $image->ext, true));
+			}
+
+			$page->set_mode("redirect");
+			$page->set_redirect(make_link("post/list"));
+		}
 	}
 
 	public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) {
@@ -30,5 +41,12 @@ class RegenThumb extends Extension {
 			$event->add_part($this->theme->get_buttons_html($event->image->id));
 		}
 	}
+
+	public function onPostListBuilding(PostListBuildingEvent $event) {
+		global $user;
+		if($user->can("delete_image") && !empty($event->search_terms)) {
+			$event->add_control($this->theme->mtr_html(implode(" ", $event->search_terms)));
+		}
+	}
 }
 
diff --git a/ext/regen_thumb/theme.php b/ext/regen_thumb/theme.php
index e7de3afb..aec02c9d 100644
--- a/ext/regen_thumb/theme.php
+++ b/ext/regen_thumb/theme.php
@@ -9,7 +9,7 @@ class RegenThumbTheme extends Themelet {
 	 */
 	public function get_buttons_html($image_id) {
 		return "
-			".make_form(make_link("regen_thumb"))."
+			".make_form(make_link("regen_thumb/one"))."
 			<input type='hidden' name='image_id' value='$image_id'>
 			<input type='submit' value='Regenerate Thumbnail'>
 			</form>
@@ -29,5 +29,15 @@ class RegenThumbTheme extends Themelet {
 		$page->add_block(new NavBlock());
 		$page->add_block(new Block("Thumbnail", $this->build_thumb_html($image)));
 	}
+
+	public function mtr_html($terms) {
+		$h_terms = html_escape($terms);
+		$html = make_form(make_link("regen_thumb/mass"), "POST") . "
+				<input type='hidden' name='tags' value='$h_terms'>
+				<input type='submit' value='Regen all thumbs' onclick='return confirm(\"This can use a lot of CPU time.\\nAre you sure you want to do this?\")'>
+			</form>
+		";
+		return $html;
+	}
 }
 

From 9f4caaddea85c7a05d1e645c26a9989dc5037771 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Thu, 1 Jun 2017 20:44:26 +0100
Subject: [PATCH 22/41] remove some unused variables

---
 ext/view/theme.php | 2 --
 1 file changed, 2 deletions(-)

diff --git a/ext/view/theme.php b/ext/view/theme.php
index 64bd1a57..17b96694 100644
--- a/ext/view/theme.php
+++ b/ext/view/theme.php
@@ -30,11 +30,9 @@ class ViewImageTheme extends Themelet {
 
 	protected function build_pin(Image $image) {
 		if(isset($_GET['search'])) {
-			$search_terms = explode(' ', $_GET['search']);
 			$query = "search=".url_escape($_GET['search']);
 		}
 		else {
-			$search_terms = array();
 			$query = null;
 		}
 

From d105644d1b4ee2154dd54d023342b0ca627d6561 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Thu, 8 Jun 2017 09:36:59 +0100
Subject: [PATCH 23/41] use php sorting for alphabetic tag list, as utf8 sort
 ignores punctuation

---
 ext/tag_list/main.php | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/ext/tag_list/main.php b/ext/tag_list/main.php
index c97ebac4..c7804e69 100644
--- a/ext/tag_list/main.php
+++ b/ext/tag_list/main.php
@@ -269,7 +269,7 @@ class TagList extends Extension {
 		$cache_key = data_path("cache/tag_alpha-" . md5("ta" . $tags_min . $starts_with) . ".html");
 		if(file_exists($cache_key)) {return file_get_contents($cache_key);}
 
-		$tag_data = $database->get_all($database->scoreql_to_sql("
+		$tag_data = $database->get_pairs($database->scoreql_to_sql("
 				SELECT tag, count
 				FROM tags
 				WHERE count >= :tags_min
@@ -296,8 +296,10 @@ class TagList extends Extension {
 		mb_internal_encoding('UTF-8');
 		
 		$lastLetter = "";
-		foreach($tag_data as $row) {
-			$tag = $row['tag'];
+		# postres utf8 string sort ignores punctuation, so we get "aza, a-zb, azc"
+		# which breaks down into "az, a-, az" :(
+		ksort($tag_data, SORT_STRING | SORT_FLAG_CASE);
+		foreach($tag_data as $tag => $count) {
 			if($lastLetter != mb_strtolower(substr($tag, 0, count($starts_with)+1))) {
 				$lastLetter = mb_strtolower(substr($tag, 0, count($starts_with)+1));
 				$h_lastLetter = html_escape($lastLetter); 
@@ -305,7 +307,6 @@ class TagList extends Extension {
 			}
 			$link = $this->tag_link($tag);
 			$h_tag = html_escape($tag);
-			$count = $row['count'];
 			$html .= "<a href='$link'>$h_tag&nbsp;($count)</a>\n";
 		}
 

From 94ec37029a054f371e91a034d029737739d42ea1 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Thu, 8 Jun 2017 09:37:21 +0100
Subject: [PATCH 24/41] stub more mb_ functions

---
 core/util.inc.php | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/core/util.inc.php b/core/util.inc.php
index 842918dd..0ad31d44 100644
--- a/core/util.inc.php
+++ b/core/util.inc.php
@@ -963,6 +963,10 @@ if (!function_exists('mb_strlen')) {
 	function mb_strlen($str, $encoding) {
 		return strlen($str);
 	}
+	function mb_internal_encoding($encoding) {}
+	function mb_strtolower($str) {
+		return strtolower($str);
+	}
 }
 
 /**

From 2f083f76081b935d3561a6ed4f20263b01a4b2c8 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Thu, 8 Jun 2017 09:37:38 +0100
Subject: [PATCH 25/41] more useful memcached error messages

---
 core/database.class.php | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/core/database.class.php b/core/database.class.php
index 2ab4230e..b63d3d1d 100644
--- a/core/database.class.php
+++ b/core/database.class.php
@@ -399,6 +399,7 @@ class MemcachedCache implements CacheEngine {
 	 */
 	public function get($key) {
 		assert('!is_null($key)');
+		$key = urlencode($key);
 
 		$val = $this->memcache->get($key);
 		$res = $this->memcache->getResultCode();
@@ -416,7 +417,7 @@ class MemcachedCache implements CacheEngine {
 			return false;
 		}
 		else {
-			error_log("Memcached error: $res");
+			error_log("Memcached error during get($key): $res");
 		}
 	}
 
@@ -427,11 +428,16 @@ class MemcachedCache implements CacheEngine {
 	 */
 	public function set($key, $val, $time=0) {
 		assert('!is_null($key)');
+		$key = urlencode($key);
 
 		$this->memcache->set($key, $val, $time);
+		$res = $this->memcache->getResultCode();
 		if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
 			file_put_contents("data/cache.log", "Cache set: $key ($time)\n", FILE_APPEND);
 		}
+		if($res != Memcached::RES_SUCCESS) {
+			error_log("Memcached error during set($key): $res");
+		}
 	}
 
 	/**
@@ -439,11 +445,16 @@ class MemcachedCache implements CacheEngine {
 	 */
 	public function delete($key) {
 		assert('!is_null($key)');
+		$key = urlencode($key);
 
 		$this->memcache->delete($key);
+		$res = $this->memcache->getResultCode();
 		if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
 			file_put_contents("data/cache.log", "Cache delete: $key\n", FILE_APPEND);
 		}
+		if($res != Memcached::RES_SUCCESS && $res != Memcached::RES_NOTFOUND) {
+			error_log("Memcached error during delete($key): $res");
+		}
 	}
 
 	/**

From 8fcf721045af4ef1dc7ec607d510404ce9852c6f Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Thu, 20 Jul 2017 23:28:55 +0100
Subject: [PATCH 26/41] use image link for video

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

diff --git a/ext/handle_video/theme.php b/ext/handle_video/theme.php
index 83d14104..21d4ac61 100644
--- a/ext/handle_video/theme.php
+++ b/ext/handle_video/theme.php
@@ -11,7 +11,7 @@ class VideoFileHandlerTheme extends Themelet {
 		$loop = $config->get_bool("video_playback_loop");
 		$player = make_link('lib/vendor/swf/flashmediaelement.swf');
 
-		$html = "Video not playing? <a href='" . $image->parse_link_template(make_link('image/$id/$id%20-%20$tags.$ext')) . "'>Click here</a> to download the file.<br/>";
+		$html = "Video not playing? <a href='$ilink'>Click here</a> to download the file.<br/>";
 
 		//Browser media format support: https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
 		$supportedExts = ['mp4' => 'video/mp4', 'm4v' => 'video/mp4', 'ogv' => 'video/ogg', 'webm' => 'video/webm', 'flv' => 'video/flv'];

From 3c3529a4ccc43808367cf875e070c0f61b8611ef Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Thu, 20 Jul 2017 23:29:17 +0100
Subject: [PATCH 27/41] don't respond to autocomplete requests for blank string

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

diff --git a/ext/tag_list/main.php b/ext/tag_list/main.php
index c7804e69..8649a18a 100644
--- a/ext/tag_list/main.php
+++ b/ext/tag_list/main.php
@@ -46,7 +46,7 @@ class TagList extends Extension {
 			$this->theme->display_page($page);
 		}
 		else if($event->page_matches("api/internal/tag_list/complete")) {
-			if(!isset($_GET["s"])) return;
+			if(!isset($_GET["s"]) || $_GET["s"] == "" || $_GET["s"] == "_") return;
 
 			//$limit = 0;
 			$cache_key = "autocomplete-" . strtolower($_GET["s"]);

From 5a6728209a6f1f281637f4091d6bf845afeb13ab Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Tue, 22 Aug 2017 01:04:33 +0100
Subject: [PATCH 28/41] improve cache logging

---
 core/database.class.php | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/core/database.class.php b/core/database.class.php
index b63d3d1d..ec1a7ce2 100644
--- a/core/database.class.php
+++ b/core/database.class.php
@@ -405,7 +405,7 @@ class MemcachedCache implements CacheEngine {
 		$res = $this->memcache->getResultCode();
 
 		if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
-			$hit = $res == Memcached::RES_SUCCESS ? "miss" : "hit";
+			$hit = $res == Memcached::RES_SUCCESS ? "hit" : "miss";
 			file_put_contents("data/cache.log", "Cache $hit: $key\n", FILE_APPEND);
 		}
 		if($res == Memcached::RES_SUCCESS) {
@@ -690,7 +690,7 @@ class Database {
 	 * @param string $sql
 	 */
 	private function count_execs($db, $sql, $inputarray) {
-		if ((defined('DEBUG_SQL') && DEBUG_SQL === true) || (!defined('DEBUG_SQL') && @$_GET['DEBUG_SQL'])) {
+		if((DEBUG_SQL === true) || (is_null(DEBUG_SQL) && @$_GET['DEBUG_SQL'])) {
 			$sql = trim(preg_replace('/\s+/msi', ' ', $sql));
 			if(isset($inputarray) && is_array($inputarray) && !empty($inputarray)) {
 				$text = $sql." -- ".join(", ", $inputarray)."\n";
@@ -707,7 +707,7 @@ class Database {
 	}
 
 	private function count_time($method, $start) {
-		if ((defined('DEBUG_SQL') && DEBUG_SQL === true) || (!defined('DEBUG_SQL') && @$_GET['DEBUG_SQL'])) {
+		if((DEBUG_SQL === true) || (is_null(DEBUG_SQL) && @$_GET['DEBUG_SQL'])) {
 			$text = $method.":".(microtime(true) - $start)."\n";
 			file_put_contents("data/sql.log", $text, FILE_APPEND);
 		}

From d875ab66a1d46225343ad395f05f3f93991dda72 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Tue, 22 Aug 2017 01:05:18 +0100
Subject: [PATCH 29/41] 60 second post-list cache

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

diff --git a/ext/index/main.php b/ext/index/main.php
index 49e53781..29d225ea 100644
--- a/ext/index/main.php
+++ b/ext/index/main.php
@@ -265,7 +265,7 @@ class Index extends Extension {
 					$images = $database->cache->get("post-list:$page_number");
 					if(!$images) {
 						$images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms);
-						$database->cache->set("post-list:$page_number", $images, 600);
+						$database->cache->set("post-list:$page_number", $images, 60);
 					}
 				}
 				else {

From 35bd51e5138bbd29adc7b365e62c63b043796192 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Wed, 23 Aug 2017 00:42:19 +0100
Subject: [PATCH 30/41] use 'count()' + result->get_one() to count images,
 rather than 'select *' + result->rowcount()...

---
 core/imageboard.pack.php | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php
index 4975c050..0ee89a09 100644
--- a/core/imageboard.pack.php
+++ b/core/imageboard.pack.php
@@ -282,8 +282,7 @@ class Image {
 		}
 		else {
 			$querylet = Image::build_search_querylet($tags);
-			$result = $database->execute($querylet->sql, $querylet->variables);
-			return $result->rowCount();
+			return $database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables);
 		}
 	}
 

From 473c0f0bcb7507561fd300bf71a17f1eed652946 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Thu, 24 Aug 2017 10:17:24 +0100
Subject: [PATCH 31/41] explicitly mark some block types as ignored when
 calculating 404ness

---
 core/block.class.php    | 8 ++++++++
 ext/blocks/main.php     | 4 +++-
 ext/chatbox/main.php    | 1 +
 ext/handle_404/main.php | 5 +----
 4 files changed, 13 insertions(+), 5 deletions(-)

diff --git a/core/block.class.php b/core/block.class.php
index 1fbe0e3f..8fe1bfbc 100644
--- a/core/block.class.php
+++ b/core/block.class.php
@@ -44,6 +44,14 @@ class Block {
 	 */
 	public $id;
 
+	/**
+	 * Should this block count as content for the sake of
+	 * the 404 handler
+	 *
+	 * @var boolean
+	 */
+	public $is_content = true;
+
 	/**
 	 * Construct a block.
 	 *
diff --git a/ext/blocks/main.php b/ext/blocks/main.php
index 43043370..d1f0f6cf 100644
--- a/ext/blocks/main.php
+++ b/ext/blocks/main.php
@@ -42,7 +42,9 @@ class Blocks extends Extension {
 		foreach($blocks as $block) {
 			$path = implode("/", $event->args);
 			if(strlen($path) < 4000 && fnmatch($block['pages'], $path)) {
-				$page->add_block(new Block($block['title'], $block['content'], $block['area'], $block['priority']));
+				$b = new Block($block['title'], $block['content'], $block['area'], $block['priority']);
+				$b->is_content = false;
+				$page->add_block($b);
 			}
 		}
 
diff --git a/ext/chatbox/main.php b/ext/chatbox/main.php
index 1ac474c4..80081d46 100644
--- a/ext/chatbox/main.php
+++ b/ext/chatbox/main.php
@@ -30,6 +30,7 @@ class Chatbox extends Extension {
 		// loads the chatbox at the set location
 		$html = "<div id=\"yshout\"></div>";
 		$chatblock = new Block("Chatbox", $html, "main", 97);
+		$chatblock->is_content = false;
 		$page->add_block($chatblock);
 	}
 }
diff --git a/ext/handle_404/main.php b/ext/handle_404/main.php
index b7996848..a17e80f7 100644
--- a/ext/handle_404/main.php
+++ b/ext/handle_404/main.php
@@ -43,10 +43,7 @@ class Handle404 extends Extension {
 	private function count_main($blocks) {
 		$n = 0;
 		foreach($blocks as $block) {
-			if($block->section == "main") $n++; // more hax.
-		}
-		if(ext_is_live("Chatbox")) {
-			$n--; // even more hax.
+			if($block->section == "main" && $block->is_content) $n++; // more hax.
 		}
 		return $n;
 	}

From abe473ffd68ee0eb6370b613f34635413e689414 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Thu, 24 Aug 2017 10:17:39 +0100
Subject: [PATCH 32/41] count blank header as null

---
 core/block.class.php | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/core/block.class.php b/core/block.class.php
index 8fe1bfbc..0fffb41f 100644
--- a/core/block.class.php
+++ b/core/block.class.php
@@ -66,7 +66,11 @@ class Block {
 		$this->body = $body;
 		$this->section = $section;
 		$this->position = $position;
-		$this->id = preg_replace('/[^\w]/', '',str_replace(' ', '_', is_null($id) ? (is_null($header) ? md5($body) : $header) . $section : $id));
+
+		if(is_null($id)) {
+			$id = (empty($header) ? md5($body) : $header) . $section;
+		}
+		$this->id = preg_replace('/[^\w]/', '',str_replace(' ', '_', $id));
 	}
 
 	/**

From cdbb4e8c7b24f21ce4cd0c7b513f08bde215af1a Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Sun, 17 Sep 2017 15:09:25 +0100
Subject: [PATCH 33/41] add resize/script.js

---
 ext/resize/script.js | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)
 create mode 100644 ext/resize/script.js

diff --git a/ext/resize/script.js b/ext/resize/script.js
new file mode 100644
index 00000000..21edb03d
--- /dev/null
+++ b/ext/resize/script.js
@@ -0,0 +1,19 @@
+$(function() {
+	var original_width = $("#original_width").val();
+	var original_height = $("#original_height").val();
+
+	$("#resize_width").change(function() {
+		if($("#resize_aspect").prop("checked")) {
+			$("#resize_height").val(
+				Math.round($("#resize_width").val() / original_width * original_height)
+			);
+		}
+	});
+	$("#resize_height").change(function() {
+		if($("#resize_aspect").prop("checked")) {
+			$("#resize_width").val(
+				Math.round($("#resize_height").val() / original_height * original_width)
+			);
+		}
+	});
+});

From e1d6ff0f4ee4006a904c0ee2122e45bce88719eb Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Sun, 17 Sep 2017 17:08:08 +0100
Subject: [PATCH 34/41] composer update

---
 composer.lock      | 250 ++++++++++++++++++---------------------------
 tests/test-deep.sh |   2 +-
 2 files changed, 100 insertions(+), 152 deletions(-)

diff --git a/composer.lock b/composer.lock
index 4eed5ce6..bd153b0b 100644
--- a/composer.lock
+++ b/composer.lock
@@ -18,23 +18,11 @@
                 "type": "zip",
                 "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/3a43d7e563314bf32970b773dd31ecf2b90813dd",
                 "reference": "3a43d7e563314bf32970b773dd31ecf2b90813dd",
-                "shasum": ""
-            },
-            "type": "bower-asset-library",
-            "extra": {
-                "bower-asset-main": "dist/jquery.js",
-                "bower-asset-ignore": [
-                    "package.json"
-                ]
+                "shasum": null
             },
+            "type": "bower-asset",
             "license": [
                 "MIT"
-            ],
-            "keywords": [
-                "browser",
-                "javascript",
-                "jquery",
-                "library"
             ]
         },
         {
@@ -49,19 +37,12 @@
                 "type": "zip",
                 "url": "https://api.github.com/repos/rmm5t/jquery-timeago/zipball/67c11951ae9b6020341c1056a42b5406162db40c",
                 "reference": "67c11951ae9b6020341c1056a42b5406162db40c",
-                "shasum": ""
+                "shasum": null
             },
             "require": {
                 "bower-asset/jquery": ">=1.4"
             },
-            "type": "bower-asset-library",
-            "extra": {
-                "bower-asset-main": "jquery.timeago.js",
-                "bower-asset-ignore": [
-                    "test",
-                    "vendor"
-                ]
-            },
+            "type": "bower-asset",
             "license": [
                 "MIT"
             ]
@@ -78,25 +59,9 @@
                 "type": "zip",
                 "url": "https://api.github.com/repos/js-cookie/js-cookie/zipball/5c830fb71a2bd3acce9cb733d692e13316991891",
                 "reference": "5c830fb71a2bd3acce9cb733d692e13316991891",
-                "shasum": ""
-            },
-            "type": "bower-asset-library",
-            "extra": {
-                "bower-asset-main": [
-                    "src/js.cookie.js"
-                ],
-                "bower-asset-ignore": [
-                    "travis.sh",
-                    "test",
-                    "Gruntfile.js",
-                    "package.json",
-                    ".gitignore",
-                    ".jshintignore",
-                    ".jshintrc",
-                    ".tm_properties",
-                    ".travis.yml"
-                ]
+                "shasum": null
             },
+            "type": "bower-asset",
             "license": [
                 "MIT"
             ]
@@ -106,38 +71,18 @@
             "version": "2.21.1",
             "source": {
                 "type": "git",
-                "url": "https://github.com/johndyer/mediaelement.git",
+                "url": "https://github.com/mediaelement/mediaelement.git",
                 "reference": "6e80b260172f4ddc3b0bbee046775d2ba4c6f9b7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/johndyer/mediaelement/zipball/6e80b260172f4ddc3b0bbee046775d2ba4c6f9b7",
+                "url": "https://api.github.com/repos/mediaelement/mediaelement/zipball/6e80b260172f4ddc3b0bbee046775d2ba4c6f9b7",
                 "reference": "6e80b260172f4ddc3b0bbee046775d2ba4c6f9b7",
-                "shasum": ""
-            },
-            "type": "bower-asset-library",
-            "extra": {
-                "bower-asset-main": [
-                    "./build/mediaelement-and-player.js",
-                    "./build/mediaelementplayer.css"
-                ],
-                "bower-asset-ignore": [
-                    "**",
-                    "!/build/**",
-                    "!/bower.json",
-                    "!/README*",
-                    "!/changelog*"
-                ]
+                "shasum": null
             },
+            "type": "bower-asset",
             "license": [
                 "MIT"
-            ],
-            "description": "HTML5 <video> and <audio> made easy.",
-            "keywords": [
-                "audio",
-                "html5",
-                "shim",
-                "video"
             ]
         },
         {
@@ -180,7 +125,7 @@
                 "sort",
                 "table"
             ],
-            "time": "2015-12-03 01:22:52"
+            "time": "2015-12-03T01:22:52+00:00"
         },
         {
             "name": "dapphp/securimage",
@@ -325,7 +270,7 @@
                 "recaptcha",
                 "spam"
             ],
-            "time": "2017-03-09 18:57:45"
+            "time": "2017-03-09T18:57:45+00:00"
         },
         {
             "name": "ifixit/php-akismet",
@@ -345,28 +290,28 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/instantiator.git",
-                "reference": "5acd2bd8c2b600ad5cc4c9180ebf0a930604d6a5"
+                "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/5acd2bd8c2b600ad5cc4c9180ebf0a930604d6a5",
-                "reference": "5acd2bd8c2b600ad5cc4c9180ebf0a930604d6a5",
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda",
+                "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3,<8.0-DEV"
+                "php": "^7.1"
             },
             "require-dev": {
                 "athletic/athletic": "~0.1.8",
                 "ext-pdo": "*",
                 "ext-phar": "*",
-                "phpunit/phpunit": "~4.0",
-                "squizlabs/php_codesniffer": "~2.0"
+                "phpunit/phpunit": "^6.2.3",
+                "squizlabs/php_codesniffer": "^3.0.2"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "1.2.x-dev"
                 }
             },
             "autoload": {
@@ -391,20 +336,20 @@
                 "constructor",
                 "instantiate"
             ],
-            "time": "2017-02-16 16:15:51"
+            "time": "2017-07-22T11:58:36+00:00"
         },
         {
             "name": "myclabs/deep-copy",
-            "version": "1.6.0",
+            "version": "1.x-dev",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe"
+                "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/5a5a9fc8025a08d8919be87d6884d5a92520cefe",
-                "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102",
+                "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102",
                 "shasum": ""
             },
             "require": {
@@ -433,7 +378,7 @@
                 "object",
                 "object graph"
             ],
-            "time": "2017-01-26T22:05:40+00:00"
+            "time": "2017-04-12T18:52:22+00:00"
         },
         {
             "name": "phpdocumentor/reflection-common",
@@ -441,12 +386,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
-                "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c"
+                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
-                "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
                 "shasum": ""
             },
             "require": {
@@ -487,26 +432,26 @@
                 "reflection",
                 "static analysis"
             ],
-            "time": "2015-12-27 11:43:31"
+            "time": "2017-09-11T18:02:19+00:00"
         },
         {
             "name": "phpdocumentor/reflection-docblock",
-            "version": "3.1.1",
+            "version": "4.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
-                "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e"
+                "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e",
-                "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2d3d238c433cf69caeb4842e97a3223a116f94b2",
+                "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5",
+                "php": "^7.0",
                 "phpdocumentor/reflection-common": "^1.0@dev",
-                "phpdocumentor/type-resolver": "^0.2.0",
+                "phpdocumentor/type-resolver": "^0.4.0",
                 "webmozart/assert": "^1.0"
             },
             "require-dev": {
@@ -532,24 +477,24 @@
                 }
             ],
             "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
-            "time": "2016-09-30T07:12:33+00:00"
+            "time": "2017-08-30T18:51:59+00:00"
         },
         {
             "name": "phpdocumentor/type-resolver",
-            "version": "0.2.1",
+            "version": "0.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/TypeResolver.git",
-                "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb"
+                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb",
-                "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb",
+                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
+                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5",
+                "php": "^5.5 || ^7.0",
                 "phpdocumentor/reflection-common": "^1.0"
             },
             "require-dev": {
@@ -579,7 +524,7 @@
                     "email": "me@mikevanriel.com"
                 }
             ],
-            "time": "2016-11-25T06:54:22+00:00"
+            "time": "2017-07-14T14:27:02+00:00"
         },
         {
             "name": "phpspec/prophecy",
@@ -587,18 +532,18 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "abe41cb27f4e4207c6f54a09272969fe55e0bbff"
+                "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/abe41cb27f4e4207c6f54a09272969fe55e0bbff",
-                "reference": "abe41cb27f4e4207c6f54a09272969fe55e0bbff",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
+                "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
                 "shasum": ""
             },
             "require": {
                 "doctrine/instantiator": "^1.0.2",
                 "php": "^5.3|^7.0",
-                "phpdocumentor/reflection-docblock": "^2.0|^3.0.2",
+                "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
                 "sebastian/comparator": "^1.1|^2.0",
                 "sebastian/recursion-context": "^1.0|^2.0|^3.0"
             },
@@ -642,7 +587,7 @@
                 "spy",
                 "stub"
             ],
-            "time": "2017-03-03 17:09:02"
+            "time": "2017-09-04T11:05:03+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
@@ -650,12 +595,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "cb9dd0ea37df29fa3c6ae5ac26c3408a7609cb4f"
+                "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/cb9dd0ea37df29fa3c6ae5ac26c3408a7609cb4f",
-                "reference": "cb9dd0ea37df29fa3c6ae5ac26c3408a7609cb4f",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d",
+                "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d",
                 "shasum": ""
             },
             "require": {
@@ -705,7 +650,7 @@
                 "testing",
                 "xunit"
             ],
-            "time": "2017-03-07 10:28:00"
+            "time": "2017-04-02T07:44:40+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
@@ -752,7 +697,7 @@
                 "filesystem",
                 "iterator"
             ],
-            "time": "2016-10-03 07:40:28"
+            "time": "2016-10-03T07:40:28+00:00"
         },
         {
             "name": "phpunit/php-text-template",
@@ -842,7 +787,7 @@
             "keywords": [
                 "timer"
             ],
-            "time": "2017-03-07 15:42:04"
+            "time": "2017-03-07T15:42:04+00:00"
         },
         {
             "name": "phpunit/php-token-stream",
@@ -850,20 +795,20 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-token-stream.git",
-                "reference": "9ddb181faa4a3841fe131c357fd01de75cbb4da9"
+                "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9ddb181faa4a3841fe131c357fd01de75cbb4da9",
-                "reference": "9ddb181faa4a3841fe131c357fd01de75cbb4da9",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9a02332089ac48e704c70f6cefed30c224e3c0b0",
+                "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0",
                 "shasum": ""
             },
             "require": {
                 "ext-tokenizer": "*",
-                "php": "^5.6 || ^7.0"
+                "php": "^7.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^5.7 || ^6.0"
+                "phpunit/phpunit": "^6.2.4"
             },
             "type": "library",
             "extra": {
@@ -891,7 +836,7 @@
             "keywords": [
                 "tokenizer"
             ],
-            "time": "2017-03-07 07:36:57"
+            "time": "2017-08-20T05:47:52+00:00"
         },
         {
             "name": "phpunit/phpunit",
@@ -899,12 +844,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "d421807ce5f372ecaf0bff3a5d41f5c60a18329f"
+                "reference": "4eba3374803c6c0903145e8940844e6f1d665c07"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d421807ce5f372ecaf0bff3a5d41f5c60a18329f",
-                "reference": "d421807ce5f372ecaf0bff3a5d41f5c60a18329f",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4eba3374803c6c0903145e8940844e6f1d665c07",
+                "reference": "4eba3374803c6c0903145e8940844e6f1d665c07",
                 "shasum": ""
             },
             "require": {
@@ -922,7 +867,7 @@
                 "phpunit/php-timer": "^1.0.6",
                 "phpunit/phpunit-mock-objects": "^3.2",
                 "sebastian/comparator": "^1.2.4",
-                "sebastian/diff": "~1.2",
+                "sebastian/diff": "^1.4.3",
                 "sebastian/environment": "^1.3.4 || ^2.0",
                 "sebastian/exporter": "~2.0",
                 "sebastian/global-state": "^1.1",
@@ -973,7 +918,7 @@
                 "testing",
                 "xunit"
             ],
-            "time": "2017-03-09 13:47:42"
+            "time": "2017-09-01T08:38:37+00:00"
         },
         {
             "name": "phpunit/phpunit-mock-objects",
@@ -981,12 +926,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
-                "reference": "4001a301f86fc006c32f532a741ab613f2bd8990"
+                "reference": "a23b761686d50a560cc56233b9ecf49597cc9118"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/4001a301f86fc006c32f532a741ab613f2bd8990",
-                "reference": "4001a301f86fc006c32f532a741ab613f2bd8990",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118",
+                "reference": "a23b761686d50a560cc56233b9ecf49597cc9118",
                 "shasum": ""
             },
             "require": {
@@ -1032,7 +977,7 @@
                 "mock",
                 "xunit"
             ],
-            "time": "2017-03-07 08:47:31"
+            "time": "2017-06-30T09:13:00+00:00"
         },
         {
             "name": "sebastian/code-unit-reverse-lookup",
@@ -1077,7 +1022,7 @@
             ],
             "description": "Looks up which function or method a line of code belongs to",
             "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
-            "time": "2017-03-04 10:23:55"
+            "time": "2017-03-04T10:23:55+00:00"
         },
         {
             "name": "sebastian/comparator",
@@ -1141,20 +1086,20 @@
                 "compare",
                 "equality"
             ],
-            "time": "2017-03-07 10:34:43"
+            "time": "2017-03-07T10:34:43+00:00"
         },
         {
             "name": "sebastian/diff",
-            "version": "dev-master",
+            "version": "1.4.x-dev",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/diff.git",
-                "reference": "763d7adeb8c35d2af2b04c0f6cafeee059074dfb"
+                "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/763d7adeb8c35d2af2b04c0f6cafeee059074dfb",
-                "reference": "763d7adeb8c35d2af2b04c0f6cafeee059074dfb",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4",
+                "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4",
                 "shasum": ""
             },
             "require": {
@@ -1193,27 +1138,27 @@
             "keywords": [
                 "diff"
             ],
-            "time": "2017-03-07 07:26:53"
+            "time": "2017-05-22T07:24:03+00:00"
         },
         {
             "name": "sebastian/environment",
-            "version": "dev-master",
+            "version": "2.0.x-dev",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "144fedf9aa8e3f1c52199f3634eb699cb59741d9"
+                "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/144fedf9aa8e3f1c52199f3634eb699cb59741d9",
-                "reference": "144fedf9aa8e3f1c52199f3634eb699cb59741d9",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac",
+                "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac",
                 "shasum": ""
             },
             "require": {
                 "php": "^5.6 || ^7.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^5.7 || ^6.0"
+                "phpunit/phpunit": "^5.0"
             },
             "type": "library",
             "extra": {
@@ -1243,7 +1188,7 @@
                 "environment",
                 "hhvm"
             ],
-            "time": "2017-03-07 12:59:58"
+            "time": "2016-11-26T07:53:53+00:00"
         },
         {
             "name": "sebastian/exporter",
@@ -1310,7 +1255,7 @@
                 "export",
                 "exporter"
             ],
-            "time": "2017-03-07 10:36:49"
+            "time": "2017-03-07T10:36:49+00:00"
         },
         {
             "name": "sebastian/global-state",
@@ -1361,7 +1306,7 @@
             "keywords": [
                 "global state"
             ],
-            "time": "2017-02-23 14:11:06"
+            "time": "2017-02-23T14:11:06+00:00"
         },
         {
             "name": "sebastian/object-enumerator",
@@ -1407,7 +1352,7 @@
             ],
             "description": "Traverses array structures and object graphs to enumerate all referenced objects",
             "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
-            "time": "2017-03-07 10:37:45"
+            "time": "2017-03-07T10:37:45+00:00"
         },
         {
             "name": "sebastian/recursion-context",
@@ -1460,7 +1405,7 @@
             ],
             "description": "Provides functionality to recursively process PHP variables",
             "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
-            "time": "2017-03-08 08:21:15"
+            "time": "2017-03-08T08:21:15+00:00"
         },
         {
             "name": "sebastian/resource-operations",
@@ -1502,7 +1447,7 @@
             ],
             "description": "Provides a list of PHP built-in functions that operate on resources",
             "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
-            "time": "2016-10-03 07:43:09"
+            "time": "2016-10-03T07:43:09+00:00"
         },
         {
             "name": "sebastian/version",
@@ -1545,27 +1490,30 @@
             ],
             "description": "Library that helps with managing the version number of Git-hosted PHP projects",
             "homepage": "https://github.com/sebastianbergmann/version",
-            "time": "2016-10-03 07:35:21"
+            "time": "2016-10-03T07:35:21+00:00"
         },
         {
             "name": "symfony/yaml",
-            "version": "dev-master",
+            "version": "3.4.x-dev",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "d1abb473764f82339862b114b45b3cd424b2d1a3"
+                "reference": "a0e15688972f012156cf1ffa076fe1203bce6bc9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/d1abb473764f82339862b114b45b3cd424b2d1a3",
-                "reference": "d1abb473764f82339862b114b45b3cd424b2d1a3",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/a0e15688972f012156cf1ffa076fe1203bce6bc9",
+                "reference": "a0e15688972f012156cf1ffa076fe1203bce6bc9",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^5.5.9|>=7.0.8"
+            },
+            "conflict": {
+                "symfony/console": "<3.4"
             },
             "require-dev": {
-                "symfony/console": "~2.8|~3.0"
+                "symfony/console": "~3.4|~4.0"
             },
             "suggest": {
                 "symfony/console": "For validating YAML files using the lint command"
@@ -1573,7 +1521,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "3.4-dev"
                 }
             },
             "autoload": {
@@ -1600,7 +1548,7 @@
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2017-03-07 16:54:31"
+            "time": "2017-09-17T10:10:45+00:00"
         },
         {
             "name": "webmozart/assert",
@@ -1650,7 +1598,7 @@
                 "check",
                 "validate"
             ],
-            "time": "2016-11-23 20:04:41"
+            "time": "2016-11-23T20:04:41+00:00"
         }
     ],
     "aliases": [],
diff --git a/tests/test-deep.sh b/tests/test-deep.sh
index 8e63ef98..bc630357 100755
--- a/tests/test-deep.sh
+++ b/tests/test-deep.sh
@@ -4,6 +4,6 @@ php \
 	-d extension=xdebug.so \
 	-d xdebug.profiler_output_dir=./data/prof/ \
 	-d xdebug.profiler_enable=1 \
-	./phpunit.phar \
+	./vendor/bin/phpunit \
 		--config tests/phpunit.xml \
 		--coverage-clover data/coverage.clover

From a32bc6448c4916591d5791d185483487627df386 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Sun, 17 Sep 2017 18:59:48 +0100
Subject: [PATCH 35/41] make user list slightly more useful

---
 core/user.class.php | 13 -------------
 core/util.inc.php   | 12 ++++++++++--
 ext/user/main.php   | 33 ++++++++++++++++++++++++---------
 ext/user/theme.php  | 44 +++++++++++++++++++++++++++++++++++++++++---
 4 files changed, 75 insertions(+), 27 deletions(-)

diff --git a/core/user.class.php b/core/user.class.php
index e2b30a1d..662300cd 100644
--- a/core/user.class.php
+++ b/core/user.class.php
@@ -140,19 +140,6 @@ class User {
 		}
 	}
 
-	/**
-	 * @param int $offset
-	 * @param int $limit
-	 * @return array
-	 */
-	public static function by_list(/*int*/ $offset, /*int*/ $limit=50) {
-		assert('is_numeric($offset)', var_export($offset, true));
-		assert('is_numeric($limit)', var_export($limit, true));
-		global $database;
-		$rows = $database->get_all("SELECT * FROM users WHERE id >= :start AND id < :end", array("start"=>$offset, "end"=>$offset+$limit));
-		return array_map("_new_user", $rows);
-	}
-
 
 	/* useful user object functions start here */
 
diff --git a/core/util.inc.php b/core/util.inc.php
index 81582cdf..6b0e081c 100644
--- a/core/util.inc.php
+++ b/core/util.inc.php
@@ -555,7 +555,15 @@ function make_http(/*string*/ $link) {
  */
 function make_form($target, $method="POST", $multipart=False, $form_id="", $onsubmit="") {
 	global $user;
-	$auth = $user->get_auth_html();
+	if($method == "GET") {
+		$link = html_escape($target);
+		$target = make_link($target);
+		$extra_inputs = "<input type='hidden' name='q' value='$link'>";
+	}
+	else {
+		$extra_inputs = $user->get_auth_html();
+	}
+
 	$extra = empty($form_id) ? '' : 'id="'. $form_id .'"';
 	if($multipart) {
 		$extra .= " enctype='multipart/form-data'";
@@ -563,7 +571,7 @@ function make_form($target, $method="POST", $multipart=False, $form_id="", $onsu
 	if($onsubmit) {
 		$extra .= ' onsubmit="'.$onsubmit.'"';
 	}
-	return '<form action="'.$target.'" method="'.$method.'" '.$extra.'>'.$auth;
+	return '<form action="'.$target.'" method="'.$method.'" '.$extra.'>'.$extra_inputs;
 }
 
 /**
diff --git a/ext/user/main.php b/ext/user/main.php
index c2bc2f8e..263af280 100644
--- a/ext/user/main.php
+++ b/ext/user/main.php
@@ -95,7 +95,7 @@ class UserPage extends Extension {
 	}
 
 	public function onPageRequest(PageRequestEvent $event) {
-		global $config, $page, $user;
+		global $config, $database, $page, $user;
 
 		$this->show_user_info();
 
@@ -115,15 +115,30 @@ class UserPage extends Extension {
 				$this->page_create();
 			}
 			else if($event->get_arg(0) == "list") {
-// select users.id,name,joindate,admin,
-// (select count(*) from images where images.owner_id=users.id) as images,
-// (select count(*) from comments where comments.owner_id=users.id) as comments from users;
+				$offset = 0;
+				$limit = 50;
 
-// select users.id,name,joindate,admin,image_count,comment_count
-// from users
-// join (select owner_id,count(*) as image_count from images group by owner_id) as _images on _images.owner_id=users.id
-// join (select owner_id,count(*) as comment_count from comments group by owner_id) as _comments on _comments.owner_id=users.id;
-				$this->theme->display_user_list($page, User::by_list(0), $user);
+				$q = "SELECT * FROM users WHERE id >= :start AND id < :end";
+				$a = array("start"=>$offset, "end"=>$offset+$limit);
+
+				if(@$_GET['username']) {
+					$q .= " AND name LIKE :name";
+					$a["name"] = '%' . $_GET['username'] . '%';
+				}
+
+				if(@$_GET['email']) {
+					$q .= " AND name LIKE :email";
+					$a["email"] = '%' . $_GET['email'] . '%';
+				}
+
+				if(@$_GET['class']) {
+					$q .= " AND class LIKE :class";
+					$a["class"] = $_GET['class'];
+				}
+
+				$rows = $database->get_all($q, $a);
+				$users = array_map("_new_user", $rows);
+				$this->theme->display_user_list($page, $users, $user);
 			}
 			else if($event->get_arg(0) == "logout") {
 				$this->page_logout();
diff --git a/ext/user/theme.php b/ext/user/theme.php
index ef3b7809..6f16a86c 100644
--- a/ext/user/theme.php
+++ b/ext/user/theme.php
@@ -9,18 +9,56 @@ class UserPageTheme extends Themelet {
 			"There should be a login box to the left"));
 	}
 
+	/**
+	 * @param Page $page
+	 * @param User[] $users
+	 * @param User $user
+	 */
 	public function display_user_list(Page $page, $users, User $user) {
 		$page->set_title("User List");
 		$page->set_heading("User List");
 		$page->add_block(new NavBlock());
-		$html = "<table>";
-		$html .= "<tr><td>Name</td></tr>";
+
+		$html = "<table class='zebra'>";
+
+		$html .= "<tr>";
+		$html .= "<td>Name</td>";
+		if($user->can('delete_user'))
+			$html .= "<td>Email</td>";
+		$html .= "<td>Class</td>";
+		$html .= "<td>Action</td>";
+		$html .= "</tr>";
+
+		$h_username = html_escape(@$_GET['username']);
+		$h_email = html_escape(@$_GET['email']);
+		$h_class = html_escape(@$_GET['class']);
+
+		$html .= "<tr>" . make_form("user_admin/list", "GET");
+		$html .= "<td><input type='text' name='username' value='$h_username'/></td>";
+		if($user->can('delete_user'))
+			$html .= "<td><input type='email' name='email' value='$h_email'/></td>";
+		$html .= "<td><input type='text' name='class' value='$h_class'/></td>";
+		$html .= "<td><input type='submit' value='Search'/></td>";
+		$html .= "</form></tr>";
+
 		foreach($users as $duser) {
+			$h_name = html_escape($duser->name);
+			$h_email = html_escape($duser->email);
+			$h_class = html_escape($duser->class->name);
+			$u_link = make_link("user/" . url_escape($duser->name));
+			$u_posts = make_link("post/list/user_id=" . url_escape($duser->id) . "/1");
+
 			$html .= "<tr>";
-			$html .= "<td><a href='".make_link("user/".url_escape($duser->name))."'>".html_escape($duser->name)."</a></td>";
+			$html .= "<td><a href='$u_link'>$h_name</a></td>";
+			if($user->can('delete_user'))
+				$html .= "<td>$h_email</td>";
+			$html .= "<td>$h_class</td>";
+			$html .= "<td><a href='$u_posts'>Show Posts</a></td>";
 			$html .= "</tr>";
 		}
+
 		$html .= "</table>";
+
 		$page->add_block(new Block("Users", $html));
 	}
 

From 2c0e49507e23e8050cdc5c833b1303d745190bdb Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Sun, 17 Sep 2017 19:06:10 +0100
Subject: [PATCH 36/41] limit / offset

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

diff --git a/ext/user/main.php b/ext/user/main.php
index 263af280..c3c8f776 100644
--- a/ext/user/main.php
+++ b/ext/user/main.php
@@ -118,8 +118,8 @@ class UserPage extends Extension {
 				$offset = 0;
 				$limit = 50;
 
-				$q = "SELECT * FROM users WHERE id >= :start AND id < :end";
-				$a = array("start"=>$offset, "end"=>$offset+$limit);
+				$q = "SELECT * FROM users WHERE 1=1";
+				$a = array("offset"=>$offset, "limit"=>$limit);
 
 				if(@$_GET['username']) {
 					$q .= " AND name LIKE :name";
@@ -136,6 +136,8 @@ class UserPage extends Extension {
 					$a["class"] = $_GET['class'];
 				}
 
+				$q .=  " LIMIT :limit OFFSET :offset";
+
 				$rows = $database->get_all($q, $a);
 				$users = array_map("_new_user", $rows);
 				$this->theme->display_user_list($page, $users, $user);

From 5763b77e2b5f69b7f6a9482612c00c04b075d23d Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Sun, 17 Sep 2017 19:10:10 +0100
Subject: [PATCH 37/41] strnorm

---
 ext/user/main.php | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/ext/user/main.php b/ext/user/main.php
index c3c8f776..44fdda31 100644
--- a/ext/user/main.php
+++ b/ext/user/main.php
@@ -122,12 +122,12 @@ class UserPage extends Extension {
 				$a = array("offset"=>$offset, "limit"=>$limit);
 
 				if(@$_GET['username']) {
-					$q .= " AND name LIKE :name";
+					$q .= " AND SCORE_STRNORM(name) LIKE SCORE_STRNORM(:name)";
 					$a["name"] = '%' . $_GET['username'] . '%';
 				}
 
 				if(@$_GET['email']) {
-					$q .= " AND name LIKE :email";
+					$q .= " AND SCORE_STRNORM(name) LIKE SCORE_STRNORM(:email)";
 					$a["email"] = '%' . $_GET['email'] . '%';
 				}
 
@@ -138,7 +138,7 @@ class UserPage extends Extension {
 
 				$q .=  " LIMIT :limit OFFSET :offset";
 
-				$rows = $database->get_all($q, $a);
+				$rows = $database->get_all($database->scoreql_to_sql($q), $a);
 				$users = array_map("_new_user", $rows);
 				$this->theme->display_user_list($page, $users, $user);
 			}

From 186ea553483f95d82cee1bec040cb41ac5b798e5 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Sun, 17 Sep 2017 19:11:51 +0100
Subject: [PATCH 38/41] safety

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

diff --git a/ext/user/main.php b/ext/user/main.php
index 44fdda31..12d279d9 100644
--- a/ext/user/main.php
+++ b/ext/user/main.php
@@ -126,7 +126,7 @@ class UserPage extends Extension {
 					$a["name"] = '%' . $_GET['username'] . '%';
 				}
 
-				if(@$_GET['email']) {
+				if($user->can('delete_user') && @$_GET['email']) {
 					$q .= " AND SCORE_STRNORM(name) LIKE SCORE_STRNORM(:email)";
 					$a["email"] = '%' . $_GET['email'] . '%';
 				}

From 1566ff7eaaa356e4cc5264bb2544ca53d0341763 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Sun, 17 Sep 2017 19:37:30 +0100
Subject: [PATCH 39/41] un-confuse phpunit

---
 core/_bootstrap.inc.php | 12 ++++++------
 tests/test-deep.sh      |  4 ++--
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/core/_bootstrap.inc.php b/core/_bootstrap.inc.php
index e8d6559a..47be22aa 100644
--- a/core/_bootstrap.inc.php
+++ b/core/_bootstrap.inc.php
@@ -18,17 +18,17 @@ _sanitise_environment();
 
 // load base files
 ctx_log_start("Opening files");
-$files = array_merge(
+$_shm_files = array_merge(
 	zglob("core/*.php"),
 	zglob("ext/{".ENABLED_EXTS."}/main.php")
 );
-foreach($files as $filename) {
-	if(basename($filename)[0] != "_") {
-		require_once $filename;
+foreach($_shm_files as $_shm_filename) {
+	if(basename($_shm_filename)[0] != "_") {
+		require_once $_shm_filename;
 	}
 }
-unset($files);
-unset($filename);
+unset($_shm_files);
+unset($_shm_filename);
 ctx_log_endok();
 
 // connect to the database
diff --git a/tests/test-deep.sh b/tests/test-deep.sh
index bc630357..d08c2012 100755
--- a/tests/test-deep.sh
+++ b/tests/test-deep.sh
@@ -1,9 +1,9 @@
 #!/bin/sh
 php \
-	-d extension.dir=/usr/lib/php/extensions/no-debug-non-zts-20121212/ \
-	-d extension=xdebug.so \
 	-d xdebug.profiler_output_dir=./data/prof/ \
 	-d xdebug.profiler_enable=1 \
 	./vendor/bin/phpunit \
 		--config tests/phpunit.xml \
 		--coverage-clover data/coverage.clover
+#	-d extension.dir=/usr/local/Cellar/php71-xdebug/2.5.5/ \
+#	-d extension=xdebug.so \

From 236b444ccdfa1a6a5554c272fa1908a46f2cabb5 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Sun, 17 Sep 2017 19:38:44 +0100
Subject: [PATCH 40/41] fix test

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

diff --git a/ext/regen_thumb/test.php b/ext/regen_thumb/test.php
index 7deb66df..417b35c7 100644
--- a/ext/regen_thumb/test.php
+++ b/ext/regen_thumb/test.php
@@ -6,7 +6,7 @@ class RegenThumbTest extends ShimmiePHPUnitTestCase {
 		$this->get_page("post/view/$image_id");
 
 		$_POST['image_id'] = $image_id;
-		$this->get_page("regen_thumb");
+		$this->get_page("regen_thumb/one");
 		$this->assert_title("Thumbnail Regenerated");
 
 		# FIXME: test that the thumb's modified time has been updated

From 4796ee9f005deb380419e038f025176b7b817964 Mon Sep 17 00:00:00 2001
From: Shish <shish@shishnet.org>
Date: Mon, 18 Sep 2017 23:51:34 +0100
Subject: [PATCH 41/41] rebuild composer.lock with php5.6

---
 composer.json |  2 +-
 composer.lock | 90 ++++++++++++++++++++-------------------------------
 2 files changed, 36 insertions(+), 56 deletions(-)

diff --git a/composer.json b/composer.json
index 7a4b1c62..73b7cd7a 100644
--- a/composer.json
+++ b/composer.json
@@ -32,7 +32,7 @@
 
 		"bower-asset/jquery"         : "1.12.3",
 		"bower-asset/jquery-timeago" : "1.5.2",
-		"bower-asset/tablesorter"    : "2.*",
+		"bower-asset/tablesorter"    : "dev-master",
 		"bower-asset/mediaelement"   : "2.21.1",
 		"bower-asset/js-cookie"      : "2.1.1"
 	},
diff --git a/composer.lock b/composer.lock
index bd153b0b..0be19b19 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "a450c93bec1d8f352be123e31bf84ac6",
+    "content-hash": "040335a85a560b3bdd3dcf55490c98a1",
     "packages": [
         {
             "name": "bower-asset/jquery",
@@ -97,34 +97,12 @@
                 "type": "zip",
                 "url": "https://api.github.com/repos/christianbach/tablesorter/zipball/774576308e8a25aa9d68b7fe3069b79543992d7a",
                 "reference": "774576308e8a25aa9d68b7fe3069b79543992d7a",
-                "shasum": ""
-            },
-            "type": "bower-asset-library",
-            "extra": {
-                "bower-asset-main": [
-                    "jquery.metadata.js",
-                    "jquery.tablesorter.min.js"
-                ],
-                "bower-asset-ignore": [
-                    "**/.*",
-                    "node_modules",
-                    "bower_components",
-                    "test",
-                    "tests"
-                ],
-                "branch-alias": {
-                    "dev-master": "2.0.5-dev"
-                }
+                "shasum": null
             },
+            "type": "bower-asset",
             "license": [
                 "MIT,GPL"
             ],
-            "description": "Flexible client-side table sorting",
-            "keywords": [
-                "client-side",
-                "sort",
-                "table"
-            ],
             "time": "2015-12-03T01:22:52+00:00"
         },
         {
@@ -286,32 +264,32 @@
     "packages-dev": [
         {
             "name": "doctrine/instantiator",
-            "version": "dev-master",
+            "version": "1.0.x-dev",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/instantiator.git",
-                "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda"
+                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda",
-                "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda",
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
+                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1"
+                "php": ">=5.3,<8.0-DEV"
             },
             "require-dev": {
                 "athletic/athletic": "~0.1.8",
                 "ext-pdo": "*",
                 "ext-phar": "*",
-                "phpunit/phpunit": "^6.2.3",
-                "squizlabs/php_codesniffer": "^3.0.2"
+                "phpunit/phpunit": "~4.0",
+                "squizlabs/php_codesniffer": "~2.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.2.x-dev"
+                    "dev-master": "1.0.x-dev"
                 }
             },
             "autoload": {
@@ -336,7 +314,7 @@
                 "constructor",
                 "instantiate"
             ],
-            "time": "2017-07-22T11:58:36+00:00"
+            "time": "2015-06-14T21:17:01+00:00"
         },
         {
             "name": "myclabs/deep-copy",
@@ -436,22 +414,22 @@
         },
         {
             "name": "phpdocumentor/reflection-docblock",
-            "version": "4.1.1",
+            "version": "3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
-                "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2"
+                "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2d3d238c433cf69caeb4842e97a3223a116f94b2",
-                "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157",
+                "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0",
+                "php": ">=5.5",
                 "phpdocumentor/reflection-common": "^1.0@dev",
-                "phpdocumentor/type-resolver": "^0.4.0",
+                "phpdocumentor/type-resolver": "^0.3.0",
                 "webmozart/assert": "^1.0"
             },
             "require-dev": {
@@ -477,20 +455,20 @@
                 }
             ],
             "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
-            "time": "2017-08-30T18:51:59+00:00"
+            "time": "2017-08-08T06:39:58+00:00"
         },
         {
             "name": "phpdocumentor/type-resolver",
-            "version": "0.4.0",
+            "version": "0.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/TypeResolver.git",
-                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
+                "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
-                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
+                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fb3933512008d8162b3cdf9e18dba9309b7c3773",
+                "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773",
                 "shasum": ""
             },
             "require": {
@@ -524,7 +502,7 @@
                     "email": "me@mikevanriel.com"
                 }
             ],
-            "time": "2017-07-14T14:27:02+00:00"
+            "time": "2017-06-03T08:32:36+00:00"
         },
         {
             "name": "phpspec/prophecy",
@@ -791,29 +769,29 @@
         },
         {
             "name": "phpunit/php-token-stream",
-            "version": "dev-master",
+            "version": "1.4.x-dev",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-token-stream.git",
-                "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0"
+                "reference": "958103f327daef5dd0bb328dec53e0a9e43cfaf7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9a02332089ac48e704c70f6cefed30c224e3c0b0",
-                "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/958103f327daef5dd0bb328dec53e0a9e43cfaf7",
+                "reference": "958103f327daef5dd0bb328dec53e0a9e43cfaf7",
                 "shasum": ""
             },
             "require": {
                 "ext-tokenizer": "*",
-                "php": "^7.0"
+                "php": ">=5.3.3"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.2.4"
+                "phpunit/phpunit": "~4.2"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0-dev"
+                    "dev-master": "1.4-dev"
                 }
             },
             "autoload": {
@@ -836,7 +814,7 @@
             "keywords": [
                 "tokenizer"
             ],
-            "time": "2017-08-20T05:47:52+00:00"
+            "time": "2017-03-07T08:21:50+00:00"
         },
         {
             "name": "phpunit/phpunit",
@@ -1603,7 +1581,9 @@
     ],
     "aliases": [],
     "minimum-stability": "dev",
-    "stability-flags": [],
+    "stability-flags": {
+        "bower-asset/tablesorter": 20
+    },
     "prefer-stable": false,
     "prefer-lowest": false,
     "platform": {