805 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			805 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| /*
 | |
|  * Name: Ouroboros API
 | |
|  * Author: Diftraku <diftraku[at]derpy.me>
 | |
|  * Description: Ouroboros-like API for Shimmie
 | |
|  * Version: 0.2
 | |
|  * Documentation:
 | |
|  *   Currently working features
 | |
|  *   <ul>
 | |
|  *     <li>Post:
 | |
|  *       <ul>
 | |
|  *         <li>Index/List</li>
 | |
|  *         <li>Show</li>
 | |
|  *         <li>Create</li>
 | |
|  *       </ul>
 | |
|  *     </li>
 | |
|  *     <li>Tag:
 | |
|  *       <ul>
 | |
|  *         <li>Index/List</li>
 | |
|  *       </ul>
 | |
|  *     </li>
 | |
|  *   </ul>
 | |
|  *   Tested to work with CartonBox using "Danbooru 1.18.x" as site type.
 | |
|  *   Does not work with Andbooru or Danbooru Gallery for reasons beyond me, took me a while to figure rating "u" is bad...
 | |
|  *   Lots of Ouroboros/Danbooru specific values use their defaults (or what I gathered them to be default)
 | |
|  *   and tons of stuff not supported directly in Shimmie is botched to work
 | |
|  */
 | |
| 
 | |
| 
 | |
| class _SafeOuroborosImage
 | |
| {
 | |
|     /**
 | |
|      * Author
 | |
|      */
 | |
| 
 | |
|     /**
 | |
|      * Post author
 | |
|      * @var string
 | |
|      */
 | |
|     public $author = '';
 | |
|     /**
 | |
|      * Post author user ID
 | |
|      * @var integer
 | |
|      */
 | |
|     public $creator_id = null;
 | |
| 
 | |
|     /**
 | |
|      * Image
 | |
|      */
 | |
| 
 | |
|     /**
 | |
|      * Image height
 | |
|      * @var integer
 | |
|      */
 | |
|     public $height = null;
 | |
|     /**
 | |
|      * Image width
 | |
|      * @var integer
 | |
|      */
 | |
|     public $width = null;
 | |
|     /**
 | |
|      * File extension
 | |
|      * @var string
 | |
|      */
 | |
|     public $file_ext = '';
 | |
|     /**
 | |
|      * File Size in bytes
 | |
|      * @var integer
 | |
|      */
 | |
|     public $file_size = null;
 | |
|     /**
 | |
|      * URL to the static file
 | |
|      * @var string
 | |
|      */
 | |
|     public $file_url = '';
 | |
|     /**
 | |
|      * File MD5 hash
 | |
|      * @var string
 | |
|      */
 | |
|     public $md5 = '';
 | |
| 
 | |
|     /**
 | |
|      * Post Meta
 | |
|      */
 | |
| 
 | |
|     /**
 | |
|      * (Unknown) Change
 | |
|      * @var integer
 | |
|      */
 | |
|     public $change = null;
 | |
|     /**
 | |
|      * Timestamp for post creation
 | |
|      * @var integer
 | |
|      */
 | |
|     public $created_at = null;
 | |
|     /**
 | |
|      * Post ID
 | |
|      * @var integer
 | |
|      */
 | |
|     public $id = null;
 | |
|     /**
 | |
|      * Parent post ID
 | |
|      * @var integer
 | |
|      */
 | |
|     public $parent_id = null;
 | |
|     /**
 | |
|      * Post content rating
 | |
|      * @var string
 | |
|      */
 | |
|     public $rating = 'q';
 | |
|     /**
 | |
|      * Post score
 | |
|      * @var integer
 | |
|      */
 | |
|     public $score = 1;
 | |
|     /**
 | |
|      * Post source
 | |
|      * @var string
 | |
|      */
 | |
|     public $source = '';
 | |
|     /**
 | |
|      * Post status
 | |
|      * @var string
 | |
|      */
 | |
|     public $status = '';
 | |
|     /**
 | |
|      * Post tags
 | |
|      * @var string
 | |
|      */
 | |
|     public $tags = 'tagme';
 | |
|     /**
 | |
|      * Flag if the post has child posts
 | |
|      * @var bool
 | |
|      */
 | |
|     public $has_children = false;
 | |
|     /**
 | |
|      * Flag if the post has comments
 | |
|      * @var bool
 | |
|      */
 | |
|     public $has_comments = false;
 | |
|     /**
 | |
|      * Flag if the post has notes
 | |
|      * @var bool
 | |
|      */
 | |
|     public $has_notes = false;
 | |
|     /**
 | |
|      * Post description
 | |
|      * @var string
 | |
|      */
 | |
|     public $description = '';
 | |
| 
 | |
|     /**
 | |
|      * Thumbnail
 | |
|      */
 | |
| 
 | |
|     /**
 | |
|      * Thumbnail Height
 | |
|      * @var integer
 | |
|      */
 | |
|     public $preview_height = null;
 | |
|     /**
 | |
|      * Thumbnail URL
 | |
|      * @var string
 | |
|      */
 | |
|     public $preview_url = '';
 | |
|     /**
 | |
|      * Thumbnail Width
 | |
|      * @var integer
 | |
|      */
 | |
|     public $preview_width = null;
 | |
| 
 | |
|     /**
 | |
|      * Downscaled Image
 | |
|      */
 | |
| 
 | |
|     /**
 | |
|      * Downscaled image height
 | |
|      * @var integer
 | |
|      */
 | |
|     public $sample_height = null;
 | |
|     /**
 | |
|      * Downscaled image
 | |
|      * @var string
 | |
|      */
 | |
|     public $sample_url = '';
 | |
|     /**
 | |
|      * Downscaled image
 | |
|      * @var integer
 | |
|      */
 | |
|     public $sample_width = null;
 | |
| 
 | |
|     public function __construct(Image $img)
 | |
|     {
 | |
|         global $config;
 | |
|         // author
 | |
|         $author = $img->get_owner();
 | |
|         $this->author = $author->name;
 | |
|         $this->creator_id = intval($author->id);
 | |
| 
 | |
|         // file
 | |
|         $this->height = intval($img->height);
 | |
|         $this->width = intval($img->width);
 | |
|         $this->file_ext = $img->ext;
 | |
|         $this->file_size = intval($img->filesize);
 | |
|         $this->file_url = make_http($img->get_image_link());
 | |
|         $this->md5 = $img->hash;
 | |
| 
 | |
|         // meta
 | |
|         $this->change = intval($img->id); //DaFug is this even supposed to do? ChangeID?
 | |
|         // Should be JSON specific, just strip this when converting to XML
 | |
|         $this->created_at = ['n' => 123456789, 's' => strtotime($img->posted), 'json_class' => 'Time'];
 | |
|         $this->id = intval($img->id);
 | |
|         $this->parent_id = null;
 | |
|         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') {
 | |
|                     $this->rating = $img->rating;
 | |
|                 }
 | |
|             }
 | |
