plugin = $plugin; } /** * Register CLI commands */ public static function register($plugin) { $instance = new self($plugin); WP_CLI::add_command('mls test', array($instance, 'test')); WP_CLI::add_command('mls status', array($instance, 'status')); WP_CLI::add_command('mls sync', array($instance, 'sync')); WP_CLI::add_command('mls run', array($instance, 'run')); WP_CLI::add_command('mls stats', array($instance, 'stats')); WP_CLI::add_command('mls cache', array($instance, 'cache')); WP_CLI::add_command('mls recovery', array($instance, 'recovery')); WP_CLI::add_command('mls media', array($instance, 'media')); } /** * Test API connection and authentication. * * ## OPTIONS * * * : Test type: connection or auth * * [--verbose] * : Show detailed information * * ## EXAMPLES * * wp mls test connection * wp mls test auth * wp mls test connection --verbose * * @subcommand test */ public function test($args, $assoc_args) { $type = isset($args[0]) ? $args[0] : 'connection'; $verbose = isset($assoc_args['verbose']); $api_client = $this->plugin->get_api_client(); switch ($type) { case 'connection': WP_CLI::line('Testing connection to MLS Grid API...'); $result = $api_client->test_connection(); if ($result['success']) { WP_CLI::success('Connection successful!'); WP_CLI::line(sprintf('Response time: %dms', $result['response_time'])); if ($verbose && !empty($result['endpoints'])) { WP_CLI::line('Available endpoints:'); foreach ($result['endpoints'] as $endpoint) { WP_CLI::line(' - ' . $endpoint); } } } else { WP_CLI::error('Connection failed: ' . $result['error']); } break; case 'auth': WP_CLI::line('Testing API authentication...'); $options = $this->plugin->get_options(); if (!$options->get_api_token()) { WP_CLI::error('No API token configured. Set MLSGRID_ACCESS_TOKEN in wp-config.php'); } $result = $api_client->test_auth(); if ($result['success']) { WP_CLI::success('Authentication successful!'); WP_CLI::line('Originating System: ' . $result['originating_system']); } else { WP_CLI::error('Authentication failed: ' . $result['error']); } break; default: WP_CLI::error("Unknown test type: {$type}. Use 'connection' or 'auth'."); } } /** * Show sync status and rate limits. * * ## OPTIONS * * [] * : Status type: sync, rate-limits, or all (default) * * ## EXAMPLES * * wp mls status * wp mls status sync * wp mls status rate-limits * * @subcommand status */ public function status($args, $assoc_args) { $type = isset($args[0]) ? $args[0] : 'all'; if ($type === 'all' || $type === 'sync') { $this->show_sync_status(); } if ($type === 'all' || $type === 'rate-limits') { $this->show_rate_limits(); } } /** * Show sync status */ private function show_sync_status() { $sync_engine = $this->plugin->get_sync_engine(); $status = $sync_engine->get_status(); WP_CLI::line(''); WP_CLI::line('=== Sync Status ==='); if ($status['running_sync']) { WP_CLI::warning('Sync currently running'); WP_CLI::line(sprintf( ' Type: %s | Started: %s | Processed: %d', $status['running_sync']->sync_type, $status['running_sync']->started_at, $status['running_sync']->records_processed )); } if ($status['last_sync']) { WP_CLI::line('Last completed sync:'); WP_CLI::line(sprintf( ' Type: %s | Completed: %s', $status['last_sync']->sync_type, $status['last_sync']->completed_at )); WP_CLI::line(sprintf( ' Records: %d processed, %d created, %d updated, %d deleted', $status['last_sync']->records_processed, $status['last_sync']->records_created, $status['last_sync']->records_updated, $status['last_sync']->records_deleted )); } else { WP_CLI::line('No completed syncs found.'); } if ($status['last_failed']) { WP_CLI::warning('Last failed sync:'); WP_CLI::line(' Error: ' . $status['last_failed']->last_error); } WP_CLI::line(''); } /** * Show rate limit status */ private function show_rate_limits() { $rate_limiter = $this->plugin->get_rate_limiter(); $status = $rate_limiter->get_status(); WP_CLI::line('=== Rate Limits ==='); WP_CLI::line(sprintf( 'Hourly: %d / %d requests (%d remaining)', $status['hourly']['used'], $status['hourly']['limit'], $status['hourly']['remaining'] )); WP_CLI::line(sprintf( 'Daily: %d / %d requests (%d remaining)', $status['daily']['used'], $status['daily']['limit'], $status['daily']['remaining'] )); WP_CLI::line(sprintf( 'Data: %s / 4GB this hour', size_format($status['bytes_this_hour']) )); WP_CLI::line(''); } /** * Run property sync. * * ## OPTIONS * * * : Sync type: full, incremental, media, or resume * * [--dry-run] * : Show what would be synced without making changes * * [--limit=] * : Limit number of records to process * * [--id=] * : Sync state ID to resume (for resume command) * * [--force] * : Force re-download of media (for media command) * * [--quiet] * : Suppress progress output * * [--verbose] * : Show detailed output including API requests and responses * * ## EXAMPLES * * wp mls sync full * wp mls sync full --dry-run --limit=10 * wp mls sync incremental * wp mls sync incremental --verbose * wp mls sync media --limit=100 * wp mls sync resume --id=5 * * @subcommand sync */ public function sync($args, $assoc_args) { $type = isset($args[0]) ? $args[0] : 'incremental'; $dry_run = isset($assoc_args['dry-run']); $limit = isset($assoc_args['limit']) ? (int) $assoc_args['limit'] : null; $quiet = isset($assoc_args['quiet']); $verbose = isset($assoc_args['verbose']); $sync_engine = $this->plugin->get_sync_engine(); // Progress callback for CLI $progress_callback = null; if (!$quiet) { $progress_callback = function($event, $data = array()) use ($verbose) { if ($verbose) { // Verbose mode: full line output $this->output_verbose_event($event, $data); } else { // Compact mode: single character symbols switch ($event) { case 'property_created': echo '.'; break; case 'property_updated': echo '#'; break; case 'property_deleted': echo 'x'; break; case 'property_skipped': echo '-'; break; case 'property_error': echo '!'; break; case 'media_downloaded': echo 'P'; break; case 'media_skipped': echo 'p'; break; case 'media_error': echo 'E'; break; case 'page_complete': echo '|'; break; case 'api_request': // Silent in compact mode break; case 'api_response': // Silent in compact mode break; } } }; } switch ($type) { case 'full': WP_CLI::line('Starting full sync...'); if ($dry_run) { WP_CLI::line('DRY RUN - No changes will be made'); } if (!$quiet) { $this->print_progress_legend($verbose); } $result = $sync_engine->run_full_sync($dry_run, $limit, $progress_callback); if (!$quiet) { echo "\n"; } $this->output_sync_result($result); break; case 'incremental': WP_CLI::line('Starting incremental sync...'); if ($dry_run) { WP_CLI::line('DRY RUN - No changes will be made'); } if (!$quiet) { $this->print_progress_legend($verbose); } $result = $sync_engine->run_incremental_sync($dry_run, $progress_callback); if (!$quiet) { echo "\n"; } $this->output_sync_result($result); break; case 'media': // Media is now on-demand, this sync type is deprecated WP_CLI::line('Note: "wp mls sync media" is deprecated.'); WP_CLI::line('Media is now fetched on-demand when properties are viewed on the website.'); WP_CLI::line(''); WP_CLI::line('Use "wp mls media status" to see cache statistics.'); WP_CLI::line('Use "wp mls media fetch --listing=" to pre-cache a specific listing.'); break; case 'resume': $sync_id = isset($assoc_args['id']) ? (int) $assoc_args['id'] : null; if (!$sync_id) { WP_CLI::error('Please specify --id= to resume'); } WP_CLI::line("Resuming sync #{$sync_id}..."); if (!$quiet) { $this->print_progress_legend($verbose); } $result = $sync_engine->resume_sync($sync_id, $progress_callback); if (!$quiet) { echo "\n"; } $this->output_sync_result($result); break; default: WP_CLI::error("Unknown sync type: {$type}. Use 'full', 'incremental', 'media', or 'resume'."); } } /** * Run smart sync - autonomous self-healing sync. * * This is the recommended command for automated/cron usage. It automatically * determines the best action based on current state: * * - If a sync is running: abort (prevents duplicate syncs) * - If a previous sync failed/interrupted: resume it * - If no data exists: run full sync * - Otherwise: run incremental sync * * Failed syncs are automatically recoverable on the next run. * * ## OPTIONS * * [--quiet] * : Suppress progress output (still shows status messages) * * [--verbose] * : Show detailed output including API requests * * [--silent] * : Suppress all output except errors (for cron) * * ## EXAMPLES * * wp mls run # Smart sync with progress * wp mls run --quiet # Smart sync, status only * wp mls run --verbose # Smart sync with full details * wp mls run --silent # For cron jobs * * @subcommand run */ public function run($args, $assoc_args) { $quiet = isset($assoc_args['quiet']); $verbose = isset($assoc_args['verbose']); $silent = isset($assoc_args['silent']); $sync_engine = $this->plugin->get_sync_engine(); // Status callback for high-level messages $status_callback = null; if (!$silent) { $status_callback = function($message, $level = 'info') { $timestamp = date('H:i:s'); switch ($level) { case 'warning': WP_CLI::warning("[{$timestamp}] {$message}"); break; case 'error': WP_CLI::warning("[{$timestamp}] {$message}"); break; default: WP_CLI::line("[{$timestamp}] {$message}"); } }; } // Progress callback for record-level progress $progress_callback = null; if (!$quiet && !$silent) { $progress_callback = function($event, $data = array()) use ($verbose) { if ($verbose) { $this->output_verbose_event($event, $data); } else { switch ($event) { case 'property_created': echo '.'; break; case 'property_updated': echo '#'; break; case 'property_deleted': echo 'x'; break; case 'property_error': echo '!'; break; case 'page_complete': echo '|'; break; } } }; } if (!$silent) { WP_CLI::line(''); WP_CLI::line('=== MLS Smart Sync ==='); WP_CLI::line(''); } // Run smart sync $result = $sync_engine->smart_sync($progress_callback, $status_callback); // Handle aborted case (sync already running) if (isset($result['action']) && $result['action'] === 'aborted') { if (!$silent) { WP_CLI::warning('Sync aborted: ' . ($result['reason'] ?? 'Unknown reason')); } return; } // Output newline after progress dots if (!$quiet && !$silent && !$verbose) { echo "\n"; } // Output results if (!$silent) { $action_labels = array( 'full' => 'Full sync', 'incremental' => 'Incremental sync', 'resumed' => 'Resumed sync', ); $action_label = $action_labels[$result['action']] ?? 'Sync'; if ($result['success']) { WP_CLI::success("{$action_label} completed successfully!"); } else { WP_CLI::warning("{$action_label} failed: " . ($result['error'] ?? 'Unknown error')); WP_CLI::line('The sync can be resumed on the next run.'); } if (isset($result['stats'])) { $stats = $result['stats']; WP_CLI::line(sprintf( 'Processed: %d | Created: %d | Updated: %d | Deleted: %d | Errors: %d', $stats['processed'], $stats['created'], $stats['updated'], $stats['deleted'], $stats['errors'] )); } } // Exit with error code if failed (for cron monitoring) if (!$result['success']) { WP_CLI::halt(1); } } /** * Print progress legend * * @param bool $verbose Whether verbose mode is enabled */ private function print_progress_legend($verbose = false) { if (!$verbose) { WP_CLI::line('Legend: .=new #=updated x=deleted -=skipped !=error P=photo p=photo-skip E=photo-error |=page'); echo "\n"; } } /** * Output verbose event information * * @param string $event Event name * @param array $data Event data */ private function output_verbose_event($event, $data) { $timestamp = date('H:i:s'); switch ($event) { case 'api_request': WP_CLI::line("[{$timestamp}] API REQUEST: {$data['method']} {$data['url']}"); if (!empty($data['params'])) { WP_CLI::line(" Params: " . json_encode($data['params'])); } break; case 'api_response': $status = $data['success'] ? 'OK' : 'ERROR'; WP_CLI::line("[{$timestamp}] API RESPONSE: {$status} ({$data['status_code']}) - {$data['record_count']} records, {$data['response_time']}ms"); if (!$data['success'] && !empty($data['error'])) { WP_CLI::warning(" Error: {$data['error']}"); } break; case 'property_created': $key = $data['listing_key'] ?? 'unknown'; $address = $data['address'] ?? ''; WP_CLI::line("[{$timestamp}] CREATED: {$key} {$address}"); break; case 'property_updated': $key = $data['listing_key'] ?? 'unknown'; $address = $data['address'] ?? ''; WP_CLI::line("[{$timestamp}] UPDATED: {$key} {$address}"); break; case 'property_deleted': $key = $data['listing_key'] ?? 'unknown'; WP_CLI::line("[{$timestamp}] DELETED: {$key}"); break; case 'property_skipped': $key = $data['listing_key'] ?? 'unknown'; WP_CLI::line("[{$timestamp}] SKIPPED: {$key} (dry-run)"); break; case 'property_error': $key = $data['listing_key'] ?? 'unknown'; $error = $data['error'] ?? 'Unknown error'; WP_CLI::warning("[{$timestamp}] ERROR: {$key} - {$error}"); break; case 'media_downloaded': $key = $data['media_key'] ?? 'unknown'; $listing = $data['listing_key'] ?? ''; WP_CLI::line("[{$timestamp}] MEDIA OK: {$key}" . ($listing ? " ({$listing})" : "")); break; case 'media_skipped': $key = $data['media_key'] ?? 'unknown'; WP_CLI::line("[{$timestamp}] MEDIA SKIP: {$key} (already downloaded)"); break; case 'media_error': $key = $data['media_key'] ?? 'unknown'; $error = $data['error'] ?? 'Download failed'; WP_CLI::warning("[{$timestamp}] MEDIA ERROR: {$key} - {$error}"); break; case 'page_complete': $processed = $data['processed'] ?? 0; WP_CLI::line("[{$timestamp}] PAGE COMPLETE: {$processed} records processed so far"); break; } } /** * Output sync result */ private function output_sync_result($result) { if ($result['success']) { WP_CLI::success('Sync completed successfully!'); } else { WP_CLI::error('Sync failed: ' . $result['error']); } $stats = $result['stats']; WP_CLI::line(sprintf( 'Processed: %d | Created: %d | Updated: %d | Deleted: %d | Errors: %d', $stats['processed'], $stats['created'], $stats['updated'], $stats['deleted'], $stats['errors'] )); // Show missing media log info if there are failures $media_handler = $this->plugin->get_media_handler(); $missing_count = $media_handler->get_missing_count(); if ($missing_count > 0) { WP_CLI::line(sprintf( 'Missing media log: %s (%d entries)', $media_handler->get_missing_log_path(), $missing_count )); } } /** * Show database statistics. * * ## EXAMPLES * * wp mls stats * * @subcommand stats */ public function stats($args, $assoc_args) { $db = $this->plugin->get_db(); $stats = $db->get_stats(); WP_CLI::line(''); WP_CLI::line('=== MLS Database Statistics ==='); WP_CLI::line(''); WP_CLI::line('Properties:'); WP_CLI::line(sprintf(' Total: %d', $stats['total_properties'])); WP_CLI::line(sprintf(' Active: %d', $stats['active_properties'])); WP_CLI::line(sprintf(' Pending: %d', $stats['pending_properties'])); WP_CLI::line(sprintf(' Sold: %d', $stats['sold_properties'])); WP_CLI::line(''); WP_CLI::line('Media:'); WP_CLI::line(sprintf(' Total records: %d', $stats['total_media'])); WP_CLI::line(sprintf(' Downloaded: %d', $stats['downloaded_media'])); WP_CLI::line(sprintf(' Pending: %d', $stats['total_media'] - $stats['downloaded_media'])); WP_CLI::line(''); // Show distinct values $query = $this->plugin->get_query(); $cities = $query->get_distinct_cities('Active'); WP_CLI::line('Cities with active listings: ' . count($cities)); if (count($cities) <= 20) { WP_CLI::line(' ' . implode(', ', $cities)); } WP_CLI::line(''); } /** * Manage cache. * * ## OPTIONS * * * : Action: clear, clear-listing, cleanup, missing * * [--confirm] * : Confirm destructive operations * * [--listing=] * : Listing key for clear-listing * * [--clear] * : Clear the missing media log (for missing action) * * [--limit=] * : Limit output lines (for missing action) * * ## EXAMPLES * * wp mls cache clear --confirm * wp mls cache clear-listing --listing=NST123456 * wp mls cache cleanup * wp mls cache missing * wp mls cache missing --limit=20 * wp mls cache missing --clear * * @subcommand cache */ public function cache($args, $assoc_args) { $action = isset($args[0]) ? $args[0] : null; switch ($action) { case 'clear': if (!isset($assoc_args['confirm'])) { WP_CLI::error('This will delete ALL synced data. Add --confirm to proceed.'); } WP_CLI::line('Clearing all MLS data...'); $db = $this->plugin->get_db(); $db->truncate_data(); // Also clear media files $media_handler = $this->plugin->get_media_handler(); $upload_dir = $media_handler->get_upload_dir(); if (is_dir($upload_dir)) { WP_CLI::line('Clearing media files...'); // Note: This is a simplified clear - in production you'd want more careful deletion $this->recursive_delete($upload_dir); wp_mkdir_p($upload_dir); } // Also clear missing log $media_handler->clear_missing_log(); WP_CLI::success('Cache cleared successfully.'); break; case 'clear-listing': $listing_key = isset($assoc_args['listing']) ? $assoc_args['listing'] : null; if (!$listing_key) { WP_CLI::error('Please specify --listing='); } $media_handler = $this->plugin->get_media_handler(); $media_handler->delete_property_media($listing_key); global $wpdb; $db = $this->plugin->get_db(); $wpdb->delete($db->properties_table(), array('listing_key' => $listing_key)); WP_CLI::success("Listing {$listing_key} cleared."); break; case 'cleanup': WP_CLI::line('Cleaning up orphaned media files...'); $media_handler = $this->plugin->get_media_handler(); $deleted = $media_handler->cleanup_orphaned_files(); WP_CLI::success("Cleaned up {$deleted} orphaned directories."); break; case 'missing': $media_handler = $this->plugin->get_media_handler(); $log_file = $media_handler->get_missing_log_path(); if (isset($assoc_args['clear'])) { $media_handler->clear_missing_log(); WP_CLI::success('Missing media log cleared.'); break; } if (!file_exists($log_file)) { WP_CLI::success('No missing media logged.'); break; } $limit = isset($assoc_args['limit']) ? (int) $assoc_args['limit'] : null; $lines = file($log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $total = count($lines); WP_CLI::line(sprintf('Missing media log: %s', $log_file)); WP_CLI::line(sprintf('Total entries: %d', $total)); WP_CLI::line(''); WP_CLI::line('Format: [timestamp] listing_key | media_key | error | url'); WP_CLI::line(str_repeat('-', 80)); $display = $limit ? array_slice($lines, 0, $limit) : $lines; foreach ($display as $line) { WP_CLI::line($line); } if ($limit && $total > $limit) { WP_CLI::line(''); WP_CLI::line(sprintf('... and %d more entries', $total - $limit)); } break; default: WP_CLI::error("Unknown action: {$action}. Use 'clear', 'clear-listing', 'cleanup', or 'missing'."); } } /** * Manage sync recovery and resumption. * * ## OPTIONS * * * : Action: list, auto, cleanup * * [--verbose] * : Show detailed output during resume * * [--quiet] * : Suppress progress output * * ## EXAMPLES * * wp mls recovery list # Show resumable syncs * wp mls recovery auto # Auto-resume most recent failed sync * wp mls recovery auto --verbose # Auto-resume with detailed output * wp mls recovery cleanup # Mark stale syncs as failed * * @subcommand recovery */ public function recovery($args, $assoc_args) { $action = isset($args[0]) ? $args[0] : 'list'; $verbose = isset($assoc_args['verbose']); $quiet = isset($assoc_args['quiet']); $sync_engine = $this->plugin->get_sync_engine(); switch ($action) { case 'list': $resumable = $sync_engine->get_resumable_syncs(); if (empty($resumable)) { WP_CLI::success('No resumable syncs found.'); break; } WP_CLI::line(''); WP_CLI::line('=== Resumable Syncs ==='); WP_CLI::line(''); foreach ($resumable as $sync) { $status_color = $sync->status === 'failed' ? '%R' : '%Y'; WP_CLI::line(sprintf( 'ID: %d | Type: %s | Status: %s | Processed: %d', $sync->id, $sync->sync_type, $sync->status, $sync->records_processed )); WP_CLI::line(sprintf( ' Started: %s | Updated: %s', $sync->started_at, $sync->updated_at )); if ($sync->last_error) { WP_CLI::warning(' Error: ' . $sync->last_error); } if ($sync->last_next_link) { WP_CLI::line(' Has resume point: Yes'); } WP_CLI::line(''); } WP_CLI::line('To resume a specific sync: wp mls sync resume --id='); WP_CLI::line('To auto-resume the most recent: wp mls recovery auto'); break; case 'auto': WP_CLI::line('Checking for resumable syncs...'); // Build progress callback $progress_callback = null; if (!$quiet) { $progress_callback = function($event, $data = array()) use ($verbose) { if ($verbose) { $this->output_verbose_event($event, $data); } else { switch ($event) { case 'property_created': echo '.'; break; case 'property_updated': echo '#'; break; case 'property_deleted': echo 'x'; break; case 'media_downloaded': echo 'P'; break; case 'media_skipped': echo 'p'; break; case 'media_error': echo 'E'; break; case 'page_complete': echo '|'; break; } } }; } $result = $sync_engine->auto_resume($progress_callback); if ($result === null) { WP_CLI::success('No syncs to resume.'); break; } if (!$quiet && !$verbose) { echo "\n"; } $this->output_sync_result($result); break; case 'cleanup': WP_CLI::line('Cleaning up stale syncs...'); $cleaned = $sync_engine->cleanup_stale_syncs(); if ($cleaned > 0) { WP_CLI::success("Marked {$cleaned} stale sync(s) as failed."); } else { WP_CLI::success('No stale syncs found.'); } break; default: WP_CLI::error("Unknown action: {$action}. Use 'list', 'auto', or 'cleanup'."); } } /** * Show media cache status and manage cached files. * * Media is now fetched on-demand when properties are viewed on the website. * This command shows cache statistics and allows management of cached files. * * ## OPTIONS * * [] * : Action: status (default), fetch, clear * * [--listing=] * : Listing key for fetch or clear actions * * [--limit=] * : For fetch action, max images to fetch (default: 1) * * ## EXAMPLES * * wp mls media status # Show cache statistics * wp mls media fetch --listing=NST123456 # Fetch images for a listing * wp mls media fetch --listing=NST123456 --limit=10 # Fetch up to 10 images * wp mls media clear --listing=NST123456 # Clear cached images for a listing * * @subcommand media */ public function media($args, $assoc_args) { $action = isset($args[0]) ? $args[0] : 'status'; $media_handler = $this->plugin->get_media_handler(); switch ($action) { case 'status': $stats = $media_handler->get_cache_stats(); WP_CLI::line(''); WP_CLI::line('=== Media Cache Status ==='); WP_CLI::line(''); WP_CLI::line(sprintf('Total media records: %d', $stats['total_media'])); WP_CLI::line(sprintf('Cached locally: %d', $stats['cached'])); WP_CLI::line(sprintf('Not yet cached: %d', $stats['uncached'])); WP_CLI::line(''); $cache_percent = $stats['total_media'] > 0 ? round(($stats['cached'] / $stats['total_media']) * 100, 1) : 0; WP_CLI::line(sprintf('Cache rate: %.1f%%', $cache_percent)); WP_CLI::line(''); WP_CLI::line('Images are fetched on-demand when properties are viewed.'); WP_CLI::line('Use "wp mls media fetch --listing=" to pre-cache specific listings.'); WP_CLI::line(''); break; case 'fetch': $listing_key = isset($assoc_args['listing']) ? $assoc_args['listing'] : null; if (!$listing_key) { WP_CLI::error('Please specify --listing='); } $limit = isset($assoc_args['limit']) ? (int) $assoc_args['limit'] : 1; WP_CLI::line(sprintf('Fetching up to %d images for listing %s...', $limit, $listing_key)); $images = $media_handler->get_listing_images($listing_key, $limit); $cached_count = 0; foreach ($images as $img) { if ($img->local_url) { $cached_count++; } } WP_CLI::line(sprintf( 'Result: %d/%d images now cached for this listing.', $cached_count, count($images) )); if ($cached_count > 0) { WP_CLI::success('Images fetched successfully.'); } elseif (count($images) === 0) { WP_CLI::warning('No media records found for this listing.'); } else { WP_CLI::warning('Failed to fetch images. Check logs for details.'); } break; case 'clear': $listing_key = isset($assoc_args['listing']) ? $assoc_args['listing'] : null; if (!$listing_key) { WP_CLI::error('Please specify --listing=. To clear all media, use "wp mls cache clear --confirm".'); } // Just clear the local files, keep metadata global $wpdb; $listing_dir = $media_handler->get_listing_dir($listing_key); if (is_dir($listing_dir)) { $this->recursive_delete($listing_dir); } // Clear local_path and local_url but keep the records $wpdb->query($wpdb->prepare( "UPDATE {$this->plugin->get_db()->media_table()} SET local_path = NULL, local_url = NULL, downloaded_at = NULL WHERE listing_key = %s", $listing_key )); WP_CLI::success(sprintf('Cleared cached images for listing %s. They will be re-fetched on demand.', $listing_key)); break; default: WP_CLI::error("Unknown action: {$action}. Use 'status', 'fetch', or 'clear'."); } } /** * Recursively delete a directory */ private function recursive_delete($dir) { if (!is_dir($dir)) { return; } $files = array_diff(scandir($dir), array('.', '..')); foreach ($files as $file) { $path = $dir . '/' . $file; if (is_dir($path)) { $this->recursive_delete($path); } else { unlink($path); } } rmdir($dir); } }