snootpaintings/ext/tag_list/main.php

618 lines
21 KiB
PHP

<?php
declare(strict_types=1);
namespace Shimmie2;
require_once "config.php";
class TagList extends Extension
{
/** @var TagListTheme */
protected Themelet $theme;
private $tagcategories = null;
public function onInitExt(InitExtEvent $event)
{
global $config;
$config->set_default_int(TagListConfig::LENGTH, 15);
$config->set_default_int(TagListConfig::POPULAR_TAG_LIST_LENGTH, 15);
$config->set_default_int(TagListConfig::TAGS_MIN, 3);
$config->set_default_string(TagListConfig::INFO_LINK, 'https://en.wikipedia.org/wiki/$tag');
$config->set_default_string(TagListConfig::OMIT_TAGS, 'tagme*');
$config->set_default_string(TagListConfig::IMAGE_TYPE, TagListConfig::TYPE_RELATED);
$config->set_default_string(TagListConfig::RELATED_SORT, TagListConfig::SORT_ALPHABETICAL);
$config->set_default_string(TagListConfig::POPULAR_SORT, TagListConfig::SORT_TAG_COUNT);
$config->set_default_bool(TagListConfig::PAGES, false);
}
public function onPageRequest(PageRequestEvent $event)
{
global $page;
if ($event->page_matches("tags")) {
$this->theme->set_navigation($this->build_navigation());
if ($event->count_args() == 0) {
$sub = "map";
} else {
$sub = $event->get_arg(0);
}
switch ($sub) {
default:
case 'map':
$this->theme->set_heading("Tag Map");
$this->theme->set_tag_list($this->build_tag_map());
break;
case 'alphabetic':
$this->theme->set_heading("Alphabetic Tag List");
$this->theme->set_tag_list($this->build_tag_alphabetic());
break;
case 'popularity':
$this->theme->set_heading("Tag List by Popularity");
$this->theme->set_tag_list($this->build_tag_popularity());
break;
}
$this->theme->display_page($page);
}
}
public function onPostListBuilding(PostListBuildingEvent $event)
{
global $config, $page;
if ($config->get_int(TagListConfig::LENGTH) > 0) {
if (!empty($event->search_terms)) {
$this->add_refine_block($page, $event->search_terms);
} else {
$this->add_popular_block($page);
}
}
}
public function onPageNavBuilding(PageNavBuildingEvent $event)
{
$event->add_nav_link("tags", new Link('tags/map'), "Tags");
}
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
{
if ($event->parent=="tags") {
$event->add_nav_link("tags_map", new Link('tags/map'), "Map");
$event->add_nav_link("tags_alphabetic", new Link('tags/alphabetic'), "Alphabetic");
$event->add_nav_link("tags_popularity", new Link('tags/popularity'), "Popularity");
}
}
public function onDisplayingImage(DisplayingImageEvent $event)
{
global $config, $page;
if ($config->get_int(TagListConfig::LENGTH) > 0) {
$type = $config->get_string(TagListConfig::IMAGE_TYPE);
if ($type == TagListConfig::TYPE_TAGS || $type == TagListConfig::TYPE_BOTH) {
if (class_exists("Shimmie2\TagCategories") and $config->get_bool(TagCategoriesConfig::SPLIT_ON_VIEW)) {
$this->add_split_tags_block($page, $event->image);
} else {
$this->add_tags_block($page, $event->image);
}
}
if ($type == TagListConfig::TYPE_RELATED || $type == TagListConfig::TYPE_BOTH) {
$this->add_related_block($page, $event->image);
}
}
}
public function onSetupBuilding(SetupBuildingEvent $event)
{
$sb = $event->panel->create_new_block("Tag Map Options");
$sb->add_int_option(TagListConfig::TAGS_MIN, "Only show tags used at least ");
$sb->add_label(" times");
$sb->add_bool_option(TagListConfig::PAGES, "<br>Paged tag lists: ");
$sb = $event->panel->create_new_block("Popular / Related Tag List");
$sb->add_int_option(TagListConfig::LENGTH, "Show top ");
$sb->add_label(" related tags");
$sb->add_int_option(TagListConfig::POPULAR_TAG_LIST_LENGTH, "<br>Show top ");
$sb->add_label(" popular tags");
$sb->start_table();
$sb->add_text_option(TagListConfig::INFO_LINK, "Tag info link", true);
$sb->add_text_option(TagListConfig::OMIT_TAGS, "Omit tags", true);
$sb->add_choice_option(
TagListConfig::IMAGE_TYPE,
TagListConfig::TYPE_CHOICES,
"Post tag list",
true
);
$sb->add_choice_option(
TagListConfig::RELATED_SORT,
TagListConfig::SORT_CHOICES,
"Sort related list by",
true
);
$sb->add_choice_option(
TagListConfig::POPULAR_SORT,
TagListConfig::SORT_CHOICES,
"Sort popular list by",
true
);
$sb->add_bool_option("tag_list_numbers", "Show tag counts", true);
$sb->end_table();
}
/**
* Get the minimum number of times a tag needs to be used
* in order to be considered in the tag list.
*/
private function get_tags_min(): int
{
if (isset($_GET['mincount'])) {
return int_escape($_GET['mincount']);
} else {
global $config;
return $config->get_int(TagListConfig::TAGS_MIN); // get the default.
}
}
private static function get_omitted_tags(): array
{
global $cache, $config, $database;
$tags_config = $config->get_string(TagListConfig::OMIT_TAGS);
$results = $cache->get("tag_list_omitted_tags:".$tags_config);
if (is_null($results)) {
$tags = explode(" ", $tags_config);
if (count($tags) == 0) {
return [];
}
$where = [];
$args = [];
$i = 0;
foreach ($tags as $tag) {
$i++;
$arg = "tag$i";
$args[$arg] = Tag::sqlify($tag);
if (!str_contains($tag, '*')
&& !str_contains($tag, '?')) {
$where[] = " tag = :$arg ";
} else {
$where[] = " tag LIKE :$arg ";
}
}
$results = $database->get_col("SELECT id FROM tags WHERE " . implode(" OR ", $where), $args);
$cache->set("tag_list_omitted_tags:" . $tags_config, $results, 600);
}
return $results;
}
private function get_starts_with(): string
{
global $config;
if (isset($_GET['starts_with'])) {
return $_GET['starts_with'] . "%";
} else {
if ($config->get_bool(TagListConfig::PAGES)) {
return "a%";
} else {
return "%";
}
}
}
private function build_az(): string
{
global $database;
$tags_min = $this->get_tags_min();
$tag_data = $database->get_col("
SELECT DISTINCT
LOWER(substr(tag, 1, 1))
FROM tags
WHERE count >= :tags_min
ORDER BY LOWER(substr(tag, 1, 1))
", ["tags_min"=>$tags_min]);
$html = "<span class='atoz'>";
foreach ($tag_data as $a) {
$html .= " <a href='".modify_current_url(["starts_with"=>$a])."'>$a</a>";
}
$html .= "</span>\n<p><hr>";
return $html;
}
private function build_navigation(): string
{
$h_index = "<a href='".make_link()."'>Index</a>";
$h_map = "<a href='".make_link("tags/map")."'>Map</a>";
$h_alphabetic = "<a href='".make_link("tags/alphabetic")."'>Alphabetic</a>";
$h_popularity = "<a href='".make_link("tags/popularity")."'>Popularity</a>";
$h_all = "<a href='".modify_current_url(["mincount"=>1])."'>Show All</a>";
return "$h_index<br>&nbsp;<br>$h_map<br>$h_alphabetic<br>$h_popularity<br>&nbsp;<br>$h_all";
}
private function build_tag_map(): string
{
global $config, $database;
$tags_min = $this->get_tags_min();
$starts_with = $this->get_starts_with();
// check if we have a cached version
$cache_key = warehouse_path(
"cache/tag_cloud",
md5("tc" . $tags_min . $starts_with . VERSION)
);
if (file_exists($cache_key)) {
return file_get_contents($cache_key);
}
// SHIT: PDO/pgsql has problems using the same named param twice -_-;;
$tag_data = $database->get_all("
SELECT
tag,
FLOOR(LOG(2.7, LOG(2.7, count - :tags_min2 + 1)+1)*1.5*100)/100 AS scaled
FROM tags
WHERE count >= :tags_min
AND LOWER(tag) LIKE LOWER(:starts_with)
ORDER BY LOWER(tag)
", ["tags_min"=>$tags_min, "tags_min2"=>$tags_min, "starts_with"=>$starts_with]);
$html = "";
if ($config->get_bool(TagListConfig::PAGES)) {
$html .= $this->build_az();
}
$tag_category_dict = [];
if (class_exists('Shimmie2\TagCategories')) {
$this->tagcategories = new TagCategories();
$tag_category_dict = $this->tagcategories->getKeyedDict();
}
foreach ($tag_data as $row) {
$h_tag = html_escape($row['tag']);
$size = sprintf("%.2f", (float)$row['scaled']);
$link = $this->theme->tag_link($row['tag']);
if ($size<0.5) {
$size = 0.5;
}
$h_tag_no_underscores = str_replace("_", " ", $h_tag);
if (class_exists('Shimmie2\TagCategories')) {
$h_tag_no_underscores = $this->tagcategories->getTagHtml($h_tag, $tag_category_dict);
}
$html .= "&nbsp;<a style='font-size: {$size}em' href='$link'>$h_tag_no_underscores</a>&nbsp;\n";
}
if (SPEED_HAX) {
file_put_contents($cache_key, $html);
}
return $html;
}
private function build_tag_alphabetic(): string
{
global $config, $database;
$tags_min = $this->get_tags_min();
$starts_with = $this->get_starts_with();
// check if we have a cached version
$cache_key = warehouse_path(
"cache/tag_alpha",
md5("ta" . $tags_min . $starts_with . VERSION)
);
if (file_exists($cache_key)) {
return file_get_contents($cache_key);
}
$tag_data = $database->get_pairs("
SELECT tag, count
FROM tags
WHERE count >= :tags_min
AND LOWER(tag) LIKE LOWER(:starts_with)
ORDER BY LOWER(tag)
", ["tags_min"=>$tags_min, "starts_with"=>$starts_with]);
$html = "";
if ($config->get_bool(TagListConfig::PAGES)) {
$html .= $this->build_az();
}
/*
strtolower() vs. mb_strtolower()
( See https://www.php.net/manual/en/function.mb-strtolower.php for more info )
PHP5's strtolower function does not support Unicode (UTF-8) properly, so
you have to use another function, mb_strtolower, to handle UTF-8 strings.
What's worse is that mb_strtolower is horribly SLOW.
It would probably be better to have a config option for the Tag List that
would allow you to specify if there are UTF-8 tags.
*/
mb_internal_encoding('UTF-8');
$tag_category_dict = [];
if (class_exists('Shimmie2\TagCategories')) {
$this->tagcategories = new TagCategories();
$tag_category_dict = $this->tagcategories->getKeyedDict();
}
$lastLetter = "";
# postres utf8 string sort ignores punctuation, so we get "aza, a-zb, azc"
# which breaks down into "az, a-, az" :(
ksort($tag_data, SORT_STRING | SORT_FLAG_CASE);
foreach ($tag_data as $tag => $count) {
// In PHP, $array["10"] sets the array key as int(10), not string("10")...
$tag = (string)$tag;
if ($lastLetter != mb_strtolower(substr($tag, 0, strlen($starts_with)+1))) {
$lastLetter = mb_strtolower(substr($tag, 0, strlen($starts_with)+1));
$h_lastLetter = html_escape($lastLetter);
$html .= "<p>$h_lastLetter<br>";
}
$link = $this->theme->tag_link($tag);
$h_tag = html_escape($tag);
if (class_exists('Shimmie2\TagCategories')) {
$h_tag = $this->tagcategories->getTagHtml($h_tag, $tag_category_dict, "&nbsp;($count)");
}
$html .= "<a href='$link'>$h_tag</a>\n";
}
if (SPEED_HAX) {
file_put_contents($cache_key, $html);
}
return $html;
}
private function build_tag_popularity(): string
{
global $database;
$tags_min = $this->get_tags_min();
// Make sure that the value of $tags_min is at least 1.
// Otherwise the database will complain if you try to do: LOG(0)
if ($tags_min < 1) {
$tags_min = 1;
}
// check if we have a cached version
$cache_key = warehouse_path(
"cache/tag_popul",
md5("tp" . $tags_min . VERSION)
);
if (file_exists($cache_key)) {
return file_get_contents($cache_key);
}
$tag_data = $database->get_all("
SELECT tag, count, FLOOR(LOG(count)) AS scaled
FROM tags
WHERE count >= :tags_min
ORDER BY count DESC, tag ASC
", ["tags_min"=>$tags_min]);
$html = "Results grouped by log<sub>10</sub>(n)";
$lastLog = "";
foreach ($tag_data as $row) {
$h_tag = html_escape($row['tag']);
$count = $row['count'];
$scaled = $row['scaled'];
if ($lastLog != $scaled) {
$lastLog = $scaled;
$html .= "<p>$lastLog<br>";
}
$link = $this->theme->tag_link($row['tag']);
$html .= "<a href='$link'>$h_tag&nbsp;($count)</a>\n";
}
if (SPEED_HAX) {
file_put_contents($cache_key, $html);
}
return $html;
}
private function add_related_block(Page $page, Image $image): void
{
global $database, $config;
$omitted_tags = self::get_omitted_tags();
$starting_tags = $database->get_col("SELECT tag_id FROM image_tags WHERE image_id = :image_id", ["image_id" => $image->id]);
$starting_tags = array_diff($starting_tags, $omitted_tags);
if (count($starting_tags) === 0) {
// No valid starting tags, so can't look anything up
return;
}
$query = "SELECT tags.* FROM tags INNER JOIN (
SELECT it2.tag_id
FROM image_tags AS it1
INNER JOIN image_tags AS it2 ON it1.image_id=it2.image_id
AND it2.tag_id NOT IN (".implode(",", array_merge($omitted_tags, $starting_tags)).")
WHERE
it1.tag_id IN (".implode(",", $starting_tags).")
GROUP BY it2.tag_id
) A ON A.tag_id = tags.id
ORDER BY count DESC
LIMIT :tag_list_length
";
$args = ["tag_list_length" => $config->get_int(TagListConfig::LENGTH)];
$tags = $database->get_all($query, $args);
if (count($tags) > 0) {
$this->theme->display_related_block($page, $tags, "Related Tags");
}
}
private function add_split_tags_block(Page $page, Image $image)
{
global $database;
$query = "
SELECT tags.tag, tags.count
FROM tags, image_tags
WHERE tags.id = image_tags.tag_id
AND image_tags.image_id = :image_id
ORDER BY tags.count DESC
";
$args = ["image_id"=>$image->id];
$tags = $database->get_all($query, $args);
if (count($tags) > 0) {
$this->theme->display_split_related_block($page, $tags);
}
}
private function add_tags_block(Page $page, Image $image)
{
global $database;
$query = "
SELECT tags.tag, tags.count
FROM tags, image_tags
WHERE tags.id = image_tags.tag_id
AND image_tags.image_id = :image_id
ORDER BY tags.count DESC
";
$args = ["image_id"=>$image->id];
$tags = $database->get_all($query, $args);
if (count($tags) > 0) {
$this->theme->display_related_block($page, $tags, "Tags");
}
}
private function add_popular_block(Page $page)
{
global $cache, $database, $config;
$tags = $cache->get("popular_tags");
if (is_null($tags)) {
$omitted_tags = self::get_omitted_tags();
if (empty($omitted_tags)) {
$query = "
SELECT tag, count
FROM tags
WHERE count > 0
ORDER BY count DESC
LIMIT :popular_tag_list_length
";
} else {
$query = "
SELECT tag, count
FROM tags
WHERE count > 0
AND id NOT IN (".(implode(",", $omitted_tags)).")
ORDER BY count DESC
LIMIT :popular_tag_list_length
";
}
$args = ["popular_tag_list_length"=>$config->get_int(TagListConfig::POPULAR_TAG_LIST_LENGTH)];
$tags = $database->get_all($query, $args);
$cache->set("popular_tags", $tags, 600);
}
if (count($tags) > 0) {
$this->theme->display_popular_block($page, $tags);
}
}
/**
* #param string[] $search
*/
private function add_refine_block(Page $page, array $search)
{
global $config;
if (count($search) > 5) {
return;
}
$wild_tags = $search;
$related_tags = self::get_related_tags($search, $config->get_int(TagListConfig::LENGTH));
if (!empty($related_tags)) {
$this->theme->display_refine_block($page, $related_tags, $wild_tags);
}
}
public static function get_related_tags(array $search, int $limit): array
{
global $cache, $database;
$wild_tags = $search;
$cache_key = "related_tags:" . md5(Tag::implode($search));
$related_tags = $cache->get($cache_key);
if (is_null($related_tags)) {
// $search_tags = array();
$starting_tags = [];
$tags_ok = true;
foreach ($wild_tags as $tag) {
if ($tag[0] == "-" || str_starts_with($tag, "tagme")) {
continue;
}
$tag = Tag::sqlify($tag);
$tag_ids = $database->get_col("SELECT id FROM tags WHERE tag LIKE :tag AND count < 25000", ["tag" => $tag]);
// $search_tags = array_merge($search_tags,
// $database->get_col("SELECT tag FROM tags WHERE tag LIKE :tag", array("tag"=>$tag)));
$starting_tags = array_merge($starting_tags, $tag_ids);
$tags_ok = count($tag_ids) > 0;
if (!$tags_ok) {
break;
}
}
if (count($starting_tags) > 5 || count($starting_tags) === 0) {
return [];
}
$omitted_tags = self::get_omitted_tags();
$starting_tags = array_diff($starting_tags, $omitted_tags);
if (count($starting_tags) === 0) {
// No valid starting tags, so can't look anything up
return [];
}
if ($tags_ok) {
$query = "SELECT t.tag, A.calc_count AS count FROM tags t INNER JOIN (
SELECT it2.tag_id, COUNT(it2.image_id) AS calc_count
FROM image_tags AS it1 -- Got other images with the same tags
INNER JOIN image_tags AS it2 ON it1.image_id=it2.image_id
-- And filter out unwanted tags
AND it2.tag_id NOT IN (".implode(",", array_merge($omitted_tags, $starting_tags)).")
WHERE
it1.tag_id IN (".implode(",", $starting_tags).")
GROUP BY it2.tag_id) A ON A.tag_id = t.id
ORDER BY A.calc_count
DESC LIMIT :limit
";
$args = ["limit" => $limit];
$related_tags = $database->get_all($query, $args);
} else {
$related_tags = [];
}
$cache->set($cache_key, $related_tags, 60 * 60);
}
return $related_tags;
}
}