Files
homeproz/wp-content/plugins/mls-by-hansonxyz/mls-by-hansonxyz.php
T
Hanson.xyz Dev 564d556a8c Add US geo data tables, filter bounds API, and URL hash state management
- Add mls_geo_cities and mls_geo_zipcodes tables with 29,880 cities and 33,144 zip codes
- Add get_filter_bounds() method to reposition map when filters don't intersect current view
- Move all URL state (filters, page, scroll, map position) to hash to avoid WordPress 404s
- Add filter bounds AJAX endpoint for map repositioning on filter change

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-17 15:07:33 -06:00

457 lines
13 KiB
PHP

<?php
/**
* Plugin Name: MLS by HansonXyz
* Plugin URI: https://hanson.xyz
* Description: Syncs MLS Grid API data (NorthStar MLS) into local database with CLI tools and public API for themes/plugins.
* Version: 1.0.0
* Author: HansonXyz
* Author URI: https://hanson.xyz
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: mls-by-hansonxyz
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Plugin constants
define('MLS_PLUGIN_VERSION', '1.0.0');
define('MLS_PLUGIN_FILE', __FILE__);
define('MLS_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('MLS_PLUGIN_URL', plugin_dir_url(__FILE__));
define('MLS_PLUGIN_BASENAME', plugin_basename(__FILE__));
define('MLS_DB_VERSION', '1.1.0');
// Database table names (without prefix)
define('MLS_TABLE_PROPERTIES', 'mls_properties');
define('MLS_TABLE_MEDIA', 'mls_media');
define('MLS_TABLE_SYNC_STATE', 'mls_sync_state');
define('MLS_TABLE_RATE_LIMITS', 'mls_rate_limits');
define('MLS_TABLE_SYNC_LOG', 'mls_sync_log');
define('MLS_TABLE_MEDIA_LOG', 'mls_media_log');
define('MLS_TABLE_GEO_CITIES', 'mls_geo_cities');
define('MLS_TABLE_GEO_ZIPCODES', 'mls_geo_zipcodes');
// HomeProz office MLS ID for identifying our listings
define('MLS_HOMEPROZ_OFFICE_ID', 'NST253235');
// Allowed states for MLS queries (MN and IA only)
define('MLS_ALLOWED_STATES', array('MN', 'IA'));
/**
* Main plugin class
*/
final class MLS_Plugin {
/**
* Single instance
*/
private static $instance = null;
/**
* Plugin components
*/
private $db;
private $options;
private $logger;
private $rate_limiter;
private $api_client;
private $sync_engine;
private $media_handler;
private $image_endpoint;
private $query;
private $cluster;
/**
* Get single instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->load_dependencies();
$this->init_hooks();
}
/**
* Load required files
*/
private function load_dependencies() {
// Core classes
require_once MLS_PLUGIN_DIR . 'includes/class-mls-db.php';
require_once MLS_PLUGIN_DIR . 'includes/class-mls-options.php';
require_once MLS_PLUGIN_DIR . 'includes/class-mls-logger.php';
require_once MLS_PLUGIN_DIR . 'includes/class-mls-rate-limiter.php';
require_once MLS_PLUGIN_DIR . 'includes/class-mls-api-client.php';
require_once MLS_PLUGIN_DIR . 'includes/class-mls-sync-engine.php';
require_once MLS_PLUGIN_DIR . 'includes/class-mls-media-handler.php';
require_once MLS_PLUGIN_DIR . 'includes/class-mls-image-endpoint.php';
require_once MLS_PLUGIN_DIR . 'includes/class-mls-query.php';
require_once MLS_PLUGIN_DIR . 'includes/class-mls-cluster.php';
// Activation/Deactivation
require_once MLS_PLUGIN_DIR . 'includes/class-mls-activator.php';
require_once MLS_PLUGIN_DIR . 'includes/class-mls-deactivator.php';
// Admin
if (is_admin()) {
require_once MLS_PLUGIN_DIR . 'admin/class-mls-admin.php';
}
// WP-CLI
if (defined('WP_CLI') && WP_CLI) {
require_once MLS_PLUGIN_DIR . 'cli/class-mls-cli.php';
}
}
/**
* Initialize hooks
*/
private function init_hooks() {
// Activation/Deactivation hooks
register_activation_hook(MLS_PLUGIN_FILE, array('MLS_Activator', 'activate'));
register_deactivation_hook(MLS_PLUGIN_FILE, array('MLS_Deactivator', 'deactivate'));
// Initialize components after plugins loaded
add_action('plugins_loaded', array($this, 'init_components'));
// Check for database updates
add_action('plugins_loaded', array($this, 'check_db_updates'));
}
/**
* Initialize plugin components
*/
public function init_components() {
$this->db = new MLS_DB();
$this->options = new MLS_Options();
$this->logger = new MLS_Logger($this->db);
$this->rate_limiter = new MLS_Rate_Limiter($this->db);
$this->api_client = new MLS_API_Client($this->options, $this->rate_limiter, $this->logger);
$this->media_handler = new MLS_Media_Handler($this->db, $this->logger);
$this->image_endpoint = new MLS_Image_Endpoint($this->media_handler, $this->logger);
$this->image_endpoint->init();
$this->sync_engine = new MLS_Sync_Engine(
$this->db,
$this->api_client,
$this->media_handler,
$this->logger
);
$this->query = new MLS_Query($this->db);
$this->cluster = new MLS_Cluster($this->db);
// Register AJAX handlers
add_action('wp_ajax_mls_get_clusters', array($this, 'ajax_get_clusters'));
add_action('wp_ajax_nopriv_mls_get_clusters', array($this, 'ajax_get_clusters'));
// Initialize admin
if (is_admin()) {
new MLS_Admin($this);
}
// Initialize CLI
if (defined('WP_CLI') && WP_CLI) {
MLS_CLI::register($this);
}
}
/**
* Check and run database updates
*/
public function check_db_updates() {
$current_version = get_option('mls_db_version', '0');
if (version_compare($current_version, MLS_DB_VERSION, '<')) {
MLS_Activator::create_tables();
update_option('mls_db_version', MLS_DB_VERSION);
}
}
/**
* Get DB instance
*/
public function get_db() {
return $this->db;
}
/**
* Get Options instance
*/
public function get_options() {
return $this->options;
}
/**
* Get Logger instance
*/
public function get_logger() {
return $this->logger;
}
/**
* Get Rate Limiter instance
*/
public function get_rate_limiter() {
return $this->rate_limiter;
}
/**
* Get API Client instance
*/
public function get_api_client() {
return $this->api_client;
}
/**
* Get Sync Engine instance
*/
public function get_sync_engine() {
return $this->sync_engine;
}
/**
* Get Media Handler instance
*/
public function get_media_handler() {
return $this->media_handler;
}
/**
* Get Image Endpoint instance
*/
public function get_image_endpoint() {
return $this->image_endpoint;
}
/**
* Get Query instance
*/
public function get_query() {
return $this->query;
}
/**
* Get Cluster instance
*/
public function get_cluster() {
return $this->cluster;
}
/**
* AJAX handler for getting map clusters
*/
public function ajax_get_clusters() {
// Parse input
$zoom = isset($_REQUEST['zoom']) ? (int) $_REQUEST['zoom'] : 10;
$bounds = isset($_REQUEST['bounds']) ? array_map('floatval', (array) $_REQUEST['bounds']) : null;
$status = isset($_REQUEST['status']) ? sanitize_text_field($_REQUEST['status']) : 'Active';
$property_type = isset($_REQUEST['property_type']) ? sanitize_text_field($_REQUEST['property_type']) : null;
$city = isset($_REQUEST['city']) ? sanitize_text_field($_REQUEST['city']) : null;
$min_price = isset($_REQUEST['min_price']) ? (int) $_REQUEST['min_price'] : null;
$max_price = isset($_REQUEST['max_price']) ? (int) $_REQUEST['max_price'] : null;
$min_beds = isset($_REQUEST['min_beds']) ? (int) $_REQUEST['min_beds'] : null;
$args = array(
'zoom' => $zoom,
'bounds' => $bounds,
'status' => $status,
'property_type' => $property_type ?: null,
'city' => $city ?: null,
'min_price' => $min_price ?: null,
'max_price' => $max_price ?: null,
'min_beds' => $min_beds ?: null,
);
$result = $this->cluster->get_clusters($args);
wp_send_json_success($result);
}
}
/**
* Initialize the plugin
*/
function mls_plugin() {
return MLS_Plugin::get_instance();
}
// Start the plugin
add_action('plugins_loaded', 'mls_plugin', 0);
/**
* Global helper functions for themes/plugins
*/
/**
* Get MLS properties
*
* @param array $args Query arguments
* @return array Array of property objects
*/
function mls_get_properties($args = array()) {
$plugin = mls_plugin();
if (!$plugin->get_query()) {
return array();
}
return $plugin->get_query()->get_properties($args);
}
/**
* Get a single MLS property
*
* @param string $identifier Listing key or MLS ID
* @return object|null Property object or null
*/
function mls_get_property($identifier) {
$plugin = mls_plugin();
if (!$plugin->get_query()) {
return null;
}
return $plugin->get_query()->get_property($identifier);
}
/**
* Get media for a listing
*
* @param string $listing_key The listing key
* @return array Array of media objects
*/
function mls_get_property_media($listing_key) {
$plugin = mls_plugin();
if (!$plugin->get_query()) {
return array();
}
return $plugin->get_query()->get_property_media($listing_key);
}
/**
* Get primary image URL for a listing (on-demand fetching)
*
* Images are fetched from MLS Grid and cached locally on first request.
* Per MLS Grid rules, images must be served from our own server.
*
* @param string $listing_key The listing key
* @param bool $fetch_if_missing Whether to fetch from MLS Grid if not cached (default: true)
* @return string|null Image URL or null
*/
function mls_get_property_image($listing_key, $fetch_if_missing = true) {
$plugin = mls_plugin();
if (!$plugin->get_media_handler()) {
return null;
}
return $plugin->get_media_handler()->get_primary_image($listing_key, $fetch_if_missing);
}
/**
* Get distinct cities with listings
*
* @param string|null $status Optional status filter
* @return array Array of city names
*/
function mls_get_cities($status = null) {
$plugin = mls_plugin();
if (!$plugin->get_query()) {
return array();
}
return $plugin->get_query()->get_distinct_cities($status);
}
/**
* Check if MLS data is available
*
* @return bool True if synced data exists
*/
function mls_is_available() {
$plugin = mls_plugin();
if (!$plugin->get_query()) {
return false;
}
return $plugin->get_query()->has_data();
}
/**
* Get property count
*
* @param array $args Optional filter arguments
* @return int Property count
*/
function mls_get_property_count($args = array()) {
$plugin = mls_plugin();
if (!$plugin->get_query()) {
return 0;
}
return $plugin->get_query()->get_count($args);
}
/**
* Get geographic bounds for filtered properties
*
* @param array $args Filter arguments
* @return array|null Bounds with sw_lat, sw_lng, ne_lat, ne_lng
*/
function mls_get_filter_bounds($args = array()) {
$plugin = mls_plugin();
if (!$plugin->get_query()) {
return null;
}
return $plugin->get_query()->get_filter_bounds($args);
}
/**
* Get all images for a listing (on-demand fetching)
*
* Returns all media records with local_url populated where cached.
* Can optionally fetch first N uncached images on-demand.
*
* @param string $listing_key The listing key
* @param int $fetch_limit Max images to fetch on-demand (default: 1, 0 = none)
* @return array Array of media objects
*/
function mls_get_property_images($listing_key, $fetch_limit = 1) {
$plugin = mls_plugin();
if (!$plugin->get_media_handler()) {
return array();
}
return $plugin->get_media_handler()->get_listing_images($listing_key, $fetch_limit);
}
/**
* Get media cache statistics
*
* @return array Stats with total_media, cached, uncached counts
*/
function mls_get_cache_stats() {
$plugin = mls_plugin();
if (!$plugin->get_media_handler()) {
return array('total_media' => 0, 'cached' => 0, 'uncached' => 0);
}
return $plugin->get_media_handler()->get_cache_stats();
}
/**
* Get WebP thumbnail URL for a listing image
*
* Returns a URL to the WebP thumbnail endpoint which will:
* - Fetch the image from MLS Grid if not cached
* - Convert to WebP format using ImageMagick
* - Resize to requested dimension (800px thumb or 1800px full)
* - Cache the result for future requests
*
* @param string $listing_key The listing key
* @param int $index Image index (1-based, default: 1 for primary image)
* @param string $size 'thumb' (800px) or 'full' (1800px), default: 'thumb'
* @return string Image URL
*/
function mls_get_image_url($listing_key, $index = 1, $size = 'thumb') {
return MLS_Image_Endpoint::get_url($listing_key, $index, $size);
}