196 lines
5.1 KiB
PHP
196 lines
5.1 KiB
PHP
<?php
|
|
/**
|
|
* Indexes image attachments into mra_media_index.
|
|
*
|
|
* @package MediaRightsAudit\Internal
|
|
*/
|
|
|
|
namespace MediaRightsAudit\Internal;
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
use MediaRightsAudit\Core\Queue\Scheduler;
|
|
|
|
/**
|
|
* Populates mra_media_index with one row per image attachment.
|
|
*
|
|
* Processing order: oldest-first (ASC by attachment ID).
|
|
* Idempotent: safely re-runnable; existing rows are never overwritten.
|
|
*/
|
|
class AttachmentIndexer {
|
|
|
|
/**
|
|
* Action Scheduler hook name for background indexing.
|
|
*/
|
|
const AS_HOOK = 'mra_internal_index_batch';
|
|
|
|
/**
|
|
* Indexes one batch of not-yet-indexed image attachments.
|
|
*
|
|
* @param int $batch_size Maximum number of attachments to process.
|
|
*
|
|
* @return int Number of attachments newly added to the index.
|
|
*/
|
|
public static function index_batch( int $batch_size = 50 ): int {
|
|
global $wpdb;
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery
|
|
$rows = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT p.ID, p.guid, p.post_mime_type
|
|
FROM {$wpdb->posts} p
|
|
LEFT JOIN {$wpdb->prefix}mra_media_index i ON p.ID = i.attachment_id
|
|
WHERE p.post_type = 'attachment'
|
|
AND p.post_mime_type LIKE %s
|
|
AND p.post_status != 'trash'
|
|
AND i.attachment_id IS NULL
|
|
ORDER BY p.ID ASC
|
|
LIMIT %d",
|
|
'image/%',
|
|
$batch_size
|
|
),
|
|
ARRAY_A
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery
|
|
|
|
if ( ! $rows ) {
|
|
return 0;
|
|
}
|
|
|
|
// Prime postmeta cache so get_attached_file() and wp_get_attachment_url() are fast.
|
|
$ids = array_map( 'intval', array_column( $rows, 'ID' ) );
|
|
update_meta_cache( 'post', $ids );
|
|
|
|
$now = current_time( 'mysql', true );
|
|
$count = 0;
|
|
|
|
foreach ( $rows as $row ) {
|
|
$attachment_id = intval( $row['ID'] );
|
|
$guid = strval( $row['guid'] );
|
|
$mime_type = strval( $row['post_mime_type'] );
|
|
|
|
$file_url = wp_get_attachment_url( $attachment_id );
|
|
$file_url = $file_url ? $file_url : $guid;
|
|
$file_path = get_attached_file( $attachment_id );
|
|
$file_size = ( $file_path && file_exists( $file_path ) )
|
|
? intval( filesize( $file_path ) )
|
|
: 0;
|
|
|
|
$inserted = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prefix . 'mra_media_index',
|
|
array(
|
|
'attachment_id' => $attachment_id,
|
|
'file_url' => $file_url,
|
|
'file_name' => self::extract_filename( $file_url ),
|
|
'mime_type' => $mime_type,
|
|
'file_size' => $file_size,
|
|
'created_at' => $now,
|
|
),
|
|
array( '%d', '%s', '%s', '%s', '%d', '%s' )
|
|
);
|
|
|
|
if ( false !== $inserted ) {
|
|
++$count;
|
|
}
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Extracts the bare filename from a URL.
|
|
*
|
|
* Uses PHP's parse_url() so this method is testable without WordPress.
|
|
*
|
|
* @param string $url Absolute or relative URL.
|
|
*
|
|
* @return string Filename, or the original string if parsing fails.
|
|
*/
|
|
public static function extract_filename( string $url ): string {
|
|
$path = parse_url( $url, PHP_URL_PATH ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
|
|
return is_string( $path ) ? basename( $path ) : basename( $url );
|
|
}
|
|
|
|
/**
|
|
* Returns the number of image attachments not yet in the index.
|
|
*
|
|
* @return int
|
|
*/
|
|
public static function get_pending_count(): int {
|
|
global $wpdb;
|
|
|
|
$result = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
"SELECT COUNT(p.ID)
|
|
FROM {$wpdb->posts} p
|
|
LEFT JOIN {$wpdb->prefix}mra_media_index i ON p.ID = i.attachment_id
|
|
WHERE p.post_type = 'attachment'
|
|
AND p.post_mime_type LIKE 'image/%'
|
|
AND p.post_status != 'trash'
|
|
AND i.attachment_id IS NULL"
|
|
);
|
|
|
|
return intval( $result );
|
|
}
|
|
|
|
/**
|
|
* Returns the number of attachments currently in the index.
|
|
*
|
|
* @return int
|
|
*/
|
|
public static function get_indexed_count(): int {
|
|
global $wpdb;
|
|
|
|
$result = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
"SELECT COUNT(*) FROM {$wpdb->prefix}mra_media_index"
|
|
);
|
|
|
|
return intval( $result );
|
|
}
|
|
|
|
/**
|
|
* Processes one scheduled batch and re-queues or hands off to UsageScanner.
|
|
*
|
|
* Called by Action Scheduler via the mra_internal_index_batch hook.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function process_scheduled_batch(): void {
|
|
self::index_batch();
|
|
|
|
if ( self::get_pending_count() > 0 ) {
|
|
Scheduler::schedule_single( self::AS_HOOK );
|
|
} else {
|
|
Scheduler::schedule_single( UsageScanner::AS_HOOK );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enqueues a background indexing job if one is not already pending.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function schedule(): void {
|
|
if ( ! Scheduler::has_pending( self::AS_HOOK ) ) {
|
|
Scheduler::schedule_single( self::AS_HOOK );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes all rows from mra_media_index and mra_media_usage, forcing a full re-index on the next run.
|
|
*
|
|
* @return int Number of index rows removed.
|
|
*/
|
|
public static function reset_all(): int {
|
|
global $wpdb;
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery
|
|
$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}mra_media_index" );
|
|
$wpdb->query( "DELETE FROM {$wpdb->prefix}mra_media_usage" );
|
|
$wpdb->query( "DELETE FROM {$wpdb->prefix}mra_media_index" );
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery
|
|
|
|
return $count;
|
|
}
|
|
}
|