2021-12-14 18:32:47 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
2019-06-05 23:03:22 +00:00
|
|
|
|
2023-01-10 22:44:09 +00:00
|
|
|
namespace Shimmie2;
|
|
|
|
|
2024-01-11 00:55:05 +00:00
|
|
|
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
|
|
|
|
{
|
2024-01-20 14:10:59 +00:00
|
|
|
/**
|
|
|
|
* @var array<array{block:string,access_key:?string,confirmation_message:string,action:string,button_text:string,position:int}>
|
|
|
|
*/
|
2021-03-14 23:43:50 +00:00
|
|
|
public array $actions = [];
|
2024-01-20 14:10:59 +00:00
|
|
|
/** @var string[] */
|
2021-03-14 23:43:50 +00:00
|
|
|
public array $search_terms = [];
|
2019-06-21 20:28:20 +00:00
|
|
|
|
2024-08-31 23:04:34 +00:00
|
|
|
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);
|
2019-06-27 03:41:42 +00:00
|
|
|
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");
|
2019-06-27 03:41:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-11 21:49:12 +00:00
|
|
|
$this->actions[] = [
|
2024-01-20 14:10:59 +00:00
|
|
|
"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
|
|
|
|
{
|
2021-03-14 23:43:50 +00:00
|
|
|
public string $action;
|
2023-01-10 22:44:09 +00:00
|
|
|
public \Generator $items;
|
2024-02-10 00:05:33 +00:00
|
|
|
/** @var array<string, mixed> */
|
|
|
|
public array $params;
|
2021-03-14 23:43:50 +00:00
|
|
|
public bool $redirect = true;
|
2019-06-05 23:03:22 +00:00
|
|
|
|
2024-02-10 00:05:33 +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;
|
2024-02-10 00:05:33 +00:00
|
|
|
$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
|
|
|
|
2024-01-15 11:52:35 +00:00
|
|
|
public function onPostListBuilding(PostListBuildingEvent $event): void
|
2019-06-05 23:03:22 +00:00
|
|
|
{
|
2019-10-02 10:23:57 +00:00
|
|
|
global $page, $user;
|
2019-06-06 00:37:07 +00:00
|
|
|
|
2024-05-04 14:43:29 +00:00
|
|
|
$babbe = new BulkActionBlockBuildingEvent();
|
|
|
|
$babbe->search_terms = $event->search_terms;
|
2019-06-21 20:28:20 +00:00
|
|
|
|
2024-05-04 14:43:29 +00:00
|
|
|
send_event($babbe);
|
2019-06-12 22:44:25 +00:00
|
|
|
|
2024-05-04 14:43:29 +00:00
|
|
|
if (sizeof($babbe->actions) == 0) {
|
|
|
|
return;
|
|
|
|
}
|
2019-06-12 22:44:25 +00:00
|
|
|
|
2024-05-04 14:43:29 +00:00
|
|
|
usort($babbe->actions, [$this, "sort_blocks"]);
|
2019-06-12 22:44:25 +00:00
|
|
|
|
2024-05-04 14:43:29 +00:00
|
|
|
$this->theme->display_selector($page, $babbe->actions, Tag::implode($event->search_terms));
|
2019-06-05 23:03:22 +00:00
|
|
|
}
|
|
|
|
|
2024-01-15 11:52:35 +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)) {
|
2019-06-27 03:41:42 +00:00
|
|
|
$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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-15 11:52:35 +00:00
|
|
|
public function onCliGen(CliGenEvent $event): void
|
2019-10-04 19:50:49 +00:00
|
|
|
{
|
2024-01-11 00:55:05 +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");
|
2024-02-10 00:05:33 +00:00
|
|
|
send_event(new BulkActionEvent($action, $items, []));
|
2024-01-11 00:55:05 +00:00
|
|
|
return Command::SUCCESS;
|
|
|
|
});
|
2019-10-04 19:50:49 +00:00
|
|
|
}
|
|
|
|
|
2024-01-15 11:52:35 +00:00
|
|
|
public function onBulkAction(BulkActionEvent $event): void
|
2019-06-05 23:03:22 +00:00
|
|
|
{
|
2019-12-15 19:47:18 +00:00
|
|
|
global $page, $user;
|
2019-06-05 23:03:22 +00:00
|
|
|
|
2019-06-06 00:37:07 +00:00
|
|
|
switch ($event->action) {
|
2019-06-12 22:44:25 +00:00
|
|
|
case "bulk_delete":
|
2019-07-09 14:10:21 +00:00
|
|
|
if ($user->can(Permissions::DELETE_IMAGE)) {
|
2021-01-13 01:39:23 +00:00
|
|
|
$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;
|
2019-06-12 22:44:25 +00:00
|
|
|
case "bulk_tag":
|
2024-02-10 00:05:33 +00:00
|
|
|
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)) {
|
2024-02-10 00:05:33 +00:00
|
|
|
$tags = $event->params['bulk_tags'];
|
2019-06-05 23:03:22 +00:00
|
|
|
$replace = false;
|
2024-02-10 00:05:33 +00:00
|
|
|
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);
|
2019-12-15 19:47:18 +00:00
|
|
|
$page->flash("Tagged $i items");
|
2019-06-05 23:03:22 +00:00
|
|
|
}
|
|
|
|
break;
|
2019-06-12 22:44:25 +00:00
|
|
|
case "bulk_source":
|
2024-02-10 00:05:33 +00:00
|
|
|
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)) {
|
2024-02-10 00:05:33 +00:00
|
|
|
$source = $event->params['bulk_source'];
|
2019-06-12 22:44:25 +00:00
|
|
|
$i = $this->set_source($event->items, $source);
|
2019-12-15 19:47:18 +00:00
|
|
|
$page->flash("Set source for $i items");
|
2019-06-05 23:03:22 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-15 11:52:35 +00:00
|
|
|
public function onPageRequest(PageRequestEvent $event): void
|
2019-06-05 23:03:22 +00:00
|
|
|
{
|
|
|
|
global $page, $user;
|
2024-02-10 23:03:14 +00:00
|
|
|
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-12 22:44:25 +00:00
|
|
|
}
|
2019-06-05 23:03:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-20 14:10:59 +00:00
|
|
|
/**
|
|
|
|
* @param int[] $data
|
|
|
|
* @return \Generator<Image>
|
|
|
|
*/
|
2023-01-10 22:44:09 +00:00
|
|
|
private function yield_items(array $data): \Generator
|
2019-07-05 15:24:46 +00:00
|
|
|
{
|
|
|
|
foreach ($data as $id) {
|
2024-01-20 14:10:59 +00:00
|
|
|
$image = Image::by_id($id);
|
|
|
|
if ($image != null) {
|
|
|
|
yield $image;
|
2019-07-05 15:24:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-20 14:10:59 +00:00
|
|
|
/**
|
|
|
|
* @return \Generator<Image>
|
|
|
|
*/
|
2023-01-10 22:44:09 +00:00
|
|
|
private function yield_search_results(string $query): \Generator
|
2019-07-05 15:24:46 +00:00
|
|
|
{
|
|
|
|
$tags = Tag::explode($query);
|
2023-12-14 16:33:21 +00:00
|
|
|
return Search::find_images_iterable(0, null, $tags);
|
2019-07-05 15:24:46 +00:00
|
|
|
}
|
|
|
|
|
2024-01-20 14:10:59 +00:00
|
|
|
/**
|
|
|
|
* @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"];
|
2019-06-12 22:44:25 +00:00
|
|
|
}
|
2019-08-07 19:53:59 +00:00
|
|
|
|
2024-01-20 14:10:59 +00:00
|
|
|
/**
|
|
|
|
* @param iterable<Image> $posts
|
|
|
|
* @return array{0: int, 1: int}
|
|
|
|
*/
|
2021-01-13 01:39:23 +00:00
|
|
|
private function delete_posts(iterable $posts): array
|
2019-06-06 00:37:07 +00:00
|
|
|
{
|
2019-12-15 19:47:18 +00:00
|
|
|
global $page;
|
2019-06-05 23:03:22 +00:00
|
|
|
$total = 0;
|
2021-01-13 01:39:23 +00:00
|
|
|
$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) {
|
2021-01-13 01:39:23 +00:00
|
|
|
send_event(new AddImageHashBanEvent($post->hash, $reason));
|
2019-06-17 09:52:05 +00:00
|
|
|
}
|
|
|
|
}
|
2021-01-13 01:39:23 +00:00
|
|
|
send_event(new ImageDeletionEvent($post));
|
2019-06-05 23:03:22 +00:00
|
|
|
$total++;
|
2021-01-13 01:39:23 +00:00
|
|
|
$size += $post->filesize;
|
2023-01-10 22:44:09 +00:00
|
|
|
} catch (\Exception $e) {
|
2021-01-13 01:39:23 +00:00
|
|
|
$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
|
|
|
}
|
2021-01-13 01:39:23 +00:00
|
|
|
return [$total, $size];
|
2019-06-05 23:03:22 +00:00
|
|
|
}
|
|
|
|
|
2024-01-20 14:10:59 +00:00
|
|
|
/**
|
|
|
|
* @param iterable<Image> $items
|
|
|
|
*/
|
2019-07-05 15:24:46 +00:00
|
|
|
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) {
|
2020-10-25 19:31:58 +00:00
|
|
|
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) {
|
2019-07-05 15:24:46 +00:00
|
|
|
foreach ($items as $image) {
|
2019-06-12 22:44:25 +00:00
|
|
|
send_event(new TagSetEvent($image, $tags));
|
2019-06-05 23:03:22 +00:00
|
|
|
$total++;
|
|
|
|
}
|
|
|
|
} else {
|
2019-07-05 15:24:46 +00:00
|
|
|
foreach ($items as $image) {
|
2019-09-29 13:30:55 +00:00
|
|
|
$img_tags = array_map("strtolower", $image->get_tag_array());
|
2019-06-12 22:44:25 +00:00
|
|
|
|
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);
|
2019-07-05 15:24:46 +00:00
|
|
|
|
|
|
|
$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 {
|
2019-07-05 15:24:46 +00:00
|
|
|
$img_tags = array_merge($tags, $img_tags);
|
2019-06-05 23:03:22 +00:00
|
|
|
}
|
2019-06-12 22:44:25 +00:00
|
|
|
send_event(new TagSetEvent($image, $img_tags));
|
2019-06-05 23:03:22 +00:00
|
|
|
$total++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-09 18:22:48 +00:00
|
|
|
return $total;
|
2019-06-05 23:03:22 +00:00
|
|
|
}
|
|
|
|
|
2024-01-20 14:10:59 +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
|
|
|
{
|
2019-12-15 19:47:18 +00:00
|
|
|
global $page;
|
2019-06-05 23:03:22 +00:00
|
|
|
$total = 0;
|
2019-07-05 15:24:46 +00:00
|
|
|
foreach ($items as $image) {
|
2019-06-05 23:03:22 +00:00
|
|
|
try {
|
2019-06-12 22:44:25 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
2019-06-09 18:22:48 +00:00
|
|
|
return $total;
|
2019-06-05 23:03:22 +00:00
|
|
|
}
|
|
|
|
}
|