From b4bde94516ab28ddcbeee782628a77ff4f52845e Mon Sep 17 00:00:00 2001 From: Matthew Barbour Date: Mon, 2 Mar 2020 15:24:40 +0000 Subject: [PATCH] Added auto-tagger extension --- core/permissions.php | 1 + core/userclass.php | 2 + ext/auto_tagger/config.php | 7 + ext/auto_tagger/info.php | 12 ++ ext/auto_tagger/main.php | 331 +++++++++++++++++++++++++++++++++++++ ext/auto_tagger/test.php | 85 ++++++++++ ext/auto_tagger/theme.php | 36 ++++ 7 files changed, 474 insertions(+) create mode 100644 ext/auto_tagger/config.php create mode 100644 ext/auto_tagger/info.php create mode 100644 ext/auto_tagger/main.php create mode 100644 ext/auto_tagger/test.php create mode 100644 ext/auto_tagger/theme.php diff --git a/core/permissions.php b/core/permissions.php index 27221c66..1b514a46 100644 --- a/core/permissions.php +++ b/core/permissions.php @@ -8,6 +8,7 @@ abstract class Permissions public const MANAGE_EXTENSION_LIST = "manage_extension_list"; public const MANAGE_ALIAS_LIST = "manage_alias_list"; + public const MANAGE_AUTO_TAG = "manage_auto_tag"; public const MASS_TAG_EDIT = "mass_tag_edit"; public const VIEW_IP = "view_ip"; # view IP addresses associated with things diff --git a/core/userclass.php b/core/userclass.php index 901a1532..b5409b25 100644 --- a/core/userclass.php +++ b/core/userclass.php @@ -77,6 +77,7 @@ new UserClass("base", null, [ Permissions::MANAGE_EXTENSION_LIST => false, Permissions::MANAGE_ALIAS_LIST => false, + Permissions::MANAGE_AUTO_TAG => false, Permissions::MASS_TAG_EDIT => false, Permissions::VIEW_IP => false, # view IP addresses associated with things @@ -209,6 +210,7 @@ new UserClass("admin", "base", [ Permissions::REPLACE_IMAGE => true, Permissions::MANAGE_EXTENSION_LIST => true, Permissions::MANAGE_ALIAS_LIST => true, + Permissions::MANAGE_AUTO_TAG => true, Permissions::EDIT_IMAGE_TAG => true, Permissions::EDIT_IMAGE_SOURCE => true, Permissions::EDIT_IMAGE_OWNER => true, diff --git a/ext/auto_tagger/config.php b/ext/auto_tagger/config.php new file mode 100644 index 00000000..f7bb0902 --- /dev/null +++ b/ext/auto_tagger/config.php @@ -0,0 +1,7 @@ +"matthew@darkholme.net"]; + public $license = self::LICENSE_WTFPL; + public $description = "Provides several automatic tagging functions"; +} diff --git a/ext/auto_tagger/main.php b/ext/auto_tagger/main.php new file mode 100644 index 00000000..da32789a --- /dev/null +++ b/ext/auto_tagger/main.php @@ -0,0 +1,331 @@ +table = "auto_tagger"; + $this->base_query = "SELECT * FROM auto_tag"; + $this->primary_key = "tag"; + $this->size = 100; + $this->limit = 1000000; + $this->set_columns([ + new TextColumn("tag", "Tag"), + new TextColumn("additional_tags", "Additional Tags"), + new ActionColumn("tag"), + ]); + $this->order_by = ["tag"]; + $this->table_attrs = ["class" => "zebra"]; + } +} + +class AddAutoTagEvent extends Event +{ + /** @var string */ + public $tag; + /** @var string */ + public $additional_tags; + + public function __construct(string $tag, string $additional_tags) + { + parent::__construct(); + $this->tag = trim($tag); + $this->additional_tags = trim($additional_tags); + } +} + +class DeleteAutoTagEvent extends Event +{ + public $tag; + + public function __construct(string $tag) + { + parent::__construct(); + $this->tag = $tag; + } +} + +class AddAutoTagException extends SCoreException +{ +} + +class AutoTagger extends Extension +{ + /** @var AutoTaggerTheme */ + protected $theme; + + public function onPageRequest(PageRequestEvent $event) + { + global $config, $database, $page, $user; + + if ($event->page_matches("auto_tag")) { + if ($event->get_arg(0) == "add") { + if ($user->can(Permissions::MANAGE_AUTO_TAG)) { + $user->ensure_authed(); + $input = validate_input(["c_tag"=>"string", "c_additional_tags"=>"string"]); + try { + send_event(new AddAutoTagEvent($input['c_tag'], $input['c_additional_tags'])); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("auto_tag/list")); + } catch (AddAutoTagException $ex) { + $this->theme->display_error(500, "Error adding auto-tag", $ex->getMessage()); + } + } + } elseif ($event->get_arg(0) == "remove") { + if ($user->can(Permissions::MANAGE_AUTO_TAG)) { + $user->ensure_authed(); + $input = validate_input(["d_tag"=>"string"]); + send_event(new DeleteAutoTagEvent($input['d_tag'])); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("auto_tag/list")); + } + } elseif ($event->get_arg(0) == "list") { + $t = new AutoTaggerTable($database->raw_db()); + $t->token = $user->get_auth_token(); + $t->inputs = $_GET; + $t->size = $config->get_int(AutoTaggerConfig::ITEMS_PER_PAGE, 30); + if ($user->can(Permissions::MANAGE_AUTO_TAG)) { + $t->create_url = make_link("auto_tag/add"); + $t->delete_url = make_link("auto_tag/remove"); + } + $this->theme->display_auto_tagtable($t->table($t->query()), $t->paginator()); + } elseif ($event->get_arg(0) == "export") { + $page->set_mode(PageMode::DATA); + $page->set_type("text/csv"); + $page->set_filename("auto_tag.csv"); + $page->set_data($this->get_auto_tag_csv($database)); + } elseif ($event->get_arg(0) == "import") { + if ($user->can(Permissions::MANAGE_AUTO_TAG)) { + if (count($_FILES) > 0) { + $tmp = $_FILES['auto_tag_file']['tmp_name']; + $contents = file_get_contents($tmp); + $count = $this->add_auto_tag_csv($database, $contents); + log_info(AutoTaggerInfo::KEY, "Imported $count auto-tag definitions from file from file", "Imported $count auto-tag definitions"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("auto_tag/list")); + } else { + $this->theme->display_error(400, "No File Specified", "You have to upload a file"); + } + } else { + $this->theme->display_error(401, "Admins Only", "Only admins can edit the auto-tag list"); + } + } + } + } + + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) + { + if ($event->parent=="tags") { + $event->add_nav_link("auto_tag", new Link('auto_tag/list'), "Auto-Tag", NavLink::is_active(["auto_tag"])); + } + } + + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) + { + global $database; + + // Create the database tables + if ($this->get_version(AutoTaggerConfig::VERSION) < 1) { + $database->create_table("auto_tag", " + tag VARCHAR(128) NOT NULL PRIMARY KEY, + additional_tags VARCHAR(2000) NOT NULL + "); + + if ($database->get_driver_name() == DatabaseDriver::PGSQL) { + $database->execute('CREATE INDEX auto_tag_lower_tag_idx ON auto_tag ((lower(tag)))'); + } + $this->set_version(AutoTaggerConfig::VERSION, 1); + + log_info(AutoTaggerInfo::KEY, "extension installed"); + } + } + + public function onTagSet(TagSetEvent $event) + { + $results = $this->apply_auto_tags($event->tags); + if (!empty($results)) { + $event->tags = $results; + } + } + + public function onAddAutoTag(AddAutoTagEvent $event) + { + global $page; + $this->add_auto_tag($event->tag, $event->additional_tags); + $page->flash("Added Auto-Tag"); + } + + public function onDeleteAutoTag(DeleteAutoTagEvent $event) + { + $this->remove_auto_tag($event->tag); + } + + public function onUserBlockBuilding(UserBlockBuildingEvent $event) + { + global $user; + if ($user->can(Permissions::MANAGE_AUTO_TAG)) { + $event->add_link("Auto-Tag Editor", make_link("auto_tag/list")); + } + } + + private function get_auto_tag_csv(Database $database): string + { + $csv = ""; + $pairs = $database->get_pairs("SELECT tag, additional_tags FROM auto_tag ORDER BY tag"); + foreach ($pairs as $old => $new) { + $csv .= "\"$old\",\"$new\"\n"; + } + return $csv; + } + + private function add_auto_tag_csv(Database $database, string $csv): int + { + $csv = str_replace("\r", "\n", $csv); + $i = 0; + foreach (explode("\n", $csv) as $line) { + $parts = str_getcsv($line); + if (count($parts) == 2) { + try { + send_event(new AddAutoTagEvent($parts[0], $parts[1])); + $i++; + } catch (AddAutoTagException $ex) { + $this->theme->display_error(500, "Error adding auto-tags", $ex->getMessage()); + } + } + } + return $i; + } + + private function add_auto_tag(string $tag, string $additional_tags) + { + global $database; + if ($database->exists("SELECT * FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag"=>$tag])) { + throw new AutoTaggerException("Auto-Tag is already set for that tag"); + } else { + $tag = Tag::sanitize($tag); + $additional_tags = Tag::explode($additional_tags); + + $database->execute( + "INSERT INTO auto_tag(tag, additional_tags) VALUES(:tag, :additional_tags)", + ["tag"=>$tag, "additional_tags"=>Tag::implode($additional_tags)] + ); + + log_info( + AutoTaggerInfo::KEY, + "Added auto-tag for {$tag} -> {".implode(" ", $additional_tags)."}" + ); + + // Now we apply it to existing items + $this->apply_new_auto_tag($tag); + } + } + + private function update_auto_tag(string $tag, string $additional_tags): bool + { + global $database; + $result = $database->get_row("SELECT * FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag"=>$tag]); + + if ($result===null) { + throw new AutoTaggerException("Auto-tag not set for $tag, can't update"); + } else { + $additional_tags = Tag::explode($additional_tags); + $current_additional_tags = Tag::explode($result["additional_tags"]); + + if (!Tag::compare($additional_tags, $current_additional_tags)) { + $database->execute( + "UPDATE auto_tag SET additional_tags = :additional_tags WHERE LOWER(tag)=LOWER(:tag)", + ["tag"=>$tag, "additional_tags"=>Tag::implode($additional_tags)] + ); + + log_info( + AutoTaggerInfo::KEY, + "Updated auto-tag for {$tag} -> {".implode(" ", $additional_tags)."}", + "Updated Auto-Tag" + ); + + // Now we apply it to existing items + $this->apply_new_auto_tag($tag); + return true; + } + } + return false; + } + + private function apply_new_auto_tag(string $tag) + { + global $database; + $tag_id = $database->get_one("SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)", ["tag"=>$tag]); + if (!empty($tag_id)) { + $image_ids = $database->get_col_iterable("SELECT image_id FROM image_tags WHERE tag_id = :tag_id", ["tag_id"=>$tag_id]); + foreach ($image_ids as $image_id) { + $image = Image::by_id($image_id); + $event = new TagSetEvent($image, $image->get_tag_array()); + send_event($event); + } + } + } + + + + private function remove_auto_tag(String $tag) + { + global $database; + + $database->execute("DELETE FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag" => $tag]); + } + + /** + * #param string[] $tags_mixed + */ + private function apply_auto_tags(array $tags_mixed): ?array + { + global $database; + + while (true) { + $new_tags = []; + foreach ($tags_mixed as $tag) { + $additional_tags = $database->get_one( + "SELECT additional_tags FROM auto_tag WHERE LOWER(tag) = LOWER(:input)", + ["input" => $tag] + ); + + if (!empty($additional_tags)) { + $additional_tags = explode(" ", $additional_tags); + $new_tags = array_merge( + $new_tags, + array_udiff($additional_tags, $tags_mixed, 'strcasecmp') + ); + } + } + if (empty($new_tags)) { + break; + } + $tags_mixed = array_merge($tags_mixed, $new_tags); + } + + $results = array_intersect_key( + $tags_mixed, + array_unique(array_map('strtolower', $tags_mixed)) + ); + + + + return $results; + } + + /** + * Get the priority for this extension. + * + */ + public function get_priority(): int + { + return 30; + } +} diff --git a/ext/auto_tagger/test.php b/ext/auto_tagger/test.php new file mode 100644 index 00000000..75ad39c8 --- /dev/null +++ b/ext/auto_tagger/test.php @@ -0,0 +1,85 @@ +get_page('alias/list'); + $this->assert_response(200); + $this->assert_title("Alias List"); + } + + public function testAliasListReadOnly() + { + $this->log_in_as_user(); + $this->get_page('alias/list'); + $this->assert_title("Alias List"); + $this->assert_no_text("Add"); + + $this->log_out(); + $this->get_page('alias/list'); + $this->assert_title("Alias List"); + $this->assert_no_text("Add"); + } + + public function testAliasOneToOne() + { + $this->log_in_as_admin(); + + $this->get_page("alias/export/aliases.csv"); + $this->assert_no_text("test1"); + + send_event(new AddAliasEvent("test1", "test2")); + $this->get_page('alias/list'); + $this->assert_text("test1"); + $this->get_page("alias/export/aliases.csv"); + $this->assert_text('"test1","test2"'); + + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1"); + $this->get_page("post/view/$image_id"); # check that the tag has been replaced + $this->assert_title("Image $image_id: test2"); + $this->get_page("post/list/test1/1"); # searching for an alias should find the master tag + $this->assert_response(302); + $this->get_page("post/list/test2/1"); # check that searching for the main tag still works + $this->assert_response(302); + $this->delete_image($image_id); + + send_event(new DeleteAliasEvent("test1")); + $this->get_page('alias/list'); + $this->assert_title("Alias List"); + $this->assert_no_text("test1"); + } + + public function testAliasOneToMany() + { + $this->log_in_as_admin(); + + $this->get_page("alias/export/aliases.csv"); + $this->assert_no_text("multi"); + + send_event(new AddAliasEvent("onetag", "multi tag")); + $this->get_page('alias/list'); + $this->assert_text("multi"); + $this->assert_text("tag"); + $this->get_page("alias/export/aliases.csv"); + $this->assert_text('"onetag","multi tag"'); + + $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "onetag"); + $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "onetag"); + $this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases + $this->assert_title("multi tag"); + $this->assert_no_text("No Images Found"); + $this->get_page("post/list/multi/1"); + $this->assert_title("multi"); + $this->assert_no_text("No Images Found"); + $this->get_page("post/list/multi tag/1"); + $this->assert_title("multi tag"); + $this->assert_no_text("No Images Found"); + $this->delete_image($image_id_1); + $this->delete_image($image_id_2); + + send_event(new DeleteAliasEvent("onetag")); + $this->get_page('alias/list'); + $this->assert_title("Alias List"); + $this->assert_no_text("test1"); + } +} diff --git a/ext/auto_tagger/theme.php b/ext/auto_tagger/theme.php new file mode 100644 index 00000000..e77510f7 --- /dev/null +++ b/ext/auto_tagger/theme.php @@ -0,0 +1,36 @@ +can(Permissions::MANAGE_AUTO_TAG); + $html = " + $table + $paginator +

Download as CSV

+ "; + + $bulk_html = " + ".make_form(make_link("auto_tag/import"), 'post', true)." + + + + "; + + $page->set_title("Auto-Tag List"); + $page->set_heading("Auto-Tag List"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Auto-Tag", $html)); + if ($can_manage) { + $page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51)); + } + } +}