PageRenderTime 52ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/ext/danbooru_api/main.php

https://gitlab.com/dali99/shimmie2-Material-Theme
PHP | 396 lines | 230 code | 28 blank | 138 comment | 51 complexity | c6746355028bc19ff3a1ece808e1457c MD5 | raw file
  1. <?php
  2. /*
  3. Name: Danbooru Client API
  4. Author: JJS <jsutinen@gmail.com>
  5. Description: Allow Danbooru apps like Danbooru Uploader for Firefox to communicate with Shimmie
  6. Documentation:
  7. <p>Notes:
  8. <br>danbooru API based on documentation from danbooru 1.0 -
  9. http://attachr.com/7569
  10. <br>I've only been able to test add_post and find_tags because I use the
  11. old danbooru firefox extension for firefox 1.5
  12. <p>Functions currently implemented:
  13. <ul>
  14. <li>add_post - title and rating are currently ignored because shimmie does not support them
  15. <li>find_posts - sort of works, filename is returned as the original filename and probably won't help when it comes to actually downloading it
  16. <li>find_tags - id, name, and after_id all work but the tags parameter is ignored just like danbooru 1.0 ignores it
  17. </ul>
  18. CHANGELOG
  19. 13-OCT-08 8:00PM CST - JJS
  20. Bugfix - Properly escape source attribute
  21. 17-SEP-08 10:00PM CST - JJS
  22. Bugfix for changed page name checker in PageRequestEvent
  23. 13-APR-08 10:00PM CST - JJS
  24. Properly escape the tags returned in find_tags and find_posts - Caught by ATravelingGeek
  25. Updated extension info to be a bit more clear about its purpose
  26. Deleted add_comment code as it didn't do anything anyway
  27. 01-MAR-08 7:00PM CST - JJS
  28. Rewrote to make it compatible with Shimmie trunk again (r723 at least)
  29. It may or may not support the new file handling stuff correctly, I'm only testing with images and the danbooru uploader for firefox
  30. 21-OCT-07 9:07PM CST - JJS
  31. Turns out I actually did need to implement the new parameter names
  32. for danbooru api v1.8.1. Now danbooruup should work when used with /api/danbooru/post/create.xml
  33. Also correctly redirects the url provided by danbooruup in the event
  34. of a duplicate image.
  35. 19-OCT-07 4:46PM CST - JJS
  36. Add compatibility with danbooru api v1.8.1 style urls
  37. for find_posts and add_post. NOTE: This does not implement
  38. the changes to the parameter names, it is simply a
  39. workaround for the latest danbooruup firefox extension.
  40. Completely compatibility will probably involve a rewrite with a different URL
  41. */
  42. class DanbooruApi extends Extension {
  43. public function onPageRequest(PageRequestEvent $event) {
  44. if($event->page_matches("api") && ($event->get_arg(0) == 'danbooru')) {
  45. $this->api_danbooru($event);
  46. }
  47. }
  48. // Danbooru API
  49. private function api_danbooru(PageRequestEvent $event) {
  50. global $page;
  51. $page->set_mode("data");
  52. if(($event->get_arg(1) == 'add_post') || (($event->get_arg(1) == 'post') && ($event->get_arg(2) == 'create.xml'))) {
  53. // No XML data is returned from this function
  54. $page->set_type("text/plain");
  55. $this->api_add_post();
  56. }
  57. elseif(($event->get_arg(1) == 'find_posts') || (($event->get_arg(1) == 'post') && ($event->get_arg(2) == 'index.xml'))) {
  58. $page->set_type("application/xml");
  59. $page->set_data($this->api_find_posts());
  60. }
  61. elseif($event->get_arg(1) == 'find_tags') {
  62. $page->set_type("application/xml");
  63. $page->set_data($this->api_find_tags());
  64. }
  65. // Hackery for danbooruup 0.3.2 providing the wrong view url. This simply redirects to the proper
  66. // Shimmie view page
  67. // Example: danbooruup says the url is http://shimmie/api/danbooru/post/show/123
  68. // This redirects that to http://shimmie/post/view/123
  69. elseif(($event->get_arg(1) == 'post') && ($event->get_arg(2) == 'show')) {
  70. $fixedlocation = make_link("post/view/" . $event->get_arg(3));
  71. $page->set_mode("redirect");
  72. $page->set_redirect($fixedlocation);
  73. }
  74. }
  75. /**
  76. * Turns out I use this a couple times so let's make it a utility function
  77. * Authenticates a user based on the contents of the login and password parameters
  78. * or makes them anonymous. Does not set any cookies or anything permanent.
  79. */
  80. private function authenticate_user() {
  81. global $config, $user;
  82. if(isset($_REQUEST['login']) && isset($_REQUEST['password'])) {
  83. // Get this user from the db, if it fails the user becomes anonymous
  84. // Code borrowed from /ext/user
  85. $name = $_REQUEST['login'];
  86. $pass = $_REQUEST['password'];
  87. $duser = User::by_name_and_pass($name, $pass);
  88. if(!is_null($duser)) {
  89. $user = $duser;
  90. }
  91. else {
  92. $user = User::by_id($config->get_int("anon_id", 0));
  93. }
  94. }
  95. }
  96. /**
  97. * find_tags()
  98. * Find all tags that match the search criteria.
  99. *
  100. * Parameters
  101. * - id: A comma delimited list of tag id numbers.
  102. * - name: A comma delimited list of tag names.
  103. * - tags: any typical tag query. See Tag#parse_query for details.
  104. * - after_id: limit results to tags with an id number after after_id. Useful if you only want to refresh
  105. *
  106. * @return string
  107. */
  108. private function api_find_tags() {
  109. global $database;
  110. $results = array();
  111. if(isset($_GET['id'])) {
  112. $idlist = explode(",", $_GET['id']);
  113. foreach ($idlist as $id) {
  114. $sqlresult = $database->get_all(
  115. "SELECT id,tag,count FROM tags WHERE id = ?",
  116. array($id));
  117. foreach ($sqlresult as $row) {
  118. $results[] = array($row['count'], $row['tag'], $row['id']);
  119. }
  120. }
  121. }
  122. elseif(isset($_GET['name'])) {
  123. $namelist = explode(",", $_GET['name']);
  124. foreach ($namelist as $name) {
  125. $sqlresult = $database->get_all(
  126. "SELECT id,tag,count FROM tags WHERE tag = ?",
  127. array($name));
  128. foreach ($sqlresult as $row) {
  129. $results[] = array($row['count'], $row['tag'], $row['id']);
  130. }
  131. }
  132. }
  133. // Currently disabled to maintain identical functionality to danbooru 1.0's own "broken" find_tags
  134. elseif(false && isset($_GET['tags'])) {
  135. $start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0;
  136. $tags = Tag::explode($_GET['tags']);
  137. }
  138. else {
  139. $start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0;
  140. $sqlresult = $database->get_all(
  141. "SELECT id,tag,count FROM tags WHERE count > 0 AND id >= ? ORDER BY id DESC",
  142. array($start));
  143. foreach ($sqlresult as $row) {
  144. $results[] = array($row['count'], $row['tag'], $row['id']);
  145. }
  146. }
  147. // Tag results collected, build XML output
  148. $xml = "<tags>\n";
  149. foreach ($results as $tag) {
  150. $xml .= xml_tag("tag", array(
  151. "type" => "0",
  152. "counts" => $tag[0],
  153. "name" => $tag[1],
  154. "id" => $tag[2],
  155. ));
  156. }
  157. $xml .= "</tags>";
  158. return $xml;
  159. }
  160. /**
  161. * find_posts()
  162. * Find all posts that match the search criteria. Posts will be ordered by id descending.
  163. *
  164. * Parameters:
  165. * - md5: md5 hash to search for (comma delimited)
  166. * - id: id to search for (comma delimited)
  167. * - tags: what tags to search for
  168. * - limit: limit
  169. * - page: page number
  170. * - after_id: limit results to posts added after this id
  171. *
  172. * @return string
  173. * @throws SCoreException
  174. */
  175. private function api_find_posts() {
  176. $results = array();
  177. $this->authenticate_user();
  178. $start = 0;
  179. if(isset($_GET['md5'])) {
  180. $md5list = explode(",", $_GET['md5']);
  181. foreach ($md5list as $md5) {
  182. $results[] = Image::by_hash($md5);
  183. }
  184. $count = count($results);
  185. }
  186. elseif(isset($_GET['id'])) {
  187. $idlist = explode(",", $_GET['id']);
  188. foreach ($idlist as $id) {
  189. $results[] = Image::by_id($id);
  190. }
  191. $count = count($results);
  192. }
  193. else {
  194. $limit = isset($_GET['limit']) ? int_escape($_GET['limit']) : 100;
  195. // Calculate start offset.
  196. if (isset($_GET['page'])) // Danbooru API uses 'page' >= 1
  197. $start = (int_escape($_GET['page']) - 1) * $limit;
  198. else if (isset($_GET['pid'])) // Gelbooru API uses 'pid' >= 0
  199. $start = int_escape($_GET['pid']) * $limit;
  200. else
  201. $start = 0;
  202. $tags = isset($_GET['tags']) ? Tag::explode($_GET['tags']) : array();
  203. $count = Image::count_images($tags);
  204. $results = Image::find_images(max($start, 0), min($limit, 100), $tags);
  205. }
  206. // Now we have the array $results filled with Image objects
  207. // Let's display them
  208. $xml = "<posts count=\"{$count}\" offset=\"{$start}\">\n";
  209. foreach ($results as $img) {
  210. // Sanity check to see if $img is really an image object
  211. // If it isn't (e.g. someone requested an invalid md5 or id), break out of the this
  212. if (!is_object($img))
  213. continue;
  214. $taglist = $img->get_tag_list();
  215. $owner = $img->get_owner();
  216. $previewsize = get_thumbnail_size($img->width, $img->height);
  217. $xml .= xml_tag("post", array(
  218. "id" => $img->id,
  219. "md5" => $img->hash,
  220. "file_name" => $img->filename,
  221. "file_url" => $img->get_image_link(),
  222. "height" => $img->height,
  223. "width" => $img->width,
  224. "preview_url" => $img->get_thumb_link(),
  225. "preview_height" => $previewsize[1],
  226. "preview_width" => $previewsize[0],
  227. "rating" => "u",
  228. "date" => $img->posted,
  229. "is_warehoused" => false,
  230. "tags" => $taglist,
  231. "source" => $img->source,
  232. "score" => 0,
  233. "author" => $owner->name
  234. ));
  235. }
  236. $xml .= "</posts>";
  237. return $xml;
  238. }
  239. /**
  240. * add_post()
  241. * Adds a post to the database.
  242. *
  243. * Parameters:
  244. * - login: login
  245. * - password: password
  246. * - file: file as a multipart form
  247. * - source: source url
  248. * - title: title **IGNORED**
  249. * - tags: list of tags as a string, delimited by whitespace
  250. * - md5: MD5 hash of upload in hexadecimal format
  251. * - rating: rating of the post. can be explicit, questionable, or safe. **IGNORED**
  252. *
  253. * Notes:
  254. * - The only necessary parameter is tags and either file or source.
  255. * - 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.
  256. * - If an account is not supplied or if it doesn‘t authenticate, he post will be added anonymously.
  257. * - If the md5 parameter is supplied and does not match the hash of what‘s on the server, the post is rejected.
  258. *
  259. * Response
  260. * The response depends on the method used:
  261. * Post:
  262. * - X-Danbooru-Location set to the URL for newly uploaded post.
  263. * Get:
  264. * - Redirected to the newly uploaded post.
  265. */
  266. private function api_add_post() {
  267. global $user, $config, $page;
  268. $danboorup_kludge = 1; // danboorup for firefox makes broken links out of location: /path
  269. // Check first if a login was supplied, if it wasn't check if the user is logged in via cookie
  270. // If all that fails, it's an anonymous upload
  271. $this->authenticate_user();
  272. // Now we check if a file was uploaded or a url was provided to transload
  273. // Much of this code is borrowed from /ext/upload
  274. if (!$user->can("create_image")) {
  275. $page->set_code(409);
  276. $page->add_http_header("X-Danbooru-Errors: authentication error");
  277. return;
  278. }
  279. if (isset($_FILES['file'])) { // A file was POST'd in
  280. $file = $_FILES['file']['tmp_name'];
  281. $filename = $_FILES['file']['name'];
  282. // If both a file is posted and a source provided, I'm assuming source is the source of the file
  283. if (isset($_REQUEST['source']) && !empty($_REQUEST['source'])) {
  284. $source = $_REQUEST['source'];
  285. } else {
  286. $source = null;
  287. }
  288. } elseif (isset($_FILES['post'])) {
  289. $file = $_FILES['post']['tmp_name']['file'];
  290. $filename = $_FILES['post']['name']['file'];
  291. if (isset($_REQUEST['post']['source']) && !empty($_REQUEST['post']['source'])) {
  292. $source = $_REQUEST['post']['source'];
  293. } else {
  294. $source = null;
  295. }
  296. } elseif (isset($_REQUEST['source']) || isset($_REQUEST['post']['source'])) { // A url was provided
  297. $source = isset($_REQUEST['source']) ? $_REQUEST['source'] : $_REQUEST['post']['source'];
  298. $file = tempnam("/tmp", "shimmie_transload");
  299. $ok = transload($source, $file);
  300. if (!$ok) {
  301. $page->set_code(409);
  302. $page->add_http_header("X-Danbooru-Errors: fopen read error");
  303. return;
  304. }
  305. $filename = basename($source);
  306. } else { // Nothing was specified at all
  307. $page->set_code(409);
  308. $page->add_http_header("X-Danbooru-Errors: no input files");
  309. return;
  310. }
  311. // Get tags out of url
  312. $posttags = Tag::explode(isset($_REQUEST['tags']) ? $_REQUEST['tags'] : $_REQUEST['post']['tags']);
  313. // Was an md5 supplied? Does it match the file hash?
  314. $hash = md5_file($file);
  315. if (isset($_REQUEST['md5']) && strtolower($_REQUEST['md5']) != $hash) {
  316. $page->set_code(409);
  317. $page->add_http_header("X-Danbooru-Errors: md5 mismatch");
  318. return;
  319. }
  320. // Upload size checking is now performed in the upload extension
  321. // It is also currently broken due to some confusion over file variable ($tmp_filename?)
  322. // Does it exist already?
  323. $existing = Image::by_hash($hash);
  324. if (!is_null($existing)) {
  325. $page->set_code(409);
  326. $page->add_http_header("X-Danbooru-Errors: duplicate");
  327. $existinglink = make_link("post/view/" . $existing->id);
  328. if ($danboorup_kludge) $existinglink = make_http($existinglink);
  329. $page->add_http_header("X-Danbooru-Location: $existinglink");
  330. return;
  331. }
  332. // Fire off an event which should process the new file and add it to the db
  333. $fileinfo = pathinfo($filename);
  334. $metadata = array();
  335. $metadata['filename'] = $fileinfo['basename'];
  336. $metadata['extension'] = $fileinfo['extension'];
  337. $metadata['tags'] = $posttags;
  338. $metadata['source'] = $source;
  339. //log_debug("danbooru_api","========== NEW($filename) =========");
  340. //log_debug("danbooru_api", "upload($filename): fileinfo(".var_export($fileinfo,TRUE)."), metadata(".var_export($metadata,TRUE).")...");
  341. try {
  342. $nevent = new DataUploadEvent($file, $metadata);
  343. //log_debug("danbooru_api", "send_event(".var_export($nevent,TRUE).")");
  344. send_event($nevent);
  345. // If it went ok, grab the id for the newly uploaded image and pass it in the header
  346. $newimg = Image::by_hash($hash); // FIXME: Unsupported file doesn't throw an error?
  347. $newid = make_link("post/view/" . $newimg->id);
  348. if ($danboorup_kludge) $newid = make_http($newid);
  349. // Did we POST or GET this call?
  350. if ($_SERVER['REQUEST_METHOD'] == 'POST') {
  351. $page->add_http_header("X-Danbooru-Location: $newid");
  352. } else {
  353. $page->add_http_header("Location: $newid");
  354. }
  355. } catch (UploadException $ex) {
  356. // Did something screw up?
  357. $page->set_code(409);
  358. $page->add_http_header("X-Danbooru-Errors: exception - " . $ex->getMessage());
  359. }
  360. }
  361. }