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/ext/bulk_actions/main.php

313 lines
10 KiB
PHP
Raw Normal View History

2021-12-14 18:32:47 +00:00
<?php
declare(strict_types=1);
2019-06-05 23:03:22 +00:00
namespace Shimmie2;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\{InputInterface,InputArgument};
use Symfony\Component\Console\Output\OutputInterface;
2019-06-06 00:37:07 +00:00
class BulkActionBlockBuildingEvent extends Event
{
/**
* @var array<array{block:string,access_key:?string,confirmation_message:string,action:string,button_text:string,position:int}>
*/
public array $actions = [];
/** @var string[] */
public array $search_terms = [];
public function add_action(string $action, string $button_text, ?string $access_key = null, string $confirmation_message = "", string $block = "", int $position = 40): void
2019-06-06 00:37:07 +00:00
{
2019-09-29 13:30:55 +00:00
if (!empty($access_key)) {
2023-11-11 21:49:12 +00:00
assert(strlen($access_key) == 1);
foreach ($this->actions as $existing) {
2023-11-11 21:49:12 +00:00
if ($existing["access_key"] == $access_key) {
2024-02-11 15:47:40 +00:00
throw new UserError("Access key $access_key is already in use");
}
}
}
2023-11-11 21:49:12 +00:00
$this->actions[] = [
"block" => $block,
"access_key" => $access_key,
"confirmation_message" => $confirmation_message,
"action" => $action,
"button_text" => $button_text,
"position" => $position
];
2019-06-06 00:37:07 +00:00
}
2019-06-05 23:03:22 +00:00
}
2019-06-06 00:37:07 +00:00
class BulkActionEvent extends Event
{
public string $action;
public \Generator $items;
/** @var array<string, mixed> */
public array $params;
public bool $redirect = true;
2019-06-05 23:03:22 +00:00
/**
* @param array<string, mixed> $params
*/
public function __construct(string $action, \Generator $items, array $params)
2019-06-06 00:37:07 +00:00
{
2020-01-26 13:19:35 +00:00
parent::__construct();
2019-06-05 23:03:22 +00:00
$this->action = $action;
$this->items = $items;
$this->params = $params;
2019-06-05 23:03:22 +00:00
}
}
class BulkActions extends Extension
{
2020-02-04 00:46:36 +00:00
/** @var BulkActionsTheme */
2023-06-27 14:56:49 +00:00
protected Themelet $theme;
2020-02-04 00:46:36 +00:00
public function onPostListBuilding(PostListBuildingEvent $event): void
2019-06-05 23:03:22 +00:00
{
global $page, $user;
2019-06-06 00:37:07 +00:00
$babbe = new BulkActionBlockBuildingEvent();
$babbe->search_terms = $event->search_terms;
send_event($babbe);
if (sizeof($babbe->actions) == 0) {
return;
}
usort($babbe->actions, [$this, "sort_blocks"]);
$this->theme->display_selector($page, $babbe->actions, Tag::implode($event->search_terms));
2019-06-05 23:03:22 +00:00
}
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void
2019-06-05 23:03:22 +00:00
{
global $user;
2019-07-09 14:10:21 +00:00
if ($user->can(Permissions::DELETE_IMAGE)) {
2019-06-17 09:52:05 +00:00
$event->add_action("bulk_delete", "(D)elete", "d", "Delete selected images?", $this->theme->render_ban_reason_input(), 10);
2019-06-05 23:03:22 +00:00
}
2019-07-09 14:10:21 +00:00
if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) {
$event->add_action(
"bulk_tag",
"Tag",
"t",
"",
$this->theme->render_tag_input(),
2019-09-29 13:30:55 +00:00
10
);
2019-06-05 23:03:22 +00:00
}
2019-07-09 14:10:21 +00:00
if ($user->can(Permissions::BULK_EDIT_IMAGE_SOURCE)) {
2019-09-29 13:30:55 +00:00
$event->add_action("bulk_source", "Set (S)ource", "s", "", $this->theme->render_source_input(), 10);
2019-06-05 23:03:22 +00:00
}
}
public function onCliGen(CliGenEvent $event): void
2019-10-04 19:50:49 +00:00
{
$event->app->register('bulk-action')
->addArgument('action', InputArgument::REQUIRED)
->addArgument('query', InputArgument::REQUIRED)
->setDescription('Perform a bulk action on a given query')
->setCode(function (InputInterface $input, OutputInterface $output): int {
$action = $input->getArgument('action');
$query = $input->getArgument('query');
$items = $this->yield_search_results($query);
log_info("bulk_actions", "Performing $action on $query");
send_event(new BulkActionEvent($action, $items, []));
return Command::SUCCESS;
});
2019-10-04 19:50:49 +00:00
}
public function onBulkAction(BulkActionEvent $event): void
2019-06-05 23:03:22 +00:00
{
global $page, $user;
2019-06-05 23:03:22 +00:00
2019-06-06 00:37:07 +00:00
switch ($event->action) {
case "bulk_delete":
2019-07-09 14:10:21 +00:00
if ($user->can(Permissions::DELETE_IMAGE)) {
$i = $this->delete_posts($event->items);
$page->flash("Deleted $i[0] items, totaling ".human_filesize($i[1]));
2019-06-05 23:03:22 +00:00
}
break;
case "bulk_tag":
if (!isset($event->params['bulk_tags'])) {
2019-06-05 23:03:22 +00:00
return;
}
2019-07-09 14:10:21 +00:00
if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) {
$tags = $event->params['bulk_tags'];
2019-06-05 23:03:22 +00:00
$replace = false;
if (isset($event->params['bulk_tags_replace']) && $event->params['bulk_tags_replace'] == "true") {
2019-06-05 23:03:22 +00:00
$replace = true;
}
2019-06-06 00:37:07 +00:00
2023-11-11 21:49:12 +00:00
$i = $this->tag_items($event->items, $tags, $replace);
$page->flash("Tagged $i items");
2019-06-05 23:03:22 +00:00
}
break;
case "bulk_source":
if (!isset($event->params['bulk_source'])) {
2019-06-05 23:03:22 +00:00
return;
}
2019-07-09 14:10:21 +00:00
if ($user->can(Permissions::BULK_EDIT_IMAGE_SOURCE)) {
$source = $event->params['bulk_source'];
$i = $this->set_source($event->items, $source);
$page->flash("Set source for $i items");
2019-06-05 23:03:22 +00:00
}
break;
}
}
public function onPageRequest(PageRequestEvent $event): void
2019-06-05 23:03:22 +00:00
{
global $page, $user;
if ($event->page_matches("bulk_action", method: "POST", permission: Permissions::PERFORM_BULK_ACTIONS)) {
2024-02-10 00:08:55 +00:00
$action = $event->req_POST('bulk_action');
2024-02-11 15:47:40 +00:00
$items = null;
if ($event->get_POST('bulk_selected_ids')) {
$data = json_decode($event->req_POST('bulk_selected_ids'));
if (!is_array($data) || empty($data)) {
throw new InvalidInput("No ids specified in bulk_selected_ids");
2019-06-05 23:03:22 +00:00
}
2024-02-11 15:47:40 +00:00
$items = $this->yield_items($data);
} elseif ($event->get_POST('bulk_query')) {
$query = $event->req_POST('bulk_query');
$items = $this->yield_search_results($query);
} else {
throw new InvalidInput("No ids selected and no query present, cannot perform bulk operation on entire collection");
}
2019-06-05 23:03:22 +00:00
2024-02-11 15:47:40 +00:00
shm_set_timeout(null);
$bae = send_event(new BulkActionEvent($action, $items, $event->POST));
2020-10-29 01:28:46 +00:00
2024-02-11 15:47:40 +00:00
if ($bae->redirect) {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(referer_or(make_link()));
}
2019-06-05 23:03:22 +00:00
}
}
/**
* @param int[] $data
* @return \Generator<Image>
*/
private function yield_items(array $data): \Generator
{
foreach ($data as $id) {
$image = Image::by_id($id);
if ($image != null) {
yield $image;
}
}
}
/**
* @return \Generator<Image>
*/
private function yield_search_results(string $query): \Generator
{
$tags = Tag::explode($query);
return Search::find_images_iterable(0, null, $tags);
}
/**
* @param array{position: int} $a
* @param array{position: int} $b
*/
private function sort_blocks(array $a, array $b): int
2019-06-14 12:47:50 +00:00
{
return $a["position"] - $b["position"];
}
/**
* @param iterable<Image> $posts
* @return array{0: int, 1: int}
*/
private function delete_posts(iterable $posts): array
2019-06-06 00:37:07 +00:00
{
global $page;
2019-06-05 23:03:22 +00:00
$total = 0;
$size = 0;
foreach ($posts as $post) {
2019-06-05 23:03:22 +00:00
try {
2024-01-20 01:03:01 +00:00
if (Extension::is_enabled(ImageBanInfo::KEY) && isset($_POST['bulk_ban_reason'])) {
2019-06-17 09:52:05 +00:00
$reason = $_POST['bulk_ban_reason'];
if ($reason) {
send_event(new AddImageHashBanEvent($post->hash, $reason));
2019-06-17 09:52:05 +00:00
}
}
send_event(new ImageDeletionEvent($post));
2019-06-05 23:03:22 +00:00
$total++;
$size += $post->filesize;
} catch (\Exception $e) {
$page->flash("Error while removing {$post->id}: " . $e->getMessage());
2019-06-05 23:03:22 +00:00
}
2019-06-06 00:37:07 +00:00
}
return [$total, $size];
2019-06-05 23:03:22 +00:00
}
/**
* @param iterable<Image> $items
*/
private function tag_items(iterable $items, string $tags, bool $replace): int
2019-06-06 00:37:07 +00:00
{
2019-06-05 23:03:22 +00:00
$tags = Tag::explode($tags);
$pos_tag_array = [];
$neg_tag_array = [];
foreach ($tags as $new_tag) {
if (str_starts_with($new_tag, '-')) {
2019-06-05 23:03:22 +00:00
$neg_tag_array[] = substr($new_tag, 1);
} else {
$pos_tag_array[] = $new_tag;
}
}
$total = 0;
if ($replace) {
foreach ($items as $image) {
send_event(new TagSetEvent($image, $tags));
2019-06-05 23:03:22 +00:00
$total++;
}
} else {
foreach ($items as $image) {
2019-09-29 13:30:55 +00:00
$img_tags = array_map("strtolower", $image->get_tag_array());
2019-06-05 23:03:22 +00:00
if (!empty($neg_tag_array)) {
2019-09-29 13:30:55 +00:00
$neg_tag_array = array_map("strtolower", $neg_tag_array);
$img_tags = array_merge($pos_tag_array, $img_tags);
2019-06-05 23:03:22 +00:00
$img_tags = array_diff($img_tags, $neg_tag_array);
} else {
$img_tags = array_merge($tags, $img_tags);
2019-06-05 23:03:22 +00:00
}
send_event(new TagSetEvent($image, $img_tags));
2019-06-05 23:03:22 +00:00
$total++;
}
}
return $total;
2019-06-05 23:03:22 +00:00
}
/**
* @param iterable<Image> $items
*/
2023-08-17 17:12:36 +00:00
private function set_source(iterable $items, string $source): int
2019-06-06 00:37:07 +00:00
{
global $page;
2019-06-05 23:03:22 +00:00
$total = 0;
foreach ($items as $image) {
2019-06-05 23:03:22 +00:00
try {
send_event(new SourceSetEvent($image, $source));
2019-06-05 23:03:22 +00:00
$total++;
2023-01-11 11:15:26 +00:00
} catch (\Exception $e) {
2020-01-26 13:19:35 +00:00
$page->flash("Error while setting source for {$image->id}: " . $e->getMessage());
2019-06-05 23:03:22 +00:00
}
}
return $total;
2019-06-05 23:03:22 +00:00
}
}