b6df4dbb92
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>
372 lines
11 KiB
PHP
Executable File
372 lines
11 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* Favicon Management
|
|
*
|
|
* Handles favicon generation from uploaded source image using ImageMagick.
|
|
* Generates all required sizes and outputs appropriate HTML.
|
|
*
|
|
* @package HomeProz
|
|
*/
|
|
|
|
// Prevent direct access
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Get the favicon directory path and URL
|
|
*/
|
|
function homeproz_get_favicon_paths() {
|
|
$upload_dir = wp_upload_dir();
|
|
return array(
|
|
'dir' => $upload_dir['basedir'] . '/favicon',
|
|
'url' => $upload_dir['baseurl'] . '/favicon',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Process favicon when Theme Options are saved
|
|
*/
|
|
add_action('acf/save_post', 'homeproz_process_favicon', 20);
|
|
function homeproz_process_favicon($post_id) {
|
|
// Only process on options page
|
|
if ($post_id !== 'options') {
|
|
return;
|
|
}
|
|
|
|
$favicon_source_id = get_field('theme_favicon_source', 'option');
|
|
|
|
if (!$favicon_source_id) {
|
|
return;
|
|
}
|
|
|
|
// Get the source image path
|
|
$source_path = get_attached_file($favicon_source_id);
|
|
|
|
if (!$source_path || !file_exists($source_path)) {
|
|
return;
|
|
}
|
|
|
|
// Check if we need to regenerate (compare source modification time)
|
|
$paths = homeproz_get_favicon_paths();
|
|
$version_file = $paths['dir'] . '/.version';
|
|
$source_mtime = filemtime($source_path);
|
|
|
|
if (file_exists($version_file)) {
|
|
$stored_version = file_get_contents($version_file);
|
|
if ($stored_version === $favicon_source_id . ':' . $source_mtime) {
|
|
// Already processed this version
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Generate favicons
|
|
$result = homeproz_generate_favicons($source_path);
|
|
|
|
if ($result) {
|
|
// Store version to avoid re-processing
|
|
file_put_contents($version_file, $favicon_source_id . ':' . $source_mtime);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate all favicon sizes using ImageMagick
|
|
*/
|
|
function homeproz_generate_favicons($source_path) {
|
|
$paths = homeproz_get_favicon_paths();
|
|
$favicon_dir = $paths['dir'];
|
|
|
|
// Create directory if it doesn't exist
|
|
if (!file_exists($favicon_dir)) {
|
|
wp_mkdir_p($favicon_dir);
|
|
}
|
|
|
|
// Check if ImageMagick is available
|
|
$convert_path = trim(shell_exec('which convert 2>/dev/null'));
|
|
if (empty($convert_path)) {
|
|
error_log('HomeProz Favicon: ImageMagick convert command not found');
|
|
return false;
|
|
}
|
|
|
|
// Verify source image dimensions
|
|
$image_info = getimagesize($source_path);
|
|
if (!$image_info || $image_info[0] < 256 || $image_info[1] < 256) {
|
|
error_log('HomeProz Favicon: Source image must be at least 256x256 pixels');
|
|
return false;
|
|
}
|
|
|
|
$source_escaped = escapeshellarg($source_path);
|
|
|
|
// Define all the sizes we need to generate
|
|
$png_sizes = array(
|
|
'favicon-16x16.png' => 16,
|
|
'favicon-32x32.png' => 32,
|
|
'favicon-48x48.png' => 48,
|
|
'apple-touch-icon.png' => 180,
|
|
'android-chrome-192x192.png' => 192,
|
|
'android-chrome-512x512.png' => 512,
|
|
'mstile-150x150.png' => 150,
|
|
);
|
|
|
|
$success = true;
|
|
|
|
// Generate PNG files
|
|
foreach ($png_sizes as $filename => $size) {
|
|
$output_path = $favicon_dir . '/' . $filename;
|
|
$output_escaped = escapeshellarg($output_path);
|
|
|
|
// Use ImageMagick to resize with high quality
|
|
$cmd = sprintf(
|
|
'%s %s -resize %dx%d -gravity center -background transparent -extent %dx%d %s 2>&1',
|
|
escapeshellarg($convert_path),
|
|
$source_escaped,
|
|
$size,
|
|
$size,
|
|
$size,
|
|
$size,
|
|
$output_escaped
|
|
);
|
|
|
|
exec($cmd, $output, $return_var);
|
|
|
|
if ($return_var !== 0) {
|
|
error_log('HomeProz Favicon: Failed to generate ' . $filename . ': ' . implode("\n", $output));
|
|
$success = false;
|
|
}
|
|
}
|
|
|
|
// Generate favicon.ico (multi-resolution ICO file)
|
|
$ico_path = $favicon_dir . '/favicon.ico';
|
|
$ico_escaped = escapeshellarg($ico_path);
|
|
|
|
// Create temporary files for ICO sizes
|
|
$temp_16 = $favicon_dir . '/temp-16.png';
|
|
$temp_32 = $favicon_dir . '/temp-32.png';
|
|
$temp_48 = $favicon_dir . '/temp-48.png';
|
|
|
|
// Generate temp files
|
|
foreach (array(16 => $temp_16, 32 => $temp_32, 48 => $temp_48) as $size => $temp_path) {
|
|
$cmd = sprintf(
|
|
'%s %s -resize %dx%d -gravity center -background transparent -extent %dx%d %s 2>&1',
|
|
escapeshellarg($convert_path),
|
|
$source_escaped,
|
|
$size,
|
|
$size,
|
|
$size,
|
|
$size,
|
|
escapeshellarg($temp_path)
|
|
);
|
|
exec($cmd, $output, $return_var);
|
|
}
|
|
|
|
// Combine into ICO
|
|
$cmd = sprintf(
|
|
'%s %s %s %s %s 2>&1',
|
|
escapeshellarg($convert_path),
|
|
escapeshellarg($temp_16),
|
|
escapeshellarg($temp_32),
|
|
escapeshellarg($temp_48),
|
|
$ico_escaped
|
|
);
|
|
exec($cmd, $output, $return_var);
|
|
|
|
// Clean up temp files
|
|
@unlink($temp_16);
|
|
@unlink($temp_32);
|
|
@unlink($temp_48);
|
|
|
|
if ($return_var !== 0) {
|
|
error_log('HomeProz Favicon: Failed to generate favicon.ico: ' . implode("\n", $output));
|
|
$success = false;
|
|
}
|
|
|
|
// Generate web manifest
|
|
homeproz_generate_web_manifest($favicon_dir);
|
|
|
|
// Generate browserconfig.xml for Windows
|
|
homeproz_generate_browserconfig($favicon_dir);
|
|
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* Generate site.webmanifest file
|
|
*/
|
|
function homeproz_generate_web_manifest($favicon_dir) {
|
|
$paths = homeproz_get_favicon_paths();
|
|
|
|
$manifest = array(
|
|
'name' => get_bloginfo('name'),
|
|
'short_name' => 'HomeProz',
|
|
'icons' => array(
|
|
array(
|
|
'src' => $paths['url'] . '/android-chrome-192x192.png',
|
|
'sizes' => '192x192',
|
|
'type' => 'image/png',
|
|
),
|
|
array(
|
|
'src' => $paths['url'] . '/android-chrome-512x512.png',
|
|
'sizes' => '512x512',
|
|
'type' => 'image/png',
|
|
),
|
|
),
|
|
'theme_color' => '#0A0A0A',
|
|
'background_color' => '#0A0A0A',
|
|
'display' => 'standalone',
|
|
);
|
|
|
|
$manifest_path = $favicon_dir . '/site.webmanifest';
|
|
file_put_contents($manifest_path, json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
}
|
|
|
|
/**
|
|
* Generate browserconfig.xml for Windows tiles
|
|
*/
|
|
function homeproz_generate_browserconfig($favicon_dir) {
|
|
$paths = homeproz_get_favicon_paths();
|
|
|
|
$xml = '<?xml version="1.0" encoding="utf-8"?>
|
|
<browserconfig>
|
|
<msapplication>
|
|
<tile>
|
|
<square150x150logo src="' . esc_url($paths['url'] . '/mstile-150x150.png') . '"/>
|
|
<TileColor>#0A0A0A</TileColor>
|
|
</tile>
|
|
</msapplication>
|
|
</browserconfig>';
|
|
|
|
$config_path = $favicon_dir . '/browserconfig.xml';
|
|
file_put_contents($config_path, $xml);
|
|
}
|
|
|
|
/**
|
|
* Output favicon HTML in head
|
|
*/
|
|
add_action('wp_head', 'homeproz_output_favicon_html', 2);
|
|
function homeproz_output_favicon_html() {
|
|
$favicon_source_id = get_field('theme_favicon_source', 'option');
|
|
|
|
if (!$favicon_source_id) {
|
|
return;
|
|
}
|
|
|
|
$paths = homeproz_get_favicon_paths();
|
|
$favicon_dir = $paths['dir'];
|
|
$favicon_url = $paths['url'];
|
|
|
|
// Check if favicons exist
|
|
if (!file_exists($favicon_dir . '/favicon.ico')) {
|
|
return;
|
|
}
|
|
|
|
// Get version for cache busting (use directory modification time)
|
|
$version = filemtime($favicon_dir . '/favicon.ico');
|
|
|
|
echo "\n<!-- Favicons -->\n";
|
|
|
|
// Standard favicons
|
|
echo '<link rel="icon" type="image/x-icon" href="' . esc_url($favicon_url . '/favicon.ico') . '?v=' . $version . '">' . "\n";
|
|
echo '<link rel="icon" type="image/png" sizes="16x16" href="' . esc_url($favicon_url . '/favicon-16x16.png') . '?v=' . $version . '">' . "\n";
|
|
echo '<link rel="icon" type="image/png" sizes="32x32" href="' . esc_url($favicon_url . '/favicon-32x32.png') . '?v=' . $version . '">' . "\n";
|
|
echo '<link rel="icon" type="image/png" sizes="48x48" href="' . esc_url($favicon_url . '/favicon-48x48.png') . '?v=' . $version . '">' . "\n";
|
|
|
|
// Apple Touch Icon
|
|
echo '<link rel="apple-touch-icon" sizes="180x180" href="' . esc_url($favicon_url . '/apple-touch-icon.png') . '?v=' . $version . '">' . "\n";
|
|
|
|
// Android/Chrome
|
|
echo '<link rel="manifest" href="' . esc_url($favicon_url . '/site.webmanifest') . '?v=' . $version . '">' . "\n";
|
|
|
|
// Microsoft
|
|
echo '<meta name="msapplication-TileImage" content="' . esc_url($favicon_url . '/mstile-150x150.png') . '?v=' . $version . '">' . "\n";
|
|
echo '<meta name="msapplication-config" content="' . esc_url($favicon_url . '/browserconfig.xml') . '?v=' . $version . '">' . "\n";
|
|
}
|
|
|
|
/**
|
|
* Disable WordPress Site Icon from Customizer to avoid conflicts
|
|
*/
|
|
add_action('customize_register', 'homeproz_disable_site_icon', 20);
|
|
function homeproz_disable_site_icon($wp_customize) {
|
|
// Remove the site icon control
|
|
$wp_customize->remove_control('site_icon');
|
|
}
|
|
|
|
/**
|
|
* Remove default WordPress site icon output
|
|
*/
|
|
add_action('init', 'homeproz_remove_default_site_icon');
|
|
function homeproz_remove_default_site_icon() {
|
|
// Remove site icon from wp_head
|
|
remove_action('wp_head', 'wp_site_icon', 99);
|
|
}
|
|
|
|
/**
|
|
* Filter to disable site icon in REST API responses
|
|
*/
|
|
add_filter('get_site_icon_url', 'homeproz_filter_site_icon_url', 10, 3);
|
|
function homeproz_filter_site_icon_url($url, $size, $blog_id) {
|
|
$favicon_source_id = get_field('theme_favicon_source', 'option');
|
|
|
|
if ($favicon_source_id) {
|
|
$paths = homeproz_get_favicon_paths();
|
|
|
|
// Return appropriate size
|
|
if ($size <= 16) {
|
|
return $paths['url'] . '/favicon-16x16.png';
|
|
} elseif ($size <= 32) {
|
|
return $paths['url'] . '/favicon-32x32.png';
|
|
} elseif ($size <= 48) {
|
|
return $paths['url'] . '/favicon-48x48.png';
|
|
} elseif ($size <= 150) {
|
|
return $paths['url'] . '/mstile-150x150.png';
|
|
} elseif ($size <= 180) {
|
|
return $paths['url'] . '/apple-touch-icon.png';
|
|
} elseif ($size <= 192) {
|
|
return $paths['url'] . '/android-chrome-192x192.png';
|
|
} else {
|
|
return $paths['url'] . '/android-chrome-512x512.png';
|
|
}
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Admin notice if ImageMagick is not available
|
|
*/
|
|
add_action('admin_notices', 'homeproz_favicon_admin_notice');
|
|
function homeproz_favicon_admin_notice() {
|
|
// Only show on theme options page
|
|
$screen = get_current_screen();
|
|
if (!$screen || $screen->id !== 'toplevel_page_theme-options') {
|
|
return;
|
|
}
|
|
|
|
$convert_path = trim(shell_exec('which convert 2>/dev/null'));
|
|
if (empty($convert_path)) {
|
|
echo '<div class="notice notice-error"><p><strong>Favicon Generation:</strong> ImageMagick is not installed on this server. Favicon generation will not work until ImageMagick is available.</p></div>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Force regenerate favicons (can be called manually or via WP-CLI)
|
|
*/
|
|
function homeproz_regenerate_favicons() {
|
|
$favicon_source_id = get_field('theme_favicon_source', 'option');
|
|
|
|
if (!$favicon_source_id) {
|
|
return false;
|
|
}
|
|
|
|
$source_path = get_attached_file($favicon_source_id);
|
|
|
|
if (!$source_path || !file_exists($source_path)) {
|
|
return false;
|
|
}
|
|
|
|
// Delete version file to force regeneration
|
|
$paths = homeproz_get_favicon_paths();
|
|
@unlink($paths['dir'] . '/.version');
|
|
|
|
return homeproz_generate_favicons($source_path);
|
|
}
|