diff --git a/contrib/danbooru_api/main.php b/contrib/danbooru_api/main.php
new file mode 100644
index 00000000..e693d781
--- /dev/null
+++ b/contrib/danbooru_api/main.php
@@ -0,0 +1,383 @@
+
+Notes:
+danbooru API based on documentation from danbooru 1.0 - http://attachr.com/7569
+I've only been able to test add_post and find_tags beacuse I use the old danbooru firefox extension for firefox 1.5
+
+Functions currently implemented:
+add_comment - NOT DONE YET, waiting on some backend shimmie code :)
+add_post - title and rating are currently ignored because shimmie does not support them
+find_posts - sort of works, filename is returned as the original filename and probably won't help when it comes to actually downloading it
+find_tags - id, name, and after_id all work but the tags parameter is ignored just like danbooru 1.0 ignores it
+
+*/
+
+class DanbooruApi extends Extension
+{
+ // Receive the event
+ public function receive_event($event)
+ {
+ // Check if someone is accessing /api/danbooru (us)
+ if(is_a($event, 'PageRequestEvent') && ($event->page_name == "api") && ($event->get_arg(0) == 'danbooru'))
+ {
+ // execute the danbooru processing code
+ $this->api_danbooru($event);
+ }
+ }
+
+ // Danbooru API
+ private function api_danbooru($event)
+ {
+ global $page;
+ global $config;
+ global $database;
+ global $user;
+ $page->set_mode("data");
+ $page->set_type("application/xml");
+ //debug
+ //$page->set_type("text/plain");
+
+ $results = array();
+
+ /*
+ add_comment()
+ Adds a comment to a post.
+ Parameters
+ * body: the body of the comment
+ * post_id: the post id
+ * login: your login
+ * password: your password Response
+ * 200: success
+ * 500: error. response body will the the error message.
+ */
+ if($event->get_arg(1) == 'add_comment')
+ {
+ // On error the response body is the error message so plain text is fine
+ $page->set_type("text/plain");
+ // We do wish to auth the user if possible, if it fails treat as anonymous
+ $this->authenticate_user();
+ // Check if anonymous commenting is allowed before proceeding
+ if($config->get_bool("comment_anon") || !$user->is_anonymous())
+ {
+ // Did the user supply a post_id and a comment body?
+ if(isset($_REQUEST['post_id']) && isset($_REQUEST['body']) && trim($_REQUEST['body']) != "")
+ {
+ // waiting for someone to write an event handler for the comments extension :)
+ } else
+ {
+ // User didn't supply necessary parameters, tell them that
+ header("HTTP/1.0 500 Internal Server Error");
+ $page->set_data("You forgot to supply either a post id or the body of your comment");
+ }
+ } else
+ {
+ header("HTTP/1.0 500 Internal Server Error");
+ $page->set_data("You supplied an invalid login or password or anonymous commenting is currently disabled");
+ }
+ }
+
+ /*
+ add_post()
+ Adds a post to the database.
+ Parameters
+ * login: login
+ * password: password
+ * file: file as a multipart form
+ * source: source url
+ * title: title **IGNORED**
+ * tags: list of tags as a string, delimited by whitespace
+ * md5: MD5 hash of upload in hexadecimal format
+ * rating: rating of the post. can be explicit, questionable, or safe. **IGNORED**
+ Notes
+ * The only necessary parameter is tags and either file or source.
+ * If you want to sign your post, you need a way to authenticate your account, either by supplying login and password, or by supplying a cookie.
+ * If an account is not supplied or if it doesn‘t authenticate, he post will be added anonymously.
+ * If the md5 parameter is supplied and does not match the hash of what‘s on the server, the post is rejected.
+ Response
+ The response depends on the method used:
+ Post
+ * X-Danbooru-Location set to the URL for newly uploaded post.
+ Get
+ * Redirected to the newly uploaded post.
+ */
+ if($event->get_arg(1) == 'add_post')
+ {
+ // No XML data is returned from this function
+ $page->set_type("text/plain");
+ // Check first if a login was supplied, if it wasn't check if the user is logged in via cookie
+ // If all that fails, it's an anonymous upload
+ $this->authenticate_user();
+ // Now we check if a file was uploaded or a url was provided to transload
+ // Much of this code is borrowed from /ext/upload
+ if($config->get_bool("upload_anon") || !$user->is_anonymous())
+ {
+ $file = null;
+ $filename = "";
+ $source = "";
+
+ if(isset($_FILES['file']))
+ { // A file was POST'd in
+ $file = $_FILES['file']['tmp_name'];
+ $filename = $file['name'];
+ // If both a file is posted and a source provided, I'm assuming source is the source of the file
+ $source = isset($_REQUEST['source']) ? $_REQUEST['source'] : "";
+ } elseif(isset($_REQUEST['source']))
+ { // A url was provided
+ $url = $_REQUEST['source'];
+ $source = $url;
+ $tmp_filename = tempnam("/tmp", "shimmie_transload");
+
+ // Are we using fopen wrappers or curl?
+ if($config->get_string("transload_engine") == "fopen")
+ {
+ $fp = fopen($url, "r");
+ if(!$fp) {
+ header("HTTP/1.0 409 Conflict");
+ header("X-Danbooru-Errors: fopen read error");
+ }
+
+ $data = "";
+ $length = 0;
+ while(!feof($fp) && $length <= $config->get_int('upload_size'))
+ {
+ $data .= fread($fp, 8192);
+ $length = strlen($data);
+ }
+ fclose($fp);
+
+ $fp = fopen($tmp_filename, "w");
+ fwrite($fp, $data);
+ fclose($fp);
+ }
+
+ if($config->get_string("transload_engine") == "curl")
+ {
+ $ch = curl_init($url);
+ $fp = fopen($tmp_filename, "w");
+
+ curl_setopt($ch, CURLOPT_FILE, $fp);
+ curl_setopt($ch, CURLOPT_HEADER, 0);
+
+ curl_exec($ch);
+ curl_close($ch);
+ fclose($fp);
+ }
+ $file = $tmp_filename;
+ $filename = basename($url);
+ } else
+ { // Nothing was specified at all
+ header("HTTP/1.0 409 Conflict");
+ header("X-Danbooru-Errors: no input files");
+ return;
+ }
+
+ // Now that we have some sort of physical file, process it
+ $image = new Image($file, $filename, $_REQUEST['tags'], $source);
+ // This occurs if the uploaded file is not an image
+ if(!$image->is_ok())
+ {
+ header("HTTP/1.0 409 Conflict");
+ header("X-Danbooru-Errors: unknown");
+ return;
+ }
+ // Was an md5 supplied? Does it match the image hash?
+ if(isset($_REQUEST['md5']))
+ {
+ if($_REQUEST['md5'] != $image->hash)
+ {
+ header("HTTP/1.0 409 Conflict");
+ header("X-Danbooru-Errors: md5 mismatch");
+ return;
+ }
+ }
+ // Is the image too large?
+ if(filesize($file['tmp_name']) > $config->get_int('upload_size'))
+ {
+ header("HTTP/1.0 409 Conflict");
+ header("X-Danbooru-Errors: too large");
+ return;
+ }
+ // Does it exist already?
+ $existing = $database->get_image_by_hash($image->hash);
+ if(!is_null($existing)) {
+ header("HTTP/1.0 409 Conflict");
+ header("X-Danbooru-Errors: duplicate");
+ $existinglink = "http://" . $_SERVER['HTTP_HOST'] . make_link("post/view/" . $existing->id);
+ header("X-Danbooru-Location: $existinglink");
+ }
+
+ // Fire off an event which should process the new image and add it to the db
+ $nevent = new UploadingImageEvent($image);
+ send_event($nevent);
+ // Did something screw up?
+ if($event->vetoed) {
+ header("X-Danbooru-Errors: $event->veto_reason");
+ return;
+ } else
+ { // If it went ok, grab the id for the newly uploaded image and pass it in the header
+ $newimg = $database->get_image_by_hash($image->hash);
+ $newid = "http://" . $_SERVER['HTTP_HOST'] . make_link("post/view/" . $newimg->id);
+ // Did we POST or GET this call?
+ if($_SERVER['REQUEST_METHOD'] == 'POST')
+ {
+ header("X-You-Win: yes");
+ header("X-Danbooru-Location: $newid");
+ }
+ else
+ header("Location: $newid");
+ }
+ } else
+ {
+ header("X-Danbooru-Errors: authentication error");
+ return;
+ }
+ }
+
+ /*
+ find_posts()
+ Find all posts that match the search criteria. Posts will be ordered by id descending.
+ Parameters
+ * md5: md5 hash to search for (comma delimited)
+ * id: id to search for (comma delimited)
+ * tags: what tags to search for
+ * limit: limit
+ * offset: offset
+ * after_id: limit results to posts added after this id
+ */
+ if($event->get_arg(1) == 'find_posts')
+ {
+ if(isset($_GET['md5']))
+ {
+ $md5list = explode(",",$_GET['md5']);
+ foreach($md5list as $md5)
+ {
+ $results[] = $database->get_image_by_hash($md5);
+ }
+ } elseif(isset($_GET['id']))
+ {
+ $idlist = explode(",",$_GET['id']);
+ foreach($idlist as $id)
+ {
+ $results[] = $database->get_image($id);
+ }
+ } else
+ {
+ $limit = isset($_GET['limit']) ? int_escape($_GET['limit']) : 100;
+ $start = isset($_GET['offset']) ? int_escape($_GET['offset']) : 0;
+ $tags = isset($_GET['tags']) ? tag_explode($_GET['tags']) : array();
+ $results = $database->get_images($start,$limit,$tags);
+ }
+
+ // Now we have the array $results filled with Image objects
+ // Let's display them
+ $xml = "\n";
+ foreach($results as $img)
+ {
+ // Sanity check to see if $img is really an image object
+ // If it isn't (e.g. someone requested an invalid md5 or id), break out of the this
+ if(!is_object($img))
+ continue;
+ $taglist = $img->get_tag_list();
+ $owner = $img->get_owner();
+ $xml .= "hash\" rating=\"Questionable\" date=\"$img->posted\" is_warehoused=\"false\" file_name=\"$img->filename\" tags=\"$taglist\" source=\"$img->source\" score=\"0\" id=\"$img->id\" author=\"$owner->name\"/>\n";
+ }
+ $xml .= "";
+ $page->set_data($xml);
+ }
+
+ /*
+ find_tags() Find all tags that match the search criteria.
+ Parameters
+ * id: A comma delimited list of tag id numbers.
+ * name: A comma delimited list of tag names.
+ * tags: any typical tag query. See Tag#parse_query for details.
+ * after_id: limit results to tags with an id number after after_id. Useful if you only want to refresh
+ */
+ if($event->get_arg(1) == 'find_tags')
+ {
+ if(isset($_GET['id']))
+ {
+ $idlist = explode(",",$_GET['id']);
+ foreach($idlist as $id)
+ {
+ $sqlresult = $database->execute("SELECT id,tag,count FROM tags WHERE id = ?", array($id));
+ if(!$sqlresult->EOF)
+ {
+ $results[] = array($sqlresult->fields['count'], $sqlresult->fields['tag'], $sqlresult->fields['id']);
+ }
+ }
+ } elseif(isset($_GET['name']))
+ {
+ $namelist = explode(",",$_GET['name']);
+ foreach($namelist as $name)
+ {
+ $sqlresult = $database->execute("SELECT id,tag,count FROM tags WHERE tag = ?", array($name));
+ if(!$sqlresult->EOF)
+ {
+ $results[] = array($sqlresult->fields['count'], $sqlresult->fields['tag'], $sqlresult->fields['id']);
+ }
+ }
+ }
+ /* Currently disabled to maintain identical functionality to danbooru 1.0's own "broken" find_tags
+ elseif(isset($_GET['tags']))
+ {
+ $start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0;
+ $tags = tag_explode($_GET['tags']);
+
+ }
+ */
+ else
+ {
+ $start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0;
+ $sqlresult = $database->execute("SELECT id,tag,count FROM tags WHERE count > 0 AND id >= ? ORDER BY id DESC",array($start));
+ while(!$sqlresult->EOF)
+ {
+ $results[] = array($sqlresult->fields['count'], $sqlresult->fields['tag'], $sqlresult->fields['id']);
+ $sqlresult->MoveNext();
+ }
+ }
+
+ // Tag results collected, build XML output
+ $xml = "\n";
+ foreach($results as $tag)
+ {
+ $xml .= "\n";
+ }
+ $xml .= "";
+ $page->set_data($xml);
+ }
+ }
+
+ // Turns out I use this a couple times so let's make it a utility function
+ // Authenticates a user based on the contents of the login and password parameters
+ // or makes them anonymous. Does not set any cookies or anything permanent.
+ private function authenticate_user()
+ {
+ global $database;
+ global $user;
+
+ if(isset($_REQUEST['login']) && isset($_REQUEST['password']))
+ {
+ // Get this user from the db, if it fails the user becomes anonymous
+ // Code borrowed from /ext/user
+ $name = $_REQUEST['login'];
+ $pass = $_REQUEST['password'];
+ $hash = md5( strtolower($name) . $pass );
+ $duser = $database->get_user_by_name_and_hash($name, $hash);
+ if(!is_null($duser)) {
+ $user = $duser;
+ } else
+ {
+ $user = $database->get_user_by_id($config->get_int("anon_id", 0));
+ }
+ }
+ }
+}
+
+add_event_listener(new DanbooruApi());
+?>
\ No newline at end of file