This repository has been archived on 2024-09-05. You can view files and clone it, but cannot push or open issues or pull requests.
shimmie2/core/imageboard/image.php

1039 lines
31 KiB
PHP
Raw Normal View History

<?php
2009-07-19 07:38:13 +00:00
/**
2014-04-29 05:33:03 +00:00
* Class Image
*
* An object representing an entry in the images table.
*
* As of 2.2, this no longer necessarily represents an
* image per se, but could be a video, sound file, or any
* other supported upload type.
*/
class Image
{
private static $tag_n = 0; // temp hack
public static $order_sql = null; // this feels ugly
2015-08-02 18:29:25 +00:00
/** @var null|int */
public $id = null;
/** @var int */
public $height;
/** @var int */
public $width;
/** @var string */
public $hash;
public $filesize;
/** @var string */
public $filename;
/** @var string */
public $ext;
/** @var string[]|null */
public $tag_array;
2014-04-28 06:23:45 +00:00
/** @var int */
public $owner_id;
2017-03-12 08:18:26 +00:00
/** @var string */
public $owner_ip;
/** @var string */
public $posted;
/** @var string */
public $source;
/** @var boolean */
public $locked = false;
/**
* One will very rarely construct an image directly, more common
* would be to use Image::by_id, Image::by_hash, etc.
*/
public function __construct(?array $row=null)
{
if (!is_null($row)) {
foreach ($row as $name => $value) {
// some databases use table.name rather than name
$name = str_replace("images.", "", $name);
$this->$name = $value; // hax, this is likely the cause of much scrutinizer-ci complaints.
}
$this->locked = bool_escape($this->locked);
assert(is_numeric($this->id));
assert(is_numeric($this->height));
assert(is_numeric($this->width));
}
}
2019-05-29 17:23:29 +00:00
public static function by_id(int $id): ?Image
{
global $database;
$row = $database->get_row("SELECT * FROM images WHERE images.id=:id", ["id"=>$id]);
return ($row ? new Image($row) : null);
}
2019-05-29 17:23:29 +00:00
public static function by_hash(string $hash): ?Image
{
global $database;
$row = $database->get_row("SELECT images.* FROM images WHERE hash=:hash", ["hash"=>$hash]);
return ($row ? new Image($row) : null);
}
2019-05-29 17:23:29 +00:00
public static function by_random(array $tags=[]): ?Image
{
$max = Image::count_images($tags);
if ($max < 1) {
return null;
} // From Issue #22 - opened by HungryFeline on May 30, 2011.
$rand = mt_rand(0, $max-1);
$set = Image::find_images($rand, 1, $tags);
if (count($set) > 0) {
return $set[0];
} else {
return null;
}
}
/**
* Search for an array of images
*
* #param string[] $tags
* #return Image[]
*/
public static function find_images(int $start, int $limit, array $tags=[]): array
{
global $database, $user, $config;
$images = [];
if ($start < 0) {
$start = 0;
}
if ($limit < 1) {
$limit = 1;
}
if (SPEED_HAX) {
if (!$user->can("big_search") and count($tags) > 3) {
throw new SCoreException("Anonymous users may only search for up to 3 tags at a time");
}
}
$result = null;
if (SEARCH_ACCEL) {
$result = Image::get_accelerated_result($tags, $start, $limit);
}
if (!$result) {
$querylet = Image::build_search_querylet($tags);
$querylet->append(new Querylet(" ORDER BY ".(Image::$order_sql ?: "images.".$config->get_string("index_order"))));
$querylet->append(new Querylet(" LIMIT :limit OFFSET :offset", ["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);
}
Image::$order_sql = null;
return $images;
}
/*
* Accelerator stuff
*/
2019-05-29 17:23:29 +00:00
public static function get_acceleratable(array $tags): ?array
{
$ret = [
"yays" => [],
"nays" => [],
];
$yays = 0;
$nays = 0;
foreach ($tags as $tag) {
if (!preg_match("/^-?[a-zA-Z0-9_-]+$/", $tag)) {
2019-05-29 17:23:29 +00:00
return null;
}
if ($tag[0] == "-") {
$nays++;
$ret["nays"][] = substr($tag, 1);
} else {
$yays++;
$ret["yays"][] = $tag;
}
}
if ($yays > 1 || $nays > 0) {
return $ret;
}
2019-05-29 17:23:29 +00:00
return null;
}
2019-05-29 17:23:29 +00:00
public static function get_accelerated_result(array $tags, int $offset, int $limit): ?PDOStatement
{
global $database;
$req = Image::get_acceleratable($tags);
if (!$req) {
return null;
}
$req["offset"] = $offset;
$req["limit"] = $limit;
$response = Image::query_accelerator($req);
$list = implode(",", $response);
if ($list) {
$result = $database->execute("SELECT * FROM images WHERE id IN ($list) ORDER BY images.id DESC");
} else {
$result = $database->execute("SELECT * FROM images WHERE 1=0 ORDER BY images.id DESC");
}
return $result;
}
2019-05-29 17:23:29 +00:00
public static function get_accelerated_count(array $tags): ?int
{
$req = Image::get_acceleratable($tags);
if (!$req) {
return null;
}
$req["count"] = true;
return Image::query_accelerator($req);
}
public static function query_accelerator($req)
{
$fp = @fsockopen("127.0.0.1", 21212);
if (!$fp) {
return null;
}
fwrite($fp, json_encode($req));
$data = "";
while (($buffer = fgets($fp, 4096)) !== false) {
$data .= $buffer;
}
if (!feof($fp)) {
die("Error: unexpected fgets() fail in query_accelerator($req)\n");
}
fclose($fp);
return json_decode($data);
}
/*
* Image-related utility functions
*/
/**
* Count the number of image results for a given search
*
* #param string[] $tags
*/
public static function count_images(array $tags=[]): int
{
global $database;
$tag_count = count($tags);
if ($tag_count === 0) {
$total = $database->cache->get("image-count");
if (!$total) {
$total = $database->get_one("SELECT COUNT(*) FROM images");
$database->cache->set("image-count", $total, 600);
}
} elseif ($tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) {
$total = $database->get_one(
$database->scoreql_to_sql("SELECT count FROM tags WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag)"),
["tag"=>$tags[0]]
);
} else {
$total = Image::get_accelerated_count($tags);
if (is_null($total)) {
$querylet = Image::build_search_querylet($tags);
$total = $database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables);
}
}
if (is_null($total)) {
return 0;
}
return $total;
}
/**
* Count the number of pages for a given search
*
* #param string[] $tags
*/
public static function count_pages(array $tags=[]): float
{
global $config;
return ceil(Image::count_images($tags) / $config->get_int('index_images'));
}
/*
* Accessors & mutators
*/
/**
* Find the next image in the sequence.
*
* Rather than simply $this_id + 1, one must take into account
* deleted images and search queries
*
* #param string[] $tags
*/
public function get_next(array $tags=[], bool $next=true): ?Image
{
global $database;
if ($next) {
$gtlt = "<";
$dir = "DESC";
} else {
$gtlt = ">";
$dir = "ASC";
}
if (count($tags) === 0) {
$row = $database->get_row('
2016-06-07 01:05:19 +00:00
SELECT images.*
FROM images
WHERE images.id '.$gtlt.' '.$this->id.'
ORDER BY images.id '.$dir.'
LIMIT 1
');
} else {
$tags[] = 'id'. $gtlt . $this->id;
$querylet = Image::build_search_querylet($tags);
$querylet->append_sql(' ORDER BY images.id '.$dir.' LIMIT 1');
$row = $database->get_row($querylet->sql, $querylet->variables);
}
return ($row ? new Image($row) : null);
}
/**
* The reverse of get_next
*
* #param string[] $tags
*/
public function get_prev(array $tags=[]): ?Image
{
return $this->get_next($tags, false);
}
/**
* Find the User who owns this Image
*/
public function get_owner(): User
{
return User::by_id($this->owner_id);
}
/**
* Set the image's owner.
*/
2019-05-29 17:23:29 +00:00
public function set_owner(User $owner): void
{
global $database;
if ($owner->id != $this->owner_id) {
$database->execute("
2016-06-07 01:05:19 +00:00
UPDATE images
SET owner_id=:owner_id
WHERE id=:id
", ["owner_id"=>$owner->id, "id"=>$this->id]);
log_info("core_image", "Owner for Image #{$this->id} set to {$owner->name}", null, ["image_id" => $this->id]);
}
}
/**
* Get this image's tags as an array.
*
* #return string[]
*/
public function get_tag_array(): array
{
global $database;
if (!isset($this->tag_array)) {
$this->tag_array = $database->get_col("
2016-06-07 01:05:19 +00:00
SELECT tag
FROM image_tags
JOIN tags ON image_tags.tag_id = tags.id
WHERE image_id=:id
ORDER BY tag
", ["id"=>$this->id]);
}
return $this->tag_array;
}
/**
* Get this image's tags as a string.
*/
public function get_tag_list(): string
{
return Tag::implode($this->get_tag_array());
}
/**
* Get the URL for the full size image
*/
public function get_image_link(): string
{
return $this->get_link('image_ilink', '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext');
}
/**
* Get the URL for the thumbnail
*/
public function get_thumb_link(): string
{
return $this->get_link('image_tlink', '_thumbs/$hash/thumb.jpg', 'thumb/$id.jpg');
}
/**
* Check configured template for a link, then try nice URL, then plain URL
*/
private function get_link(string $template, string $nice, string $plain): string
{
global $config;
$image_link = $config->get_string($template);
if (!empty($image_link)) {
if (!(strpos($image_link, "://") > 0) && !startsWith($image_link, "/")) {
$image_link = make_link($image_link);
}
return $this->parse_link_template($image_link);
} elseif ($config->get_bool('nice_urls', false)) {
return $this->parse_link_template(make_link($nice));
} else {
return $this->parse_link_template(make_link($plain));
}
}
/**
* Get the tooltip for this image, formatted according to the
* configured template.
*/
public function get_tooltip(): string
{
global $config;
$tt = $this->parse_link_template($config->get_string('image_tip'), "no_escape");
// Removes the size tag if the file is an mp3
if ($this->ext === 'mp3') {
$iitip = $tt;
$mp3tip = ["0x0"];
$h_tip = str_replace($mp3tip, " ", $iitip);
// Makes it work with a variation of the default tooltips (I.E $tags // $filesize // $size)
$justincase = [" //", "// ", " //", "// ", " "];
if (strstr($h_tip, " ")) {
$h_tip = html_escape(str_replace($justincase, "", $h_tip));
} else {
$h_tip = html_escape($h_tip);
}
return $h_tip;
} else {
return $tt;
}
}
/**
* Figure out where the full size image is on disk.
*/
public function get_image_filename(): string
{
return warehouse_path("images", $this->hash);
}
/**
* Figure out where the thumbnail is on disk.
*/
public function get_thumb_filename(): string
{
return warehouse_path("thumbs", $this->hash);
}
/**
* Get the original filename.
*/
public function get_filename(): string
{
return $this->filename;
}
/**
* Get the image's mime type.
*/
public function get_mime_type(): string
{
return getMimeType($this->get_image_filename(), $this->get_ext());
}
/**
* Get the image's filename extension
*/
public function get_ext(): string
{
return $this->ext;
}
/**
* Get the image's source URL
*/
2019-05-28 18:00:23 +00:00
public function get_source(): ?string
{
return $this->source;
}
/**
* Set the image's source URL
*/
public function set_source(string $new_source): void
{
global $database;
$old_source = $this->source;
if (empty($new_source)) {
$new_source = null;
}
if ($new_source != $old_source) {
$database->execute("UPDATE images SET source=:source WHERE id=:id", ["source"=>$new_source, "id"=>$this->id]);
log_info("core_image", "Source for Image #{$this->id} set to: $new_source (was $old_source)", null, ["image_id" => $this->id]);
}
}
/**
* Check if the image is locked.
*/
public function is_locked(): bool
{
return $this->locked;
}
2019-05-29 17:23:29 +00:00
public function set_locked(bool $tf): void
{
global $database;
$ln = $tf ? "Y" : "N";
$sln = $database->scoreql_to_sql('SCORE_BOOL_'.$ln);
$sln = str_replace("'", "", $sln);
$sln = str_replace('"', "", $sln);
if (bool_escape($sln) !== $this->locked) {
$database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn"=>$sln, "id"=>$this->id]);
log_info("core_image", "Setting Image #{$this->id} lock to: $ln", null, ["image_id" => $this->id]);
}
}
/**
* Delete all tags from this image.
*
* Normally in preparation to set them to a new set.
*/
public function delete_tags_from_image(): void
{
global $database;
if ($database->get_driver_name() == "mysql") {
//mysql < 5.6 has terrible subquery optimization, using EXISTS / JOIN fixes this
$database->execute(
"
UPDATE tags t
INNER JOIN image_tags it ON t.id = it.tag_id
SET count = count - 1
WHERE it.image_id = :id",
["id"=>$this->id]
);
} else {
$database->execute("
UPDATE tags
SET count = count - 1
2016-06-07 01:05:19 +00:00
WHERE id IN (
SELECT tag_id
FROM image_tags
WHERE image_id = :id
)
", ["id"=>$this->id]);
}
$database->execute("
2016-06-07 01:05:19 +00:00
DELETE
FROM image_tags
WHERE image_id=:id
", ["id"=>$this->id]);
}
/**
* Set the tags for this image.
*/
2019-05-29 17:23:29 +00:00
public function set_tags(array $unfiltered_tags): void
{
global $database;
$unfiltered_tags = array_unique($unfiltered_tags);
$tags = [];
foreach ($unfiltered_tags as $tag) {
if (mb_strlen($tag, 'UTF-8') > 255) {
flash_message("Can't set a tag longer than 255 characters");
continue;
}
if (startsWith($tag, "-")) {
flash_message("Can't set a tag which starts with a minus");
continue;
}
$tags[] = $tag;
}
if (count($tags) <= 0) {
throw new SCoreException('Tried to set zero tags');
}
if (Tag::implode($tags) != $this->get_tag_list()) {
// delete old
$this->delete_tags_from_image();
// insert each new tags
foreach ($tags as $tag) {
$id = $database->get_one(
$database->scoreql_to_sql("
2016-06-07 01:05:19 +00:00
SELECT id
FROM tags
WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag)
"),
["tag"=>$tag]
);
if (empty($id)) {
// a new tag
$database->execute(
"INSERT INTO tags(tag) VALUES (:tag)",
["tag"=>$tag]
);
$database->execute(
"INSERT INTO image_tags(image_id, tag_id)
VALUES(:id, (SELECT id FROM tags WHERE tag = :tag))",
["id"=>$this->id, "tag"=>$tag]
);
} else {
// user of an existing tag
$database->execute("
2016-06-07 01:05:19 +00:00
INSERT INTO image_tags(image_id, tag_id)
VALUES(:iid, :tid)
", ["iid"=>$this->id, "tid"=>$id]);
}
$database->execute(
$database->scoreql_to_sql("
2016-06-07 01:05:19 +00:00
UPDATE tags
SET count = count + 1
WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag)
"),
["tag"=>$tag]
);
}
log_info("core_image", "Tags for Image #{$this->id} set to: ".Tag::implode($tags), null, ["image_id" => $this->id]);
$database->cache->delete("image-{$this->id}-tags");
}
}
/**
* Send list of metatags to be parsed.
*
* #param string[] $metatags
*/
public function parse_metatags(array $metatags, int $image_id): void
{
foreach ($metatags as $tag) {
$ttpe = new TagTermParseEvent($tag, $image_id, true);
send_event($ttpe);
}
}
/**
* Delete this image from the database and disk
*/
public function delete(): void
{
global $database;
$this->delete_tags_from_image();
$database->execute("DELETE FROM images WHERE id=:id", ["id"=>$this->id]);
log_info("core_image", 'Deleted Image #'.$this->id.' ('.$this->hash.')', null, ["image_id" => $this->id]);
unlink($this->get_image_filename());
unlink($this->get_thumb_filename());
}
/**
* This function removes an image (and thumbnail) from the DISK ONLY.
* It DOES NOT remove anything from the database.
*/
public function remove_image_only(): void
{
log_info("core_image", 'Removed Image File ('.$this->hash.')', null, ["image_id" => $this->id]);
@unlink($this->get_image_filename());
@unlink($this->get_thumb_filename());
}
public function parse_link_template(string $tmpl, string $_escape="url_escape", int $n=0): string
{
global $config;
// don't bother hitting the database if it won't be used...
$tags = "";
if (strpos($tmpl, '$tags') !== false) { // * stabs dynamically typed languages with a rusty spoon *
$tags = $this->get_tag_list();
$tags = str_replace("/", "", $tags);
$tags = preg_replace("/^\.+/", "", $tags);
}
$base_href = $config->get_string('base_href');
$fname = $this->get_filename();
$base_fname = strpos($fname, '.') ? substr($fname, 0, strrpos($fname, '.')) : $fname;
$tmpl = str_replace('$id', $this->id, $tmpl);
$tmpl = str_replace('$hash_ab', substr($this->hash, 0, 2), $tmpl);
$tmpl = str_replace('$hash_cd', substr($this->hash, 2, 2), $tmpl);
$tmpl = str_replace('$hash', $this->hash, $tmpl);
$tmpl = str_replace('$tags', $_escape($tags), $tmpl);
$tmpl = str_replace('$base', $base_href, $tmpl);
$tmpl = str_replace('$ext', $this->ext, $tmpl);
$tmpl = str_replace('$size', "{$this->width}x{$this->height}", $tmpl);
$tmpl = str_replace('$filesize', to_shorthand_int($this->filesize), $tmpl);
$tmpl = str_replace('$filename', $_escape($base_fname), $tmpl);
$tmpl = str_replace('$title', $_escape($config->get_string("title")), $tmpl);
$tmpl = str_replace('$date', $_escape(autodate($this->posted, false)), $tmpl);
// nothing seems to use this, sending the event out to 50 exts is a lot of overhead
if (!SPEED_HAX) {
$plte = new ParseLinkTemplateEvent($tmpl, $this);
send_event($plte);
$tmpl = $plte->link;
}
static $flexihash = null;
static $fh_last_opts = null;
$matches = [];
if (preg_match("/(.*){(.*)}(.*)/", $tmpl, $matches)) {
$pre = $matches[1];
$opts = $matches[2];
$post = $matches[3];
if ($opts != $fh_last_opts) {
$fh_last_opts = $opts;
$flexihash = new Flexihash\Flexihash();
foreach (explode(",", $opts) as $opt) {
$parts = explode("=", $opt);
$parts_count = count($parts);
$opt_val = "";
$opt_weight = 0;
if ($parts_count === 2) {
$opt_val = $parts[0];
$opt_weight = $parts[1];
} elseif ($parts_count === 1) {
$opt_val = $parts[0];
$opt_weight = 1;
}
$flexihash->addTarget($opt_val, $opt_weight);
}
}
// $choice = $flexihash->lookup($pre.$post);
$choices = $flexihash->lookupList($this->hash, $n+1); // hash doesn't change
$choice = $choices[$n];
$tmpl = $pre.$choice.$post;
}
return $tmpl;
}
/**
* #param string[] $terms
*/
private static function build_search_querylet(array $terms): Querylet
{
global $database;
$tag_querylets = [];
$img_querylets = [];
$positive_tag_count = 0;
$negative_tag_count = 0;
/*
* Turn a bunch of strings into a bunch of TagQuerylet
* and ImgQuerylet objects
*/
$stpe = new SearchTermParseEvent(null, $terms);
send_event($stpe);
if ($stpe->is_querylet_set()) {
foreach ($stpe->get_querylets() as $querylet) {
$img_querylets[] = new ImgQuerylet($querylet, true);
}
}
foreach ($terms as $term) {
$positive = true;
if (is_string($term) && !empty($term) && ($term[0] == '-')) {
$positive = false;
$term = substr($term, 1);
}
if (strlen($term) === 0) {
continue;
}
$stpe = new SearchTermParseEvent($term, $terms);
send_event($stpe);
if ($stpe->is_querylet_set()) {
foreach ($stpe->get_querylets() as $querylet) {
$img_querylets[] = new ImgQuerylet($querylet, $positive);
}
} else {
// if the whole match is wild, skip this;
// if not, translate into SQL
if (str_replace("*", "", $term) != "") {
$term = str_replace('_', '\_', $term);
$term = str_replace('%', '\%', $term);
$term = str_replace('*', '%', $term);
$tag_querylets[] = new TagQuerylet($term, $positive);
if ($positive) {
$positive_tag_count++;
} else {
$negative_tag_count++;
}
}
}
}
/*
* Turn a bunch of Querylet objects into a base query
*
* Must follow the format
*
* SELECT images.*
* FROM (...) AS images
* WHERE (...)
*
* ie, return a set of images.* columns, and end with a WHERE
*/
// no tags, do a simple search
if ($positive_tag_count === 0 && $negative_tag_count === 0) {
$query = new Querylet("
2016-06-06 23:19:41 +00:00
SELECT images.*
FROM images
WHERE 1=1
");
}
// one positive tag (a common case), do an optimised search
elseif ($positive_tag_count === 1 && $negative_tag_count === 0) {
# "LIKE" to account for wildcards
$query = new Querylet($database->scoreql_to_sql("
2016-06-07 00:34:38 +00:00
SELECT *
FROM (
SELECT images.*
FROM images
JOIN image_tags ON images.id=image_tags.image_id
JOIN tags ON image_tags.tag_id=tags.id
2016-06-07 00:39:23 +00:00
WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:tag)
2016-06-07 00:34:38 +00:00
GROUP BY images.id
) AS images
WHERE 1=1
"), ["tag"=>$tag_querylets[0]->tag]);
}
// more than one positive tag, or more than zero negative tags
else {
if ($database->get_driver_name() === "mysql") {
$query = Image::build_ugly_search_querylet($tag_querylets);
} else {
$query = Image::build_accurate_search_querylet($tag_querylets);
}
}
/*
* Merge all the image metadata searches into one generic querylet
* and append to the base querylet with "AND blah"
*/
if (!empty($img_querylets)) {
$n = 0;
$img_sql = "";
$img_vars = [];
foreach ($img_querylets as $iq) {
if ($n++ > 0) {
$img_sql .= " AND";
}
if (!$iq->positive) {
$img_sql .= " NOT";
}
$img_sql .= " (" . $iq->qlet->sql . ")";
$img_vars = array_merge($img_vars, $iq->qlet->variables);
}
$query->append_sql(" AND ");
$query->append(new Querylet($img_sql, $img_vars));
}
return $query;
}
/**
* WARNING: this description is no longer accurate, though it does get across
* the general idea - the actual method has a few extra optimisations
*
* "foo bar -baz user=foo" becomes
*
* SELECT * FROM images WHERE
* images.id IN (SELECT image_id FROM image_tags WHERE tag='foo')
* AND images.id IN (SELECT image_id FROM image_tags WHERE tag='bar')
* AND NOT images.id IN (SELECT image_id FROM image_tags WHERE tag='baz')
* AND images.id IN (SELECT id FROM images WHERE owner_name='foo')
*
* This is:
* A) Incredibly simple:
* Each search term maps to a list of image IDs
* B) Runs really fast on a good database:
* These lists are calculated once, and the set intersection taken
* C) Runs really slow on bad databases:
* All the subqueries are executed every time for every row in the
* images table. Yes, MySQL does suck this much.
*
* #param TagQuerylet[] $tag_querylets
*/
private static function build_accurate_search_querylet(array $tag_querylets): Querylet
{
global $database;
$positive_tag_id_array = [];
$negative_tag_id_array = [];
foreach ($tag_querylets as $tq) {
$tag_ids = $database->get_col(
$database->scoreql_to_sql("
2016-06-07 00:34:38 +00:00
SELECT id
FROM tags
2016-06-07 01:05:19 +00:00
WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:tag)
2016-06-07 00:34:38 +00:00
"),
["tag" => $tq->tag]
);
if ($tq->positive) {
$positive_tag_id_array = array_merge($positive_tag_id_array, $tag_ids);
if (count($tag_ids) == 0) {
# one of the positive tags had zero results, therefor there
# can be no results; "where 1=0" should shortcut things
return new Querylet("
2016-06-07 00:34:38 +00:00
SELECT images.*
FROM images
WHERE 1=0
");
}
} else {
$negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids);
}
}
assert($positive_tag_id_array || $negative_tag_id_array, @$_GET['q']);
$wheres = [];
if (!empty($positive_tag_id_array)) {
$positive_tag_id_list = join(', ', $positive_tag_id_array);
$wheres[] = "tag_id IN ($positive_tag_id_list)";
}
if (!empty($negative_tag_id_array)) {
$negative_tag_id_list = join(', ', $negative_tag_id_array);
$wheres[] = "tag_id NOT IN ($negative_tag_id_list)";
}
$wheres_str = join(" AND ", $wheres);
return new Querylet("
2016-06-07 00:34:38 +00:00
SELECT images.*
FROM images
2016-06-07 01:05:19 +00:00
WHERE images.id IN (
SELECT image_id
FROM image_tags
WHERE $wheres_str
GROUP BY image_id
HAVING COUNT(image_id) >= :search_score
)
", ["search_score"=>count($positive_tag_id_array)]);
}
/**
* this function exists because mysql is a turd, see the docs for
* build_accurate_search_querylet() for a full explanation
*
* #param TagQuerylet[] $tag_querylets
*/
private static function build_ugly_search_querylet(array $tag_querylets): Querylet
{
global $database;
$positive_tag_count = 0;
foreach ($tag_querylets as $tq) {
if ($tq->positive) {
$positive_tag_count++;
}
}
// only negative tags - shortcut to fail
if ($positive_tag_count == 0) {
// TODO: This isn't currently implemented.
// SEE: https://github.com/shish/shimmie2/issues/66
return new Querylet("
2016-06-06 23:19:41 +00:00
SELECT images.*
FROM images
2016-06-07 00:34:38 +00:00
WHERE 1=0
2016-06-06 23:19:41 +00:00
");
}
// merge all the tag querylets into one generic one
$sql = "0";
$terms = [];
foreach ($tag_querylets as $tq) {
$sign = $tq->positive ? "+" : "-";
$sql .= ' '.$sign.' IF(SUM(tag LIKE :tag'.Image::$tag_n.'), 1, 0)';
$terms['tag'.Image::$tag_n] = $tq->tag;
Image::$tag_n++;
}
$tag_search = new Querylet($sql, $terms);
$tag_id_array = [];
foreach ($tag_querylets as $tq) {
$tag_ids = $database->get_col(
$database->scoreql_to_sql("
2016-06-07 00:34:38 +00:00
SELECT id
FROM tags
2016-06-07 01:05:19 +00:00
WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:tag)
2016-06-07 00:34:38 +00:00
"),
["tag" => $tq->tag]
);
$tag_id_array = array_merge($tag_id_array, $tag_ids);
if ($tq->positive && count($tag_ids) == 0) {
# one of the positive tags had zero results, therefor there
# can be no results; "where 1=0" should shortcut things
return new Querylet("
2009-05-11 10:52:48 +00:00
SELECT images.*
FROM images
WHERE 1=0
");
}
}
2016-06-06 23:19:41 +00:00
Image::$tag_n = 0;
return new Querylet('
2016-06-07 00:34:38 +00:00
SELECT *
FROM (
2016-06-07 01:05:19 +00:00
SELECT images.*, ('.$tag_search->sql.') AS score
2016-06-07 00:34:38 +00:00
FROM images
LEFT JOIN image_tags ON image_tags.image_id = images.id
JOIN tags ON image_tags.tag_id = tags.id
WHERE tags.id IN (' . join(', ', $tag_id_array) . ')
GROUP BY images.id
HAVING score = :score
) AS images
WHERE 1=1
', array_merge(
$tag_search->variables,
["score"=>$positive_tag_count]
));
}
}