From 6a4031dfd59bbd04ca01dd4fc2c57138afa6ca41 Mon Sep 17 00:00:00 2001
From: Diftraku <diftraku@derpy.me>
Date: Tue, 3 Dec 2013 00:45:07 +0200
Subject: [PATCH 01/30] Bumping Ouroros API to v0.2: now with XML support and
 post creation!

---
 ext/ouroboros_api/main.php | 546 +++++++++++++++++++++++++++++++------
 1 file changed, 470 insertions(+), 76 deletions(-)

diff --git a/ext/ouroboros_api/main.php b/ext/ouroboros_api/main.php
index a2413e42..4ff9553c 100644
--- a/ext/ouroboros_api/main.php
+++ b/ext/ouroboros_api/main.php
@@ -3,6 +3,7 @@
  * Name: Ouroboros API
  * Author: Diftraku <diftraku[at]derpy.me>
  * Description: Ouroboros-like API for Shimmie
+ * Version: 0.2
  * Documentation:
  *   Currently working features
  *   <ul>
@@ -210,7 +211,10 @@ class _SafeOuroborosImage
         $this->parent_id = null;
         if (defined('ENABLED_EXTS')) {
             if (strstr(ENABLED_EXTS, 'rating') !== false) {
-                //$this->rating = $img->rating;
+                // 'u' is not a "valid" rating
+                if($img->rating == 's' || $img->rating == 'q' || $img->rating == 'e') {
+                    $this->rating = $img->rating;
+                }
             }
             if (strstr(ENABLED_EXTS, 'numeric_score') !== false) {
                 $this->score = $img->numeric_score;
@@ -234,6 +238,70 @@ class _SafeOuroborosImage
         $this->sample_url = make_http($img->get_image_link());
     }
 }
