Snapshot: MLS sync fixes, image refresh, plugin/theme updates
MLS plugin fixes from this session: - Fix silent insert failures: location column NOT NULL was rejecting wpdb->insert calls, causing ~18k new properties since Dec 2025 to be lost. Inserts now build raw SQL with ST_PointFromText so the spatial column is populated atomically. - Auto-refresh expired media URLs in MLS_Media_Handler::fetch_and_cache(), guarded by a property-level GET_LOCK so concurrent fetches share one API refresh. - Normalize WP_Error to null in mls_get_property_image() so callers can rely on the documented string|null contract. - Support comma-separated property_type filters in MLS_Query and MLS_Cluster so the homepage "View All Commercial" link (?property_type=Commercial+Sale,Land,Farm) actually filters correctly. - Incremental sync now looks back 10 minutes past the latest modification timestamp as a safety margin against missed records. - Smart sync exits silently (info-level, not warning) when a full sync is in progress. Operational: - New cron: weekly full sync Sundays at 3 AM (/usr/local/bin/mls-full-sync). - New cron: hourly 2GB cap on mls-thumbnails/ and cache/transformed-images/ (/usr/local/bin/mls-image-cache-cap). - Logrotate config for wp-content/debug.log (2-day retention, daily rotation, delaycompress). Repo policy: - CLAUDE.md updated with explicit "commit everything except build artifacts" policy. - .gitignore: untrack runtime image caches and debug.log rotations. Other modifications in this snapshot are pre-existing in-flight theme/plugin/db_content_updates work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,10 +17,15 @@ if (!defined('ABSPATH')) {
|
||||
class MLS_Media_Handler {
|
||||
|
||||
/**
|
||||
* Upload subdirectory for MLS media
|
||||
* Upload subdirectory for MLS media (standard cache - subject to garbage collection)
|
||||
*/
|
||||
const UPLOAD_SUBDIR = 'mls-listings';
|
||||
|
||||
/**
|
||||
* Upload subdirectory for persistent HomeProz media (never garbage collected)
|
||||
*/
|
||||
const PERSISTENT_SUBDIR = 'mls-listings-persistent';
|
||||
|
||||
/**
|
||||
* Database instance
|
||||
*/
|
||||
@@ -161,7 +166,7 @@ class MLS_Media_Handler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base upload directory for MLS media
|
||||
* Get base upload directory for MLS media (standard cache)
|
||||
*
|
||||
* @return string Absolute path
|
||||
*/
|
||||
@@ -171,7 +176,7 @@ class MLS_Media_Handler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base upload URL for MLS media
|
||||
* Get base upload URL for MLS media (standard cache)
|
||||
*
|
||||
* @return string URL
|
||||
*/
|
||||
@@ -181,14 +186,113 @@ class MLS_Media_Handler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage directory for a specific listing
|
||||
* Get base upload directory for persistent HomeProz media
|
||||
*
|
||||
* @param string $listing_key Listing key
|
||||
* @return string Absolute path
|
||||
*/
|
||||
public function get_listing_dir($listing_key) {
|
||||
public function get_persistent_upload_dir() {
|
||||
$upload_dir = wp_upload_dir();
|
||||
return $upload_dir['basedir'] . '/' . self::PERSISTENT_SUBDIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base upload URL for persistent HomeProz media
|
||||
*
|
||||
* @return string URL
|
||||
*/
|
||||
public function get_persistent_upload_url() {
|
||||
$upload_dir = wp_upload_dir();
|
||||
return $upload_dir['baseurl'] . '/' . self::PERSISTENT_SUBDIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a listing is a HomeProz listing
|
||||
*
|
||||
* @param string $listing_key Listing key
|
||||
* @return bool True if HomeProz listing
|
||||
*/
|
||||
public function is_homeproz_listing($listing_key) {
|
||||
global $wpdb;
|
||||
|
||||
$is_homeproz = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT is_homeproz FROM {$this->db->properties_table()} WHERE listing_key = %s",
|
||||
$listing_key
|
||||
));
|
||||
|
||||
return (bool) $is_homeproz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage directory for a specific listing
|
||||
*
|
||||
* HomeProz listings use the persistent cache directory.
|
||||
* Other listings use the standard cache directory (subject to garbage collection).
|
||||
*
|
||||
* @param string $listing_key Listing key
|
||||
* @param bool|null $is_homeproz Override HomeProz check (null = look up from DB)
|
||||
* @return string Absolute path
|
||||
*/
|
||||
public function get_listing_dir($listing_key, $is_homeproz = null) {
|
||||
$prefix = substr($listing_key, 0, 2);
|
||||
return $this->get_upload_dir() . '/' . $prefix . '/' . $listing_key;
|
||||
|
||||
// Determine if HomeProz if not explicitly provided
|
||||
if ($is_homeproz === null) {
|
||||
$is_homeproz = $this->is_homeproz_listing($listing_key);
|
||||
}
|
||||
|
||||
$base_dir = $is_homeproz ? $this->get_persistent_upload_dir() : $this->get_upload_dir();
|
||||
return $base_dir . '/' . $prefix . '/' . $listing_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URL for a listing's media
|
||||
*
|
||||
* @param string $listing_key Listing key
|
||||
* @param bool|null $is_homeproz Override HomeProz check (null = look up from DB)
|
||||
* @return string URL
|
||||
*/
|
||||
public function get_listing_url($listing_key, $is_homeproz = null) {
|
||||
$prefix = substr($listing_key, 0, 2);
|
||||
|
||||
if ($is_homeproz === null) {
|
||||
$is_homeproz = $this->is_homeproz_listing($listing_key);
|
||||
}
|
||||
|
||||
$base_url = $is_homeproz ? $this->get_persistent_upload_url() : $this->get_upload_url();
|
||||
return $base_url . '/' . $prefix . '/' . $listing_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find existing cached file for a listing, checking both persistent and standard cache
|
||||
*
|
||||
* @param string $listing_key Listing key
|
||||
* @param string $filename Filename to find
|
||||
* @return array|null ['path' => absolute path, 'url' => url, 'persistent' => bool] or null
|
||||
*/
|
||||
public function find_cached_file($listing_key, $filename) {
|
||||
$prefix = substr($listing_key, 0, 2);
|
||||
|
||||
// Check persistent cache first (HomeProz listings)
|
||||
$persistent_path = $this->get_persistent_upload_dir() . '/' . $prefix . '/' . $listing_key . '/' . $filename;
|
||||
if (file_exists($persistent_path)) {
|
||||
return array(
|
||||
'path' => $persistent_path,
|
||||
'url' => $this->get_persistent_upload_url() . '/' . $prefix . '/' . $listing_key . '/' . $filename,
|
||||
'persistent' => true,
|
||||
);
|
||||
}
|
||||
|
||||
// Check standard cache
|
||||
$standard_path = $this->get_upload_dir() . '/' . $prefix . '/' . $listing_key . '/' . $filename;
|
||||
if (file_exists($standard_path)) {
|
||||
return array(
|
||||
'path' => $standard_path,
|
||||
'url' => $this->get_upload_url() . '/' . $prefix . '/' . $listing_key . '/' . $filename,
|
||||
'persistent' => false,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,15 +345,40 @@ class MLS_Media_Handler {
|
||||
);
|
||||
|
||||
if ($existing) {
|
||||
// Check if URL changed - if so, clear cached file
|
||||
if ($existing->media_url !== ($media['MediaURL'] ?? null) && $existing->local_path) {
|
||||
$file_path = $this->get_upload_dir() . '/' . $existing->local_path;
|
||||
if (file_exists($file_path)) {
|
||||
unlink($file_path);
|
||||
// Check if URL changed
|
||||
$url_changed = $existing->media_url !== ($media['MediaURL'] ?? null);
|
||||
|
||||
if ($url_changed && $existing->local_path) {
|
||||
// Check if this is a HomeProz listing
|
||||
$is_homeproz = $this->is_homeproz_listing($listing_key);
|
||||
|
||||
if ($is_homeproz) {
|
||||
// HomeProz: Keep existing cached file, don't reset local_path
|
||||
// The existing image continues to work even if MLS URL expires
|
||||
// Only replace if a new download succeeds later
|
||||
} else {
|
||||
// Non-HomeProz: Delete old cached files to save space
|
||||
$filename = basename($existing->local_path);
|
||||
$prefix = substr($listing_key, 0, 2);
|
||||
|
||||
// Delete from standard cache
|
||||
$standard_path = $this->get_upload_dir() . '/' . $prefix . '/' . $listing_key . '/' . $filename;
|
||||
if (file_exists($standard_path)) {
|
||||
unlink($standard_path);
|
||||
}
|
||||
|
||||
// Also delete WebP versions
|
||||
$webp_filename = preg_replace('/\.[^.]+$/', '.webp', $filename);
|
||||
$webp_standard = $this->get_upload_dir() . '/' . $prefix . '/' . $listing_key . '/' . $webp_filename;
|
||||
if (file_exists($webp_standard)) {
|
||||
unlink($webp_standard);
|
||||
}
|
||||
|
||||
$data['local_path'] = null;
|
||||
$data['local_url'] = null;
|
||||
$data['downloaded_at'] = null;
|
||||
$data['download_status'] = 'pending';
|
||||
}
|
||||
$data['local_path'] = null;
|
||||
$data['local_url'] = null;
|
||||
$data['downloaded_at'] = null;
|
||||
}
|
||||
|
||||
$wpdb->update(
|
||||
@@ -269,7 +398,7 @@ class MLS_Media_Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete orphaned media records
|
||||
// Delete orphaned media records and their files
|
||||
if (!empty($received_keys)) {
|
||||
$placeholders = implode(',', array_fill(0, count($received_keys), '%s'));
|
||||
$values = array_merge(array($listing_key), $received_keys);
|
||||
@@ -282,21 +411,220 @@ class MLS_Media_Handler {
|
||||
|
||||
foreach ($orphaned as $record) {
|
||||
if ($record->local_path) {
|
||||
$file_path = $this->get_upload_dir() . '/' . $record->local_path;
|
||||
if (file_exists($file_path)) {
|
||||
unlink($file_path);
|
||||
$filename = basename($record->local_path);
|
||||
$prefix = substr($listing_key, 0, 2);
|
||||
|
||||
// Delete from both directories
|
||||
$paths_to_delete = array(
|
||||
$this->get_upload_dir() . '/' . $prefix . '/' . $listing_key . '/' . $filename,
|
||||
$this->get_persistent_upload_dir() . '/' . $prefix . '/' . $listing_key . '/' . $filename,
|
||||
);
|
||||
|
||||
// Also include WebP versions
|
||||
$webp_filename = preg_replace('/\.[^.]+$/', '.webp', $filename);
|
||||
$paths_to_delete[] = $this->get_upload_dir() . '/' . $prefix . '/' . $listing_key . '/' . $webp_filename;
|
||||
$paths_to_delete[] = $this->get_persistent_upload_dir() . '/' . $prefix . '/' . $listing_key . '/' . $webp_filename;
|
||||
|
||||
foreach ($paths_to_delete as $path) {
|
||||
if (file_exists($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
$wpdb->delete($this->db->media_table(), array('id' => $record->id));
|
||||
}
|
||||
}
|
||||
|
||||
// Update property's media_expires_at with earliest expiration from all media URLs
|
||||
$this->update_property_media_expiration($listing_key);
|
||||
|
||||
return array('stored' => $stored, 'skipped' => $skipped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and cache all images for a HomeProz listing
|
||||
*
|
||||
* Called during sync to immediately cache images for HomeProz listings
|
||||
* so they're available even after the listing is sold and removed from MLS.
|
||||
*
|
||||
* @param string $listing_key Listing key
|
||||
* @param callable|null $progress_callback Progress callback
|
||||
* @param int $delay_seconds Delay between each image download (default 10s to respect API limits)
|
||||
* @return array Stats with 'downloaded', 'skipped', and 'errors' counts
|
||||
*/
|
||||
public function download_homeproz_images($listing_key, $progress_callback = null, $delay_seconds = 10) {
|
||||
global $wpdb;
|
||||
|
||||
$stats = array('downloaded' => 0, 'skipped' => 0, 'errors' => 0);
|
||||
|
||||
// Get all media records that haven't been downloaded yet
|
||||
$media_records = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$this->db->media_table()}
|
||||
WHERE listing_key = %s AND media_url IS NOT NULL AND download_status = 'pending'
|
||||
ORDER BY media_order ASC",
|
||||
$listing_key
|
||||
));
|
||||
|
||||
if (empty($media_records)) {
|
||||
return $stats;
|
||||
}
|
||||
|
||||
$total_records = count($media_records);
|
||||
$current = 0;
|
||||
|
||||
foreach ($media_records as $media) {
|
||||
$current++;
|
||||
|
||||
// Check if already cached (check both directories)
|
||||
// Try known local_path first, then search by media_order pattern
|
||||
$found_file = null;
|
||||
|
||||
if ($media->local_path) {
|
||||
$filename = basename($media->local_path);
|
||||
|
||||
// Check for WebP version first
|
||||
$webp_filename = preg_replace('/\.[^.]+$/', '.webp', $filename);
|
||||
$found_file = $this->find_cached_file($listing_key, $webp_filename);
|
||||
|
||||
// Check for original file
|
||||
if (!$found_file) {
|
||||
$found_file = $this->find_cached_file($listing_key, $filename);
|
||||
}
|
||||
}
|
||||
|
||||
// If no local_path or file not found, search by media_order pattern
|
||||
if (!$found_file) {
|
||||
$extensions = array('webp', 'jpg', 'jpeg', 'png', 'gif');
|
||||
foreach ($extensions as $ext) {
|
||||
$pattern_file = $media->media_order . '.' . $ext;
|
||||
$found_file = $this->find_cached_file($listing_key, $pattern_file);
|
||||
if ($found_file) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If file exists on disk, update database and skip download
|
||||
if ($found_file) {
|
||||
// Update database to reflect the cached file
|
||||
$prefix = substr($listing_key, 0, 2);
|
||||
$filename = basename($found_file['path']);
|
||||
$relative_path = $prefix . '/' . $listing_key . '/' . $filename;
|
||||
|
||||
$wpdb->update(
|
||||
$this->db->media_table(),
|
||||
array(
|
||||
'local_path' => $relative_path,
|
||||
'local_url' => $found_file['url'],
|
||||
'download_status' => 'completed',
|
||||
'downloaded_at' => current_time('mysql'),
|
||||
),
|
||||
array('id' => $media->id)
|
||||
);
|
||||
|
||||
$stats['skipped']++;
|
||||
if ($progress_callback) {
|
||||
call_user_func($progress_callback, 'media_skipped', array('media_key' => $media->media_key));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Download the image
|
||||
$url = $this->fetch_and_cache($media);
|
||||
if ($url) {
|
||||
$stats['downloaded']++;
|
||||
if ($progress_callback) {
|
||||
call_user_func($progress_callback, 'media_downloaded', array('media_key' => $media->media_key));
|
||||
}
|
||||
|
||||
// Rate limit: delay between image downloads to respect MLS API limits
|
||||
// Only delay if there are more images to download
|
||||
if ($delay_seconds > 0 && $current < $total_records) {
|
||||
sleep($delay_seconds);
|
||||
}
|
||||
} else {
|
||||
$stats['errors']++;
|
||||
if ($progress_callback) {
|
||||
call_user_func($progress_callback, 'media_error', array('media_key' => $media->media_key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a property's media_expires_at field based on its media URLs
|
||||
*
|
||||
* Finds the earliest expiration timestamp from all media URLs
|
||||
* and sets it on the property record.
|
||||
*
|
||||
* @param string $listing_key Listing key
|
||||
*/
|
||||
public function update_property_media_expiration($listing_key) {
|
||||
global $wpdb;
|
||||
|
||||
// Get all media URLs for this property
|
||||
$media_urls = $wpdb->get_col($wpdb->prepare(
|
||||
"SELECT media_url FROM {$this->db->media_table()}
|
||||
WHERE listing_key = %s AND media_url IS NOT NULL",
|
||||
$listing_key
|
||||
));
|
||||
|
||||
if (empty($media_urls)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the earliest expiration timestamp
|
||||
$earliest_expires = null;
|
||||
|
||||
foreach ($media_urls as $url) {
|
||||
$expires = $this->extract_url_expiration($url);
|
||||
if ($expires !== null) {
|
||||
if ($earliest_expires === null || $expires < $earliest_expires) {
|
||||
$earliest_expires = $expires;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the property record
|
||||
if ($earliest_expires !== null) {
|
||||
$expires_at = gmdate('Y-m-d H:i:s', $earliest_expires);
|
||||
$wpdb->update(
|
||||
$this->db->properties_table(),
|
||||
array('media_expires_at' => $expires_at),
|
||||
array('listing_key' => $listing_key),
|
||||
array('%s'),
|
||||
array('%s')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract expiration timestamp from a media URL
|
||||
*
|
||||
* MLS Grid media URLs contain an 'expires' parameter with Unix timestamp.
|
||||
*
|
||||
* @param string $media_url The media URL
|
||||
* @return int|null Unix timestamp or null if not found
|
||||
*/
|
||||
public function extract_url_expiration($media_url) {
|
||||
if (empty($media_url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/expires=(\d+)/', $media_url, $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image URL for a media record, fetching on-demand if needed
|
||||
*
|
||||
* Checks both persistent (HomeProz) and standard cache directories.
|
||||
*
|
||||
* @param int|object $media Media ID or media record object
|
||||
* @param bool $fetch_if_missing Whether to fetch if not cached
|
||||
* @return string|null Local URL or null
|
||||
@@ -316,24 +644,29 @@ class MLS_Media_Handler {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Already cached - check for WebP version first
|
||||
if ($media->local_url && $media->local_path) {
|
||||
$file_path = $this->get_upload_dir() . '/' . $media->local_path;
|
||||
$actual_path = $this->prefer_webp_path($file_path);
|
||||
if (file_exists($actual_path)) {
|
||||
// If WebP version exists, return WebP URL
|
||||
if ($actual_path !== $file_path) {
|
||||
$webp_path = preg_replace('/\.[^.]+$/', '.webp', $media->local_path);
|
||||
return $this->get_upload_url() . '/' . $webp_path;
|
||||
}
|
||||
return $media->local_url;
|
||||
// Check for cached file in both directories
|
||||
if ($media->local_path) {
|
||||
$filename = basename($media->local_path);
|
||||
|
||||
// Check for WebP version first
|
||||
$webp_filename = preg_replace('/\.[^.]+$/', '.webp', $filename);
|
||||
$webp_found = $this->find_cached_file($media->listing_key, $webp_filename);
|
||||
if ($webp_found) {
|
||||
return $webp_found['url'];
|
||||
}
|
||||
|
||||
// Check for original file
|
||||
$found = $this->find_cached_file($media->listing_key, $filename);
|
||||
if ($found) {
|
||||
return $found['url'];
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch on demand
|
||||
if ($fetch_if_missing && $media->media_url) {
|
||||
$result = $this->fetch_and_cache($media);
|
||||
if ($result) {
|
||||
// Propagate WP_Error (e.g., rate limiting) or return URL
|
||||
if (is_wp_error($result) || $result) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -344,6 +677,8 @@ class MLS_Media_Handler {
|
||||
/**
|
||||
* Get primary image URL for a listing (on-demand)
|
||||
*
|
||||
* Checks both persistent (HomeProz) and standard cache directories.
|
||||
*
|
||||
* @param string $listing_key Listing key
|
||||
* @param bool $fetch_if_missing Whether to fetch if not cached
|
||||
* @return string|null Image URL
|
||||
@@ -360,16 +695,20 @@ class MLS_Media_Handler {
|
||||
$listing_key
|
||||
));
|
||||
|
||||
if ($cached) {
|
||||
$file_path = $this->get_upload_dir() . '/' . $cached->local_path;
|
||||
$actual_path = $this->prefer_webp_path($file_path);
|
||||
if (file_exists($actual_path)) {
|
||||
// If WebP version exists, return WebP URL
|
||||
if ($actual_path !== $file_path) {
|
||||
$webp_path = preg_replace('/\.[^.]+$/', '.webp', $cached->local_path);
|
||||
return $this->get_upload_url() . '/' . $webp_path;
|
||||
}
|
||||
return $cached->local_url;
|
||||
if ($cached && $cached->local_path) {
|
||||
$filename = basename($cached->local_path);
|
||||
|
||||
// Check for WebP version first
|
||||
$webp_filename = preg_replace('/\.[^.]+$/', '.webp', $filename);
|
||||
$webp_found = $this->find_cached_file($listing_key, $webp_filename);
|
||||
if ($webp_found) {
|
||||
return $webp_found['url'];
|
||||
}
|
||||
|
||||
// Check for original file
|
||||
$found = $this->find_cached_file($listing_key, $filename);
|
||||
if ($found) {
|
||||
return $found['url'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,17 +725,21 @@ class MLS_Media_Handler {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If already cached and file exists, return it - check for WebP first
|
||||
if ($media->local_url && $media->local_path) {
|
||||
$file_path = $this->get_upload_dir() . '/' . $media->local_path;
|
||||
$actual_path = $this->prefer_webp_path($file_path);
|
||||
if (file_exists($actual_path)) {
|
||||
// If WebP version exists, return WebP URL
|
||||
if ($actual_path !== $file_path) {
|
||||
$webp_path = preg_replace('/\.[^.]+$/', '.webp', $media->local_path);
|
||||
return $this->get_upload_url() . '/' . $webp_path;
|
||||
}
|
||||
return $media->local_url;
|
||||
// Check for cached file in both directories
|
||||
if ($media->local_path) {
|
||||
$filename = basename($media->local_path);
|
||||
|
||||
// Check for WebP version first
|
||||
$webp_filename = preg_replace('/\.[^.]+$/', '.webp', $filename);
|
||||
$webp_found = $this->find_cached_file($listing_key, $webp_filename);
|
||||
if ($webp_found) {
|
||||
return $webp_found['url'];
|
||||
}
|
||||
|
||||
// Check for original file
|
||||
$found = $this->find_cached_file($listing_key, $filename);
|
||||
if ($found) {
|
||||
return $found['url'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,6 +754,8 @@ class MLS_Media_Handler {
|
||||
/**
|
||||
* Get all images for a listing (on-demand for first N)
|
||||
*
|
||||
* Checks both persistent (HomeProz) and standard cache directories.
|
||||
*
|
||||
* @param string $listing_key Listing key
|
||||
* @param int $fetch_limit Max images to fetch on-demand (0 = none)
|
||||
* @return array Media records with local_url populated where available
|
||||
@@ -431,16 +776,22 @@ class MLS_Media_Handler {
|
||||
|
||||
$fetched = 0;
|
||||
foreach ($media as &$item) {
|
||||
// Check if cached and file exists - prefer WebP version
|
||||
if ($item->local_url && $item->local_path) {
|
||||
$file_path = $this->get_upload_dir() . '/' . $item->local_path;
|
||||
$actual_path = $this->prefer_webp_path($file_path);
|
||||
if (file_exists($actual_path)) {
|
||||
// If WebP version exists, update the URL
|
||||
if ($actual_path !== $file_path) {
|
||||
$webp_path = preg_replace('/\.[^.]+$/', '.webp', $item->local_path);
|
||||
$item->local_url = $this->get_upload_url() . '/' . $webp_path;
|
||||
}
|
||||
// Check for cached file in both directories - prefer WebP version
|
||||
if ($item->local_path) {
|
||||
$filename = basename($item->local_path);
|
||||
|
||||
// Check for WebP version first
|
||||
$webp_filename = preg_replace('/\.[^.]+$/', '.webp', $filename);
|
||||
$webp_found = $this->find_cached_file($listing_key, $webp_filename);
|
||||
if ($webp_found) {
|
||||
$item->local_url = $webp_found['url'];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for original file
|
||||
$found = $this->find_cached_file($listing_key, $filename);
|
||||
if ($found) {
|
||||
$item->local_url = $found['url'];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -509,23 +860,97 @@ class MLS_Media_Handler {
|
||||
}
|
||||
|
||||
try {
|
||||
// Determine if this is a HomeProz listing (determines cache location)
|
||||
$is_homeproz = $this->is_homeproz_listing($media->listing_key);
|
||||
|
||||
// Re-check if image was cached while we waited for lock
|
||||
$updated_media = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT local_path, local_url FROM {$this->db->media_table()} WHERE id = %d",
|
||||
$media->id
|
||||
));
|
||||
|
||||
// Check for existing file - first by local_path, then by media_order pattern
|
||||
$found_file = null;
|
||||
|
||||
if ($updated_media && $updated_media->local_path) {
|
||||
$file_path = $this->get_upload_dir() . '/' . $updated_media->local_path;
|
||||
$actual_path = $this->prefer_webp_path($file_path);
|
||||
if (file_exists($actual_path)) {
|
||||
// Another request cached it while we waited
|
||||
// If WebP version exists, return WebP URL
|
||||
if ($actual_path !== $file_path) {
|
||||
$webp_path = preg_replace('/\.[^.]+$/', '.webp', $updated_media->local_path);
|
||||
return $this->get_upload_url() . '/' . $webp_path;
|
||||
// Check both cache directories for existing file
|
||||
$filename = basename($updated_media->local_path);
|
||||
|
||||
// Check for WebP version first
|
||||
$webp_filename = preg_replace('/\.[^.]+$/', '.webp', $filename);
|
||||
$found_file = $this->find_cached_file($media->listing_key, $webp_filename);
|
||||
|
||||
if (!$found_file) {
|
||||
$found_file = $this->find_cached_file($media->listing_key, $filename);
|
||||
}
|
||||
}
|
||||
|
||||
// If no local_path or file not found, search by media_order pattern
|
||||
if (!$found_file) {
|
||||
$extensions = array('webp', 'jpg', 'jpeg', 'png', 'gif');
|
||||
foreach ($extensions as $ext) {
|
||||
$pattern_file = $media->media_order . '.' . $ext;
|
||||
$found_file = $this->find_cached_file($media->listing_key, $pattern_file);
|
||||
if ($found_file) {
|
||||
break;
|
||||
}
|
||||
return $updated_media->local_url;
|
||||
}
|
||||
}
|
||||
|
||||
// If file exists on disk, update database and return URL
|
||||
if ($found_file) {
|
||||
$prefix = substr($media->listing_key, 0, 2);
|
||||
$filename = basename($found_file['path']);
|
||||
$relative_path = $prefix . '/' . $media->listing_key . '/' . $filename;
|
||||
|
||||
$wpdb->update(
|
||||
$this->db->media_table(),
|
||||
array(
|
||||
'local_path' => $relative_path,
|
||||
'local_url' => $found_file['url'],
|
||||
'download_status' => 'completed',
|
||||
'downloaded_at' => current_time('mysql'),
|
||||
),
|
||||
array('id' => $media->id)
|
||||
);
|
||||
|
||||
return $found_file['url'];
|
||||
}
|
||||
|
||||
// If the media URL has expired, refresh property URLs from the API.
|
||||
// Use a property-level lock so concurrent fetches share one refresh.
|
||||
if ($this->is_url_expired($media->media_url)) {
|
||||
$refresh_lock = 'mls_url_refresh_' . $media->listing_key;
|
||||
$refresh_acquired = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT GET_LOCK(%s, %d)",
|
||||
$refresh_lock,
|
||||
15
|
||||
));
|
||||
|
||||
try {
|
||||
if ($refresh_acquired === '1') {
|
||||
// Re-check after lock — another process may have refreshed
|
||||
$latest = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT media_url FROM {$this->db->media_table()} WHERE id = %d",
|
||||
$media->id
|
||||
));
|
||||
if (!$latest || $this->is_url_expired($latest->media_url)) {
|
||||
$this->refresh_media_urls($media->listing_key);
|
||||
}
|
||||
}
|
||||
|
||||
$media = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->db->media_table()} WHERE id = %d",
|
||||
$media->id
|
||||
));
|
||||
} finally {
|
||||
if ($refresh_acquired === '1') {
|
||||
$wpdb->query($wpdb->prepare("SELECT RELEASE_LOCK(%s)", $refresh_lock));
|
||||
}
|
||||
}
|
||||
|
||||
if (!$media || empty($media->media_url) || $this->is_url_expired($media->media_url)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,6 +983,10 @@ class MLS_Media_Handler {
|
||||
'media_key' => $media->media_key,
|
||||
'status' => $status_code,
|
||||
));
|
||||
// Return error code for rate limiting so caller can handle appropriately
|
||||
if ($status_code === 429) {
|
||||
return new WP_Error('rate_limited', 'MLS Grid rate limit exceeded', array('status' => 429));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -574,8 +1003,8 @@ class MLS_Media_Handler {
|
||||
$content_type = wp_remote_retrieve_header($response, 'content-type');
|
||||
$extension = $this->get_extension_from_content_type($content_type, $media->media_url);
|
||||
|
||||
// Create directory
|
||||
$listing_dir = $this->get_listing_dir($media->listing_key);
|
||||
// Create directory (HomeProz listings go to persistent cache)
|
||||
$listing_dir = $this->get_listing_dir($media->listing_key, $is_homeproz);
|
||||
if (!file_exists($listing_dir)) {
|
||||
wp_mkdir_p($listing_dir);
|
||||
}
|
||||
@@ -600,10 +1029,11 @@ class MLS_Media_Handler {
|
||||
$content_type = 'image/webp';
|
||||
}
|
||||
|
||||
// Update database
|
||||
// Update database with correct URL based on cache location
|
||||
$prefix = substr($media->listing_key, 0, 2);
|
||||
$relative_path = $prefix . '/' . $media->listing_key . '/' . $filename;
|
||||
$local_url = $this->get_upload_url() . '/' . $relative_path;
|
||||
$base_url = $is_homeproz ? $this->get_persistent_upload_url() : $this->get_upload_url();
|
||||
$local_url = $base_url . '/' . $relative_path;
|
||||
|
||||
// Get actual file size after any conversion
|
||||
$final_size = filesize($file_path);
|
||||
@@ -616,6 +1046,7 @@ class MLS_Media_Handler {
|
||||
'file_size' => $final_size,
|
||||
'mime_type' => $content_type,
|
||||
'downloaded_at' => current_time('mysql'),
|
||||
'download_status' => 'completed',
|
||||
),
|
||||
array('id' => $media->id)
|
||||
);
|
||||
@@ -668,15 +1099,25 @@ class MLS_Media_Handler {
|
||||
/**
|
||||
* Delete all media for a property
|
||||
*
|
||||
* Deletes from both persistent and standard cache directories.
|
||||
*
|
||||
* @param string $listing_key Listing key
|
||||
*/
|
||||
public function delete_property_media($listing_key) {
|
||||
global $wpdb;
|
||||
|
||||
// Delete files
|
||||
$listing_dir = $this->get_listing_dir($listing_key);
|
||||
if (file_exists($listing_dir)) {
|
||||
$this->recursive_delete($listing_dir);
|
||||
$prefix = substr($listing_key, 0, 2);
|
||||
|
||||
// Delete from persistent cache directory
|
||||
$persistent_dir = $this->get_persistent_upload_dir() . '/' . $prefix . '/' . $listing_key;
|
||||
if (file_exists($persistent_dir)) {
|
||||
$this->recursive_delete($persistent_dir);
|
||||
}
|
||||
|
||||
// Delete from standard cache directory
|
||||
$standard_dir = $this->get_upload_dir() . '/' . $prefix . '/' . $listing_key;
|
||||
if (file_exists($standard_dir)) {
|
||||
$this->recursive_delete($standard_dir);
|
||||
}
|
||||
|
||||
// Delete records
|
||||
@@ -811,43 +1252,52 @@ class MLS_Media_Handler {
|
||||
/**
|
||||
* Clean up orphaned media files (files without database records)
|
||||
*
|
||||
* Checks both standard and persistent cache directories.
|
||||
*
|
||||
* @return int Number of directories deleted
|
||||
*/
|
||||
public function cleanup_orphaned_files() {
|
||||
global $wpdb;
|
||||
|
||||
$deleted = 0;
|
||||
$base_dir = $this->get_upload_dir();
|
||||
|
||||
if (!is_dir($base_dir)) {
|
||||
return 0;
|
||||
}
|
||||
// Check both cache directories
|
||||
$directories = array(
|
||||
$this->get_upload_dir(),
|
||||
$this->get_persistent_upload_dir(),
|
||||
);
|
||||
|
||||
foreach (scandir($base_dir) as $prefix) {
|
||||
if ($prefix === '.' || $prefix === '..' || !is_dir($base_dir . '/' . $prefix)) {
|
||||
foreach ($directories as $base_dir) {
|
||||
if (!is_dir($base_dir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$prefix_dir = $base_dir . '/' . $prefix;
|
||||
|
||||
foreach (scandir($prefix_dir) as $listing_key) {
|
||||
if ($listing_key === '.' || $listing_key === '..') {
|
||||
foreach (scandir($base_dir) as $prefix) {
|
||||
if ($prefix === '.' || $prefix === '..' || !is_dir($base_dir . '/' . $prefix)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$listing_dir = $prefix_dir . '/' . $listing_key;
|
||||
if (!is_dir($listing_dir)) {
|
||||
continue;
|
||||
}
|
||||
$prefix_dir = $base_dir . '/' . $prefix;
|
||||
|
||||
$exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->db->properties_table()} WHERE listing_key = %s",
|
||||
$listing_key
|
||||
));
|
||||
foreach (scandir($prefix_dir) as $listing_key) {
|
||||
if ($listing_key === '.' || $listing_key === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$exists) {
|
||||
$this->recursive_delete($listing_dir);
|
||||
$deleted++;
|
||||
$listing_dir = $prefix_dir . '/' . $listing_key;
|
||||
if (!is_dir($listing_dir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->db->properties_table()} WHERE listing_key = %s",
|
||||
$listing_key
|
||||
));
|
||||
|
||||
if (!$exists) {
|
||||
$this->recursive_delete($listing_dir);
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user