Files
homeproz/wp-content/themes/homeproz/inc/favicon.php
T
root b6df4dbb92 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>
2026-04-29 15:32:23 +00:00

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);
}