"posts.id DESC", 'limit' => null, 'offset' => null ), $options); $tag = strtolower(str_replace(' ', '_', $tag)); $posts = self::where("tags.name = ?", $tag)->select("posts.*")->joins("JOIN posts_tags ON posts_tags.post_id = posts.id JOIN tags ON tags.id = posts_tags.tag_id")->limit($options['limit'])->offset($options['offset'])->order($options['order'])->take(); if (!$posts) $posts = new ModelCollection(array()); return $posts; } static public function generate_sql_range_helper($arr, $field, &$c, &$params = null) { $query_passed = is_object($c); switch ($arr[0]) { case 'eq': if ($query_passed) $c->where($field." = ?", $arr[1]); else { $c[] = $field." = ?"; $params[] = $arr[1]; } break; case 'gt': if ($query_passed) $c->where($field." > ?", $arr[1]); else { $c[] = $field." > ?"; $params[] = $arr[1]; } break; case 'gte': if ($query_passed) $c->where($field." >= ?", $arr[1]); else { $c[] = $field." >= ?"; $params[] = $arr[1]; } break; case 'lt': if ($query_passed) $c->where($field." < ?", $arr[1]); else { $c[] = $field." < ?"; $params[] = $arr[1]; } break; case 'lte': if ($query_passed) $c->where($field." <= ?", $arr[1]); else { $c[] = $field." <= ?"; $params[] = $arr[1]; } break; case 'between': if ($query_passed) $c->where($field." BETWEEN ? AND ?", $arr[1], $arr[2]); else { $c[] = $field." BETWEEN ? AND ?"; $params[] = $arr[1]; $params[] = $arr[2]; } break; case 'in': if ($query_passed) $c->where($field." IN (".implode(', ', array_fill(0, count($arr[1]), '?')).")", current($arr[1])); else { $c[] = $field." IN (".implode(', ', array_fill(0, count($arr[1]), '?')).")"; $params[] = current($arr[1]); } break; } } static public function generate_sql_escape_helper($array) { return array_filter($array); } // def generate_sql_escape_helper(array) // array.map do |token| // next if token.empty? // escaped_token = token.gsub(/\\|'/, '\0\0\0\0').gsub("?", "\\\\77").gsub("%", "\\\\45") // "''" + escaped_token + "''" // end.compact // end static public function generate_sql($q, $options = array()) { if (is_array($q)) { $original_query = isset($options['original_query']) ? $options['original_query'] : null; } else { $original_query = $q; $q = Tag::parse_query($q); } # Filling default values. $q = array_merge(array_fill_keys(array('md5', 'ext', 'source', 'fav', 'user', 'rating', 'rating_negated', 'unlocked_rating', 'show_holds', 'shown_in_index', 'exclude', 'related', 'post_id', 'mpixels', 'width', 'height', 'score', 'date', 'change'), null), $q); $options = array_merge(array_fill_keys(array('pending', 'flagged', 'from_api', 'limit', 'offset', 'count', 'select', 'having'), null), $options); $conds = array('true'); $joins = array( 'posts p', ); $join_params = array(); $cond_params = array(); if (!empty($q['error'])) $conds[] = "FALSE"; self::generate_sql_range_helper($q['post_id'], "p.id", $conds, $cond_params); self::generate_sql_range_helper($q['mpixels'], "p.width*p.height/1000000.0", $conds, $cond_params); self::generate_sql_range_helper($q['width'], "p.width", $conds, $cond_params); self::generate_sql_range_helper($q['height'], "p.height", $conds, $cond_params); self::generate_sql_range_helper($q['score'], "p.score", $conds, $cond_params); self::generate_sql_range_helper($q['date'], "DATE(p.created_at)", $conds, $cond_params); self::generate_sql_range_helper($q['change'], "p.change_seq", $conds, $cond_params); if (is_string($q['md5'])) { $conds[] = "p.md5 IN (?)"; $cond_params[] = explode(',', $q['md5']); } if (is_string($q['ext'])) { $conds[] = "p.file_ext IN (?)"; $cond_params[] = explode(',', strtolower($q['ext'])); } if (isset($q['show_deleted_only'])) { if ($q['show_deleted_only']) $conds[] = "p.status = 'deleted'"; } elseif (empty($q['post_id'])) { # If a specific post_id isn't specified, default to filtering deleted posts. $conds[] = "p.status <> 'deleted'"; } if (isset($q['parent_id']) && is_numeric($q['parent_id'])) { $conds[] = "(p.parent_id = ? or p.id = ?)"; $cond_params[] = $q['parent_id']; $cond_params[] = $q['parent_id']; } elseif (isset($q['parent_id']) && $q['parent_id'] == false) { $conds[] = "p.parent_id is null"; } if (is_string($q['source'])) { $conds[] = "lower(p.source) LIKE lower(?)"; $cond_params[] = $q['source']; } if (isset($q['subscriptions'])) { preg_match('/^(.+?):(.+)$/', $q['subscriptions'], $m); $username = $m[1] ?: $q['subscriptions']; $subscription_name = $m[2]; $user = User::find_by_name($username); if ($user) { if ($post_ids = TagSubscription::find_post_ids($user->id, $subscription_name)) { $conds[] = 'p.id IN (?)'; $cond_params[] = $post_ids; } } } if (is_string($q['fav'])) { $joins[] = "JOIN favorites f ON f.post_id = p.id JOIN users fu ON f.user_id = fu.id"; $conds[] = "lower(fu.name) = lower(?)"; $cond_params[] = $q['fav']; } if (isset($q['vote_negated'])) { $joins[] = "LEFT JOIN post_votes v ON p.id = v.post_id AND v.user_id = ?"; $join_params[] = $q['vote_negated']; $conds[] = "v.score IS NULL"; } if (isset($q['vote'])) { $joins[] = "JOIN post_votes v ON p.id = v.post_id"; // $conds[] = sprintf("v.user_id = %d", $q['vote'][1]); $conds[] = 'v.user_id = ?'; $cond_params[] = $q['vote'][1]; self::generate_sql_range_helper($q['vote'][0], "v.score", $conds, $cond_params); } if (is_string($q['user'])) { $joins[] = "JOIN users u ON p.user_id = u.id"; $conds[] = "lower(u.name) = lower(?)"; $cond_params[] = $q['user']; } if (isset($q['exclude_pools'])) { foreach (array_keys($q['exclude_pools']) as $i) { if (is_int($q['exclude_pools'][$i])) { $joins[] = "LEFT JOIN pools_posts ep${i} ON (ep${i}.post_id = p.id AND ep${i}.pool_id = ?)"; $join_params[] = $q['exclude_pools'][$i]; $conds[] = "ep${i}.id IS NULL"; } if (is_string($q['exclude_pools'][$i])) { $joins[] = "LEFT JOIN pools_posts ep${i} ON ep${i}.post_id = p.id LEFT JOIN pools epp${i} ON (ep${i}.pool_id = epp${i}.id AND LOWER(epp${i}.name) LIKE ?)"; $join_params[] = "%".strtolower($q['exclude_pools'][$i])."%"; $conds[] = "ep${i}.id IS NULL"; } } } if (isset($q['pool'])) { $conds[] = "pools_posts.active = true"; if (!isset($q['order'])) $paramsool_ordering = " ORDER BY pools_posts.pool_id ASC, CAST(pools_posts.sequence AS UNSIGNED), pools_posts.post_id"; if (is_int($q['pool'])) { $joins[] = "JOIN pools_posts ON pools_posts.post_id = p.id JOIN pools ON pools_posts.pool_id = pools.id"; $conds[] = "pools.id = ".$q['pool']; } if (is_string($q['pool'])) { if ($q['pool'] == "*") $joins[] = "JOIN pools_posts ON pools_posts.post_id = p.id JOIN pools ON pools_posts.pool_id = pools.id"; else { $joins[] = "JOIN pools_posts ON pools_posts.post_id = p.id JOIN pools ON pools_posts.pool_id = pools.id"; $conds[] = "LOWER(pools.name) LIKE ?"; $cond_params[] = "%".strtolower($q['pool'])."%"; } } } # http://stackoverflow.com/questions/8106547/how-to-search-on-mysql-using-joins/8107017 $tags_index_query = array(); if (!empty($q['include']) && ($tags_include = self::generate_sql_escape_helper($q['include']))) { $joins[] = 'INNER JOIN posts_tags pti ON p.id = pti.post_id JOIN tags ti ON pti.tag_id = ti.id'; $tags_index_query[] = 'ti.name IN ('.implode(', ', array_fill(0, count($tags_include), '?')).')'; $cond_params = array_merge($cond_params, $tags_include); } if (!empty($q['related'])) { if (count($q['exclude']) > CONFIG()->tag_query_limit) { throw new Exception("You cannot search for more than ".CONFIG()->tag_query_limit." tags at a time"); } $tags_index_query[] = '('.implode(', ', array_map(function($v, $k){return 't'.($k+1).'.name';}, $q['related'], array_keys($q['related']))).') = ('.implode(', ', array_fill(0, count($q['related']), '?')).')'; $cond_params = array_merge($cond_params, $q['related']); $joins[] = implode(' ', array_map(function($k){return 'INNER JOIN posts_tags pt'.($k+1).' ON p.id = pt'.($k+1).'.post_id INNER JOIN tags t'.($k+1).' ON pt'.($k+1).'.tag_id = t'.($k+1).'.id';}, array_keys($q['related']))); } if (!empty($q['exclude'])) { if (count($q['exclude']) > CONFIG()->tag_query_limit) { throw new Exception("You cannot search for more than ".CONFIG()->tag_query_limit." tags at a time"); } $tags_index_query[] = 'NOT EXISTS (SELECT * FROM posts_tags pt INNER JOIN tags t ON pt.tag_id = t.id WHERE p.id = pt.post_id AND t.name IN ('.implode(', ', array_fill(0, count($q['exclude']), '?')).') )'; $cond_params = array_merge($cond_params, $q['exclude']); } if (!empty($tags_index_query)) { $conds[] = implode(' AND ', $tags_index_query); } if (is_string($q['rating'])) { $r = strtolower(substr($q['rating'], 0, 1)); if ($r == "s") $conds[] = "p.rating = 's'"; elseif ($r == "q") $conds[] = "p.rating = 'q'"; elseif ($r == "e") $conds[] = "p.rating = 'e'"; } if (is_string($q['rating_negated'])) { $r = strtolower(substr($q['rating_negated'], 0, 1)); if ($r == "s") $conds[] = "p.rating <> 's'"; elseif ($r == "q") $conds[] = "p.rating <> 'q'"; elseif ($r == "e") $conds[] = "p.rating <> 'e'"; } if ($q['unlocked_rating'] == true) $conds[] = "p.is_rating_locked = FALSE"; if (isset($options['flagged'])) $conds[] = "p.status = 'flagged'"; if (isset($q['show_holds'])) { if( $q['show_holds'] == 'only') $conds[] = "p.is_held"; elseif ($q['show_holds'] == 'hide') $conds[] = "NOT p.is_held"; elseif ($q['show_holds'] == 'yes') {/*do nothing?*/} } else { # Hide held posts by default only when not using the API. if (!$options['from_api']) $conds[] = "NOT p.is_held"; } /** * MyImouto: Moved the following condition here so only one * of the conditions that set the pending status is met. * Before this, in post#moderate, when searching for a user's * pending posts, the SQL query would end up like this: * ... AND p.status = 'pending' ... AND p.status <> 'pending' ... */ if (isset($options['pending'])) $conds[] = "p.status = 'pending'"; else { if (isset($q['show_pending'])) { if ($q['show_pending'] == 'only') $conds[] = "p.status = 'pending'"; elseif ($q['show_pending'] == 'hide') $conds[] = "p.status <> 'pending'"; elseif ($q['show_pending'] == 'yes') {/*do nothing?*/} } else { # Hide pending posts by default only when not using the API. if (CONFIG()->hide_pending_posts && !isset($options['from_api'])) $conds[] = "p.status <> 'pending'"; } } if (isset($q['shown_in_index'])) { if ($q['shown_in_index']) $conds[] = "p.is_shown_in_index"; else $conds[] = "NOT p.is_shown_in_index"; } elseif (!$original_query && !$options['from_api']) { # Hide not shown posts by default only when not using the API. $conds[] = "p.is_shown_in_index"; } $sql = "SELECT "; if ($options['count']) $sql .= " COUNT(*)"; elseif ($options['select']) $sql .= ' '.$options['select']; else $sql .= " p.*"; $sql .= " FROM ".implode(' ', $joins); $sql .= " WHERE ".implode(' AND ', $conds); if (!$options['count']) $sql .= ' GROUP BY p.id '; if (isset($q['order']) && !$options['count']) { if ($q['order'] == "id") $sql .= " ORDER BY p.id"; elseif ($q['order'] == "id_desc") $sql .= " ORDER BY p.id DESC"; elseif ($q['order'] == "score") $sql .= " ORDER BY p.score DESC"; elseif ($q['order'] == "score_asc") $sql .= " ORDER BY p.score"; elseif ($q['order'] == "mpixels") # Use "w*h/1000000", even though "w*h" would give the same result, so this can use # the posts_mpixels index. $sql .= " ORDER BY width*height/1000000.0 DESC"; elseif ($q['order'] == "mpixels_asc") $sql .= " ORDER BY width*height/1000000.0"; elseif ($q['order'] == "portrait") $sql .= " ORDER BY 1.0*width/GREATEST(1, height)"; elseif ($q['order'] == "landscape") $sql .= " ORDER BY 1.0*width/GREATEST(1, height) DESC"; elseif ($q['order'] == "portrait_pool") { # We can only do this if we're searching for a pool. if (isset($q['pool'])) $sql .= " ORDER BY 1.0*width / GREATEST(1, height), CAST(pools_posts.sequence AS UNSIGNED), pools_posts.post_id"; } elseif ($q['order'] == "change" || $q['order'] == "change_asc") $sql .= " ORDER BY change_seq"; elseif ($q['order'] == "change_desc") $sql .= " ORDER BY change_seq DESC"; elseif ($q['order'] == "vote") { if (isset($q['vote'])) $sql .= " ORDER BY v.updated_at DESC"; } elseif ($q['order'] == "fav") { if (is_string($q['fav'])) $sql .= " ORDER BY f.id DESC"; } elseif ($q['order'] == "random") $sql .= " ORDER BY RAND()"; else $use_default_order = true; } else { $use_default_order = true; } if (isset($use_default_order) && !$options['count']) { if (isset($paramsool_ordering)) $sql .= $paramsool_ordering; else { if (!empty($options['from_api'])) { # When using the API, default to sorting by ID. $sql .= " ORDER BY p.id DESC"; } else { # MI: Added p.id DESC so posts with same index_timestamp are ordered by id. $sql .= " ORDER BY p.index_timestamp DESC, p.id DESC"; } } } if (isset($options['limit']) && isset($options['offset'])) $sql .= ' LIMIT ' . $options['offset'] . ', ' . $options['limit']; elseif (isset($options['limit'])) $sql .= " LIMIT ".$options['limit']; $params = array_merge($join_params, $cond_params); return array($sql, $params); } }