|             if (strstr(ENABLED_EXTS, 'numeric_score') !== false) {
 | |
|                 $this->score = $img->numeric_score;
 | |
|             }
 | |
|         }
 | |
|         $this->source = $img->source;
 | |
|         $this->status = 'active'; //not supported in Shimmie... yet
 | |
|         $this->tags = $img->get_tag_list();
 | |
|         $this->has_children = false;
 | |
|         $this->has_comments = false;
 | |
|         $this->has_notes = false;
 | |
| 
 | |
|         // thumb
 | |
|         $this->preview_height = $config->get_int(ImageConfig::THUMB_HEIGHT);
 | |
|         $this->preview_width = $config->get_int(ImageConfig::THUMB_WIDTH);
 | |
|         $this->preview_url = make_http($img->get_thumb_link());
 | |
| 
 | |
|         // sample (use the full image here)
 | |
|         $this->sample_height = intval($img->height);
 | |
|         $this->sample_width = intval($img->width);
 | |
|         $this->sample_url = make_http($img->get_image_link());
 | |
|     }
 | |
| }
 | |
| 
 | |
| class OuroborosPost extends _SafeOuroborosImage
 | |
| {
 | |
|     /**
 | |
|      * Multipart File
 | |
|      * @var array
 | |
|      */
 | |
|     public $file = [];
 | |
| 
 | |
|     /**
 | |
|      * 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
 | |
|      */
 | |
|     public function __construct(array $post, string $md5 = '')
 | |
|     {
 | |
|         if (array_key_exists('tags', $post)) {
 | |
|             // implode(explode()) to resolve aliases and sanitise
 | |
|             $this->tags = Tag::implode(Tag::explode(urldecode($post['tags'])));
 | |
|         }
 | |
|         if (array_key_exists('file', $post)) {
 | |
|             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(
 | |
|                 $post['rating'] == 's' ||
 | |
|                 $post['rating'] == 'q' ||
 | |
|                 $post['rating'] == 'e'
 | |
|             );
 | |
|             $this->rating = $post['rating'];
 | |
|         }
 | |
|         if (array_key_exists('source', $post)) {
 | |
|             $this->file_url = filter_var(
 | |
|                 urldecode($post['source']),
 | |
|                 FILTER_SANITIZE_URL
 | |
|             );
 | |
|         }
 | |
|         if (array_key_exists('sourceurl', $post)) {
 | |
|             $this->source = filter_var(
 | |
|                 urldecode($post['sourceurl']),
 | |
|                 FILTER_SANITIZE_URL
 | |
|             );
 | |
|         }
 | |
|         if (array_key_exists('description', $post)) {
 | |
|             $this->description = filter_var(
 | |
|                 $post['description'],
 | |
|                 FILTER_SANITIZE_STRING
 | |
|             );
 | |
|         }
 | |
|         if (array_key_exists('is_rating_locked', $post)) {
 | |
|             assert(
 | |
|                 $post['is_rating_locked'] == 'true' ||
 | |
|                 $post['is_rating_locked'] == 'false' ||
 | |
|                 $post['is_rating_locked'] == '1' ||
 | |
|                 $post['is_rating_locked'] == '0'
 | |
|             );
 | |
|             $this->is_rating_locked = $post['is_rating_locked'];
 | |
|         }
 | |
|         if (array_key_exists('is_note_locked', $post)) {
 | |
|             assert(
 | |
|                 $post['is_note_locked'] == 'true' ||
 | |
|                 $post['is_note_locked'] == 'false' ||
 | |
|                 $post['is_note_locked'] == '1' ||
 | |
|                 $post['is_note_locked'] == '0'
 | |
|             );
 | |
|             $this->is_note_locked = $post['is_note_locked'];
 | |
|         }
 | |
|         if (array_key_exists('parent_id', $post)) {
 | |
|             $this->parent_id = filter_var(
 | |
|                 $post['parent_id'],
 | |
|                 FILTER_SANITIZE_NUMBER_INT
 | |
|             );
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| class _SafeOuroborosTag
 | |
| {
 | |
|     public $ambiguous = false;
 | |
|     public $count = 0;
 | |
|     public $id = 0;
 | |
|     public $name = '';
 | |
|     public $type = 0;
 | |
| 
 | |
|     public function __construct(array $tag)
 | |
|     {
 | |
|         $this->count = $tag['count'];
 | |
|         $this->id = $tag['id'];
 | |
|         $this->name = $tag['tag'];
 | |
|     }
 | |
| }
 | |
| 
 | |
| class OuroborosAPI extends Extension
 | |
| {
 | |
|     private $event;
 | |
|     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';
 | |
|     const OK_POST_CREATE_UPDATE = 'Updated';
 | |
| 
 | |
|     public function onPageRequest(PageRequestEvent $event)
 | |
|     {
 | |
|         global $page, $user;
 | |
| 
 | |
|         if (preg_match("%\.(xml|json)$%", implode('/', $event->args), $matches) === 1) {
 | |
|             $this->event = $event;
 | |
|             $this->type = $matches[1];
 | |
|             if ($this->type == 'json') {
 | |
|                 $page->set_type('application/json; charset=utf-8');
 | |
|             } elseif ($this->type == 'xml') {
 | |
|                 $page->set_type('text/xml; charset=utf-8');
 | |
|             }
 | |
|             $page->set_mode(PageMode::DATA);
 | |
|             $this->tryAuth();
 | |
| 
 | |
|             if ($event->page_matches('post')) {
 | |
|                 if ($this->match('create')) {
 | |
|                     // Create
 | |
|                     if ($user->can(Permissions::CREATE_IMAGE)) {
 | |
|                         $md5 = !empty($_REQUEST['md5']) ? filter_var($_REQUEST['md5'], FILTER_SANITIZE_STRING) : null;
 | |
|                         $this->postCreate(new OuroborosPost($_REQUEST['post']), $md5);
 | |
|                     } else {
 | |
|                         $this->sendResponse(403, 'You cannot create new posts');
 | |
|                     }
 | |
|                 } elseif ($this->match('update')) {
 | |
|                     // Update
 | |
|                     //@todo add post update
 | |
|                 } 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')) {
 | |
|                     // 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;
 | |
|                     $tags = !empty($_REQUEST['tags']) ? filter_var($_REQUEST['tags'], FILTER_SANITIZE_STRING) : [];
 | |
|                     if (!empty($tags)) {
 | |
|                         $tags = Tag::explode($tags);
 | |
|                     }
 | |
|                     $this->postIndex($limit, $p, $tags);
 | |
|                 }
 | |
|             } 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;
 | |
|                     $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
 | |
|                     ) : '';
 | |
|                     $this->tagIndex($limit, $p, $order, $id, $after_id, $name, $name_pattern);
 | |
|                 }
 | |
|             }
 | |
|         } elseif ($event->page_matches('post/show')) {
 | |
|             $page->set_mode(PageMode::REDIRECT);
 | |
|             $page->set_redirect(make_link(str_replace('post/show', 'post/view', implode('/', $event->args))));
 | |
|             $page->display();
 | |
|             die();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Post
 | |
|      */
 | |
| 
 | |
|     /**
 | |
|      * Wrapper for post creation
 | |
|      */
 | |
|     protected function postCreate(OuroborosPost $post, string $md5 = '')
 | |
|     {
 | |
|         global $config;
 | |
|         $handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER);
 | |
|         if (!empty($md5) && !($handler == ImageConfig::COLLISION_MERGE)) {
 | |
|             $img = Image::by_hash($md5);
 | |
|             if (!is_null($img)) {
 | |
|                 $this->sendResponse(420, self::ERROR_POST_CREATE_DUPE);
 | |
|                 return;
 | |
|             }
 | |
|         }
 | |
|         $meta = [];
 | |
|         $meta['tags'] = is_array($post->tags) ? $post->tags : Tag::explode($post->tags);
 | |
|         $meta['source'] = $post->source;
 | |
|         if (defined('ENABLED_EXTS')) {
 | |
|             if (strstr(ENABLED_EXTS, 'rating') !== false) {
 | |
|                 $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 (!transload($post->file_url, $meta['file'])) {
 | |
|                 $this->sendResponse(500, 'Transloading failed');
 | |
|                 return;
 | |
|             }
 | |
|             $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);
 | |
|             return;
 | |
|         }
 | |
|         if (!empty($meta['hash'])) {
 | |
|             $img = Image::by_hash($meta['hash']);
 | |
|             if (!is_null($img)) {
 | |
|                 $handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER);
 | |
|                 if ($handler == ImageConfig::COLLISION_MERGE) {
 | |
|                     $postTags = is_array($post->tags) ? $post->tags : Tag::explode($post->tags);
 | |
|                     $merged = array_merge($postTags, $img->get_tag_array());
 | |
|                     send_event(new TagSetEvent($img, $merged));
 | |
| 
 | |
|                     // This is really the only thing besides tags we should care
 | |
|                     if (isset($meta['source'])) {
 | |
|                         send_event(new SourceSetEvent($img, $meta['source']));
 | |
|                     }
 | |
|                     $this->sendResponse(200, self::OK_POST_CREATE_UPDATE . ' ID: ' . $img->id);
 | |
|                     return;
 | |
|                 } else {
 | |
|                     $this->sendResponse(420, self::ERROR_POST_CREATE_DUPE);
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         $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);
 | |
|                 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;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Wrapper for getting a single post
 | |
|      */
 | |
|     protected function postShow(int $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 string[] $tags
 | |
|      */
 | |
|     protected function postIndex(int $limit, int $page, array $tags)
 | |
|     {
 | |
|         $start = ($page - 1) * $limit;
 | |
|         $results = Image::find_images(max($start, 0), min($limit, 100), $tags);
 | |
|         $posts = [];
 | |
|         foreach ($results as $img) {
 | |
|             if (!is_object($img)) {
 | |
|                 continue;
 | |
|             }
 | |
|             $posts[] = new _SafeOuroborosImage($img);
 | |
|         }
 | |
|         $this->sendData('post', $posts, max($start, 0));
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Tag
 | |
|      */
 | |
| 
 | |
|     protected function tagIndex(int $limit, int $page, string $order, int $id, int $after_id, string $name, string $name_pattern)
 | |
|     {
 | |
|         global $database, $config;
 | |
|         $start = ($page - 1) * $limit;
 | |
|         $tag_data = [];
 | |
|         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
 | |
|                                                     "
 | |
|                     ),
 | |
|                     ['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
 | |
|                                                     ",
 | |
|                     ['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
 | |
|                                                     ",
 | |
|                     ['tags_min' => $config->get_int('tags_min'), 'start' => $start, 'max_items' => $limit]
 | |
|                 );
 | |
|                 break;
 | |
|         }
 | |
|         $tags = [];
 | |
|         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
 | |
|      */
 | |
|     private function sendResponse(int $code = 200, string $reason = '', bool $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 = ['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);
 | |
|     }
 | |
| 
 | |
|     private function sendData(string $type = '', array $data = [], int $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);
 | |
|     }
 | |
| 
 | |
|     private function createItemXML(XMLWriter &$xml, string $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
 | |
|      */
 | |
|     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
 | |
|      */
 | |
|     private function match(string $page): bool
 | |
|     {
 | |
|         return (preg_match("%{$page}\.(xml|json)$%", implode('/', $this->event->args), $matches) === 1);
 | |
|     }
 | |
| }
 |