validateStatus(sanitize_text_field(wp_unslash($_GET[self::QUERY_VAR]))) : ''; $options = $this->getFilterOptions(); printf( ''; } /** * Get filter options with labels. * * @return array Filter value => Label pairs. */ private function getFilterOptions(): array { return [ self::STATUS_OFFLOADED => __('Offloaded', 'advanced-media-offloader'), self::STATUS_NOT_OFFLOADED => __('Not Offloaded', 'advanced-media-offloader'), self::STATUS_FAILED => __('Failed', 'advanced-media-offloader'), ]; } /** * Validate status against allowed values (whitelist). * * @param string $status The status to validate. * @return string Validated status or empty string if invalid. */ private function validateStatus(string $status): string { $allowed = [ self::STATUS_OFFLOADED, self::STATUS_NOT_OFFLOADED, self::STATUS_FAILED, ]; return in_array($status, $allowed, true) ? $status : ''; } /** * Filter media query based on selected offload status (list view). * * @param \WP_Query $query The WordPress query object. */ public function filterMediaQuery(\WP_Query $query): void { if (!is_admin() || !$query->is_main_query()) { return; } if ($query->get('post_type') !== 'attachment') { return; } if (!current_user_can('upload_files')) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter in admin list table $status = isset($_GET[self::QUERY_VAR]) ? $this->validateStatus(sanitize_text_field(wp_unslash($_GET[self::QUERY_VAR]))) : ''; if (empty($status)) { return; } $meta_query = $this->buildMetaQuery($status); if (!empty($meta_query)) { $existing_meta_query = $query->get('meta_query') ?: []; // If our meta query has a relation key, it's a complete query structure // Add it as a nested condition to preserve the structure if (isset($meta_query['relation'])) { $existing_meta_query[] = $meta_query; } else { // For simple conditions, add each one foreach ($meta_query as $condition) { $existing_meta_query[] = $condition; } } $query->set('meta_query', $existing_meta_query); } } /** * Filter AJAX query for grid view. * * @param array $query The query arguments. * @return array Modified query arguments. */ public function filterAjaxQuery(array $query): array { // WordPress strips unknown params via array_intersect_key before this filter // So we need to check the original $_REQUEST['query'] array directly $status = ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- AJAX nonce verified by WordPress core if (isset($_REQUEST['query']) && is_array($_REQUEST['query'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $request_query = wp_unslash($_REQUEST['query']); if (isset($request_query[self::QUERY_VAR]) && !empty($request_query[self::QUERY_VAR])) { $status = $this->validateStatus(sanitize_text_field($request_query[self::QUERY_VAR])); } } // Fallback: check if it somehow made it through to $query if (empty($status) && isset($query[self::QUERY_VAR]) && !empty($query[self::QUERY_VAR])) { $status = $this->validateStatus(sanitize_text_field($query[self::QUERY_VAR])); } if (empty($status)) { return $query; } $meta_query = $this->buildMetaQuery($status); if (!empty($meta_query)) { // Initialize meta_query if not set if (!isset($query['meta_query']) || !is_array($query['meta_query'])) { $query['meta_query'] = []; } // If our meta query has a relation key, it's a complete query structure // Add it as a nested condition to preserve the structure if (isset($meta_query['relation'])) { $query['meta_query'][] = $meta_query; } else { // For simple conditions, add each one foreach ($meta_query as $condition) { $query['meta_query'][] = $condition; } } } return $query; } /** * Build meta query based on offload status. * * @param string $status The offload status filter value. * @return array The meta query array. */ private function buildMetaQuery(string $status): array { switch ($status) { case self::STATUS_OFFLOADED: // Simple equality check - uses efficient INNER JOIN return [ [ 'key' => self::META_OFFLOADED, 'value' => '1', 'compare' => '=', ], ]; case self::STATUS_NOT_OFFLOADED: // Find items that are neither offloaded nor failed. // Note: NOT EXISTS queries use LEFT JOIN which can be slower on large datasets. // For better performance on large libraries, consider adding a database index // on wp_postmeta (meta_key, meta_value) or using a unified status meta key. return [ 'relation' => 'AND', [ 'relation' => 'OR', [ 'key' => self::META_OFFLOADED, 'compare' => 'NOT EXISTS', ], [ 'key' => self::META_OFFLOADED, 'value' => '1', 'compare' => '!=', ], ], [ 'key' => self::META_ERROR_LOG, 'compare' => 'NOT EXISTS', ], ]; case self::STATUS_FAILED: // Non-empty error log indicates failure. // Single condition is sufficient - INNER JOIN excludes rows without this meta key. return [ [ 'key' => self::META_ERROR_LOG, 'value' => '', 'compare' => '!=', ], ]; default: return []; } } /** * Enqueue script for grid view filter functionality. * * @param mixed $hook The current admin page hook (may be empty in some contexts). */ public function enqueueGridFilterScript($hook = ''): void { // Ensure hook is a valid string (some page builders may not pass this parameter) if (!is_string($hook) || $hook !== 'upload.php') { return; } wp_enqueue_script( 'advmo-media-library-filter', $this->getAssetUrl('js/media-library-filter.js'), ['jquery', 'media-views'], $this->getAssetVersion('js/media-library-filter.js'), true ); wp_localize_script('advmo-media-library-filter', 'advmoMediaFilter', [ 'queryVar' => self::QUERY_VAR, 'label' => __('Offload Status', 'advanced-media-offloader'), 'options' => array_merge( ['' => __('All offload statuses', 'advanced-media-offloader')], $this->getFilterOptions() ), ]); } /** * Get the URL for an asset file. * * @param string $path Relative path to the asset. * @return string Full URL to the asset. */ private function getAssetUrl(string $path): string { return plugins_url('assets/' . $path, dirname(__DIR__, 2) . '/advanced-media-offloader.php'); } /** * Get the version for an asset file based on file modification time. * * @param string $path Relative path to the asset. * @return string Version string. */ private function getAssetVersion(string $path): string { $file = dirname(__DIR__, 2) . '/assets/' . $path; if (file_exists($file)) { return (string) filemtime($file); } // @phpstan-ignore-next-line - Constant is defined in main plugin file return defined('ADVMO_VERSION') ? constant('ADVMO_VERSION') : '1.0.0'; } }