+class OuroborosPost extends _SafeOuroborosImage {
+    /**
+     * Multipart File
+     * @var array
+     */
+    public $file = array();
+
+    /**
+     * Create with rating locked
+     * @var bool
+     */
+    public $is_rating_locked = false;
+
+    /**
+     * Create with notes locked
+     * @var bool
+     */
+    public $is_note_locked = false;
+
+
+    /**
+     * Initialize an OuroborosPost for creation
+     * Mainly just acts as a wrapper and validation layer
+     * @TODO implement more validation from OuroborosAPI
+     * @param array $post
+     */
+    public function __construct(array $post) {
+        if (array_key_exists('tags', $post)) {
+            $this->tags = $post['tags'];
+        }
+        if (array_key_exists('file', $post)) {
+            assert(is_array($post['file']));
+            assert(array_key_exists('tmp_name', $post['file']));
+            assert(array_key_exists('name', $post['file']));
+            $this->file = $post['file'];
+        }
+        if (array_key_exists('rating', $post)) {
+            assert(
+                $post['rating'] == 's' ||
+                $post['rating'] == 'q' ||
+                $post['rating'] == 'e'
+            );
+            $this->rating = $post['rating'];
+        }
+        if (array_key_exists('source', $post)) {
+            $this->file_url = $post['source'];
+        }
+        if (array_key_exists('sourceurl', $post)) {
+            $this->source = $post['sourceurl'];
+        }
+        if (array_key_exists('description', $post)) {
+            $this->description = $post['description'];
+        }
+        if (array_key_exists('is_rating_locked', $post)) {
+            $this->is_rating_locked = $post['is_rating_locked'];
+        }
+        if (array_key_exists('is_note_locked', $post)) {
+            $this->is_note_locked = $post['is_note_locked'];
+        }
+        if (array_key_exists('parent_id', $post)) {
+            $this->parent_id = $post['parent_id'];
+        }
+    }
+}
 class _SafeOuroborosTag
 {
     public $ambiguous = false;
@@ -252,16 +320,39 @@ class _SafeOuroborosTag
 class OuroborosAPI extends Extension
 {
     private $event;
-    const ERROR_HTTP_200 = 'Request was successful';
-    const ERROR_HTTP_403 = 'Access denied';
-    const ERROR_HTTP_404 = 'Not found';
-    const ERROR_HTTP_420 = 'Record could not be saved';
-    const ERROR_HTTP_421 = 'User is throttled, try again later';
-    const ERROR_HTTP_422 = 'The resource is locked and cannot be modified';
-    const ERROR_HTTP_423 = 'Resource already exists';
-    const ERROR_HTTP_424 = 'The given parameters were invalid';
-    const ERROR_HTTP_500 = 'Some unknown error occurred on the server';
-    const ERROR_HTTP_503 = 'Server cannot currently handle the request, try again later';
+    private $type;
+    const HEADER_HTTP_200 = 'OK';
+    const MSG_HTTP_200 = 'Request was successful';
+
+    const HEADER_HTTP_403 = 'Forbidden';
+    const MSG_HTTP_403 = 'Access denied';
+
+    const HEADER_HTTP_404 = 'Not found';
+    const MSG_HTTP_404 = 'Not found';
+
+    const HEADER_HTTP_418 = 'I\'m a teapot';
+    const MSG_HTTP_418 = 'Short and stout';
+
+    const HEADER_HTTP_420 = 'Invalid Record';
+    const MSG_HTTP_420 = 'Record could not be saved';
+
+    const HEADER_HTTP_421 = 'User Throttled';
+    const MSG_HTTP_421 = 'User is throttled, try again later';
+
+    const HEADER_HTTP_422 = 'Locked';
+    const MSG_HTTP_422 = 'The resource is locked and cannot be modified';
+
+    const HEADER_HTTP_423 = 'Already Exists';
+    const MSG_HTTP_423 = 'Resource already exists';
+
+    const HEADER_HTTP_424 = 'Invalid Parameters';
+    const MSG_HTTP_424 = 'The given parameters were invalid';
+
+    const HEADER_HTTP_500 = 'Internal Server Error';
+    const MSG_HTTP_500 = 'Some unknown error occurred on the server';
+
+    const HEADER_HTTP_503 = 'Service Unavailable';
+    const MSG_HTTP_503 = 'Server cannot currently handle the request, try again later';
 
     const ERROR_POST_CREATE_MD5 = 'MD5 mismatch';
     const ERROR_POST_CREATE_DUPE = 'Duplicate';
@@ -272,46 +363,42 @@ class OuroborosAPI extends Extension
 
         if (preg_match("%\.(xml|json)$%", implode('/', $event->args), $matches) === 1) {
             $this->event = $event;
-            $type = $matches[1];
-            if ($type == 'json') {
+            $this->type = $matches[1];
+            if ($this->type == 'json') {
                 $page->set_type('application/json; charset=utf-8');
             }
-            elseif ($type == 'xml') {
-                $page->set_type('text/xml');
+            elseif ($this->type == 'xml') {
+                $page->set_type('text/xml; charset=utf-8');
             }
             $page->set_mode('data');
+            $this->tryAuth();
 
             if ($event->page_matches('post')) {
                 if ($this->match('create')) {
                     // Create
+                    // @TODO Should move the validation logic into OuroborosPost instead?
                     $post = array(
                         'tags' => !empty($_REQUEST['post']['tags']) ? filter_var($_REQUEST['post']['tags'], FILTER_SANITIZE_STRING) : 'tagme',
                         'file' => !empty($_REQUEST['post']['file']) ? filter_var($_REQUEST['post']['file'], FILTER_UNSAFE_RAW) : null,
-                        'rating' => !empty($_REQUEST['post']['rating']) ? filter_var($_REQUEST['post']['rating'], FILTER_SANITIZE_NUMBER_INT) : null,
-                        'source' => !empty($_REQUEST['post']['source']) ? filter_var($_REQUEST['post']['source'], FILTER_SANITIZE_URL) : null,
-                        'sourceurl' => !empty($_REQUEST['post']['sourceurl']) ? filter_var($_REQUEST['post']['sourceurl'], FILTER_SANITIZE_URL) : '',
+                        'rating' => !empty($_REQUEST['post']['rating']) ? filter_var($_REQUEST['post']['rating'], FILTER_SANITIZE_NUMBER_INT) : 'q',
+                        'source' => !empty($_REQUEST['post']['source']) ? filter_var(urldecode($_REQUEST['post']['source']), FILTER_SANITIZE_URL) : null,
+                        'sourceurl' => !empty($_REQUEST['post']['sourceurl']) ? filter_var(urldecode($_REQUEST['post']['sourceurl']), FILTER_SANITIZE_URL) : '',
                         'description' => !empty($_REQUEST['post']['description']) ? filter_var($_REQUEST['post']['description'], FILTER_SANITIZE_STRING) : '',
                         'is_rating_locked' => !empty($_REQUEST['post']['is_rating_locked']) ? filter_var($_REQUEST['post']['is_rating_locked'], FILTER_SANITIZE_NUMBER_INT) : false,
                         'is_note_locked' => !empty($_REQUEST['post']['is_note_locked']) ? filter_var($_REQUEST['post']['is_note_locked'], FILTER_SANITIZE_NUMBER_INT) : false,
                         'parent_id' => !empty($_REQUEST['post']['parent_id']) ? filter_var($_REQUEST['post']['parent_id'], FILTER_SANITIZE_NUMBER_INT) : null,
                     );
                     $md5 = !empty($_REQUEST['md5']) ? filter_var($_REQUEST['md5'], FILTER_SANITIZE_STRING) : null;
-
+                    $this->postCreate(new OuroborosPost($post), $md5);
                 }
                 elseif ($this->match('update')) {
                     // Update
+                    //@todo add post update
                 }
                 elseif ($this->match('show')) {
                     // Show
-                    if (isset($_REQUEST['id'])) {
-                        $id = $_REQUEST['id'];
-                        $posts = array();
-                        $posts[] = new _SafeOuroborosImage(Image::by_id($id));
-                        $page->set_data(json_encode($posts));
-                    }
-                    else {
-                        $page->set_data(json_encode(array()));
-                    }
+                    $id = !empty($_REQUEST['id']) ? filter_var($_REQUEST['id'], FILTER_SANITIZE_NUMBER_INT) : null;
+                    $this->postShow($id);
                 }
                 elseif ($this->match('index') || $this->match('list')) {
                     // List
@@ -321,17 +408,7 @@ class OuroborosAPI extends Extension
                     if (!empty($tags)) {
                         $tags = Tag::explode($tags);
                     }
-                    $start = ( $p - 1 ) * $limit;
-                    //var_dump($limit, $p, $tags, $start);die();
-                    $results = Image::find_images(max($start, 0), min($limit, 100), $tags);
-                    $posts = array();
-                    foreach ($results as $img) {
-                        if (!is_object($img)) {
-                            continue;
-                        }
-                        $posts[] = new _SafeOuroborosImage($img);
-                    }
-                    $page->set_data(json_encode($posts));
+                    $this->postIndex($limit, $p, $tags);
                 }
             }
             elseif ($event->page_matches('tag')) {
@@ -343,48 +420,365 @@ class OuroborosAPI extends Extension
                     $after_id = !empty($_REQUEST['after_id']) ? intval(filter_var($_REQUEST['after_id'], FILTER_SANITIZE_NUMBER_INT)) : null;
                     $name = !empty($_REQUEST['name']) ? filter_var($_REQUEST['name'], FILTER_SANITIZE_STRING) : '';
                     $name_pattern = !empty($_REQUEST['name_pattern']) ? filter_var($_REQUEST['name_pattern'], FILTER_SANITIZE_STRING) : '';
-                    $start = ( $p - 1 ) * $limit;
-                    $tag_data = array();
-                    switch ($order) {
-                        case 'name':
-                            $tag_data = $database->get_col($database->scoreql_to_sql("
-                                SELECT DISTINCT
-                                    id, SCORE_STRNORM(substr(tag, 1, 1)), count
-                                FROM tags
-                                WHERE count >= :tags_min
-                                ORDER BY SCORE_STRNORM(substr(tag, 1, 1)) LIMIT :start, :max_items
-                            "), array("tags_min" => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit));
-                            break;
-                        case 'count':
-                            $tag_data = $database->get_all("
-                                SELECT id, tag, count
-                                FROM tags
-                                WHERE count >= :tags_min
-                                ORDER BY count DESC, tag ASC LIMIT :start, :max_items
-                                ", array("tags_min" => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit));
-                            break;
-                        case 'date':
-                            $tag_data = $database->get_all("
-                                SELECT id, tag, count
-                                FROM tags
-                                WHERE count >= :tags_min
-                                ORDER BY count DESC, tag ASC LIMIT :start, :max_items
-                                ", array("tags_min" => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit));
-                            break;
-                    }
-                    $tags = array();
-                    foreach ($tag_data as $tag) {
-                        if (!is_array($tag)) {
-                            continue;
-                        }
-                        $tags[] = new _SafeOuroborosTag($tag);
-                    }
-                    $page->set_data(json_encode($tags));
+                    $this->tagIndex($limit, $p, $order, $id, $after_id, $name, $name_pattern);
                 }
             }
         }
     }
 
+    /**
+     * Post
+     */
+
+    /**
+     * Wrapper for post creation
+     * @param OuroborosPost $post
+     * @param string $md5
+     */
+    protected function postCreate(OuroborosPost $post, $md5 = '') {
+        global $page, $config, $user;
+        if (!empty($md5)) {
+            $img = Image::by_hash($md5);
+            if (!is_null($img)) {
+                $this->sendResponse(420, self::ERROR_POST_CREATE_DUPE);
+            }
+        }
+        $meta = array();
+        $meta['tags'] = $post->tags;
+        $meta['source'] = $post->source;
+        if (defined('ENABLED_EXTS')) {
+            if (strstr(ENABLED_EXTS, 'rating') !== false) {
+                $meta['rating'] = $post->rating;
+            }
+        }
+        // Check where we should try for the file
+        if (empty($post->file) && !empty($post->file_url) && filter_var($post->file_url, FILTER_VALIDATE_URL) !== false) {
+            // Transload from source
+            $meta['file'] = tempnam('/tmp', 'shimmie_transload_'.$config->get_string('transload_engine'));
+            $meta['filename'] = basename($post->file_url);
+            if ($config->get_string('transload_engine') == 'fopen') {
+                $fp = fopen($post->file_url, 'r');
+                if (!$fp) {
+                    $this->sendResponse(500, 'fopen failed');
+                }
+
+                $data = "";
+                $length = 0;
+                while (!feof($fp) && $length <= $config->get_int('upload_size')) {
+                    $data .= fread($fp, 8192);
+                    $length = strlen($data);
+                }
+                fclose($fp);
+
+                $fp = fopen($meta['file'], 'w');
+                fwrite($fp, $data);
+                fclose($fp);
+            }
+            elseif ($config->get_string('transload_engine') == 'curl') {
+                $ch = curl_init($post->file_url);
+                $fp = fopen($meta['file'], 'w');
+
+                curl_setopt($ch, CURLOPT_FILE, $fp);
+                curl_setopt($ch, CURLOPT_HEADER, 0);
+
+                curl_exec($ch);
+                curl_close($ch);
+                fclose($fp);
+            }
+            $meta['hash'] = md5_file($meta['file']);
+        }
+        else {
+            // Use file
+            $meta['file'] = $post->file['tmp_name'];
+            $meta['filename'] = $post->file['name'];
+            $meta['hash'] = md5_file($meta['file']);
+        }
+        if (!empty($md5) && $md5 !== $meta['hash']) {
+            $this->sendResponse(420, self::ERROR_POST_CREATE_MD5);
+        }
+        if (!empty($meta['hash'])) {
+            $img = Image::by_hash($meta['hash']);
+            if (!is_null($img)) {
+                $this->sendResponse(420, self::ERROR_POST_CREATE_DUPE);
+            }
+        }
+        $meta['extension'] = pathinfo($meta['filename'], PATHINFO_EXTENSION);
+        try {
+            $upload = new DataUploadEvent($meta['file'], $meta);
+            send_event($upload);
+            $image = Image::by_hash($meta['hash']);
+            if (!is_null($image)) {
+                $this->sendResponse(200, make_link('post/view/'.$image->id), true);
+            }
+            else {
+                // Fail, unsupported file?
+                $this->sendResponse(500, 'Unknown error');
+            }
+        } catch (UploadException $e) {
+            // Cleanup in case shit hit the fan
+            $this->sendResponse(500, $e->getMessage());
+        }
+    }
+
+    /**
+     * Wrapper for getting a single post
+     * @param int $id
+     */
+    protected function postShow($id = null) {
+        if (!is_null($id)) {
+            $post = new _SafeOuroborosImage(Image::by_id($id));
+            $this->sendData('post', $post);
+        }
+        else {
+            $this->sendResponse(424, 'ID is mandatory');
+        }
+    }
+
+    /**
+     * Wrapper for getting a list of posts
+     * @param $limit
+     * @param $page
+     * @param $tags
+     */
+    protected function postIndex($limit, $page, $tags) {
+        $start = ( $page - 1 ) * $limit;
+        $results = Image::find_images(max($start, 0), min($limit, 100), $tags);
+        $posts = array();
+        foreach ($results as $img) {
+            if (!is_object($img)) {
+                continue;
+            }
+            $posts[] = new _SafeOuroborosImage($img);
+        }
+        $this->sendData('post', $posts, max($start, 0));
+    }
+
+    /**
+     * Tag
+     */
+
+    /**
+     * Wrapper for getting a list of tags
+     * @param $limit
+     * @param $page
+     * @param $order
+     * @param $id
+     * @param $after_id
+     * @param $name
+     * @param $name_pattern
+     */
+    protected function tagIndex($limit, $page, $order, $id, $after_id, $name, $name_pattern) {
+        global $database, $config;
+        $start = ( $page - 1 ) * $limit;
+        $tag_data = array();
+        switch ($order) {
+            case 'name':
+                $tag_data = $database->get_col($database->scoreql_to_sql("
+                                SELECT DISTINCT
+                                    id, SCORE_STRNORM(substr(tag, 1, 1)), count
+                                FROM tags
+                                WHERE count >= :tags_min
+                                ORDER BY SCORE_STRNORM(substr(tag, 1, 1)) LIMIT :start, :max_items
+                            "), array('tags_min' => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit));
+                break;
+            case 'count':
+                $tag_data = $database->get_all("
+                                SELECT id, tag, count
+                                FROM tags
+                                WHERE count >= :tags_min
+                                ORDER BY count DESC, tag ASC LIMIT :start, :max_items
+                                ", array('tags_min' => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit));
+                break;
+            case 'date':
+                $tag_data = $database->get_all("
+                                SELECT id, tag, count
+                                FROM tags
+                                WHERE count >= :tags_min
+                                ORDER BY count DESC, tag ASC LIMIT :start, :max_items
+                                ", array('tags_min' => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit));
+                break;
+        }
+        $tags = array();
+        foreach ($tag_data as $tag) {
+            if (!is_array($tag)) {
+                continue;
+            }
+            $tags[] = new _SafeOuroborosTag($tag);
+        }
+        $this->sendData('tag', $tags, $start);
+    }
+
+    /**
+     * Utility methods
+     */
+
+    /**
+     * Sends a simple {success,reason} message to browser
+     *
+     * @param int $code HTTP equivalent code for the message
+     * @param string $reason Reason for the code
+     * @param bool $location Is $reason a location? (used mainly for post/create)
+     */
+    private function sendResponse($code = 200, $reason = '', $location = false) {
+        global $page;
+        if ($code == 200) {
+            $success = true;
+        }
+        else {
+            $success = false;
+        }
+        if (empty($reason)) {
+            if (defined("self::MSG_HTTP_{$code}")) {
+                $reason = constant("self::MSG_HTTP_{$code}");
+            }
+            else {
+                $reason = self::MSG_HTTP_418;
+            }
+        }
+        if ($code != 200) {
+            $proto = $_SERVER['SERVER_PROTOCOL'];
+            if (defined("self::HEADER_HTTP_{$code}")) {
+                $header = constant("self::HEADER_HTTP_{$code}");
+            }
+            else {
+                // I'm a teapot!
+                $code = 418;
+                $header = self::HEADER_HTTP_418;
+            }
+            header("{$proto} {$code} {$header}", true);
+        }
+        $response = array('success' => $success, 'reason' => $reason);
+        if ($this->type == 'json') {
+            if ($location !== false) {
+                $response['location'] = $response['reason'];
+                unset($response['reason']);
+            }
+            $response = json_encode($response);
+        }
+        elseif ($this->type == 'xml') {
+            // Seriously, XML sucks...
+            $xml = new XMLWriter();
+            $xml->openMemory();
+            $xml->startDocument('1.0', 'utf-8');
+            $xml->startElement('response');
+            $xml->writeAttribute('success', var_export($success, true));
+            if ($location !== false) {
+                $xml->writeAttribute('location', $reason);
+            }
+            else {
+                $xml->writeAttribute('reason', $reason);
+            }
+            $xml->endElement();
+            $xml->endDocument();
+            $response = $xml->outputMemory(true);
+            unset($xml);
+        }
+        $page->set_data($response);
+        $page->display();
+    }
+
+    /**
+     * Send data to the browser
+     * @param string $type
+     * @param mixed $data
+     * @param int $offset
+     */
+    private function sendData($type = '', $data = array(), $offset = 0) {
+        global $page;
+        $response = '';
+        if ($this->type == 'json') {
+            $response = json_encode($data);
+        }
+        elseif ($this->type == 'xml') {
+            $xml = new XMLWriter();
+            $xml->openMemory();
+            $xml->startDocument('1.0', 'utf-8');
+            if (array_key_exists(0, $data)) {
+                $xml->startElement($type.'s');
+                if ($type == 'post') {
+                    $xml->writeAttribute('count', count($data));
+                    $xml->writeAttribute('offset', $offset);
+                }
+                if ($type == 'tag') {
+                    $xml->writeAttribute('type', 'array');
+                }
+                foreach ($data as $item) {
+                    $this->createItemXML($xml, $type, $item);
+                }
+                $xml->endElement();
+            }
+            else {
+                $this->createItemXML($xml, $type, $data);
+            }
+            $xml->endDocument();
+            $response = $xml->outputMemory(true);
+            unset($xml);
+        }
+        $page->set_data($response);
+        $page->display();
+        exit;
+    }
+
+    private function createItemXML(XMLWriter &$xml, $type, $item) {
+        $xml->startElement($type);
+        foreach ($item as $key => $val) {
+            if ($key == 'created_at' && $type == 'post') {
+                $xml->writeAttribute($key, $val['s']);
+            }
+            else {
+                if (is_bool($val)) {
+                    $val = $val ? 'true' : 'false';
+                }
+                $xml->writeAttribute($key, $val);
+            }
+        }
+        $xml->endElement();
+    }
+
+    /**
+     * Try to figure who is uploading
+     *
+     * Currently checks for either user & session in request or cookies
+     * and initializes a global User
+     * @param void
+     * @return void
+     */
+    private function tryAuth() {
+        global $config, $user;
+
+        if (isset($_REQUEST['user']) && isset($_REQUEST['session'])) {
+            //Auth by session data from query
+            $name = $_REQUEST['user'];
+            $session = $_REQUEST['session'];
+            $duser = User::by_session($name, $session);
+            if (!is_null($duser)) {
+                $user = $duser;
+            }
+            else {
+                $user = User::by_id($config->get_int("anon_id", 0));
+            }
+        }
+        elseif (isset($_COOKIE[$config->get_string('cookie_prefix', 'shm').'_'.'session']) &&
+            isset($_COOKIE[$config->get_string('cookie_prefix', 'shm').'_'.'user'])
+        ) {
+            //Auth by session data from cookies
+            $session = $_COOKIE[$config->get_string('cookie_prefix', 'shm').'_'.'session'];
+            $user = $_COOKIE[$config->get_string('cookie_prefix', 'shm').'_'.'user'];
+            $duser = User::by_session($user, $session);
+            if (!is_null($duser)) {
+                $user = $duser;
+            }
+            else {
+                $user = User::by_id($config->get_int("anon_id", 0));
+            }
+        }
+    }
+
+    /**
+     * Helper for matching API methods from event
+     * @param $page
+     * @return bool
+     */
     private function match($page) {
         return (preg_match("%{$page}\.(xml|json)$%", implode('/', $this->event->args), $matches) === 1);
     }

From 095f743d57f004612f044ef60d6a005967dc7ca8 Mon Sep 17 00:00:00 2001
From: Diftraku <diftraku@derpy.me>
Date: Tue, 3 Dec 2013 01:07:27 +0200
Subject: [PATCH 02/30] Checking if the user can actually create new posts,
 seems the base DataHandlerExtension doesn't do this. Also forgot to update
 documentation!

---
 ext/ouroboros_api/main.php | 32 +++++++++++++++++++-------------
 1 file changed, 19 insertions(+), 13 deletions(-)

diff --git a/ext/ouroboros_api/main.php b/ext/ouroboros_api/main.php
index 4ff9553c..1304b9a7 100644
--- a/ext/ouroboros_api/main.php
+++ b/ext/ouroboros_api/main.php
@@ -377,19 +377,25 @@ class OuroborosAPI extends Extension
                 if ($this->match('create')) {
                     // Create
                     // @TODO Should move the validation logic into OuroborosPost instead?
-                    $post = array(
-                        'tags' => !empty($_REQUEST['post']['tags']) ? filter_var($_REQUEST['post']['tags'], FILTER_SANITIZE_STRING) : 'tagme',
-                        'file' => !empty($_REQUEST['post']['file']) ? filter_var($_REQUEST['post']['file'], FILTER_UNSAFE_RAW) : null,
-                        'rating' => !empty($_REQUEST['post']['rating']) ? filter_var($_REQUEST['post']['rating'], FILTER_SANITIZE_NUMBER_INT) : 'q',
-                        'source' => !empty($_REQUEST['post']['source']) ? filter_var(urldecode($_REQUEST['post']['source']), FILTER_SANITIZE_URL) : null,
-                        'sourceurl' => !empty($_REQUEST['post']['sourceurl']) ? filter_var(urldecode($_REQUEST['post']['sourceurl']), FILTER_SANITIZE_URL) : '',
-                        'description' => !empty($_REQUEST['post']['description']) ? filter_var($_REQUEST['post']['description'], FILTER_SANITIZE_STRING) : '',
-                        'is_rating_locked' => !empty($_REQUEST['post']['is_rating_locked']) ? filter_var($_REQUEST['post']['is_rating_locked'], FILTER_SANITIZE_NUMBER_INT) : false,
-                        'is_note_locked' => !empty($_REQUEST['post']['is_note_locked']) ? filter_var($_REQUEST['post']['is_note_locked'], FILTER_SANITIZE_NUMBER_INT) : false,
-                        'parent_id' => !empty($_REQUEST['post']['parent_id']) ? filter_var($_REQUEST['post']['parent_id'], FILTER_SANITIZE_NUMBER_INT) : null,
-                    );
-                    $md5 = !empty($_REQUEST['md5']) ? filter_var($_REQUEST['md5'], FILTER_SANITIZE_STRING) : null;
-                    $this->postCreate(new OuroborosPost($post), $md5);
+                    if($user->can("create_image")) {
+                        $post = array(
+                            'tags' => !empty($_REQUEST['post']['tags']) ? filter_var($_REQUEST['post']['tags'], FILTER_SANITIZE_STRING) : 'tagme',
+                            'file' => !empty($_REQUEST['post']['file']) ? filter_var($_REQUEST['post']['file'], FILTER_UNSAFE_RAW) : null,
+                            'rating' => !empty($_REQUEST['post']['rating']) ? filter_var($_REQUEST['post']['rating'], FILTER_SANITIZE_NUMBER_INT) : 'q',
+                            'source' => !empty($_REQUEST['post']['source']) ? filter_var(urldecode($_REQUEST['post']['source']), FILTER_SANITIZE_URL) : null,
+                            'sourceurl' => !empty($_REQUEST['post']['sourceurl']) ? filter_var(urldecode($_REQUEST['post']['sourceurl']), FILTER_SANITIZE_URL) : '',
+                            'description' => !empty($_REQUEST['post']['description']) ? filter_var($_REQUEST['post']['description'], FILTER_SANITIZE_STRING) : '',
+                            'is_rating_locked' => !empty($_REQUEST['post']['is_rating_locked']) ? filter_var($_REQUEST['post']['is_rating_locked'], FILTER_SANITIZE_NUMBER_INT) : false,
+                            'is_note_locked' => !empty($_REQUEST['post']['is_note_locked']) ? filter_var($_REQUEST['post']['is_note_locked'], FILTER_SANITIZE_NUMBER_INT) : false,
+                            'parent_id' => !empty($_REQUEST['post']['parent_id']) ? filter_var($_REQUEST['post']['parent_id'], FILTER_SANITIZE_NUMBER_INT) : null,
+                        );
+                        $md5 = !empty($_REQUEST['md5']) ? filter_var($_REQUEST['md5'], FILTER_SANITIZE_STRING) : null;
+                        $this->postCreate(new OuroborosPost($post), $md5);
+                    }
+                    else {
+                        $this->sendResponse(403, 'You cannot create new posts');
+                    }
+
                 }
                 elseif ($this->match('update')) {
                     // Update

From 1a25014564ed81c5ddaf2ac308567c9377922a8f Mon Sep 17 00:00:00 2001
From: Diftraku <diftraku@derpy.me>
Date: Tue, 3 Dec 2013 05:51:55 +0200
Subject: [PATCH 03/30] Derp, forgot I was actually giving the post[file] to
 OuroborosPost, making assert fail for null

---
 ext/ouroboros_api/main.php | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/ext/ouroboros_api/main.php b/ext/ouroboros_api/main.php
index 1304b9a7..b16abbc7 100644
--- a/ext/ouroboros_api/main.php
+++ b/ext/ouroboros_api/main.php
@@ -11,6 +11,7 @@
  *       <ul>
  *         <li>Index/List</li>
  *         <li>Show</li>
+ *         <li>Create</li>
  *       </ul>
  *     </li>
  *     <li>Tag:
@@ -269,10 +270,12 @@ class OuroborosPost extends _SafeOuroborosImage {
             $this->tags = $post['tags'];
         }
         if (array_key_exists('file', $post)) {
-            assert(is_array($post['file']));
-            assert(array_key_exists('tmp_name', $post['file']));
-            assert(array_key_exists('name', $post['file']));
-            $this->file = $post['file'];
+            if (!is_null($post['file'])) {
+                assert(is_array($post['file']));
+                assert(array_key_exists('tmp_name', $post['file']));
+                assert(array_key_exists('name', $post['file']));
+                $this->file = $post['file'];
+            }
         }
         if (array_key_exists('rating', $post)) {
             assert(

From 453d9a453bdda03ae2529e1be74781dfb6d8b1b3 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Sun, 8 Dec 2013 15:05:27 +0000
Subject: [PATCH 04/30] bookmarklet now supports danbooru2 + fixed issues with
 other sites

---
 ext/upload/bookmarklet.js | 147 +++++++++++++++++---------------------
 1 file changed, 65 insertions(+), 82 deletions(-)

diff --git a/ext/upload/bookmarklet.js b/ext/upload/bookmarklet.js
index e902d135..0c4dda31 100644
--- a/ext/upload/bookmarklet.js
+++ b/ext/upload/bookmarklet.js
@@ -1,6 +1,5 @@
 /* Imageboard to Shimmie */
 // This should work with "most" sites running Danbooru/Gelbooru/Shimmie
-// TODO: Make this use jQuery! (if we can be sure that jquery is loaded)
 // maxsize, supext, CA are set inside the bookmarklet (see theme.php)
 
 var maxsize = (maxsize.match("(?:\.*[0-9])")) * 1024; // This assumes we are only working with MB.
@@ -25,67 +24,84 @@ else if(CA === 2) { // New Tags
 }
 
 
+
 /*
- * Danbooru (oreno.imouto | konachan | sankakucomplex)
+ * Danbooru2
+ * jQuery should always active here, meaning we can use jQuery in this part of the bookmarklet.
  */
-if(document.getElementById("post_tags") !== null) {
+
+if(document.getElementById("post_tag_string") !== null) {
 	if (typeof tag !== "ftp://ftp." && chk !==1) {
-		var tag = document.getElementById("post_tags").value;
+		var tag = $('#post_tag_string').text().replace(/\n/g, "");
 	}
-	tag = tag.replace(/\+/g, "%2B"); // This should stop + not showing in tags :x
+	tag = tag.replace(/\+/g, "%2B");
 
-	var source = "http://" + document.location.hostname + document.location.href.match("\/post\/show\/[0-9]+");
-	if(source.search("oreno\\.imouto") >= 0 || source.search("konachan\\.com") >= 0) {
-		var rating = document.getElementById("stats").innerHTML.match("<li>Rating: (.*) <span")[1];
-	}
-	else {
-		var rating = document.getElementById("stats").innerHTML.match("<li>Rating: (.*)<\/li>")[1];
+	var source = "http://" + document.location.hostname + document.location.href.match("\/posts\/[0-9]+");
+
+	var rlist = $('[name="post[rating]"]');
+	for(x=0;x<3;x++){
+		var rating = (rlist[x].checked == true ? rlist[x].value : rating);
 	}
 
-	if(tag.search(/\bflash\b/)===-1){
-		var highres_url = document.getElementById("highres").href;
-		if(source.search("oreno\\.imouto") >= 0 || source.search("konachan\\.com") >= 0){ // oreno's theme seems to have moved the filesize
-			var filesize = document.getElementById("highres").innerHTML.match("[a-zA-Z0-9]+ \\(+([0-9]+\\.[0-9]+) ([a-zA-Z]+)");
+	var fileinfo = $('#sidebar > section:eq(3) > ul > :contains("Size") > a');
+	var furl = "http://" + document.location.hostname + fileinfo.attr('href');
+	var fs = fileinfo.text().split(" ");
+	var filesize = (fs[1] == "MB" ? fs[0] * 1024 : fs[0]);
+
+	if(supext.search(furl.match("[a-zA-Z0-9]+$")[0]) !== -1){
+		if(filesize <= maxsize){
+			location.href = ste+furl+"&tags="+tag+"&rating="+rating+"&source="+source;
+		}
+		else{
+			alert(toobig);
+		}
+	}
+	else{
+		alert(notsup);
+	}
+}
+
+/*
+ * konachan | sankakucomplex | gelbooru
+ */
+else if(document.getElementById('tag-sidebar') !== null) {
+	if (typeof tag !== "ftp://ftp." && chk !==1) {
+		if(document.location.href.search("sankakucomplex\\.com") >= 0 || document.location.href.search("gelbooru\\.com")){
+			var tag = document.getElementById('tag-sidebar').innerText.replace(/ /g, "_").replace(/[\?_]*(.*?)_(\(\?\)_)?[0-9]+\n/g, "$1 ");
 		}else{
-			var filesize = document.getElementById("stats").innerHTML.match("[0-9] \\(((?:\.*[0-9])) ([a-zA-Z]+)");
-		}
-		if(filesize[2] == "MB") {
-			var filesize = filesize[1] * 1024;
-		}
-		else {
-			var filesize = filesize[2].match("[0-9]+");
-		}
-
-		if(supext.search(highres_url.match("http\:\/\/.*\\.([a-z0-9]+)")[1]) !== -1) {
-			if(filesize <= maxsize) {
-				if(source.search("oreno\\.imouto") >= 0) {
-					// this regex tends to be a bit picky with tags -_-;;
-					var highres_url = highres_url.match("(http\:\/\/[a-z0-9]+\.[a-z]+\.[a-z]\/[a-z0-9]+\/[a-z0-9]+)\/[a-z0-9A-Z%_-]+(\.[a-zA-Z0-9]+)");
-					var highres_url = highres_url[1]+highres_url[2]; // this should bypass hotlink protection
-				}
-				else if(source.search("konachan\\.com") >= 0) {
-					// konachan affixs konachan.com to the start of the tags, this requires different regex
-					var highres_url = highres_url.match("(http\:\/\/[a-z0-9]+\.[a-z]+\.[a-z]\/[a-z0-9]+\/[a-z0-9]+)\/[a-z0-9A-Z%_]+\.[a-zA-Z0-9%_-]+(\.[a-z0-9A-Z]+)")
-					var highres_url = highres_url[1]+highres_url[2];
-				}
-				location.href = ste+highres_url+"&tags="+tag+"&rating="+rating+"&source="+source;
-			}
-			else{
-				alert(toobig);
-			}
-		}
-		else{
-			alert(notsup);
+			var tag = document.getElementById("post_tags").value;
 		}
 	}
-	else {
-		if(supext.search("swf") !== -1) {
-			location.href = ste+document.getElementsByName("movie")[0].value+"&tags="+tag+"&rating="+rating+"&source="+source;
+	tag = tag.replace(/\+/g, "%2B");
+
+	var source = "http://" + document.location.hostname + (document.location.href.match("\/post\/show\/[0-9]+") || encodeURIComponent(document.location.href.match(/\/index\.php\?page=post&s=view&id=[0-9]+/)));
+
+	var rating = document.getElementById("stats").innerHTML.match("Rating: ([a-zA-Z]+)")[1];
+
+	if(source.search("sankakucomplex\\.com") >= 0 || source.search("konachan\\.com") >= 0){
+		var fileinfo = document.getElementById("highres");
+		//NOTE: If highres doesn't exist, post must be flash (only sankakucomplex has flash)
+	}else if(source.search("gelbooru\\.com") >= 0){
+		var fileinfo = document.getElementById('pfd').parentNode.parentNode.getElementsByTagName('a')[0];
+		//gelbooru has no easy way to select the original image link, so we need to double check it is the correct link.
+		fileinfo = (fileinfo.getAttribute('href') == "#" ? document.getElementById('pfd').parentNode.parentNode.getElementsByTagName('a')[1] : fileinfo);
+	}
+	fileinfo = fileinfo || document.getElementsByTagName('embed')[0]; //If fileinfo is null then image is most likely flash.
+	var furl = fileinfo.href || fileinfo.src;
+	var fs = (fileinfo.innerText.match(/[0-9]+ (KB|MB)/) || ["0 KB"])[0].split(" ");
+	var filesize = (fs[1] == "MB" ? fs[0] * 1024 : fs[0]);
+
+	if(supext.search(furl.match("[a-zA-Z0-9]+$")[0]) !== -1){
+		if(filesize <= maxsize){
+			location.href = ste+furl+"&tags="+tag+"&rating="+rating+"&source="+source;
 		}
 		else{
-			alert(notsup);
+			alert(toobig);
 		}
 	}
+	else{
+		alert(notsup);
+	}
 }
 
 /* 
@@ -128,37 +144,4 @@ else if(document.getElementsByTagName("title")[0].innerHTML.search("Image [0-9.-
 			alert(notsup);
 		}
 	}
-}
-
-/*
- * Gelbooru
- */
-else if(document.getElementById("tags") !== null) {
-	if (typeof tag !=="ftp://ftp." && chk !==1) {
-		var tag = document.getElementById("tags").value;
-	}
-
-	var rating = document.getElementById("stats").innerHTML.match("<li>Rating: (.*)<\/li>")[1];
-
-	// Can't seem to grab source due to url containing a &
-	// var source="http://" + document.location.hostname + document.location.href.match("\/index\.php?page=post&amp;s=view\\&amp;id=.*");
-	
-	// Updated Nov. 24, 2013 by jgen.
-	var gmi;	
-	try {
-		gmi = document.getElementById("image").src.match(".*img[0-9]*\.gelbooru\.com[\/]+images[\/]+[0-9]+[\/]+[a-z0-9]+\.[a-z0-9]+")[0];
-		
-		// Since Gelbooru does not allow flash, no need to search for flash tag.
-		// Gelbooru doesn't show file size in statistics either...
-		if(supext.search(gmi.match("http\:\/\/.*\\.([a-z0-9]+)")[1]) !== -1){
-			location.href = ste+gmi+"&tags="+tag+"&rating="+rating;//+"&source="+source;
-		}
-		else{
-			alert(notsup);
-		}
-	}
-	catch (err)
-	{
-		alert("Unable to locate the image on the page!\n(Gelbooru may have changed the structure of their page, please file a bug.)");
-	}
-}
+}
\ No newline at end of file

From 59eb6d7ec280ab8105b6bf0579809da12e4a7f2d Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Sun, 29 Dec 2013 20:46:37 +0000
Subject: [PATCH 05/30] pool title should be unique

---
 ext/pools/main.php | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/ext/pools/main.php b/ext/pools/main.php
index b161e8bb..42bbe2c9 100644
--- a/ext/pools/main.php
+++ b/ext/pools/main.php
@@ -71,6 +71,11 @@ class Pools extends Extension {
 
 			log_info("pools", "extension installed");
 		}
+
+		if ($config->get_int("ext_pools_version") < 2){
+			$database->Execute("ALTER TABLE `pools`	ADD UNIQUE INDEX (`title`);");
+			$config->set_int("ext_pools_version", 2);
+		}
 	}
 
 	// Add a block to the Board Config / Setup

From 3e240fa78d8e24daf0088d5d723c7d8e6ed131fa Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Sun, 29 Dec 2013 22:24:34 +0000
Subject: [PATCH 06/30] return error when pool title exists + fix pool error
 reporting

---
 ext/pools/main.php  | 34 +++++++++++++++++++++++-----------
 ext/pools/theme.php | 15 ---------------
 2 files changed, 23 insertions(+), 26 deletions(-)

diff --git a/ext/pools/main.php b/ext/pools/main.php
index 42bbe2c9..e449ab1f 100644
--- a/ext/pools/main.php
+++ b/ext/pools/main.php
@@ -116,7 +116,7 @@ class Pools extends Extension {
 						$this->theme->new_pool_composer($page);
 					} else {
 						$errMessage = "You must be registered and logged in to create a new pool.";
-						$this->theme->display_error($errMessage);
+						$this->theme->display_error(401, "Error", $errMessage);
 					}
 					break;
 
@@ -127,7 +127,7 @@ class Pools extends Extension {
 						$page->set_redirect(make_link("pool/view/".$newPoolID));
 					}
 					catch(PoolCreationException $e) {
-						$this->theme->display_error($e->error);
+						$this->theme->display_error(400, "Error", $e->error);
 					}
 					break;
 
@@ -173,7 +173,7 @@ class Pools extends Extension {
 							$page->set_mode("redirect");
 							$page->set_redirect(make_link("pool/view/".$pool_id));
 						} else {
-							$this->theme->display_error("Permssion denied.");
+							$this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
 						}
 					}
 					break;
@@ -182,7 +182,7 @@ class Pools extends Extension {
 					if ($this->have_permission($user, $pool)) {
 						$this->import_posts($pool_id);
 					} else {
-						$this->theme->display_error("Permssion denied.");
+						$this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
 					}
 					break;
 
@@ -192,7 +192,7 @@ class Pools extends Extension {
 						$page->set_mode("redirect");
 						$page->set_redirect(make_link("pool/view/".$pool_id));
 					} else {
-						$this->theme->display_error("Permssion denied.");
+						$this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
 					}
 					break;
 
@@ -202,7 +202,7 @@ class Pools extends Extension {
 						$page->set_mode("redirect");
 						$page->set_redirect(make_link("pool/view/".$pool_id));
 					} else {
-						$this->theme->display_error("Permssion denied.");
+						$this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
 					}
 
 					break;
@@ -215,7 +215,7 @@ class Pools extends Extension {
 						$page->set_mode("redirect");
 						$page->set_redirect(make_link("pool/list"));
 					} else {
-						$this->theme->display_error("Permssion denied.");
+						$this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
 					}
 					break;
 
@@ -336,14 +336,16 @@ class Pools extends Extension {
 	 */
 	private function add_pool() {
 		global $user, $database;
-
+		#throw new PoolCreationException("Pool needs a title");
 		if($user->is_anonymous()) {
 			throw new PoolCreationException("You must be registered and logged in to add a image.");
 		}
 		if(empty($_POST["title"])) {
-			throw new PoolCreationException("Pool needs a title");
+			throw new PoolCreationException("Pool title is empty.");
+		}
+		if($this->get_single_pool_from_title($_POST["title"])) {
+			throw new PoolCreationException("A pool using this title already exists.");
 		}
-
 		$public = $_POST["public"] == "Y" ? "Y" : "N";
 		$database->execute("
 				INSERT INTO pools (user_id, public, title, description, date)
@@ -366,7 +368,7 @@ class Pools extends Extension {
 		global $database;
 		return $database->get_all("SELECT * FROM pools WHERE id=:id", array("id"=>$poolID));
 	}
-	
+
 	/**
 	 * Retrieve information about a pool given a pool ID.
 	 * @param $poolID Integer
@@ -377,6 +379,16 @@ class Pools extends Extension {
 		return $database->get_row("SELECT * FROM pools WHERE id=:id", array("id"=>$poolID));
 	}
 
+	/**
+	 * Retrieve information about a pool given a pool title.
+	 * @param $poolTitle Integer
+	 * @retval 2D array (with only 1 element in the one dimension)
+	 */
+	private function get_single_pool_from_title(/*string*/ $poolTitle) {
+		global $database;
+		return $database->get_row("SELECT * FROM pools WHERE title=:title", array("title"=>$poolTitle));
+	}
+
 	/**
 	 * Get all of the pool IDs that an image is in, given an image ID.
 	 * @param $imageID Integer
diff --git a/ext/pools/theme.php b/ext/pools/theme.php
index d9c397c7..39c4318e 100644
--- a/ext/pools/theme.php
+++ b/ext/pools/theme.php
@@ -389,20 +389,5 @@ class PoolsTheme extends Themelet {
 
 		$this->display_paginator($page, "pool/updated", null, $pageNumber, $totalPages);
 	}
-
-
-	/**
-	 * Display an error message to the user.
-	 */
-	public function display_error(/*int*/ $code, /*string*/ $title, /*string*/ $message) {
-		global $page;
-		
-		// Quick n' Dirty fix
-		$message = $code;
-
-		$page->set_title("Error");
-		$page->set_heading("Error");
-		$page->add_block(new Block("Error", $errMessage, "main", 10));
-	}
 }
 ?>

From b79a042bdcb68524a872038d440c15bfda7c5856 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Mon, 30 Dec 2013 01:48:07 +0000
Subject: [PATCH 07/30] added option to order pool list by created date/last
 updated/title/count

---
 ext/pools/main.php  | 17 ++++++++++++++++-
 ext/pools/script.js | 10 ++++++++++
 ext/pools/theme.php | 10 ++++++++++
 3 files changed, 36 insertions(+), 1 deletion(-)
 create mode 100644 ext/pools/script.js

diff --git a/ext/pools/main.php b/ext/pools/main.php
index e449ab1f..7cdc0374 100644
--- a/ext/pools/main.php
+++ b/ext/pools/main.php
@@ -74,6 +74,8 @@ class Pools extends Extension {
 
 		if ($config->get_int("ext_pools_version") < 2){
 			$database->Execute("ALTER TABLE `pools`	ADD UNIQUE INDEX (`title`);");
+			$database->Execute("ALTER TABLE `pools`	ADD `lastupdated` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;");
+
 			$config->set_int("ext_pools_version", 2);
 		}
 	}
@@ -314,13 +316,26 @@ class Pools extends Extension {
 
 		$poolsPerPage = $config->get_int("poolsListsPerPage");
 
+
+		$order_by = "";
+		$order = get_prefixed_cookie("ui-order-pool");
+		if($order == "created" || is_null($order)){
+			$order_by = "ORDER BY p.date DESC";
+		}elseif($order == "updated"){
+			$order_by = "ORDER BY p.lastupdated DESC";
+		}elseif($order == "name"){
+			$order_by = "ORDER BY p.title ASC";
+		}elseif($order == "count"){
+			$order_by = "ORDER BY p.posts DESC";
+		}
+
 		$pools = $database->get_all("
 				SELECT p.id, p.user_id, p.public, p.title, p.description,
 				       p.posts, u.name as user_name
 				FROM pools AS p
 				INNER JOIN users AS u
 				ON p.user_id = u.id
-				ORDER BY p.date DESC
+				$order_by
 				LIMIT :l OFFSET :o
 				", array("l"=>$poolsPerPage, "o"=>$pageNumber * $poolsPerPage)
 				);
diff --git a/ext/pools/script.js b/ext/pools/script.js
new file mode 100644
index 00000000..72ea80e6
--- /dev/null
+++ b/ext/pools/script.js
@@ -0,0 +1,10 @@
+$(function() {
+	var order_pool = $.cookie("shm_ui-order-pool");
+	$("#order_pool option[value="+order_pool+"]").attr("selected", true);
+
+	$('#order_pool').change(function(){
+		var val = $("#order_pool option:selected").val();
+		$.cookie("shm_ui-order-pool", val, {path: '/', expires: 365}); //FIXME: This won't play nice if COOKIE_PREFIX is not "shm_".
+		window.location.href = '';
+	});
+});
diff --git a/ext/pools/theme.php b/ext/pools/theme.php
index 39c4318e..cc924045 100644
--- a/ext/pools/theme.php
+++ b/ext/pools/theme.php
@@ -67,11 +67,21 @@ class PoolsTheme extends Themelet {
 			<br><a href="'.make_link("pool/updated").'">Pool Changes</a>
 		';
 
+		$order_html = '
+			<select id="order_pool">
+			  <option value="created">Recently created</option>
+			  <option value="updated">Last updated</option>
+			  <option value="name">Name</option>
+			  <option value="count">Post count</option>
+			</select>
+		';
+
 		$blockTitle = "Pools";
 		$page->set_title(html_escape($blockTitle));
 		$page->set_heading(html_escape($blockTitle));
 		$page->add_block(new Block($blockTitle, $html, "main", 10));
 		$page->add_block(new Block("Navigation", $nav_html, "left", 10));
+		$page->add_block(new Block("Order By", $order_html, "left", 15));
 
 		$this->display_paginator($page, "pool/list", null, $pageNumber, $totalPages);
 	}

From 9511569ed425270f3624e0bed2be8205ed40409e Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Mon, 30 Dec 2013 06:51:47 +0000
Subject: [PATCH 08/30] added option to edit pool description to pool edit page

---
 ext/pools/main.php  | 22 ++++++++++++++++++++++
 ext/pools/theme.php | 14 +++++++++++++-
 2 files changed, 35 insertions(+), 1 deletion(-)

diff --git a/ext/pools/main.php b/ext/pools/main.php
index 7cdc0374..6bb9a24d 100644
--- a/ext/pools/main.php
+++ b/ext/pools/main.php
@@ -209,6 +209,17 @@ class Pools extends Extension {
 
 					break;
 
+				case "edit_description":
+					if ($this->have_permission($user, $pool)) {
+						$this->edit_description();
+						$page->set_mode("redirect");
+						$page->set_redirect(make_link("pool/view/".$pool_id));
+					} else {
+						$this->theme->display_error(403, "Permission Denied", "You do not have permission to access this page");
+					}
+
+					break;
+
 				case "nuke":
 					// Completely remove the given pool.
 					//  -> Only admins and owners may do this	
@@ -508,6 +519,17 @@ class Pools extends Extension {
 		return $poolID;
 	}
 
+	/*
+	 * Allows editing of pool description.
+	 */
+	private function edit_description() {
+		global $database;
+
+		$poolID = int_escape($_POST['pool_id']);
+		$database->execute("UPDATE pools SET description=:dsc WHERE id=:pid", array("dsc"=>$_POST['description'], "pid"=>$poolID));
+
+		return $poolID;
+	}
 
 	/**
 	 * This function checks if a given image is contained within a given pool.
diff --git a/ext/pools/theme.php b/ext/pools/theme.php
index cc924045..269b9a1c 100644
--- a/ext/pools/theme.php
+++ b/ext/pools/theme.php
@@ -322,8 +322,17 @@ class PoolsTheme extends Themelet {
 	public function edit_pool(Page $page, /*array*/ $pools, /*array*/ $images) {
 		global $user;
 
-		$this->display_top($pools, "Editing Pool", true);
 
+		/* EDIT POOL DESCRIPTION */
+		$desc_html = "
+			".make_form(make_link("pool/edit_description"))."
+					<textarea name='description'>".$pools[0]['description']."</textarea><br />
+					<input type='hidden' name='pool_id' value='".$pools[0]['id']."'>
+					<input type='submit' value='Change Description' />
+			</form>
+		";
+
+		/* REMOVE POOLS */
 		$pool_images = "\n<form action='".make_link("pool/remove_posts")."' method='POST' name='checks'>";
 
 		foreach($images as $pair) {
@@ -341,6 +350,9 @@ class PoolsTheme extends Themelet {
 			"<input type='hidden' name='pool_id' value='".$pools[0]['id']."'>".
 			"</form>";
 
+		$pools[0]['description'] = ""; //This is a rogue fix to avoid showing the description twice.
+		$this->display_top($pools, "Editing Pool", true);
+		$page->add_block(new Block("Editing Description", $desc_html, "main", 28));
 		$page->add_block(new Block("Editing Posts", $pool_images, "main", 30));
 	}
 

From 9eaebfd1c28d36fd605195c7ad68e62c7ce30466 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Mon, 30 Dec 2013 06:52:17 +0000
Subject: [PATCH 09/30] if cookie doesn't exist, default to "created"

---
 ext/pools/script.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ext/pools/script.js b/ext/pools/script.js
index 72ea80e6..505718a9 100644
--- a/ext/pools/script.js
+++ b/ext/pools/script.js
@@ -1,5 +1,5 @@
 $(function() {
-	var order_pool = $.cookie("shm_ui-order-pool");
+	var order_pool = $.cookie("shm_ui-order-pool") || "created";
 	$("#order_pool option[value="+order_pool+"]").attr("selected", true);
 
 	$('#order_pool').change(function(){

From 3dd31019959625efe5e22c155bc06d9544e4a5f4 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Mon, 30 Dec 2013 08:15:10 +0000
Subject: [PATCH 10/30] added option to set pool & source via tag

---
 core/imageboard.pack.php | 12 ++++++++++++
 ext/pools/main.php       | 20 +++++++++++++++++++-
 ext/pools/theme.php      |  2 +-
 3 files changed, 32 insertions(+), 2 deletions(-)

diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php
index e36c40ee..869e7de6 100644
--- a/core/imageboard.pack.php
+++ b/core/imageboard.pack.php
@@ -476,6 +476,18 @@ class Image {
 			$this->delete_tags_from_image();
 			// insert each new tags
 			foreach($tags as $tag) {
+				if(preg_match("/^source=(.*)$/i", $tag, $matches)) {
+					$this->set_source($matches[1]);
+					continue;
+				}
+				if(preg_match("/^pool=(.*)$/i", $tag, $matches)) {
+					if(class_exists("Pools")) {
+						$pls = new Pools();
+						$pls->add_post_from_tag($matches[1], $this->id);
+					}
+					continue;
+				}
+
 				$id = $database->get_one(
 						$database->scoreql_to_sql(
 							"SELECT id FROM tags WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag)"
diff --git a/ext/pools/main.php b/ext/pools/main.php
index 6bb9a24d..22ac55d8 100644
--- a/ext/pools/main.php
+++ b/ext/pools/main.php
@@ -293,6 +293,23 @@ class Pools extends Extension {
 		}
 	}
 
+	public function add_post_from_tag(/*str*/ $poolTag, /*int*/ $imageID){
+		$poolTag = str_replace("_", " ", $poolTag);
+		//First check if pool tag is a title
+		if(ctype_digit($poolTag)){
+			//If string only contains numeric characters, assume it is $poolID
+			if($this->get_single_pool($poolTag)){ //Make sure pool exists
+				$this->add_post($poolTag, $imageID);
+			}
+		}else{
+			//If string doesn't contain only numeric characters, check to see if tag is title.
+			$pool = $this->get_single_pool_from_title($poolTag);
+			if($pool){
+				$this->add_post($pool['id'], $imageID);
+			}
+		}
+	}
+
 	/* ------------------------------------------------- */
 	/* --------------  Private Functions  -------------- */
 	/* ------------------------------------------------- */
@@ -362,7 +379,7 @@ class Pools extends Extension {
 	 */
 	private function add_pool() {
 		global $user, $database;
-		#throw new PoolCreationException("Pool needs a title");
+
 		if($user->is_anonymous()) {
 			throw new PoolCreationException("You must be registered and logged in to add a image.");
 		}
@@ -372,6 +389,7 @@ class Pools extends Extension {
 		if($this->get_single_pool_from_title($_POST["title"])) {
 			throw new PoolCreationException("A pool using this title already exists.");
 		}
+
 		$public = $_POST["public"] == "Y" ? "Y" : "N";
 		$database->execute("
 				INSERT INTO pools (user_id, public, title, description, date)
diff --git a/ext/pools/theme.php b/ext/pools/theme.php
index 269b9a1c..3b2e315c 100644
--- a/ext/pools/theme.php
+++ b/ext/pools/theme.php
@@ -350,7 +350,7 @@ class PoolsTheme extends Themelet {
 			"<input type='hidden' name='pool_id' value='".$pools[0]['id']."'>".
 			"</form>";
 
-		$pools[0]['description'] = ""; //This is a rogue fix to avoid showing the description twice.
+		$pools[0]['description'] = ""; //This is a rough fix to avoid showing the description twice.
 		$this->display_top($pools, "Editing Pool", true);
 		$page->add_block(new Block("Editing Description", $desc_html, "main", 28));
 		$page->add_block(new Block("Editing Posts", $pool_images, "main", 30));

From fd6f2ddb431e45d33b0ea3617e3a32dc461bd94d Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Mon, 30 Dec 2013 08:42:29 +0000
Subject: [PATCH 11/30] show pool navigation box on pool/view pages

---
 ext/pools/theme.php | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/ext/pools/theme.php b/ext/pools/theme.php
index 3b2e315c..444e76b5 100644
--- a/ext/pools/theme.php
+++ b/ext/pools/theme.php
@@ -166,6 +166,13 @@ class PoolsTheme extends Themelet {
 			$pool_images .= "\n".$thumb_html."\n";
 		}
 
+		$nav_html = '
+			<a href="'.make_link().'">Index</a>
+			<br><a href="'.make_link("pool/new").'">Create Pool</a>
+			<br><a href="'.make_link("pool/updated").'">Pool Changes</a>
+		';
+
+		$page->add_block(new Block("Navigation", $nav_html, "left", 10));
 		$page->add_block(new Block("Viewing Posts", $pool_images, "main", 30));		
 		$this->display_paginator($page, "pool/view/".$pools[0]['id'], null, $pageNumber, $totalPages);
 	}

From c892386bcb64a48ed06a1a30cd06aa00c332d33e Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Mon, 30 Dec 2013 10:36:32 +0000
Subject: [PATCH 12/30] remove backticks

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

diff --git a/ext/pools/main.php b/ext/pools/main.php
index 22ac55d8..008838d3 100644
--- a/ext/pools/main.php
+++ b/ext/pools/main.php
@@ -73,8 +73,8 @@ class Pools extends Extension {
 		}
 
 		if ($config->get_int("ext_pools_version") < 2){
-			$database->Execute("ALTER TABLE `pools`	ADD UNIQUE INDEX (`title`);");
-			$database->Execute("ALTER TABLE `pools`	ADD `lastupdated` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;");
+			$database->Execute("ALTER TABLE pools ADD UNIQUE INDEX (title);");
+			$database->Execute("ALTER TABLE pools ADD lastupdated TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;");
 
 			$config->set_int("ext_pools_version", 2);
 		}

From 85880804d24ded84feda563a18bf3118b2c03d40 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Tue, 31 Dec 2013 08:22:58 +0000
Subject: [PATCH 13/30] have theme.php manage block creation so themes can
 move/remove if they wish

---
 ext/numeric_score/main.php  |  6 ++----
 ext/numeric_score/theme.php | 12 ++++++------
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/ext/numeric_score/main.php b/ext/numeric_score/main.php
index ed7597ab..9cf21ab3 100644
--- a/ext/numeric_score/main.php
+++ b/ext/numeric_score/main.php
@@ -31,16 +31,14 @@ class NumericScore extends Extension {
 	public function onDisplayingImage(DisplayingImageEvent $event) {
 		global $user, $page;
 		if(!$user->is_anonymous()) {
-			$html = $this->theme->get_voter_html($event->image);
-			$page->add_block(new Block("Image Score", $html, "left", 20));
+			$this->theme->get_voter($event->image);
 		}
 	}
 
 	public function onUserPageBuilding(UserPageBuildingEvent $event) {
 		global $page, $user;
 		if($user->can("edit_other_vote")) {
-			$html = $this->theme->get_nuller_html($event->display_user);
-			$page->add_block(new Block("Votes", $html, "main", 60));
+			$this->theme->get_nuller($event->display_user);
 		}
 	}
 
diff --git a/ext/numeric_score/theme.php b/ext/numeric_score/theme.php
index baefad39..e60af00c 100644
--- a/ext/numeric_score/theme.php
+++ b/ext/numeric_score/theme.php
@@ -1,8 +1,8 @@
 <?php
 
 class NumericScoreTheme extends Themelet {
-	public function get_voter_html(Image $image) {
-		global $user;
+	public function get_voter(Image $image) {
+		global $user, $page;
 		$i_image_id = int_escape($image->id);
 		$i_score = int_escape($image->numeric_score);
 
@@ -46,11 +46,11 @@ class NumericScoreTheme extends Themelet {
 			</div>
 			";
 		}
-		return $html;
+		$page->add_block(new Block("Image Score", $html, "left", 20));
 	}
 
-	public function get_nuller_html(User $duser) {
-		global $user;
+	public function get_nuller(User $duser) {
+		global $user, $page;
 		$html = "
 			<form action='".make_link("numeric_score/remove_votes_by")."' method='POST'>
 			".$user->get_auth_html()."
@@ -58,7 +58,7 @@ class NumericScoreTheme extends Themelet {
 			<input type='submit' value='Delete all votes by this user'>
 			</form>
 		";
-		return $html;
+		$page->add_block(new Block("Votes", $html, "main", 60));
 	}
 
 	public function view_popular($images, $dte) {

From fde6558a6faeafd2e19d0870f7b2c1202b8aece4 Mon Sep 17 00:00:00 2001
From: HungryFeline <HungryFeline@users.noreply.github.com>
Date: Wed, 1 Jan 2014 01:41:11 +0100
Subject: [PATCH 14/30] Don't silently ignore invalid URLs

Scenario: Providing an invalid url via $_GET (wrong/missing extension or file isn't an image (also happens on download errors))
Behavior before: Silently redirect to index
Behavior after: Display error message
---
 core/extension.class.php | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/core/extension.class.php b/core/extension.class.php
index a1175341..0b4c767b 100644
--- a/core/extension.class.php
+++ b/core/extension.class.php
@@ -197,6 +197,9 @@ abstract class DataHandlerExtension extends Extension {
 				}
 			}
 		}
+		else{
+			throw new UploadException("Unsupported extension or file isn't an image");
+		}
 	}
 
 	public function onThumbnailGeneration(ThumbnailGenerationEvent $event) {

From 7cf79171a8a3b232044e75f0bc3955c29676b358 Mon Sep 17 00:00:00 2001
From: HungryFeline <HungryFeline@users.noreply.github.com>
Date: Wed, 1 Jan 2014 18:25:28 +0100
Subject: [PATCH 15/30] Update extension.class.php

Fix my previous commit. Also put the results of the tests into variables so we don't need to check them again.
---
 core/extension.class.php | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/core/extension.class.php b/core/extension.class.php
index 0b4c767b..3407ba10 100644
--- a/core/extension.class.php
+++ b/core/extension.class.php
@@ -142,7 +142,7 @@ abstract class DataHandlerExtension extends Extension {
 	public function onDataUpload(DataUploadEvent $event) {
 		global $user;
 
-		if($this->supported_ext($event->type) && $this->check_contents($event->tmpname)) {
+		if(($supported_ext = $this->supported_ext($event->type)) && ($check_contents = $this->check_contents($event->tmpname))) {
 			if(!move_upload_to_archive($event)) return;
 			send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
 
@@ -197,8 +197,8 @@ abstract class DataHandlerExtension extends Extension {
 				}
 			}
 		}
-		else{
-			throw new UploadException("Unsupported extension or file isn't an image");
+		elseif($supported_ext && !$check_contents){
+			throw new UploadException("Invalid or corrupted file");
 		}
 	}
 

From 25c286b71faf23231fea2346e6c5dcef1a39df15 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Thu, 2 Jan 2014 14:00:24 +0000
Subject: [PATCH 16/30] add support for using : as a metatag seperator +
 updated docs

---
 core/imageboard.pack.php  |  4 +--
 ext/danbooru_api/main.php |  8 -----
 ext/favorites/main.php    |  8 ++---
 ext/index/main.php        | 70 +++++++++++++++++++++++++--------------
 ext/notes/main.php        | 10 +++---
 ext/rating/main.php       |  4 +--
 ext/user/main.php         |  6 ++--
 7 files changed, 61 insertions(+), 49 deletions(-)

diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php
index 869e7de6..c9a2ce4a 100644
--- a/core/imageboard.pack.php
+++ b/core/imageboard.pack.php
@@ -476,11 +476,11 @@ class Image {
 			$this->delete_tags_from_image();
 			// insert each new tags
 			foreach($tags as $tag) {
-				if(preg_match("/^source=(.*)$/i", $tag, $matches)) {
+				if(preg_match("/^source[=|:](.*)$/i", $tag, $matches)) {
 					$this->set_source($matches[1]);
 					continue;
 				}
-				if(preg_match("/^pool=(.*)$/i", $tag, $matches)) {
+				if(preg_match("/^pool[=|:](.*)$/i", $tag, $matches)) {
 					if(class_exists("Pools")) {
 						$pls = new Pools();
 						$pls->add_post_from_tag($matches[1], $this->id);
diff --git a/ext/danbooru_api/main.php b/ext/danbooru_api/main.php
index f5d8773e..2ba1b82d 100644
--- a/ext/danbooru_api/main.php
+++ b/ext/danbooru_api/main.php
@@ -54,14 +54,6 @@ class DanbooruApi extends Extension {
 		}
 	}
 
-	public function onSearchTermParse(SearchTermParseEvent $event) {
-		$matches = array();
-		if(preg_match("/^md5:([0-9a-fA-F]*)$/i", $event->term, $matches)) {
-			$hash = strtolower($matches[1]);
-			$event->add_querylet(new Querylet("images.hash = '$hash'"));	// :-O
-		}
-	}
-
 	// Danbooru API
 	private function api_danbooru(PageRequestEvent $event)
 	{
diff --git a/ext/favorites/main.php b/ext/favorites/main.php
index aee35d6a..6517377e 100644
--- a/ext/favorites/main.php
+++ b/ext/favorites/main.php
@@ -117,12 +117,12 @@ class Favorites extends Extension {
 
 	public function onSearchTermParse(SearchTermParseEvent $event) {
 		$matches = array();
-		if(preg_match("/favorites(<|>|<=|>=|=)(\d+)/", $event->term, $matches)) {
-			$cmp = $matches[1];
+		if(preg_match("/^favorites([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/", $event->term, $matches)) {
+			$cmp = ltrim($matches[1], ":") ?: "=";
 			$favorites = $matches[2];
 			$event->add_querylet(new Querylet("images.id IN (SELECT id FROM images WHERE favorites $cmp $favorites)"));
 		}
-		else if(preg_match("/favorited_by=(.*)/i", $event->term, $matches)) {
+		else if(preg_match("/^favorited_by[=|:](.*)$/i", $event->term, $matches)) {
 			global $database;
 			$user = User::by_name($matches[1]);
 			if(!is_null($user)) {
@@ -134,7 +134,7 @@ class Favorites extends Extension {
 
 			$event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM user_favorites WHERE user_id = $user_id)"));
 		}
-		else if(preg_match("/favorited_by_userno=([0-9]+)/i", $event->term, $matches)) {
+		else if(preg_match("/^favorited_by_userno[=|:](\d+)$/i", $event->term, $matches)) {
 			$user_id = int_escape($matches[1]);
 			$event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM user_favorites WHERE user_id = $user_id)"));
 		}
diff --git a/ext/index/main.php b/ext/index/main.php
index f851bde7..4acc663c 100644
--- a/ext/index/main.php
+++ b/ext/index/main.php
@@ -38,36 +38,53 @@
  *        <li>id<20 -- search only the first few images
  *        <li>id>=500 -- search later images
  *      </ul>
- *    <li>user=Username, eg
+ *    <li>user=Username & poster=Username, eg
  *      <ul>
  *        <li>user=Shish -- find all of Shish's posts
+ *        <li>poster=Shish -- same as above
  *      </ul>
- *    <li>hash=md5sum, eg
+ *    <li>user_id=userID & poster_id=userID, eg
+ *      <ul>
+ *        <li>user_id=2 -- find all posts by user id 2
+ *        <li>poster_id=2 -- same as above
+ *      </ul>
+ *    <li>hash=md5sum & md5=md5sum, eg
  *      <ul>
  *        <li>hash=bf5b59173f16b6937a4021713dbfaa72 -- find the "Taiga want up!" image
+ *        <li>md5=bf5b59173f16b6937a4021713dbfaa72 -- same as above
  *      </ul>
- *    <li>filetype=type, eg
+ *    <li>filetype=type & ext=type, eg
  *      <ul>
  *        <li>filetype=png -- find all PNG images
+ *        <li>ext=png -- same as above
  *      </ul>
- *    <li>filename=blah, eg
+ *    <li>filename=blah & name=blah, eg
  *      <ul>
  *        <li>filename=kitten -- find all images with "kitten" in the original filename
+ *        <li>name=kitten -- same as above
  *      </ul>
  *    <li>posted (=, &lt;, &gt;, &lt;=, &gt;=) date, eg
  *      <ul>
  *        <li>posted&gt;=2009-12-25 posted&lt;=2010-01-01 -- find images posted between christmas and new year
  *      </ul>
+ *    <li>tags (=, &lt;, &gt;, &lt;=, &gt;=) count, eg
+ *      <ul>
+ *        <li>tags=1 -- search for images with only 1 tag
+ *        <li>tags>=10 -- search for images with 10 or more tags
+ *        <li>tags<25 -- search for images with less than 25 tags
+ *      </ul>
+ *    <li>source=url, eg
+ *      <ul>
+ *        <li>source=http://example.com -- find all images with "http://example.com" in the source
+ *      </ul>
  *  </ul>
  *  <p>Search items can be combined to search for images which match both,
  *  or you can stick "-" in front of an item to search for things that don't
  *  match it.
+ *  <p>Metatags can be followed by ":" rather than "=" if you prefer.
+ *  <br />I.E: "posted:2014-01-01", "id:>=500" etc.
  *  <p>Some search methods provided by extensions:
  *  <ul>
- *    <li>Danbooru API
- *      <ul>
- *        <li>md5:[hash] -- same as "hash=", but the API calls it by a different name
- *      </ul>
  *    <li>Numeric Score
  *      <ul>
  *        <li>score (=, &lt;, &gt;, &lt;=, &gt;=) number -- seach by score
@@ -81,11 +98,14 @@
  *    <li>Favorites
  *      <ul>
  *        <li>favorites (=, &lt;, &gt;, &lt;=, &gt;=) number -- search for images favourited a certain number of times
- *        <li>favourited_by=Username -- search for a user's choices
+ *        <li>favourited_by=Username -- search for a user's choices by username
+ *        <li>favorited_by_userno=UserID -- search for a user's choice by userID
  *      </ul>
  *    <li>Notes
  *      <ul>
  *        <li>notes (=, &lt;, &gt;, &lt;=, &gt;=) number -- search by the number of notes an image has
+ *        <li>notes_by=Username -- search for a notes created by username
+ *        <li>notes_by_userno=UserID -- search for a notes created by userID
  *      </ul>
  *  </ul>
  */
@@ -240,45 +260,45 @@ class Index extends Extension {
 	public function onSearchTermParse(SearchTermParseEvent $event) {
 		$matches = array();
 		// check for tags first as tag based searches are more common.
-		if(preg_match("/tags(<|>|<=|>=|=)(\d+)/", $event->term, $matches)) {
-			$cmp = $matches[1];
+		if(preg_match("/^tags([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/", $event->term, $matches)) {
+			$cmp = ltrim($matches[1], ":") ?: "=";
 			$tags = $matches[2];
 			$event->add_querylet(new Querylet('images.id IN (SELECT DISTINCT image_id FROM image_tags GROUP BY image_id HAVING count(image_id) '.$cmp.' '.$tags.')'));
 		}
-		else if(preg_match("/^ratio(<|>|<=|>=|=)(\d+):(\d+)$/", $event->term, $matches)) {
-			$cmp = $matches[1];
+		else if(preg_match("/^ratio([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+):(\d+)$/", $event->term, $matches)) {
+			$cmp = preg_replace('/^:/', '=', $matches[1]);
 			$args = array("width{$this->stpen}"=>int_escape($matches[2]), "height{$this->stpen}"=>int_escape($matches[3]));
 			$event->add_querylet(new Querylet("width / height $cmp :width{$this->stpen} / :height{$this->stpen}", $args));
 		}
-		else if(preg_match("/^(filesize|id)(<|>|<=|>=|=)(\d+[kmg]?b?)$/i", $event->term, $matches)) {
+		else if(preg_match("/^(filesize|id)([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+[kmg]?b?)$/i", $event->term, $matches)) {
 			$col = $matches[1];
-			$cmp = $matches[2];
+			$cmp = ltrim($matches[2], ":") ?: "=";
 			$val = parse_shorthand_int($matches[3]);
 			$event->add_querylet(new Querylet("images.$col $cmp :val{$this->stpen}", array("val{$this->stpen}"=>$val)));
 		}
-		else if(preg_match("/^(hash|md5)=([0-9a-fA-F]*)$/i", $event->term, $matches)) {
+		else if(preg_match("/^(hash|md5)[=|:]([0-9a-fA-F]*)$/i", $event->term, $matches)) {
 			$hash = strtolower($matches[2]);
 			$event->add_querylet(new Querylet('images.hash = :hash', array("hash" => $hash)));
 		}
-		else if(preg_match("/^(filetype|ext)=([a-zA-Z0-9]*)$/i", $event->term, $matches)) {
+		else if(preg_match("/^(filetype|ext)[=|:]([a-zA-Z0-9]*)$/i", $event->term, $matches)) {
 			$ext = strtolower($matches[2]);
 			$event->add_querylet(new Querylet('images.ext = :ext', array("ext" => $ext)));
 		}
-		else if(preg_match("/^(filename|name)=([a-zA-Z0-9]*)$/i", $event->term, $matches)) {
+		else if(preg_match("/^(filename|name)[=|:]([a-zA-Z0-9]*)$/i", $event->term, $matches)) {
 			$filename = strtolower($matches[2]);
 			$event->add_querylet(new Querylet("images.filename LIKE :filename{$this->stpen}", array("filename{$this->stpen}"=>"%$filename%")));
 		}
-		else if(preg_match("/^(source)=([a-zA-Z0-9]*)$/i", $event->term, $matches)) {
-			$filename = strtolower($matches[2]);
-			$event->add_querylet(new Querylet('images.source LIKE :src', array("src"=>"%$filename%")));
+		else if(preg_match("/^(source)[=|:]([a-zA-Z0-9]*)$/i", $event->term, $matches)) {
+			$source = strtolower($matches[2]);
+			$event->add_querylet(new Querylet('images.source LIKE :src', array("src"=>"%$source%")));
 		}
-		else if(preg_match("/^posted(<|>|<=|>=|=)([0-9-]*)$/", $event->term, $matches)) {
-			$cmp = $matches[1];
+		else if(preg_match("/^posted([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])([0-9-]*)$/", $event->term, $matches)) {
+			$cmp = ltrim($matches[1], ":") ?: "=";
 			$val = $matches[2];
 			$event->add_querylet(new Querylet("images.posted $cmp :posted{$this->stpen}", array("posted{$this->stpen}"=>$val)));
 		}
-		else if(preg_match("/^size(<|>|<=|>=|=)(\d+)x(\d+)$/", $event->term, $matches)) {
-			$cmp = $matches[1];
+		else if(preg_match("/^size([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)x(\d+)$/", $event->term, $matches)) {
+			$cmp = ltrim($matches[1], ":") ?: "=";
 			$args = array("width{$this->stpen}"=>int_escape($matches[2]), "height{$this->stpen}"=>int_escape($matches[3]));
 			$event->add_querylet(new Querylet("width $cmp :width{$this->stpen} AND height $cmp :height{$this->stpen}", $args));
 		}
diff --git a/ext/notes/main.php b/ext/notes/main.php
index 5bc09ef6..f29a22e9 100644
--- a/ext/notes/main.php
+++ b/ext/notes/main.php
@@ -210,16 +210,16 @@ class Notes extends Extension {
 	 */
 	public function onSearchTermParse(SearchTermParseEvent $event) {
 		$matches = array();
-		if(preg_match("/note=(.*)/i", $event->term, $matches)) {
+		if(preg_match("/^note[=|:](.*)$/i", $event->term, $matches)) {
 			$notes = int_escape($matches[1]);
 			$event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM notes WHERE note = $notes)"));
 		}
-		else if(preg_match("/notes(<|>|<=|>=|=)(\d+)/", $event->term, $matches)) {
-			$cmp = $matches[1];
+		else if(preg_match("/^notes([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)%/", $event->term, $matches)) {
+			$cmp = ltrim($matches[1], ":") ?: "=";
 			$notes = $matches[2];
 			$event->add_querylet(new Querylet("images.id IN (SELECT id FROM images WHERE notes $cmp $notes)"));
 		}
-		else if(preg_match("/notes_by=(.*)/i", $event->term, $matches)) {
+		else if(preg_match("/^notes_by[=|:](.*)$/i", $event->term, $matches)) {
 			global $database;
 			$user = User::by_name($matches[1]);
 			if(!is_null($user)) {
@@ -231,7 +231,7 @@ class Notes extends Extension {
 
 			$event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM notes WHERE user_id = $user_id)"));
 		}
-		else if(preg_match("/notes_by_userno=([0-9]+)/i", $event->term, $matches)) {
+		else if(preg_match("/^notes_by_userno[=|:](\d+)$/i", $event->term, $matches)) {
 			$user_id = int_escape($matches[1]);
 			$event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM notes WHERE user_id = $user_id)"));
 		}
diff --git a/ext/rating/main.php b/ext/rating/main.php
index be54cdb4..1b41b89f 100644
--- a/ext/rating/main.php
+++ b/ext/rating/main.php
@@ -116,7 +116,7 @@ class Ratings extends Extension {
 			$set = Ratings::privs_to_sql(Ratings::get_user_privs($user));
 			$event->add_querylet(new Querylet("rating IN ($set)"));
 		}
-		if(preg_match("/^rating=(?:([sqeu]+)|(safe|questionable|explicit|unknown))$/D", strtolower($event->term), $matches)) {
+		if(preg_match("/^rating[=|:](?:([sqeu]+)|(safe|questionable|explicit|unknown))$/D", strtolower($event->term), $matches)) {
 			$ratings = $matches[1] ? $matches[1] : array($matches[2][0]);
 			$ratings = array_intersect(str_split($ratings), str_split(Ratings::get_user_privs($user)));
 			$set = "'" . join("', '", $ratings) . "'";
@@ -199,7 +199,7 @@ class Ratings extends Extension {
 
 	private function no_rating_query($context) {
 		foreach($context as $term) {
-			if(preg_match("/^rating=/", $term)) {
+			if(preg_match("/^rating[=|:]/", $term)) {
 				return false;
 			}
 		}
diff --git a/ext/user/main.php b/ext/user/main.php
index 5a4058ca..67755004 100644
--- a/ext/user/main.php
+++ b/ext/user/main.php
@@ -309,7 +309,7 @@ class UserPage extends Extension {
 		global $user;
 
 		$matches = array();
-		if(preg_match("/^(poster|user)=(.*)$/i", $event->term, $matches)) {
+		if(preg_match("/^(poster|user)[=|:](.*)$/i", $event->term, $matches)) {
 			$duser = User::by_name($matches[2]);
 			if(!is_null($duser)) {
 				$user_id = $duser->id;
@@ -319,11 +319,11 @@ class UserPage extends Extension {
 			}
 			$event->add_querylet(new Querylet("images.owner_id = $user_id"));
 		}
-		else if(preg_match("/^(poster|user)_id=([0-9]+)$/i", $event->term, $matches)) {
+		else if(preg_match("/^(poster|user)_id[=|:]([0-9]+)$/i", $event->term, $matches)) {
 			$user_id = int_escape($matches[2]);
 			$event->add_querylet(new Querylet("images.owner_id = $user_id"));
 		}
-		else if($user->can("view_ip") && preg_match("/^(poster|user)_ip=([0-9\.]+)$/i", $event->term, $matches)) {
+		else if($user->can("view_ip") && preg_match("/^(poster|user)_ip[=|:]([0-9\.]+)$/i", $event->term, $matches)) {
 			$user_ip = $matches[2]; // FIXME: ip_escape?
 			$event->add_querylet(new Querylet("images.owner_ip = '$user_ip'"));
 		}

From 14899e79ad1ec00acb556607ca0928b517045911 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Thu, 2 Jan 2014 14:10:08 +0000
Subject: [PATCH 17/30] added height & width metatags

---
 ext/index/main.php | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/ext/index/main.php b/ext/index/main.php
index 4acc663c..20820fe2 100644
--- a/ext/index/main.php
+++ b/ext/index/main.php
@@ -21,6 +21,16 @@
  *        <li>size&gt;=500x500 -- no small images
  *        <li>size&lt;1000x1000 -- no large images
  *      </ul>
+ *    <li>width (=, &lt;, &gt;, &lt;=, &gt;=) width, eg
+ *      <ul>
+ *        <li>width=1024 -- find images with 1024 width
+ *        <li>width>2000 -- find images bigger than 2000 width
+ *      </ul>
+ *    <li>height (=, &lt;, &gt;, &lt;=, &gt;=) height, eg
+ *      <ul>
+ *        <li>height=768 -- find images with 768 height
+ *        <li>height>1000 -- find images bigger than 1000 height
+ *      </ul>
  *    <li>ratio (=, &lt;, &gt;, &lt;=, &gt;=) width : height, eg
  *      <ul>
  *        <li>ratio=4:3, ratio=16:9 -- standard wallpaper
@@ -302,6 +312,14 @@ class Index extends Extension {
 			$args = array("width{$this->stpen}"=>int_escape($matches[2]), "height{$this->stpen}"=>int_escape($matches[3]));
 			$event->add_querylet(new Querylet("width $cmp :width{$this->stpen} AND height $cmp :height{$this->stpen}", $args));
 		}
+		else if(preg_match("/^width([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/", $event->term, $matches)) {
+			$cmp = ltrim($matches[1], ":") ?: "=";
+			$event->add_querylet(new Querylet("width $cmp :width{$this->stpen}", array("width{$this->stpen}"=>int_escape($matches[2]))));
+		}
+		else if(preg_match("/^height([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/", $event->term, $matches)) {
+			$cmp = ltrim($matches[1], ":") ?: "=";
+			$event->add_querylet(new Querylet("height $cmp :height{$this->stpen}",array("height{$this->stpen}"=>int_escape($matches[2]))));
+		}
 
 		$this->stpen++;
 	}

From 9f06a5c5656049c923d5c6361c0217d010729b86 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Fri, 3 Jan 2014 00:41:28 +0000
Subject: [PATCH 18/30] fix search not working properly for aliases to multiple
 tags fix issue 359

---
 core/imageboard.pack.php | 27 +++++++++++++++++++--------
 1 file changed, 19 insertions(+), 8 deletions(-)

diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php
index c9a2ce4a..eb34035e 100644
--- a/core/imageboard.pack.php
+++ b/core/imageboard.pack.php
@@ -663,6 +663,8 @@ class Image {
 			}
 		}
 
+		$terms = Tag::resolve_aliases($terms);
+
 		// parse the words that are searched for into
 		// various types of querylet
 		foreach($terms as $term) {
@@ -675,8 +677,6 @@ class Image {
 				continue;
 			}
 
-			$term = Tag::resolve_alias($term);
-
 			$stpe = new SearchTermParseEvent($term, $terms);
 			send_event($stpe);
 			if($stpe->is_querylet_set()) {
@@ -824,6 +824,8 @@ class Image {
 			}
 		}
 
+		$terms = Tag::resolve_aliases($terms);
+
 		reset($terms); // rewind to first element in array.
 		
 		// turn each term into a specific type of querylet
@@ -833,8 +835,6 @@ class Image {
 				$negative = true;
 				$term = substr($term, 1);
 			}
-			
-			$term = Tag::resolve_alias($term);
 
 			$stpe = new SearchTermParseEvent($term, $terms);
 			send_event($stpe);
@@ -1082,11 +1082,22 @@ class Tag {
 		assert(is_array($tags));
 
 		$new = array();
-		foreach($tags as $tag) {
-			$new_set = explode(' ', Tag::resolve_alias($tag));
-			foreach($new_set as $new_one) {
-				$new[] = $new_one;
+
+		$i = 0;
+		$tag_count = count($tags);
+		while($i<$tag_count) {
+			$aliases = explode(' ', Tag::resolve_alias($tags[$i]));
+			foreach($aliases as $alias){
+				if(!in_array($alias, $new)){
+					if($tags[$i] == $alias){
+						$new[] = $alias;
+					}elseif(!in_array($alias, $tags)){
+						$tags[] = $alias;
+						$tag_count++;
+					}
+				}
 			}
+			$i++;
 		}
 
 		$new = array_iunique($new); // remove any duplicate tags

From 9cae856df7a408ec874a7b270eb0e03e6678a774 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Fri, 3 Jan 2014 00:58:53 +0000
Subject: [PATCH 19/30] use the Content-Disposition header for filename &
 Content-Type for extension if either doesn't exist, it will fallback to using
 pathinfo

---
 core/util.inc.php   | 64 +++++++++++++++++++++++++++++++++++++++++----
 ext/upload/main.php | 14 +++++-----
 2 files changed, 67 insertions(+), 11 deletions(-)

diff --git a/core/util.inc.php b/core/util.inc.php
index b1331a4a..b63c0d43 100644
--- a/core/util.inc.php
+++ b/core/util.inc.php
@@ -801,17 +801,24 @@ function transload($url, $mfile) {
 		$ch = curl_init($url);
 		$fp = fopen($mfile, "w");
 
-		curl_setopt($ch, CURLOPT_FILE, $fp);
-		curl_setopt($ch, CURLOPT_HEADER, 0);
+		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+		curl_setopt($ch, CURLOPT_VERBOSE, 1);
+		curl_setopt($ch, CURLOPT_HEADER, 1);
 		curl_setopt($ch, CURLOPT_REFERER, $url);
 		curl_setopt($ch, CURLOPT_USERAGENT, "Shimmie-".VERSION);
 		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
 
-		curl_exec($ch);
+		$response = curl_exec($ch);
+
+		$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
+		$headers = http_parse_headers(implode("\n", preg_split('/\R/', rtrim(substr($response, 0, $header_size)))));
+		$body = substr($response, $header_size);
+
 		curl_close($ch);
+		fwrite($fp, $body);
 		fclose($fp);
 
-		return true;
+		return $headers;
 	}
 
 	if($config->get_string("transload_engine") === "wget") {
@@ -839,12 +846,59 @@ function transload($url, $mfile) {
 		fwrite($fp, $data);
 		fclose($fp);
 
-		return true;
+		$headers = http_parse_headers(implode("\n", $http_response_header));
+
+		return $headers;
 	}
 
 	return false;
 }
 
+if (!function_exists('http_parse_headers')) { #http://www.php.net/manual/en/function.http-parse-headers.php#112917
+	function http_parse_headers ($raw_headers){
+		$headers = array(); // $headers = [];
+
+		foreach (explode("\n", $raw_headers) as $i => $h) {
+			$h = explode(':', $h, 2);
+
+			if (isset($h[1])){
+				if(!isset($headers[$h[0]])){
+					$headers[$h[0]] = trim($h[1]);
+				}else if(is_array($headers[$h[0]])){
+					$tmp = array_merge($headers[$h[0]],array(trim($h[1])));
+					$headers[$h[0]] = $tmp;
+				}else{
+					$tmp = array_merge(array($headers[$h[0]]),array(trim($h[1])));
+					$headers[$h[0]] = $tmp;
+				}
+			}
+		}
+		return $headers;
+	}
+}
+
+function getExtension ($mime_type){
+	if(empty($mime_type)){
+		return false;
+	}
+
+	$extensions = array(
+		'image/jpeg' => 'jpg',
+		'image/gif' => 'gif',
+		'image/png' => 'png',
+		'application/x-shockwave-flash' => 'swf',
+		'image/x-icon' => 'ico',
+		'image/svg+xml' => 'svg',
+		'audio/mpeg' => 'mp3',
+		'video/x-flv' => 'flv',
+		'audio/mp4' => 'mp4',
+		'video/mp4' => 'mp4',
+		'audio/webm' => 'webm',
+		'video/webm' => 'webm'
+	);
+
+    return $extensions[$mime_type];
+}
 
 $_included = array();
 /**
diff --git a/ext/upload/main.php b/ext/upload/main.php
index 2e7a073e..b92fc479 100644
--- a/ext/upload/main.php
+++ b/ext/upload/main.php
@@ -329,9 +329,12 @@ class Upload extends Extension {
 		}
 
 		$tmp_filename = tempnam(ini_get('upload_tmp_dir'), "shimmie_transload");
-		$filename = basename($url);
 
-		if(!transload($url, $tmp_filename)) {
+		$headers = transload($url, $tmp_filename);
+		$h_filename = (isset($headers['Content-Disposition']) ? preg_replace('/^.*filename="([^ ]+)"/i', '$1', $headers['Content-Disposition']) : null);
+		$filename = $h_filename ?: basename($url);
+
+		if(!$headers) {
 			$this->theme->display_upload_error($page, "Error with ".html_escape($filename),
 				"Error reading from ".html_escape($url));
 			return false;
@@ -341,12 +344,11 @@ class Upload extends Extension {
 			$this->theme->display_upload_error($page, "Error with ".html_escape($filename),
 				"No data found -- perhaps the site has hotlink protection?");
 			$ok = false;
-		}
-		else {
+		}else{
 			global $user;
 			$pathinfo = pathinfo($url);
-			$metadata['filename'] = $pathinfo['basename'];
-			$metadata['extension'] = $pathinfo['extension'];
+			$metadata['filename'] = $filename;
+			$metadata['extension'] = getExtension($headers['Content-Type']) ?: $pathinfo['extension'];
 			$metadata['tags'] = $tags;
 			$metadata['source'] = $source;
 			

From 2c2f27ca640985d49e3eb5134b53516687e91d80 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Fri, 3 Jan 2014 01:24:55 +0000
Subject: [PATCH 20/30] add order metatag not too happy with how this
 works...but it does work

---
 core/imageboard.pack.php | 48 ++++++++++++++++++++++------------------
 ext/index/main.php       | 13 +++++++++++
 2 files changed, 40 insertions(+), 21 deletions(-)

diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php
index eb34035e..ac4d0ea5 100644
--- a/core/imageboard.pack.php
+++ b/core/imageboard.pack.php
@@ -26,6 +26,7 @@
 $tag_n = 0; // temp hack
 $_flexihash = null;
 $_fh_last_opts = null;
+$order_sql = null; // this feels ugly
 
 require_once "lib/flexihash.php";
 
@@ -114,7 +115,7 @@ class Image {
 		assert(is_numeric($start));
 		assert(is_numeric($limit));
 		assert(is_array($tags));
-		global $database, $user;
+		global $database, $user, $order_sql;
 
 		$images = array();
 
@@ -128,13 +129,15 @@ class Image {
 		}
 
 		$querylet = Image::build_search_querylet($tags);
-		$querylet->append(new Querylet("ORDER BY images.id DESC LIMIT :limit OFFSET :offset", array("limit"=>$limit, "offset"=>$start)));
+		$querylet->append(new Querylet($order_sql ?: " ORDER BY images.id DESC"));
+		$querylet->append(new Querylet(" LIMIT :limit OFFSET :offset", array("limit"=>$limit, "offset"=>$start)));
 		#var_dump($querylet->sql); var_dump($querylet->variables);
 		$result = $database->execute($querylet->sql, $querylet->variables);
 
 		while($row = $result->fetch()) {
 			$images[] = new Image($row);
 		}
+		$order_sql = null;
 		return $images;
 	}
 
@@ -663,8 +666,6 @@ class Image {
 			}
 		}
 
-		$terms = Tag::resolve_aliases($terms);
-
 		// parse the words that are searched for into
 		// various types of querylet
 		foreach($terms as $term) {
@@ -677,6 +678,15 @@ class Image {
 				continue;
 			}
 
+			$aliases = explode(" ", Tag::resolve_alias($term));
+			$found = array_search($term, $aliases);
+			if($found !== false){
+				unset($aliases[$found]);
+			}else{
+				$term = array_shift($aliases);
+			}
+			foreach($aliases as $alias)	array_push($terms, $alias);
+
 			$stpe = new SearchTermParseEvent($term, $terms);
 			send_event($stpe);
 			if($stpe->is_querylet_set()) {
@@ -824,8 +834,6 @@ class Image {
 			}
 		}
 
-		$terms = Tag::resolve_aliases($terms);
-
 		reset($terms); // rewind to first element in array.
 		
 		// turn each term into a specific type of querylet
@@ -835,6 +843,15 @@ class Image {
 				$negative = true;
 				$term = substr($term, 1);
 			}
+			
+			$aliases = explode(" ", Tag::resolve_alias($term));
+			$found = array_search($term, $aliases);
+			if($found !== false){
+				unset($aliases[$found]);
+			}else{
+				$term = array_shift($aliases);
+			}
+			foreach($aliases as $alias)	array_push($terms, $alias);
 
 			$stpe = new SearchTermParseEvent($term, $terms);
 			send_event($stpe);
@@ -1082,22 +1099,11 @@ class Tag {
 		assert(is_array($tags));
 
 		$new = array();
-
-		$i = 0;
-		$tag_count = count($tags);
-		while($i<$tag_count) {
-			$aliases = explode(' ', Tag::resolve_alias($tags[$i]));
-			foreach($aliases as $alias){
-				if(!in_array($alias, $new)){
-					if($tags[$i] == $alias){
-						$new[] = $alias;
-					}elseif(!in_array($alias, $tags)){
-						$tags[] = $alias;
-						$tag_count++;
-					}
-				}
+		foreach($tags as $tag) {
+			$new_set = explode(' ', Tag::resolve_alias($tag));
+			foreach($new_set as $new_one) {
+				$new[] = $new_one;
 			}
-			$i++;
 		}
 
 		$new = array_iunique($new); // remove any duplicate tags
diff --git a/ext/index/main.php b/ext/index/main.php
index 20820fe2..b2df34f0 100644
--- a/ext/index/main.php
+++ b/ext/index/main.php
@@ -87,6 +87,11 @@
  *      <ul>
  *        <li>source=http://example.com -- find all images with "http://example.com" in the source
  *      </ul>
+ *    <li>order (id, width, height, filesize, filename)_(ASC, DESC), eg
+ *      <ul>
+ *        <li>order=width -- find all images sorted from highest > lowest width
+ *        <li>order=filesize_asc -- find all images sorted from lowest > highest filesize
+ *      </ul>
  *  </ul>
  *  <p>Search items can be combined to search for images which match both,
  *  or you can stick "-" in front of an item to search for things that don't
@@ -320,6 +325,14 @@ class Index extends Extension {
 			$cmp = ltrim($matches[1], ":") ?: "=";
 			$event->add_querylet(new Querylet("height $cmp :height{$this->stpen}",array("height{$this->stpen}"=>int_escape($matches[2]))));
 		}
+		else if(preg_match("/^order[=|:](id|width|height|filesize|filename)[_]?(desc|asc)?$/i", $event->term, $matches)){
+			global $order_sql;
+			$order = strtolower($matches[1]);
+			$sort = isset($matches[2]) ? strtoupper($matches[2]) : (preg_match("/^(id|filename)$/", $matches[1]) ? "ASC" : "DESC");
+			// $event->add_querylet(new Querylet("ORDER BY images.:order :sort", array("order" => $order, "sort" => $sort)));
+			$event->add_querylet(new Querylet("1=1")); //small hack to avoid metatag being treated as normal tag
+			$order_sql = " ORDER BY images.$order $sort";
+		}
 
 		$this->stpen++;
 	}

From 152f5fbf26791c142eedc9bedb5d7ac0505b378d Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Fri, 3 Jan 2014 08:24:47 +0000
Subject: [PATCH 21/30] add config option for default order

---
 core/imageboard.pack.php | 4 ++--
 ext/index/main.php       | 6 +++---
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php
index ac4d0ea5..0bf145e9 100644
--- a/core/imageboard.pack.php
+++ b/core/imageboard.pack.php
@@ -115,7 +115,7 @@ class Image {
 		assert(is_numeric($start));
 		assert(is_numeric($limit));
 		assert(is_array($tags));
-		global $database, $user, $order_sql;
+		global $database, $user, $config, $order_sql;
 
 		$images = array();
 
@@ -129,7 +129,7 @@ class Image {
 		}
 
 		$querylet = Image::build_search_querylet($tags);
-		$querylet->append(new Querylet($order_sql ?: " ORDER BY images.id DESC"));
+		$querylet->append(new Querylet(" ORDER BY images.".($order_sql ?: $config->get_string("index_order"))));
 		$querylet->append(new Querylet(" LIMIT :limit OFFSET :offset", array("limit"=>$limit, "offset"=>$start)));
 		#var_dump($querylet->sql); var_dump($querylet->variables);
 		$result = $database->execute($querylet->sql, $querylet->variables);
diff --git a/ext/index/main.php b/ext/index/main.php
index b2df34f0..1831ec68 100644
--- a/ext/index/main.php
+++ b/ext/index/main.php
@@ -176,6 +176,7 @@ class Index extends Extension {
 		global $config;
 		$config->set_default_int("index_images", 24);
 		$config->set_default_bool("index_tips", true);
+		$config->set_default_string("index_order", "id DESC");
 	}
 
 	public function onPageRequest(PageRequestEvent $event) {
@@ -327,11 +328,10 @@ class Index extends Extension {
 		}
 		else if(preg_match("/^order[=|:](id|width|height|filesize|filename)[_]?(desc|asc)?$/i", $event->term, $matches)){
 			global $order_sql;
-			$order = strtolower($matches[1]);
+			$ord = strtolower($matches[1]);
 			$sort = isset($matches[2]) ? strtoupper($matches[2]) : (preg_match("/^(id|filename)$/", $matches[1]) ? "ASC" : "DESC");
-			// $event->add_querylet(new Querylet("ORDER BY images.:order :sort", array("order" => $order, "sort" => $sort)));
+			$order_sql = "$ord $sort";
 			$event->add_querylet(new Querylet("1=1")); //small hack to avoid metatag being treated as normal tag
-			$order_sql = " ORDER BY images.$order $sort";
 		}
 
 		$this->stpen++;

From 7d49e21792297a79566241aa0f5e76a1b5734711 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Sun, 5 Jan 2014 11:52:09 +0000
Subject: [PATCH 22/30] readability + moved stuff

---
 core/util.inc.php  | 47 +++++++++++++++++++++++-----------------------
 ext/index/main.php |  5 +++--
 2 files changed, 27 insertions(+), 25 deletions(-)

diff --git a/core/util.inc.php b/core/util.inc.php
index b63c0d43..09dbe7fe 100644
--- a/core/util.inc.php
+++ b/core/util.inc.php
@@ -556,6 +556,30 @@ function getMimeType($file, $ext="") {
 	return 'application/octet-stream';
 }
 
+
+function getExtension ($mime_type){
+	if(empty($mime_type)){
+		return false;
+	}
+
+	$extensions = array(
+		'image/jpeg' => 'jpg',
+		'image/gif' => 'gif',
+		'image/png' => 'png',
+		'application/x-shockwave-flash' => 'swf',
+		'image/x-icon' => 'ico',
+		'image/svg+xml' => 'svg',
+		'audio/mpeg' => 'mp3',
+		'video/x-flv' => 'flv',
+		'audio/mp4' => 'mp4',
+		'video/mp4' => 'mp4',
+		'audio/webm' => 'webm',
+		'video/webm' => 'webm'
+	);
+
+    return $extensions[$mime_type];
+}
+
 /**
  * @private
  */
@@ -877,29 +901,6 @@ if (!function_exists('http_parse_headers')) { #http://www.php.net/manual/en/func
 	}
 }
 
-function getExtension ($mime_type){
-	if(empty($mime_type)){
-		return false;
-	}
-
-	$extensions = array(
-		'image/jpeg' => 'jpg',
-		'image/gif' => 'gif',
-		'image/png' => 'png',
-		'application/x-shockwave-flash' => 'swf',
-		'image/x-icon' => 'ico',
-		'image/svg+xml' => 'svg',
-		'audio/mpeg' => 'mp3',
-		'video/x-flv' => 'flv',
-		'audio/mp4' => 'mp4',
-		'video/mp4' => 'mp4',
-		'audio/webm' => 'webm',
-		'video/webm' => 'webm'
-	);
-
-    return $extensions[$mime_type];
-}
-
 $_included = array();
 /**
  * Get the active contents of a .php file
diff --git a/ext/index/main.php b/ext/index/main.php
index 1831ec68..45f71f02 100644
--- a/ext/index/main.php
+++ b/ext/index/main.php
@@ -87,7 +87,7 @@
  *      <ul>
  *        <li>source=http://example.com -- find all images with "http://example.com" in the source
  *      </ul>
- *    <li>order (id, width, height, filesize, filename)_(ASC, DESC), eg
+ *    <li>order=(id, width, height, filesize, filename)_(ASC, DESC), eg
  *      <ul>
  *        <li>order=width -- find all images sorted from highest > lowest width
  *        <li>order=filesize_asc -- find all images sorted from lowest > highest filesize
@@ -329,7 +329,8 @@ class Index extends Extension {
 		else if(preg_match("/^order[=|:](id|width|height|filesize|filename)[_]?(desc|asc)?$/i", $event->term, $matches)){
 			global $order_sql;
 			$ord = strtolower($matches[1]);
-			$sort = isset($matches[2]) ? strtoupper($matches[2]) : (preg_match("/^(id|filename)$/", $matches[1]) ? "ASC" : "DESC");
+			$default_order_for_column = preg_match("/^(id|filename)$/", $matches[1]) ? "ASC" : "DESC";
+			$sort = isset($matches[2]) ? strtoupper($matches[2]) : $default_order_for_column;
 			$order_sql = "$ord $sort";
 			$event->add_querylet(new Querylet("1=1")); //small hack to avoid metatag being treated as normal tag
 		}

From 325da1111913ccbbe5824c2d1e5cd7b6a5937b8f Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Mon, 13 Jan 2014 09:13:56 +0000
Subject: [PATCH 23/30] artist/comment/numeric_score metatags now work using :
 also updated docs

---
 ext/artists/main.php       |  2 +-
 ext/comment/main.php       |  8 ++++----
 ext/index/main.php         | 12 ++++++++++++
 ext/numeric_score/main.php | 12 ++++++------
 4 files changed, 23 insertions(+), 11 deletions(-)

diff --git a/ext/artists/main.php b/ext/artists/main.php
index d426d512..984b12af 100644
--- a/ext/artists/main.php
+++ b/ext/artists/main.php
@@ -45,7 +45,7 @@ class Artists extends Extension {
 
 	public function onSearchTermParse(SearchTermParseEvent $event) {
 		$matches = array();
-		if(preg_match("/^author=(.*)$/", $event->term, $matches)) {
+		if(preg_match("/^author[=|:](.*)$/", $event->term, $matches)) {
 			$char = $matches[1];
 			$event->add_querylet(new Querylet("Author = :author_char", array("author_char"=>$char)));
 		}
diff --git a/ext/comment/main.php b/ext/comment/main.php
index 48d4f013..1c472626 100644
--- a/ext/comment/main.php
+++ b/ext/comment/main.php
@@ -262,12 +262,12 @@ class CommentList extends Extension {
 
 	public function onSearchTermParse(SearchTermParseEvent $event) {
 		$matches = array();
-		if(preg_match("/comments(<|>|<=|>=|=)(\d+)/", $event->term, $matches)) {
-			$cmp = $matches[1];
+		if(preg_match("/^comments([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/", $event->term, $matches)) {
+			$cmp = ltrim($matches[1], ":") ?: "=";
 			$comments = $matches[2];
 			$event->add_querylet(new Querylet("images.id IN (SELECT DISTINCT image_id FROM comments GROUP BY image_id HAVING count(image_id) $cmp $comments)"));
 		}
-		else if(preg_match("/commented_by=(.*)/i", $event->term, $matches)) {
+		else if(preg_match("/^commented_by[=|:](.*)$/i", $event->term, $matches)) {
 			global $database;
 			$user = User::by_name($matches[1]);
 			if(!is_null($user)) {
@@ -279,7 +279,7 @@ class CommentList extends Extension {
 
 			$event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM comments WHERE owner_id = $user_id)"));
 		}
-		else if(preg_match("/commented_by_userid=([0-9]+)/i", $event->term, $matches)) {
+		else if(preg_match("/^commented_by_userno[=|:]([0-9]+)$/i", $event->term, $matches)) {
 			$user_id = int_escape($matches[1]);
 			$event->add_querylet(new Querylet("images.id IN (SELECT image_id FROM comments WHERE owner_id = $user_id)"));
 		}
diff --git a/ext/index/main.php b/ext/index/main.php
index 45f71f02..bd4f3e30 100644
--- a/ext/index/main.php
+++ b/ext/index/main.php
@@ -105,6 +105,8 @@
  *        <li>score (=, &lt;, &gt;, &lt;=, &gt;=) number -- seach by score
  *        <li>upvoted_by=Username -- search for a user's likes
  *        <li>downvoted_by=Username -- search for a user's dislikes
+ *        <li>upvoted_by_id=UserID -- search for a user's likes by user ID
+ *        <li>downvoted_by_id=UserID -- search for a user's dislikes by user ID
  *      </ul>
  *    <li>Image Rating
  *      <ul>
@@ -122,6 +124,16 @@
  *        <li>notes_by=Username -- search for a notes created by username
  *        <li>notes_by_userno=UserID -- search for a notes created by userID
  *      </ul>
+ *    <li>Artists
+ *      <ul>
+ *        <li>author=ArtistName -- search for images by artist
+ *      </ul>
+ *    <li>Image Comments
+ *      <ul>
+ *        <li>comments (=, &lt;, &gt;, &lt;=, &gt;=) number -- search for images by number of comments
+ *        <li>commented_by=Username -- search for a user's comments by username
+ *        <li>commented_by_userno=UserID -- search for a user's comments by userID
+ *      </ul>
  *  </ul>
  */
 
diff --git a/ext/numeric_score/main.php b/ext/numeric_score/main.php
index 9cf21ab3..2da74577 100644
--- a/ext/numeric_score/main.php
+++ b/ext/numeric_score/main.php
@@ -217,12 +217,12 @@ class NumericScore extends Extension {
 
 	public function onSearchTermParse(SearchTermParseEvent $event) {
 		$matches = array();
-		if(preg_match("/^score(<|<=|=|>=|>)(-?\d+)$/", $event->term, $matches)) {
-			$cmp = $matches[1];
+		if(preg_match("/^score([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(-?\d+)$/", $event->term, $matches)) {
+			$cmp = ltrim($matches[1], ":") ?: "=";
 			$score = $matches[2];
 			$event->add_querylet(new Querylet("numeric_score $cmp $score"));
 		}
-		if(preg_match("/^upvoted_by=(.*)$/", $event->term, $matches)) {
+		if(preg_match("/^upvoted_by[=|:](.*)$/", $event->term, $matches)) {
 			$duser = User::by_name($matches[1]);
 			if(is_null($duser)) {
 				throw new SearchTermParseException(
@@ -232,7 +232,7 @@ class NumericScore extends Extension {
 				"images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=1)",
 				array("ns_user_id"=>$duser->id)));
 		}
-		if(preg_match("/^downvoted_by=(.*)$/", $event->term, $matches)) {
+		if(preg_match("/^downvoted_by[=|:](.*)$/", $event->term, $matches)) {
 			$duser = User::by_name($matches[1]);
 			if(is_null($duser)) {
 				throw new SearchTermParseException(
@@ -242,13 +242,13 @@ class NumericScore extends Extension {
 				"images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=-1)",
 				array("ns_user_id"=>$duser->id)));
 		}
-		if(preg_match("/^upvoted_by_id=(\d+)$/", $event->term, $matches)) {
+		if(preg_match("/^upvoted_by_id[=|:](\d+)$/", $event->term, $matches)) {
 			$iid = int_escape($matches[1]);
 			$event->add_querylet(new Querylet(
 				"images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=1)",
 				array("ns_user_id"=>$iid)));
 		}
-		if(preg_match("/^downvoted_by_id=(\d+)$/", $event->term, $matches)) {
+		if(preg_match("/^downvoted_by_id[=|:](\d+)$/", $event->term, $matches)) {
 			$iid = int_escape($matches[1]);
 			$event->add_querylet(new Querylet(
 				"images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=-1)",

From ae4da2b410234f425224369f98d06283551ef90a Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Mon, 13 Jan 2014 10:03:38 +0000
Subject: [PATCH 24/30] add option for getMimeType to return list of extensions

---
 core/util.inc.php | 26 ++++++++------------------
 1 file changed, 8 insertions(+), 18 deletions(-)

diff --git a/core/util.inc.php b/core/util.inc.php
index 09dbe7fe..a4c5b244 100644
--- a/core/util.inc.php
+++ b/core/util.inc.php
@@ -509,14 +509,15 @@ function captcha_check() {
 * @param string &$file File path
 * @return string
 */
-function getMimeType($file, $ext="") {
+function getMimeType($file, $ext="", $list=false) {
 
 	// Static extension lookup
 	$ext = strtolower($ext);
 	static $exts = array(
 		'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png',
 		'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon',
-		'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf',
+		'swf' => 'application/x-shockwave-flash', 'video/x-flv' => 'flv',
+		'svg' => 'image/svg+xml', 'pdf' => 'application/pdf',
 		'zip' => 'application/zip', 'gz' => 'application/x-gzip',
 		'tar' => 'application/x-tar', 'bz' => 'application/x-bzip',
 		'bz2' => 'application/x-bzip2', 'txt' => 'text/plain',
@@ -529,6 +530,8 @@ function getMimeType($file, $ext="") {
 		'mp4' => 'video/mp4', 'ogv' => 'video/ogg', 'webm' => 'video/webm'
 	);
 
+	if ($list == true){ return $exts; }
+
 	if (isset($exts[$ext])) { return $exts[$ext]; }
 
 	$type = false;
@@ -562,22 +565,9 @@ function getExtension ($mime_type){
 		return false;
 	}
 
-	$extensions = array(
-		'image/jpeg' => 'jpg',
-		'image/gif' => 'gif',
-		'image/png' => 'png',
-		'application/x-shockwave-flash' => 'swf',
-		'image/x-icon' => 'ico',
-		'image/svg+xml' => 'svg',
-		'audio/mpeg' => 'mp3',
-		'video/x-flv' => 'flv',
-		'audio/mp4' => 'mp4',
-		'video/mp4' => 'mp4',
-		'audio/webm' => 'webm',
-		'video/webm' => 'webm'
-	);
-
-    return $extensions[$mime_type];
+	$extensions = getMimeType(null, null, true);
+	$ext = array_search($mime_type, $extensions);
+	return ($ext ?: false);
 }
 
 /**

From ce256f5bf4a7a883243d063a89c628ddc92c8e76 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Tue, 14 Jan 2014 06:12:34 +0000
Subject: [PATCH 25/30] added pool & pool_by_name search metatags

---
 ext/index/main.php | 13 +++++++++----
 ext/pools/main.php | 16 ++++++++++++++++
 2 files changed, 25 insertions(+), 4 deletions(-)

diff --git a/ext/index/main.php b/ext/index/main.php
index bd4f3e30..0aadaf2b 100644
--- a/ext/index/main.php
+++ b/ext/index/main.php
@@ -121,8 +121,8 @@
  *    <li>Notes
  *      <ul>
  *        <li>notes (=, &lt;, &gt;, &lt;=, &gt;=) number -- search by the number of notes an image has
- *        <li>notes_by=Username -- search for a notes created by username
- *        <li>notes_by_userno=UserID -- search for a notes created by userID
+ *        <li>notes_by=Username -- search for images contains notes created by username
+ *        <li>notes_by_userno=UserID -- search for images contains notes created by userID
  *      </ul>
  *    <li>Artists
  *      <ul>
@@ -131,8 +131,13 @@
  *    <li>Image Comments
  *      <ul>
  *        <li>comments (=, &lt;, &gt;, &lt;=, &gt;=) number -- search for images by number of comments
- *        <li>commented_by=Username -- search for a user's comments by username
- *        <li>commented_by_userno=UserID -- search for a user's comments by userID
+ *        <li>commented_by=Username -- search for images contains user's comments by username
+ *        <li>commented_by_userno=UserID -- search for images contains user's comments by userID
+ *      </ul>
+ *    <li>Pools
+ *      <ul>
+ *        <li>pool=PoolID -- search for images in a pool by PoolID
+ *        <li>pool_by_name=PoolName -- search for images in a pool by PoolName. underscores are replaced with spaces
  *      </ul>
  *  </ul>
  */
diff --git a/ext/pools/main.php b/ext/pools/main.php
index 008838d3..8431a653 100644
--- a/ext/pools/main.php
+++ b/ext/pools/main.php
@@ -293,6 +293,22 @@ class Pools extends Extension {
 		}
 	}
 
+	public function onSearchTermParse(SearchTermParseEvent $event) {
+		$matches = array();
+		if(preg_match("/^pool[=|:]([0-9]+)$/", $event->term, $matches)) {
+			$poolID = $matches[1];
+			$event->add_querylet(new Querylet("images.id IN (SELECT DISTINCT image_id FROM pool_images WHERE pool_id = $poolID)"));
+		}
+		else if(preg_match("/^pool_by_name[=|:](.*)$/", $event->term, $matches)) {
+			$poolTitle = str_replace("_", " ", $matches[1]);
+
+			$pool = $this->get_single_pool_from_title($poolTitle);
+			$poolID = 0;
+			if ($pool){ $poolID = $pool['id']; }
+			$event->add_querylet(new Querylet("images.id IN (SELECT DISTINCT image_id FROM pool_images WHERE pool_id = $poolID)"));
+		}
+	}
+
 	public function add_post_from_tag(/*str*/ $poolTag, /*int*/ $imageID){
 		$poolTag = str_replace("_", " ", $poolTag);
 		//First check if pool tag is a title

From 55ff224ac0bd6354ef21a37cf9175a836f92a389 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Tue, 14 Jan 2014 06:27:12 +0000
Subject: [PATCH 26/30] added any/none options to the source/pool metatags

---
 ext/index/main.php | 14 +++++++++++---
 ext/pools/main.php | 10 ++++++++--
 2 files changed, 19 insertions(+), 5 deletions(-)

diff --git a/ext/index/main.php b/ext/index/main.php
index 0aadaf2b..4d41ed20 100644
--- a/ext/index/main.php
+++ b/ext/index/main.php
@@ -83,9 +83,11 @@
  *        <li>tags>=10 -- search for images with 10 or more tags
  *        <li>tags<25 -- search for images with less than 25 tags
  *      </ul>
- *    <li>source=url, eg
+ *    <li>source=(URL, any, none) eg
  *      <ul>
  *        <li>source=http://example.com -- find all images with "http://example.com" in the source
+ *        <li>source=any -- find all images with a source
+ *        <li>source=none -- find all images without a source
  *      </ul>
  *    <li>order=(id, width, height, filesize, filename)_(ASC, DESC), eg
  *      <ul>
@@ -136,7 +138,7 @@
  *      </ul>
  *    <li>Pools
  *      <ul>
- *        <li>pool=PoolID -- search for images in a pool by PoolID
+ *        <li>pool=(PoolID, any, none) -- search for images in a pool by PoolID.
  *        <li>pool_by_name=PoolName -- search for images in a pool by PoolName. underscores are replaced with spaces
  *      </ul>
  *  </ul>
@@ -323,7 +325,13 @@ class Index extends Extension {
 		}
 		else if(preg_match("/^(source)[=|:]([a-zA-Z0-9]*)$/i", $event->term, $matches)) {
 			$source = strtolower($matches[2]);
-			$event->add_querylet(new Querylet('images.source LIKE :src', array("src"=>"%$source%")));
+
+			if(preg_match("/^(any|none)$/", $source)){
+				$not = ($source == "any" ? "NOT" : "");
+				$event->add_querylet(new Querylet("images.source IS $not NULL"));
+			}else{
+				$event->add_querylet(new Querylet('images.source LIKE :src', array("src"=>"%$source%")));
+			}
 		}
 		else if(preg_match("/^posted([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])([0-9-]*)$/", $event->term, $matches)) {
 			$cmp = ltrim($matches[1], ":") ?: "=";
diff --git a/ext/pools/main.php b/ext/pools/main.php
index 8431a653..6bcd8760 100644
--- a/ext/pools/main.php
+++ b/ext/pools/main.php
@@ -295,9 +295,15 @@ class Pools extends Extension {
 
 	public function onSearchTermParse(SearchTermParseEvent $event) {
 		$matches = array();
-		if(preg_match("/^pool[=|:]([0-9]+)$/", $event->term, $matches)) {
+		if(preg_match("/^pool[=|:]([0-9]+|any|none)$/", $event->term, $matches)) {
 			$poolID = $matches[1];
-			$event->add_querylet(new Querylet("images.id IN (SELECT DISTINCT image_id FROM pool_images WHERE pool_id = $poolID)"));
+
+			if(preg_match("/^(any|none)$/", $poolID)){
+				$not = ($poolID == "none" ? "NOT" : "");
+				$event->add_querylet(new Querylet("images.id $not IN (SELECT DISTINCT image_id FROM pool_images)"));
+			}else{
+				$event->add_querylet(new Querylet("images.id IN (SELECT DISTINCT image_id FROM pool_images WHERE pool_id = $poolID)"));
+			}
 		}
 		else if(preg_match("/^pool_by_name[=|:](.*)$/", $event->term, $matches)) {
 			$poolTitle = str_replace("_", " ", $matches[1]);

From b5f70de49638241686ba64e7e9f6b02e4c6cfe28 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Tue, 14 Jan 2014 07:52:45 +0000
Subject: [PATCH 27/30] change source metatag regex to allow searching for URLs

---
 ext/index/main.php | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/ext/index/main.php b/ext/index/main.php
index 4d41ed20..9cf5abfc 100644
--- a/ext/index/main.php
+++ b/ext/index/main.php
@@ -123,8 +123,8 @@
  *    <li>Notes
  *      <ul>
  *        <li>notes (=, &lt;, &gt;, &lt;=, &gt;=) number -- search by the number of notes an image has
- *        <li>notes_by=Username -- search for images contains notes created by username
- *        <li>notes_by_userno=UserID -- search for images contains notes created by userID
+ *        <li>notes_by=Username -- search for images containing notes created by username
+ *        <li>notes_by_userno=UserID -- search for images containing notes created by userID
  *      </ul>
  *    <li>Artists
  *      <ul>
@@ -133,8 +133,8 @@
  *    <li>Image Comments
  *      <ul>
  *        <li>comments (=, &lt;, &gt;, &lt;=, &gt;=) number -- search for images by number of comments
- *        <li>commented_by=Username -- search for images contains user's comments by username
- *        <li>commented_by_userno=UserID -- search for images contains user's comments by userID
+ *        <li>commented_by=Username -- search for images containing user's comments by username
+ *        <li>commented_by_userno=UserID -- search for images containing user's comments by userID
  *      </ul>
  *    <li>Pools
  *      <ul>
@@ -323,7 +323,7 @@ class Index extends Extension {
 			$filename = strtolower($matches[2]);
 			$event->add_querylet(new Querylet("images.filename LIKE :filename{$this->stpen}", array("filename{$this->stpen}"=>"%$filename%")));
 		}
-		else if(preg_match("/^(source)[=|:]([a-zA-Z0-9]*)$/i", $event->term, $matches)) {
+		else if(preg_match("/^(source)[=|:](.*)$/i", $event->term, $matches)) {
 			$source = strtolower($matches[2]);
 
 			if(preg_match("/^(any|none)$/", $source)){

From b856b132354f7c359d43ec1d214d3643187f2fbf Mon Sep 17 00:00:00 2001
From: Diftraku <diftraku@derpy.me>
Date: Wed, 15 Jan 2014 23:06:27 +0200
Subject: [PATCH 28/30] Fix EXIF data throwing a notice when showing an image

---
 ext/handle_pixel/theme.php | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/ext/handle_pixel/theme.php b/ext/handle_pixel/theme.php
index 4099e97f..74f9af4c 100644
--- a/ext/handle_pixel/theme.php
+++ b/ext/handle_pixel/theme.php
@@ -13,6 +13,10 @@ class PixelFileHandlerTheme extends Themelet {
 				foreach ($exif as $key => $section) {
 					foreach ($section as $name => $val) {
 						if($key == "IFD0") {
+                            // Cheap fix for array'd values in EXIF-data
+                            if (is_array($val)) {
+                                $val = implode(',', $val);
+                            }
 							$head .= html_escape("$name: $val")."<br>\n";
 						}
 					}

From 85303d232eecccb4685d75b3ba63c1ef04a506d6 Mon Sep 17 00:00:00 2001
From: Diftraku <diftraku@derpy.me>
Date: Wed, 15 Jan 2014 23:28:40 +0200
Subject: [PATCH 29/30] Fixing stuff with API output being output twice

Also some code formatting and a redirect from post/show for clients such
as CartonBox so you can actually view the image after opening it in the
browser on the client.
---
 ext/ouroboros_api/main.php | 297 +++++++++++++++++++++----------------
 1 file changed, 168 insertions(+), 129 deletions(-)

diff --git a/ext/ouroboros_api/main.php b/ext/ouroboros_api/main.php
index b16abbc7..cbfb38ae 100644
--- a/ext/ouroboros_api/main.php
+++ b/ext/ouroboros_api/main.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Name: Ouroboros API
  * Author: Diftraku <diftraku[at]derpy.me>
@@ -213,7 +214,7 @@ class _SafeOuroborosImage
         if (defined('ENABLED_EXTS')) {
             if (strstr(ENABLED_EXTS, 'rating') !== false) {
                 // 'u' is not a "valid" rating
-                if($img->rating == 's' || $img->rating == 'q' || $img->rating == 'e') {
+                if ($img->rating == 's' || $img->rating == 'q' || $img->rating == 'e') {
                     $this->rating = $img->rating;
                 }
             }
@@ -239,7 +240,9 @@ class _SafeOuroborosImage
         $this->sample_url = make_http($img->get_image_link());
     }
 }
-class OuroborosPost extends _SafeOuroborosImage {
+
+class OuroborosPost extends _SafeOuroborosImage
+{
     /**
      * Multipart File
      * @var array
@@ -265,7 +268,8 @@ class OuroborosPost extends _SafeOuroborosImage {
      * @TODO implement more validation from OuroborosAPI
      * @param array $post
      */
-    public function __construct(array $post) {
+    public function __construct(array $post)
+    {
         if (array_key_exists('tags', $post)) {
             $this->tags = $post['tags'];
         }
@@ -305,6 +309,7 @@ class OuroborosPost extends _SafeOuroborosImage {
         }
     }
 }
+
 class _SafeOuroborosTag
 {
     public $ambiguous = false;
@@ -320,6 +325,7 @@ class _SafeOuroborosTag
         $this->name = $tag['tag'];
     }
 }
+
 class OuroborosAPI extends Extension
 {
     private $event;
@@ -369,8 +375,7 @@ class OuroborosAPI extends Extension
             $this->type = $matches[1];
             if ($this->type == 'json') {
                 $page->set_type('application/json; charset=utf-8');
-            }
-            elseif ($this->type == 'xml') {
+            } elseif ($this->type == 'xml') {
                 $page->set_type('text/xml; charset=utf-8');
             }
             $page->set_mode('data');
@@ -380,59 +385,105 @@ class OuroborosAPI extends Extension
                 if ($this->match('create')) {
                     // Create
                     // @TODO Should move the validation logic into OuroborosPost instead?
-                    if($user->can("create_image")) {
+                    if ($user->can("create_image")) {
                         $post = array(
-                            'tags' => !empty($_REQUEST['post']['tags']) ? filter_var($_REQUEST['post']['tags'], FILTER_SANITIZE_STRING) : 'tagme',
-                            'file' => !empty($_REQUEST['post']['file']) ? filter_var($_REQUEST['post']['file'], FILTER_UNSAFE_RAW) : null,
-                            'rating' => !empty($_REQUEST['post']['rating']) ? filter_var($_REQUEST['post']['rating'], FILTER_SANITIZE_NUMBER_INT) : 'q',
-                            'source' => !empty($_REQUEST['post']['source']) ? filter_var(urldecode($_REQUEST['post']['source']), FILTER_SANITIZE_URL) : null,
-                            'sourceurl' => !empty($_REQUEST['post']['sourceurl']) ? filter_var(urldecode($_REQUEST['post']['sourceurl']), FILTER_SANITIZE_URL) : '',
-                            'description' => !empty($_REQUEST['post']['description']) ? filter_var($_REQUEST['post']['description'], FILTER_SANITIZE_STRING) : '',
-                            'is_rating_locked' => !empty($_REQUEST['post']['is_rating_locked']) ? filter_var($_REQUEST['post']['is_rating_locked'], FILTER_SANITIZE_NUMBER_INT) : false,
-                            'is_note_locked' => !empty($_REQUEST['post']['is_note_locked']) ? filter_var($_REQUEST['post']['is_note_locked'], FILTER_SANITIZE_NUMBER_INT) : false,
-                            'parent_id' => !empty($_REQUEST['post']['parent_id']) ? filter_var($_REQUEST['post']['parent_id'], FILTER_SANITIZE_NUMBER_INT) : null,
+                            'tags' => !empty($_REQUEST['post']['tags']) ? filter_var(
+                                    urldecode($_REQUEST['post']['tags']),
+                                    FILTER_SANITIZE_STRING
+                                ) : 'tagme',
+                            'file' => !empty($_REQUEST['post']['file']) ? filter_var(
+                                    $_REQUEST['post']['file'],
+                                    FILTER_UNSAFE_RAW
+                                ) : null,
+                            'rating' => !empty($_REQUEST['post']['rating']) ? filter_var(
+                                    $_REQUEST['post']['rating'],
+                                    FILTER_SANITIZE_NUMBER_INT
+                                ) : 'q',
+                            'source' => !empty($_REQUEST['post']['source']) ? filter_var(
+                                    urldecode($_REQUEST['post']['source']),
+                                    FILTER_SANITIZE_URL
+                                ) : null,
+                            'sourceurl' => !empty($_REQUEST['post']['sourceurl']) ? filter_var(
+                                    urldecode($_REQUEST['post']['sourceurl']),
+                                    FILTER_SANITIZE_URL
+                                ) : '',
+                            'description' => !empty($_REQUEST['post']['description']) ? filter_var(
+                                    $_REQUEST['post']['description'],
+                                    FILTER_SANITIZE_STRING
+                                ) : '',
+                            'is_rating_locked' => !empty($_REQUEST['post']['is_rating_locked']) ? filter_var(
+                                    $_REQUEST['post']['is_rating_locked'],
+                                    FILTER_SANITIZE_NUMBER_INT
+                                ) : false,
+                            'is_note_locked' => !empty($_REQUEST['post']['is_note_locked']) ? filter_var(
+                                    $_REQUEST['post']['is_note_locked'],
+                                    FILTER_SANITIZE_NUMBER_INT
+                                ) : false,
+                            'parent_id' => !empty($_REQUEST['post']['parent_id']) ? filter_var(
+                                    $_REQUEST['post']['parent_id'],
+                                    FILTER_SANITIZE_NUMBER_INT
+                                ) : null,
                         );
                         $md5 = !empty($_REQUEST['md5']) ? filter_var($_REQUEST['md5'], FILTER_SANITIZE_STRING) : null;
                         $this->postCreate(new OuroborosPost($post), $md5);
-                    }
-                    else {
+                    } else {
                         $this->sendResponse(403, 'You cannot create new posts');
                     }
 
-                }
-                elseif ($this->match('update')) {
+                } elseif ($this->match('update')) {
                     // Update
                     //@todo add post update
-                }
-                elseif ($this->match('show')) {
+                } elseif ($this->match('show')) {
                     // Show
                     $id = !empty($_REQUEST['id']) ? filter_var($_REQUEST['id'], FILTER_SANITIZE_NUMBER_INT) : null;
                     $this->postShow($id);
-                }
-                elseif ($this->match('index') || $this->match('list')) {
+                } elseif ($this->match('index') || $this->match('list')) {
                     // List
-                    $limit = !empty($_REQUEST['limit']) ? intval(filter_var($_REQUEST['limit'], FILTER_SANITIZE_NUMBER_INT)) : 45;
-                    $p = !empty($_REQUEST['page']) ? intval(filter_var($_REQUEST['page'], FILTER_SANITIZE_NUMBER_INT)) : 1;
+                    $limit = !empty($_REQUEST['limit']) ? intval(
+                        filter_var($_REQUEST['limit'], FILTER_SANITIZE_NUMBER_INT)
+                    ) : 45;
+                    $p = !empty($_REQUEST['page']) ? intval(
+                        filter_var($_REQUEST['page'], FILTER_SANITIZE_NUMBER_INT)
+                    ) : 1;
                     $tags = !empty($_REQUEST['tags']) ? filter_var($_REQUEST['tags'], FILTER_SANITIZE_STRING) : array();
                     if (!empty($tags)) {
                         $tags = Tag::explode($tags);
                     }
                     $this->postIndex($limit, $p, $tags);
                 }
-            }
-            elseif ($event->page_matches('tag')) {
+            } elseif ($event->page_matches('tag')) {
                 if ($this->match('index') || $this->match('list')) {
-                    $limit = !empty($_REQUEST['limit']) ? intval(filter_var($_REQUEST['limit'], FILTER_SANITIZE_NUMBER_INT)) : 50;
-                    $p = !empty($_REQUEST['page']) ? intval(filter_var($_REQUEST['page'], FILTER_SANITIZE_NUMBER_INT)) : 1;
-                    $order = (!empty($_REQUEST['order']) && ($_REQUEST['order'] == 'date' || $_REQUEST['order'] == 'count' || $_REQUEST['order'] == 'name')) ? filter_var($_REQUEST['order'], FILTER_SANITIZE_STRING) : 'date';
-                    $id = !empty($_REQUEST['id']) ? intval(filter_var($_REQUEST['id'], FILTER_SANITIZE_NUMBER_INT)) : null;
-                    $after_id = !empty($_REQUEST['after_id']) ? intval(filter_var($_REQUEST['after_id'], FILTER_SANITIZE_NUMBER_INT)) : null;
+                    $limit = !empty($_REQUEST['limit']) ? intval(
+                        filter_var($_REQUEST['limit'], FILTER_SANITIZE_NUMBER_INT)
+                    ) : 50;
+                    $p = !empty($_REQUEST['page']) ? intval(
+                        filter_var($_REQUEST['page'], FILTER_SANITIZE_NUMBER_INT)
+                    ) : 1;
+                    $order = (!empty($_REQUEST['order']) && ($_REQUEST['order'] == 'date' || $_REQUEST['order'] == 'count' || $_REQUEST['order'] == 'name')) ? filter_var(
+                        $_REQUEST['order'],
+                        FILTER_SANITIZE_STRING
+                    ) : 'date';
+                    $id = !empty($_REQUEST['id']) ? intval(
+                        filter_var($_REQUEST['id'], FILTER_SANITIZE_NUMBER_INT)
+                    ) : null;
+                    $after_id = !empty($_REQUEST['after_id']) ? intval(
+                        filter_var($_REQUEST['after_id'], FILTER_SANITIZE_NUMBER_INT)
+                    ) : null;
                     $name = !empty($_REQUEST['name']) ? filter_var($_REQUEST['name'], FILTER_SANITIZE_STRING) : '';
-                    $name_pattern = !empty($_REQUEST['name_pattern']) ? filter_var($_REQUEST['name_pattern'], FILTER_SANITIZE_STRING) : '';
+                    $name_pattern = !empty($_REQUEST['name_pattern']) ? filter_var(
+                        $_REQUEST['name_pattern'],
+                        FILTER_SANITIZE_STRING
+                    ) : '';
                     $this->tagIndex($limit, $p, $order, $id, $after_id, $name, $name_pattern);
                 }
             }
+        } elseif ($event->page_matches('post/show')) {
+            $page->set_mode('redirect');
+            $page->set_redirect(make_link(str_replace('post/show', 'post/view', implode('/', $event->args))));
+            $page->display();
+            die();
         }
+
     }
 
     /**
@@ -444,12 +495,14 @@ class OuroborosAPI extends Extension
      * @param OuroborosPost $post
      * @param string $md5
      */
-    protected function postCreate(OuroborosPost $post, $md5 = '') {
+    protected function postCreate(OuroborosPost $post, $md5 = '')
+    {
         global $page, $config, $user;
         if (!empty($md5)) {
             $img = Image::by_hash($md5);
             if (!is_null($img)) {
                 $this->sendResponse(420, self::ERROR_POST_CREATE_DUPE);
+                return;
             }
         }
         $meta = array();
@@ -461,42 +514,20 @@ class OuroborosAPI extends Extension
             }
         }
         // Check where we should try for the file
-        if (empty($post->file) && !empty($post->file_url) && filter_var($post->file_url, FILTER_VALIDATE_URL) !== false) {
+        if (empty($post->file) && !empty($post->file_url) && filter_var(
+                $post->file_url,
+                FILTER_VALIDATE_URL
+            ) !== false
+        ) {
             // Transload from source
-            $meta['file'] = tempnam('/tmp', 'shimmie_transload_'.$config->get_string('transload_engine'));
+            $meta['file'] = tempnam('/tmp', 'shimmie_transload_' . $config->get_string('transload_engine'));
             $meta['filename'] = basename($post->file_url);
-            if ($config->get_string('transload_engine') == 'fopen') {
-                $fp = fopen($post->file_url, 'r');
-                if (!$fp) {
-                    $this->sendResponse(500, 'fopen failed');
-                }
-
-                $data = "";
-                $length = 0;
-                while (!feof($fp) && $length <= $config->get_int('upload_size')) {
-                    $data .= fread($fp, 8192);
-                    $length = strlen($data);
-                }
-                fclose($fp);
-
-                $fp = fopen($meta['file'], 'w');
-                fwrite($fp, $data);
-                fclose($fp);
-            }
-            elseif ($config->get_string('transload_engine') == 'curl') {
-                $ch = curl_init($post->file_url);
-                $fp = fopen($meta['file'], 'w');
-
-                curl_setopt($ch, CURLOPT_FILE, $fp);
-                curl_setopt($ch, CURLOPT_HEADER, 0);
-
-                curl_exec($ch);
-                curl_close($ch);
-                fclose($fp);
+            if (!transload($post->file_url, $meta['file'])) {
+                $this->sendResponse(500, 'Transloading failed');
+                return;
             }
             $meta['hash'] = md5_file($meta['file']);
-        }
-        else {
+        } else {
             // Use file
             $meta['file'] = $post->file['tmp_name'];
             $meta['filename'] = $post->file['name'];
@@ -504,11 +535,13 @@ class OuroborosAPI extends Extension
         }
         if (!empty($md5) && $md5 !== $meta['hash']) {
             $this->sendResponse(420, self::ERROR_POST_CREATE_MD5);
+            return;
         }
         if (!empty($meta['hash'])) {
             $img = Image::by_hash($meta['hash']);
             if (!is_null($img)) {
                 $this->sendResponse(420, self::ERROR_POST_CREATE_DUPE);
+                return;
             }
         }
         $meta['extension'] = pathinfo($meta['filename'], PATHINFO_EXTENSION);
@@ -517,15 +550,17 @@ class OuroborosAPI extends Extension
             send_event($upload);
             $image = Image::by_hash($meta['hash']);
             if (!is_null($image)) {
-                $this->sendResponse(200, make_link('post/view/'.$image->id), true);
-            }
-            else {
+                $this->sendResponse(200, make_link('post/view/' . $image->id), true);
+                return;
+            } else {
                 // Fail, unsupported file?
                 $this->sendResponse(500, 'Unknown error');
+                return;
             }
         } catch (UploadException $e) {
             // Cleanup in case shit hit the fan
             $this->sendResponse(500, $e->getMessage());
+            return;
         }
     }
 
@@ -533,12 +568,12 @@ class OuroborosAPI extends Extension
      * Wrapper for getting a single post
      * @param int $id
      */
-    protected function postShow($id = null) {
+    protected function postShow($id = null)
+    {
         if (!is_null($id)) {
             $post = new _SafeOuroborosImage(Image::by_id($id));
             $this->sendData('post', $post);
-        }
-        else {
+        } else {
             $this->sendResponse(424, 'ID is mandatory');
         }
     }
@@ -549,8 +584,9 @@ class OuroborosAPI extends Extension
      * @param $page
      * @param $tags
      */
-    protected function postIndex($limit, $page, $tags) {
-        $start = ( $page - 1 ) * $limit;
+    protected function postIndex($limit, $page, $tags)
+    {
+        $start = ($page - 1) * $limit;
         $results = Image::find_images(max($start, 0), min($limit, 100), $tags);
         $posts = array();
         foreach ($results as $img) {
@@ -576,35 +612,47 @@ class OuroborosAPI extends Extension
      * @param $name
      * @param $name_pattern
      */
-    protected function tagIndex($limit, $page, $order, $id, $after_id, $name, $name_pattern) {
+    protected function tagIndex($limit, $page, $order, $id, $after_id, $name, $name_pattern)
+    {
         global $database, $config;
-        $start = ( $page - 1 ) * $limit;
+        $start = ($page - 1) * $limit;
         $tag_data = array();
         switch ($order) {
             case 'name':
-                $tag_data = $database->get_col($database->scoreql_to_sql("
-                                SELECT DISTINCT
-                                    id, SCORE_STRNORM(substr(tag, 1, 1)), count
-                                FROM tags
-                                WHERE count >= :tags_min
-                                ORDER BY SCORE_STRNORM(substr(tag, 1, 1)) LIMIT :start, :max_items
-                            "), array('tags_min' => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit));
+                $tag_data = $database->get_col(
+                    $database->scoreql_to_sql(
+                        "
+                                                        SELECT DISTINCT
+                                                            id, SCORE_STRNORM(substr(tag, 1, 1)), count
+                                                        FROM tags
+                                                        WHERE count >= :tags_min
+                                                        ORDER BY SCORE_STRNORM(substr(tag, 1, 1)) LIMIT :start, :max_items
+                                                    "
+                    ),
+                    array('tags_min' => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit)
+                );
                 break;
             case 'count':
-                $tag_data = $database->get_all("
-                                SELECT id, tag, count
-                                FROM tags
-                                WHERE count >= :tags_min
-                                ORDER BY count DESC, tag ASC LIMIT :start, :max_items
-                                ", array('tags_min' => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit));
+                $tag_data = $database->get_all(
+                    "
+                                                    SELECT id, tag, count
+                                                    FROM tags
+                                                    WHERE count >= :tags_min
+                                                    ORDER BY count DESC, tag ASC LIMIT :start, :max_items
+                                                    ",
+                    array('tags_min' => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit)
+                );
                 break;
             case 'date':
-                $tag_data = $database->get_all("
-                                SELECT id, tag, count
-                                FROM tags
-                                WHERE count >= :tags_min
-                                ORDER BY count DESC, tag ASC LIMIT :start, :max_items
-                                ", array('tags_min' => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit));
+                $tag_data = $database->get_all(
+                    "
+                                                    SELECT id, tag, count
+                                                    FROM tags
+                                                    WHERE count >= :tags_min
+                                                    ORDER BY count DESC, tag ASC LIMIT :start, :max_items
+                                                    ",
+                    array('tags_min' => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit)
+                );
                 break;
         }
         $tags = array();
@@ -628,19 +676,18 @@ class OuroborosAPI extends Extension
      * @param string $reason Reason for the code
      * @param bool $location Is $reason a location? (used mainly for post/create)
      */
-    private function sendResponse($code = 200, $reason = '', $location = false) {
+    private function sendResponse($code = 200, $reason = '', $location = false)
+    {
         global $page;
         if ($code == 200) {
             $success = true;
-        }
-        else {
+        } else {
             $success = false;
         }
         if (empty($reason)) {
             if (defined("self::MSG_HTTP_{$code}")) {
                 $reason = constant("self::MSG_HTTP_{$code}");
-            }
-            else {
+            } else {
                 $reason = self::MSG_HTTP_418;
             }
         }
@@ -648,8 +695,7 @@ class OuroborosAPI extends Extension
             $proto = $_SERVER['SERVER_PROTOCOL'];
             if (defined("self::HEADER_HTTP_{$code}")) {
                 $header = constant("self::HEADER_HTTP_{$code}");
-            }
-            else {
+            } else {
                 // I'm a teapot!
                 $code = 418;
                 $header = self::HEADER_HTTP_418;
@@ -663,8 +709,7 @@ class OuroborosAPI extends Extension
                 unset($response['reason']);
             }
             $response = json_encode($response);
-        }
-        elseif ($this->type == 'xml') {
+        } elseif ($this->type == 'xml') {
             // Seriously, XML sucks...
             $xml = new XMLWriter();
             $xml->openMemory();
@@ -673,8 +718,7 @@ class OuroborosAPI extends Extension
             $xml->writeAttribute('success', var_export($success, true));
             if ($location !== false) {
                 $xml->writeAttribute('location', $reason);
-            }
-            else {
+            } else {
                 $xml->writeAttribute('reason', $reason);
             }
             $xml->endElement();
@@ -683,7 +727,6 @@ class OuroborosAPI extends Extension
             unset($xml);
         }
         $page->set_data($response);
-        $page->display();
     }
 
     /**
@@ -692,18 +735,18 @@ class OuroborosAPI extends Extension
      * @param mixed $data
      * @param int $offset
      */
-    private function sendData($type = '', $data = array(), $offset = 0) {
+    private function sendData($type = '', $data = array(), $offset = 0)
+    {
         global $page;
         $response = '';
         if ($this->type == 'json') {
             $response = json_encode($data);
-        }
-        elseif ($this->type == 'xml') {
+        } elseif ($this->type == 'xml') {
             $xml = new XMLWriter();
             $xml->openMemory();
             $xml->startDocument('1.0', 'utf-8');
             if (array_key_exists(0, $data)) {
-                $xml->startElement($type.'s');
+                $xml->startElement($type . 's');
                 if ($type == 'post') {
                     $xml->writeAttribute('count', count($data));
                     $xml->writeAttribute('offset', $offset);
@@ -715,8 +758,7 @@ class OuroborosAPI extends Extension
                     $this->createItemXML($xml, $type, $item);
                 }
                 $xml->endElement();
-            }
-            else {
+            } else {
                 $this->createItemXML($xml, $type, $data);
             }
             $xml->endDocument();
@@ -724,17 +766,15 @@ class OuroborosAPI extends Extension
             unset($xml);
         }
         $page->set_data($response);
-        $page->display();
-        exit;
     }
 
-    private function createItemXML(XMLWriter &$xml, $type, $item) {
+    private function createItemXML(XMLWriter &$xml, $type, $item)
+    {
         $xml->startElement($type);
         foreach ($item as $key => $val) {
             if ($key == 'created_at' && $type == 'post') {
                 $xml->writeAttribute($key, $val['s']);
-            }
-            else {
+            } else {
                 if (is_bool($val)) {
                     $val = $val ? 'true' : 'false';
                 }
@@ -752,7 +792,8 @@ class OuroborosAPI extends Extension
      * @param void
      * @return void
      */
-    private function tryAuth() {
+    private function tryAuth()
+    {
         global $config, $user;
 
         if (isset($_REQUEST['user']) && isset($_REQUEST['session'])) {
@@ -762,22 +803,19 @@ class OuroborosAPI extends Extension
             $duser = User::by_session($name, $session);
             if (!is_null($duser)) {
                 $user = $duser;
-            }
-            else {
+            } else {
                 $user = User::by_id($config->get_int("anon_id", 0));
             }
-        }
-        elseif (isset($_COOKIE[$config->get_string('cookie_prefix', 'shm').'_'.'session']) &&
-            isset($_COOKIE[$config->get_string('cookie_prefix', 'shm').'_'.'user'])
+        } elseif (isset($_COOKIE[$config->get_string('cookie_prefix', 'shm') . '_' . 'session']) &&
+            isset($_COOKIE[$config->get_string('cookie_prefix', 'shm') . '_' . 'user'])
         ) {
             //Auth by session data from cookies
-            $session = $_COOKIE[$config->get_string('cookie_prefix', 'shm').'_'.'session'];
-            $user = $_COOKIE[$config->get_string('cookie_prefix', 'shm').'_'.'user'];
+            $session = $_COOKIE[$config->get_string('cookie_prefix', 'shm') . '_' . 'session'];
+            $user = $_COOKIE[$config->get_string('cookie_prefix', 'shm') . '_' . 'user'];
             $duser = User::by_session($user, $session);
             if (!is_null($duser)) {
                 $user = $duser;
-            }
-            else {
+            } else {
                 $user = User::by_id($config->get_int("anon_id", 0));
             }
         }
@@ -788,7 +826,8 @@ class OuroborosAPI extends Extension
      * @param $page
      * @return bool
      */
-    private function match($page) {
+    private function match($page)
+    {
         return (preg_match("%{$page}\.(xml|json)$%", implode('/', $this->event->args), $matches) === 1);
     }
 }

From c07dc2e0abe0f387096ccdc9c0e427c37713e226 Mon Sep 17 00:00:00 2001
From: Daku <dakutree@codeanimu.net>
Date: Thu, 16 Jan 2014 03:28:23 +0000
Subject: [PATCH 30/30] use resolve_aliases rather than resolve_alias

---
 core/imageboard.pack.php | 41 +++++++++++++++++++---------------------
 1 file changed, 19 insertions(+), 22 deletions(-)

diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php
index 0bf145e9..6d5cd1cd 100644
--- a/core/imageboard.pack.php
+++ b/core/imageboard.pack.php
@@ -666,6 +666,8 @@ class Image {
 			}
 		}
 
+		$terms = Tag::resolve_aliases($terms);
+
 		// parse the words that are searched for into
 		// various types of querylet
 		foreach($terms as $term) {
@@ -678,15 +680,6 @@ class Image {
 				continue;
 			}
 
-			$aliases = explode(" ", Tag::resolve_alias($term));
-			$found = array_search($term, $aliases);
-			if($found !== false){
-				unset($aliases[$found]);
-			}else{
-				$term = array_shift($aliases);
-			}
-			foreach($aliases as $alias)	array_push($terms, $alias);
-
 			$stpe = new SearchTermParseEvent($term, $terms);
 			send_event($stpe);
 			if($stpe->is_querylet_set()) {
@@ -834,6 +827,8 @@ class Image {
 			}
 		}
 
+		$terms = Tag::resolve_aliases($terms);
+
 		reset($terms); // rewind to first element in array.
 		
 		// turn each term into a specific type of querylet
@@ -843,15 +838,6 @@ class Image {
 				$negative = true;
 				$term = substr($term, 1);
 			}
-			
-			$aliases = explode(" ", Tag::resolve_alias($term));
-			$found = array_search($term, $aliases);
-			if($found !== false){
-				unset($aliases[$found]);
-			}else{
-				$term = array_shift($aliases);
-			}
-			foreach($aliases as $alias)	array_push($terms, $alias);
 
 			$stpe = new SearchTermParseEvent($term, $terms);
 			send_event($stpe);
@@ -1099,11 +1085,22 @@ class Tag {
 		assert(is_array($tags));
 
 		$new = array();
-		foreach($tags as $tag) {
-			$new_set = explode(' ', Tag::resolve_alias($tag));
-			foreach($new_set as $new_one) {
-				$new[] = $new_one;
+
+		$i = 0;
+		$tag_count = count($tags);
+		while($i<$tag_count) {
+			$aliases = explode(' ', Tag::resolve_alias($tags[$i]));
+			foreach($aliases as $alias){
+				if(!in_array($alias, $new)){
+					if($tags[$i] == $alias){
+						$new[] = $alias;
+					}elseif(!in_array($alias, $tags)){
+						$tags[] = $alias;
+						$tag_count++;
+					}
+				}
 			}
+			$i++;
 		}
 
 		$new = array_iunique($new); // remove any duplicate tags