Files
2026-04-28 15:13:50 +02:00

272 lines
7.5 KiB
PHP

<?php
namespace AIOSEO\Plugin\Common\Ai;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles AI bulk actions for post types and the Media Library.
*
* @since 4.9.6
*/
class BulkActions {
/**
* The required capability to use bulk generation.
*
* @since 4.9.6
*
* @var string
*/
const REQUIRED_CAPABILITY = 'aioseo_page_ai_content_settings';
/**
* Class constructor.
*
* @since 4.9.6
*/
public function __construct() {
add_action( 'admin_init', [ $this, 'registerBulkActions' ] );
}
/**
* Register bulk actions for all public post types.
*
* @since 4.9.6
*
* @return void
*/
public function registerBulkActions() {
$postTypes = aioseo()->helpers->getPublicPostTypes( false, false, true );
foreach ( $postTypes as $postType ) {
if ( 'attachment' === $postType['name'] ) {
continue;
}
add_filter( 'bulk_actions-edit-' . $postType['name'], [ $this, 'addBulkAction' ] );
add_filter( 'handle_bulk_actions-edit-' . $postType['name'], [ $this, 'handleBulkAction' ], 10, 3 );
}
// Media Library bulk actions.
add_filter( 'bulk_actions-upload', [ $this, 'addMediaBulkAction' ] );
add_filter( 'handle_bulk_actions-upload', [ $this, 'handleMediaBulkAction' ], 10, 3 );
// Row action is registered only when the AIOSEO Details column is active,
// since it emits an event that the column's Vue component handles.
add_action( 'aioseo_details_column_activated', [ $this, 'registerMediaRowAction' ] );
}
/**
* Registers the media row action when the AIOSEO Details column is activated for attachments.
*
* @since 4.9.6
*
* @param string $postType The post type the column was activated for.
* @return void
*/
public function registerMediaRowAction( $postType ) {
if ( 'attachment' === $postType ) {
add_filter( 'media_row_actions', [ $this, 'addMediaRowAction' ], 10, 2 );
}
}
/**
* Add the AI bulk actions to a post type list.
*
* @since 4.9.6
*
* @param array $bulkActions The existing bulk actions.
* @return array The modified bulk actions.
*/
public function addBulkAction( $bulkActions ) {
if ( ! current_user_can( self::REQUIRED_CAPABILITY ) || $this->isTrashView() ) {
return $bulkActions;
}
$bulkActions[ AIOSEO_PLUGIN_SHORT_NAME ] = array_map( function ( $config ) {
return $config['label'];
}, $this->getPostListActions() );
return $bulkActions;
}
/**
* Add the AI bulk actions to the Media Library.
*
* @since 4.9.6
*
* @param array $bulkActions The existing bulk actions.
* @return array The modified bulk actions.
*/
public function addMediaBulkAction( $bulkActions ) {
if ( ! current_user_can( self::REQUIRED_CAPABILITY ) || $this->isTrashView() ) {
return $bulkActions;
}
$label = AIOSEO_PLUGIN_SHORT_NAME;
$actions = array_map( function ( $config ) {
return $config['label'];
}, $this->getMediaActions() );
$bulkActions[ $label ] = isset( $bulkActions[ $label ] )
? array_merge( $bulkActions[ $label ], $actions )
: $actions;
return $bulkActions;
}
/**
* Handle a post list bulk action when triggered.
*
* @since 4.9.6
*
* @param string $redirectTo The redirect URL.
* @param string $doAction The action being taken.
* @param array $postIds The array of post IDs.
* @return string The redirect URL.
*/
public function handleBulkAction( $redirectTo, $doAction, $postIds ) {
if ( ! current_user_can( self::REQUIRED_CAPABILITY ) ) {
return $redirectTo;
}
$actions = $this->getPostListActions();
if ( ! isset( $actions[ $doAction ] ) || empty( $postIds ) ) {
return $redirectTo;
}
return $this->buildRedirectUrl( $postIds, $actions[ $doAction ]['type'] );
}
/**
* Handle the Media Library bulk action when triggered.
*
* @since 4.9.6
*
* @param string $redirectTo The redirect URL.
* @param string $doAction The action being taken.
* @param array $postIds The array of post IDs.
* @return string The redirect URL.
*/
public function handleMediaBulkAction( $redirectTo, $doAction, $postIds ) {
if ( ! current_user_can( self::REQUIRED_CAPABILITY ) ) {
return $redirectTo;
}
$actions = $this->getMediaActions();
if ( ! isset( $actions[ $doAction ] ) || empty( $postIds ) ) {
return $redirectTo;
}
return $this->buildRedirectUrl( $postIds, $actions[ $doAction ]['type'] );
}
/**
* Add a row action to generate alt text with AI for individual images in the Media Library.
*
* @since 4.9.6
*
* @param array $actions The existing row actions.
* @param \WP_Post $post The current attachment post object.
* @return array The modified row actions.
*/
public function addMediaRowAction( $actions, $post ) {
if (
! current_user_can( self::REQUIRED_CAPABILITY ) ||
! current_user_can( 'edit_post', $post->ID ) ||
! aioseo()->helpers->attachmentIs( 'image', $post->ID ) ||
! aioseo()->addons->getLoadedAddon( 'imageSeo' )
) {
return $actions;
}
$onclick = sprintf(
'event.preventDefault(); window.aioseoBus && window.aioseoBus.$emit(\'generateAltInline%d\')',
$post->ID
);
$actions['aioseo_generate_alt'] = sprintf(
'<a href="#" onclick="%1$s">%2$s</a>',
esc_attr( $onclick ),
esc_html__( 'Generate Alt Text with AI', 'all-in-one-seo-pack' )
);
return $actions;
}
/**
* Whether the current list view is filtered to trashed posts.
*
* @since 4.9.6
*
* @return bool
*/
private function isTrashView() {
// Mirrors how WordPress core determines the trash view in WP_Posts_List_Table and WP_Media_List_Table.
// phpcs:disable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
$postStatus = sanitize_text_field( wp_unslash( $_REQUEST['post_status'] ?? '' ) );
$attachmentFilter = sanitize_text_field( wp_unslash( $_REQUEST['attachment-filter'] ?? '' ) );
// phpcs:enable HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
return 'trash' === $postStatus || 'trash' === $attachmentFilter;
}
/**
* Returns the post list bulk action definitions.
* Single source of truth for post list action keys, labels, and types.
*
* @since 4.9.6
*
* @return array[] Keyed by action slug, each with 'label' and 'type'.
*/
private function getPostListActions() {
return [
'aioseo_generate_ai_titles' => [
'label' => __( 'Generate SEO Titles with AI', 'all-in-one-seo-pack' ),
'type' => 'title'
],
'aioseo_generate_ai_descriptions' => [
'label' => __( 'Generate Meta Descriptions with AI', 'all-in-one-seo-pack' ),
'type' => 'description'
],
];
}
/**
* Returns the Media Library bulk action definitions.
* Single source of truth for media action keys, labels, and types.
*
* @since 4.9.6
*
* @return array[] Keyed by action slug, each with 'label' and 'type'.
*/
private function getMediaActions() {
return [
'aioseo_generate_ai_alt_text' => [
'label' => __( 'Generate Alt Text for Images with AI', 'all-in-one-seo-pack' ),
'type' => 'alt'
],
];
}
/**
* Build the redirect URL for the AI bulk generate page.
*
* @since 4.9.6
*
* @param array $postIds The array of post IDs.
* @param string $type The generation type (title, description, alt).
* @return string The redirect URL.
*/
private function buildRedirectUrl( $postIds, $type ) {
return add_query_arg(
[
'ids' => implode( ',', array_map( 'intval', $postIds ) ),
'type' => $type
],
admin_url( 'admin.php?page=aioseo-ai-bulk-generate' )
);
}
}