Merge pull request #697 from sanmadjack/pull

Auto-tag extension and small changes
This commit is contained in:
Shish 2020-03-05 01:19:41 +00:00 committed by GitHub
commit 04bfdf895f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 447 additions and 0 deletions

View File

@ -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

View File

@ -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,

View File

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
abstract class AutoTaggerConfig
{
public const VERSION = "ext_auto_tagger_ver";
public const ITEMS_PER_PAGE = "auto_tagger_items_per_page";
}

12
ext/auto_tagger/info.php Normal file
View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
class AutoTaggerInfo extends ExtensionInfo
{
public const KEY = "auto_tagger";
public $key = self::KEY;
public $name = "Auto-Tagger";
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public $license = self::LICENSE_WTFPL;
public $description = "Provides several automatic tagging functions";
}

331
ext/auto_tagger/main.php Normal file
View File

@ -0,0 +1,331 @@
<?php declare(strict_types=1);
require_once 'config.php';
use MicroCRUD\ActionColumn;
use MicroCRUD\TextColumn;
use MicroCRUD\Table;
class AutoTaggerTable extends Table
{
public function __construct(\FFSPHP\PDO $db)
{
parent::__construct($db);
$this->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;
}
}

58
ext/auto_tagger/test.php Normal file
View File

@ -0,0 +1,58 @@
<?php declare(strict_types=1);
class AutoTaggerTest extends ShimmiePHPUnitTestCase
{
public function testAutoTaggerList()
{
$this->get_page('auto_tag/list');
$this->assert_response(200);
$this->assert_title("Auto-Tag");
}
public function testAutoTaggerListReadOnly()
{
$this->log_in_as_user();
$this->get_page('auto_tag/list');
$this->assert_title("Auto-Tag");
$this->assert_no_text("value=\"Add\"");
$this->log_out();
$this->get_page('auto_tag/list');
$this->assert_title("Auto-Tag");
$this->assert_no_text("value=\"Add\"");
}
public function testAutoTagger()
{
$this->log_in_as_admin();
$this->get_page("auto_tag/export/auto_tag.csv");
$this->assert_no_text("test1");
send_event(new AddAutoTagEvent("test1", "test2"));
$this->get_page('auto_tag/list');
$this->assert_text("test1");
$this->assert_text("test2");
$this->get_page("auto_tag/export/auto_tag.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: test1 test2");
$this->delete_image($image_id);
send_event(new AddAutoTagEvent("test2", "test3"));
$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: test1 test2 test3");
$this->delete_image($image_id);
send_event(new DeleteAutoTagEvent("test1"));
send_event(new DeleteAutoTagEvent("test2"));
$this->get_page('auto_tag/list');
$this->assert_title("Auto-Tag");
$this->assert_no_text("test1");
$this->assert_no_text("test2");
$this->assert_no_text("test3");
}
}

36
ext/auto_tagger/theme.php Normal file
View File

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
class AutoTaggerTheme extends Themelet
{
/**
* Show a page of auto-tag definitions.
*
* Note: $can_manage = whether things like "add new alias" should be shown
*/
public function display_auto_tagtable($table, $paginator): void
{
global $page, $user;
$can_manage = $user->can(Permissions::MANAGE_AUTO_TAG);
$html = "
$table
$paginator
<p><a href='".make_link("auto_tag/export/auto_tag.csv")."' download='auto_tag.csv'>Download as CSV</a></p>
";
$bulk_html = "
".make_form(make_link("auto_tag/import"), 'post', true)."
<input type='file' name='auto_tag_file'>
<input type='submit' value='Upload List'>
</form>
";
$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));
}
}
}