['aiw_directory_item', 'aiwest_directory_item', 'aiwest_directory_ite'], self::LEAD_CPT => ['aiw_directory_lead', 'aiwest_directory_lead', 'aiwest_directory_lea'], ]; foreach ($migrations as $new_type => $legacy_types) { foreach ($legacy_types as $legacy_type) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time migration of plugin-owned post type keys. $wpdb->update( $wpdb->posts, ['post_type' => $new_type], ['post_type' => $legacy_type], ['%s'], ['%s'] ); } } update_option(self::MIGRATION_OPTION, 'yes', false); update_option('aiwest_1615_directory_post_types_migrated', 'yes', false); } /** * Preserve the former partner directory behavior on upgraded sites while * giving clean installations a useful single-business starting mode. * Existing records without a type are treated as partner records. */ public static function migrate_directory_mode() { if (get_option(self::MODE_MIGRATION_OPTION, '') === 'yes') return; $record_ids = get_posts([ 'post_type' => self::CPT, 'post_status' => ['publish', 'draft', 'pending', 'private'], 'posts_per_page' => 500, 'fields' => 'ids', 'no_found_rows' => true, ]); foreach ($record_ids as $post_id) { if (get_post_meta((int) $post_id, '_aiwest_directory_record_type', true) === '') { update_post_meta((int) $post_id, '_aiwest_directory_record_type', 'partner'); } } if (get_option('aiwest_directory_mode', '') === '') { update_option('aiwest_directory_mode', $record_ids ? 'partner_directory' : 'business_location'); } update_option(self::MODE_MIGRATION_OPTION, 'yes', false); } public static function maybe_enqueue_frontend_assets() { if (!is_singular()) return; $post = get_post(); if (!$post) return; $content = (string) $post->post_content; if (!has_shortcode($content, 'aiwest_directory_map') && !has_shortcode($content, 'aiwest_company_map')) return; self::enqueue_frontend_assets(); } protected static function enqueue_frontend_assets() { wp_enqueue_style( 'aiwest-leaflet', AIWEST_URL . 'assets/vendor/leaflet/leaflet.css', [], '1.9.4' ); wp_enqueue_style( 'aiwest-directory-map', AIWEST_URL . 'assets/directory-map.css', ['aiwest-leaflet'], AIWEST_VERSION ); wp_enqueue_script( 'aiwest-leaflet', AIWEST_URL . 'assets/vendor/leaflet/leaflet.js', [], '1.9.4', true ); wp_enqueue_script( 'aiwest-directory-map', AIWEST_URL . 'assets/directory-map.js', ['aiwest-leaflet'], AIWEST_VERSION, true ); } /** * Replace the retired directory status name without changing directory * records, visibility or recommendation priority. */ public static function migrate_legacy_directory_status() { if (!current_user_can('manage_options')) return; if (get_option('aiwest_166_directory_status_migrated', '') === 'yes') return; // One-time bounded migration query for the plugin's directory status metadata. $legacy_items = get_posts([ 'post_type' => self::CPT, 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- One-time migration of plugin-owned directory metadata. 'meta_key' => '_aiwest_directory_status', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- One-time migration of the retired status value. 'meta_value' => 'premium', ]); foreach ($legacy_items as $post_id) { update_post_meta((int) $post_id, '_aiwest_directory_status', 'featured'); } update_option('aiwest_166_directory_status_migrated', 'yes', false); } protected static function normalize_directory_status($status) { return $status === 'premium' ? 'featured' : $status; } public static function labels() { return [ 'directory_name' => sanitize_text_field((string) get_option('aiwest_directory_name', __('Locations & Partners Map', 'ai-webmaster-studio'))), 'item_singular' => sanitize_text_field((string) get_option('aiwest_directory_item_singular', __('Location', 'ai-webmaster-studio'))), 'item_plural' => sanitize_text_field((string) get_option('aiwest_directory_item_plural', __('Locations & Partners', 'ai-webmaster-studio'))), 'button_text' => sanitize_text_field((string) get_option('aiwest_directory_button_text', __('Find with AI', 'ai-webmaster-studio'))), 'industry_context' => get_option('aiwest_directory_industry_context', ''), 'map_provider' => get_option('aiwest_directory_map_provider', 'osm'), 'google_api_key' => get_option('aiwest_directory_google_api_key', ''), 'recommendation_source' => get_option('aiwest_directory_recommendation_source', 'catalog_only'), 'show_header' => get_option('aiwest_directory_show_header', 'yes'), 'map_page_url' => '/ai-directory-map/', 'map_description' => sanitize_textarea_field((string) get_option('aiwest_directory_map_description', __('Find our business location, branches or approved partners on the map.', 'ai-webmaster-studio'))), 'mode' => self::sanitize_mode(get_option('aiwest_directory_mode', 'business_location')), 'layout' => self::sanitize_layout(get_option('aiwest_directory_layout', 'map_above')), 'width' => self::sanitize_width(get_option('aiwest_directory_width', 'wide')), 'height' => self::sanitize_height(get_option('aiwest_directory_height', '650')), 'split_ratio' => self::sanitize_split_ratio(get_option('aiwest_directory_split_ratio', '70_30')), 'directions_provider' => self::sanitize_directions_provider(get_option('aiwest_directory_directions_provider', 'google')), 'lead_button_text' => self::lead_button_label(), 'lead_flow_mode' => get_option('aiwest_directory_lead_flow_mode', 'send_immediately'), 'email_from_name' => get_option('aiwest_directory_email_from_name', self::default_email_from_name()), 'email_from_email' => get_option('aiwest_directory_email_from_email', self::default_email_from_email()), ]; } public static function modes() { return [ 'business_location' => __('Business Location', 'ai-webmaster-studio'), 'business_locations' => __('Business Locations', 'ai-webmaster-studio'), 'partner_directory' => __('Partner Directory', 'ai-webmaster-studio'), 'company_partners' => __('Company + Partners', 'ai-webmaster-studio'), ]; } protected static function sanitize_mode($value) { $value = sanitize_key((string) $value); return array_key_exists($value, self::modes()) ? $value : 'business_location'; } protected static function sanitize_layout($value) { $value = sanitize_key((string) $value); return in_array($value, ['map_above', 'split', 'map_only', 'results_only'], true) ? $value : 'map_above'; } protected static function sanitize_width($value) { $value = sanitize_key((string) $value); return in_array($value, ['content', 'wide', 'full'], true) ? $value : 'wide'; } protected static function sanitize_height($value) { $height = absint($value); return (string) max(320, min(1000, $height ?: 650)); } protected static function sanitize_split_ratio($value) { $value = sanitize_key((string) $value); return in_array($value, ['70_30', '60_40', '50_50'], true) ? $value : '70_30'; } protected static function sanitize_directions_provider($value) { $value = sanitize_key((string) $value); return in_array($value, ['google', 'osm', 'apple'], true) ? $value : 'google'; } protected static function mode_uses_partners($mode) { return in_array(self::sanitize_mode($mode), ['partner_directory', 'company_partners'], true); } protected static function mode_uses_business($mode) { return in_array(self::sanitize_mode($mode), ['business_location', 'business_locations', 'company_partners'], true); } protected static function lead_button_label() { $label = get_option('aiwest_directory_lead_button_text', 'Generate Inquiry'); $label = is_string($label) ? trim($label) : ''; $legacy = ['Wyślij zapytanie do wybranych', 'Stwórz / wygeneruj zapytanie do wybranych', 'Wygeneruj zapytanie do wybranych']; if ($label === '' || in_array($label, $legacy, true)) { return __('Generate Inquiry', 'ai-webmaster-studio'); } return $label; } protected static function default_email_from_name() { $name = wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES); $name = trim((string)$name); return $name !== '' ? $name : 'AI Webmaster Studio'; } protected static function default_email_from_email() { $host = wp_parse_url(home_url(), PHP_URL_HOST); $host = is_string($host) ? preg_replace('/^www\./i', '', strtolower($host)) : ''; if ($host && strpos($host, '.') !== false) return 'contact@' . $host; $admin = get_option('admin_email'); return is_email($admin) ? $admin : ''; } protected static function email_headers($reply_to = '') { $name = get_option('aiwest_directory_email_from_name', self::default_email_from_name()); $email = get_option('aiwest_directory_email_from_email', self::default_email_from_email()); $name = trim(wp_specialchars_decode((string)$name, ENT_QUOTES)); $email = sanitize_email($email); if ($name === '') $name = self::default_email_from_name(); if (!$email || !is_email($email)) $email = self::default_email_from_email(); $headers = ['Content-Type: text/plain; charset=UTF-8']; if ($email && is_email($email)) $headers[] = 'From: ' . $name . ' <' . $email . '>'; $reply_to = sanitize_email($reply_to); if ($reply_to && is_email($reply_to)) $headers[] = 'Reply-To: ' . $reply_to; return $headers; } public static function active_provider() { $labels = self::labels(); if ($labels['map_provider'] === 'google' && !empty($labels['google_api_key'])) { return 'google'; } return 'osm'; } protected static function admin_page_url($args = []) { $base = admin_url('admin.php'); $defaults = [ 'page' => 'aiwest-site-builder', 'tab' => 'directory-map', ]; return add_query_arg(array_merge($defaults, (array) $args), $base); } public static function register_content_types() { $labels = self::labels(); register_post_type(self::CPT, [ 'labels' => [ 'name' => $labels['item_plural'], 'singular_name' => $labels['item_singular'], 'add_new_item' => __('Add Map Location or Partner', 'ai-webmaster-studio'), 'edit_item' => __('Edit Map Location or Partner', 'ai-webmaster-studio'), 'new_item' => __('New Map Location or Partner', 'ai-webmaster-studio'), 'view_item' => __('View Map Record', 'ai-webmaster-studio'), 'search_items' => __('Search Map Records', 'ai-webmaster-studio'), 'not_found' => __('No map records found', 'ai-webmaster-studio'), ], 'public' => false, 'show_ui' => true, 'show_in_menu' => false, 'show_in_rest' => true, 'supports' => ['title', 'editor', 'thumbnail', 'excerpt'], 'capability_type' => 'post', 'map_meta_cap' => true, ]); register_post_type(self::LEAD_CPT, [ 'labels' => [ 'name' => __('Directory Inquiries', 'ai-webmaster-studio'), 'singular_name' => __('Directory Inquiry', 'ai-webmaster-studio'), 'edit_item' => __('View Inquiry', 'ai-webmaster-studio'), 'search_items' => __('Search Inquiries', 'ai-webmaster-studio'), 'not_found' => __('No inquiries found', 'ai-webmaster-studio'), ], 'public' => false, 'show_ui' => true, 'show_in_menu' => false, 'supports' => ['title', 'editor'], 'capability_type' => 'post', 'map_meta_cap' => true, ]); register_taxonomy(self::TAX_CATEGORY, [self::CPT], [ 'label' => __('Directory Categories', 'ai-webmaster-studio'), 'public' => false, 'show_ui' => true, 'show_in_rest' => true, 'hierarchical' => true, ]); register_taxonomy(self::TAX_REGION, [self::CPT], [ 'label' => __('Regions / Areas', 'ai-webmaster-studio'), 'public' => false, 'show_ui' => true, 'show_in_rest' => true, 'hierarchical' => true, ]); register_taxonomy(self::TAX_SERVICE, [self::CPT], [ 'label' => __('Services / Needs', 'ai-webmaster-studio'), 'public' => false, 'show_ui' => true, 'show_in_rest' => true, 'hierarchical' => false, ]); } public static function add_meta_boxes() { add_meta_box('aiwest_directory_location', __('AI Webmaster Studio — Map and Matching Data', 'ai-webmaster-studio'), [__CLASS__, 'render_meta_box'], self::CPT, 'normal', 'high'); add_meta_box('aiwest_directory_lead_details', __('AI Webmaster Studio — Inquiry Details', 'ai-webmaster-studio'), [__CLASS__, 'render_lead_meta_box'], self::LEAD_CPT, 'normal', 'high'); } public static function render_meta_box($post) { wp_nonce_field('aiwest_directory_meta', 'aiwest_directory_meta_nonce'); $fields = [ 'address' => [__('Address', 'ai-webmaster-studio'), __('e.g. 10 Example Street', 'ai-webmaster-studio')], 'city' => [__('City', 'ai-webmaster-studio'), __('e.g. London', 'ai-webmaster-studio')], 'region' => [__('Region / state / area', 'ai-webmaster-studio'), __('e.g. Greater London', 'ai-webmaster-studio')], 'country' => [__('Country', 'ai-webmaster-studio'), __('e.g. United Kingdom', 'ai-webmaster-studio')], 'lat' => [__('Latitude', 'ai-webmaster-studio'), '54.352025'], 'lng' => [__('Longitude', 'ai-webmaster-studio'), '18.646638'], 'phone' => [__('Phone', 'ai-webmaster-studio'), ''], 'email' => [__('Email', 'ai-webmaster-studio'), ''], 'website' => [__('Website', 'ai-webmaster-studio'), 'https://...'], 'opening_hours' => [__('Opening hours', 'ai-webmaster-studio'), __('e.g. Monday–Friday 09:00–17:00', 'ai-webmaster-studio')], 'marker_label' => [__('Marker label', 'ai-webmaster-studio'), __('Optional short map label', 'ai-webmaster-studio')], 'service_area' => [__('Service area', 'ai-webmaster-studio'), __('e.g. London + 80 km', 'ai-webmaster-studio')], 'features' => [__('Features / services / specialties', 'ai-webmaster-studio'), __('e.g. modular homes, fast delivery, full-service execution', 'ai-webmaster-studio')], 'admin_note' => [__('Administrator note', 'ai-webmaster-studio'), __('Internal note not visible to users', 'ai-webmaster-studio')], 'priority' => [__('Recommendation priority 0–100', 'ai-webmaster-studio'), '70'], ]; echo '
'; foreach ($fields as $key => $def) { [$label, $placeholder] = $def; $value = get_post_meta($post->ID, '_aiwest_directory_' . $key, true); $type = in_array($key, ['lat','lng','priority'], true) ? 'number' : 'text'; $is_coordinate = in_array($key, ['lat','lng'], true); $is_wide = in_array($key, ['features','admin_note','service_area','opening_hours'], true); if ($is_wide) echo '

'; else echo '

'; echo '
'; if ($is_wide) { echo ''; } else { echo ''; } echo '

'; } $record_type = get_post_meta($post->ID, '_aiwest_directory_record_type', true) ?: 'partner'; echo '


'; $status = self::normalize_directory_status(get_post_meta($post->ID, '_aiwest_directory_status', true) ?: 'active'); $visible = get_post_meta($post->ID, '_aiwest_directory_visible', true) ?: 'yes'; echo '


'; echo '


'; echo '
'; echo '

' . esc_html(__('This is one business location, branch or partner record. Only published and visible records are shown publicly.', 'ai-webmaster-studio')) . '

'; } public static function render_lead_meta_box($post) { wp_nonce_field('aiwest_directory_lead_meta', 'aiwest_directory_lead_meta_nonce'); $email = get_post_meta($post->ID, '_aiwest_lead_user_email', true); $name = get_post_meta($post->ID, '_aiwest_lead_user_name', true); $phone = get_post_meta($post->ID, '_aiwest_lead_user_phone', true); $location = get_post_meta($post->ID, '_aiwest_lead_location', true); $need = get_post_meta($post->ID, '_aiwest_lead_need', true); $partners = get_post_meta($post->ID, '_aiwest_lead_partners', true); $sent_at = get_post_meta($post->ID, '_aiwest_lead_sent_at', true); $status = get_post_meta($post->ID, '_aiwest_lead_status', true) ?: 'new'; $sent_to_partners = (int)get_post_meta($post->ID, '_aiwest_lead_sent_to_partners', true); $flow_mode = get_post_meta($post->ID, '_aiwest_lead_flow_mode', true); echo '
'; echo '

' . esc_html(__('Lead status:', 'ai-webmaster-studio')) . '

'; echo '

' . esc_html(__('Name / contact:', 'ai-webmaster-studio')) . ' ' . esc_html($name ?: '—') . '

'; echo '

' . esc_html(__('User email:', 'ai-webmaster-studio')) . ' ' . esc_html($email) . '

'; echo '

' . esc_html(__('Phone:', 'ai-webmaster-studio')) . ' ' . esc_html($phone ?: '—') . '

'; echo '

' . esc_html(__('Location:', 'ai-webmaster-studio')) . ' ' . esc_html($location) . '

'; echo '

' . esc_html(__('Need:', 'ai-webmaster-studio')) . ' ' . esc_html($need) . '

'; echo '

' . esc_html(__('Processing mode:', 'ai-webmaster-studio')) . ' ' . esc_html($flow_mode ?: '—') . '

'; echo '

' . esc_html(__('Sent / saved:', 'ai-webmaster-studio')) . ' ' . esc_html($sent_at) . '

'; echo '

' . esc_html(__('Messages sent to partners:', 'ai-webmaster-studio')) . ' ' . esc_html((string)$sent_to_partners) . '

'; if (is_array($partners)) { echo '

' . esc_html(__('Selected partners', 'ai-webmaster-studio')) . '

'; } echo '

' . esc_html(__('Status supports manual workflow handling: new inquiry, in progress, sent, completed or rejected.', 'ai-webmaster-studio')) . '

'; echo '
'; } protected static function lead_statuses() { return [ 'new' => __('New', 'ai-webmaster-studio'), 'admin_review' => __('Waiting for administrator approval', 'ai-webmaster-studio'), 'sent' => __('Sent to partners', 'ai-webmaster-studio'), 'in_progress' => __('In progress', 'ai-webmaster-studio'), 'completed' => __('Completed', 'ai-webmaster-studio'), 'rejected' => __('Rejected', 'ai-webmaster-studio'), 'no_partner_email' => __('No partner emails', 'ai-webmaster-studio'), ]; } public static function save_lead_meta($post_id, $post) { if (!wp_verify_nonce(AIWEST_Security::post_text('aiwest_directory_lead_meta_nonce'), 'aiwest_directory_lead_meta')) return; if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if (!current_user_can('edit_post', $post_id)) return; $status = AIWEST_Security::post_key('aiwest_lead_status', 'new'); $allowed = array_keys(self::lead_statuses()); update_post_meta($post_id, '_aiwest_lead_status', in_array($status, $allowed, true) ? $status : 'new'); } public static function save_meta($post_id, $post) { if (!wp_verify_nonce(AIWEST_Security::post_text('aiwest_directory_meta_nonce'), 'aiwest_directory_meta')) return; if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if (!current_user_can('edit_post', $post_id)) return; $keys = ['address','city','region','country','lat','lng','phone','email','website','opening_hours','marker_label','service_area','features','admin_note','priority','status','visible','record_type']; foreach ($keys as $key) { $field = 'aiwest_directory_' . $key; if (!AIWEST_Security::post_has($field)) continue; $raw = in_array($key, ['features','admin_note','service_area','opening_hours'], true) ? AIWEST_Security::post_textarea($field) : AIWEST_Security::post_text($field); if (in_array($key, ['features','admin_note','service_area','opening_hours'], true)) $value = $raw; elseif ($key === 'email') $value = sanitize_email($raw); elseif ($key === 'website') $value = esc_url_raw($raw); elseif (in_array($key, ['lat','lng'], true)) $value = is_numeric($raw) ? (string)floatval($raw) : ''; elseif ($key === 'priority') $value = (string)max(0, min(100, intval($raw))); elseif ($key === 'status') $value = in_array($raw, ['draft','active','partner','featured','paused'], true) ? $raw : 'active'; elseif ($key === 'visible') $value = $raw === 'no' ? 'no' : 'yes'; elseif ($key === 'record_type') $value = in_array($raw, ['primary_business','branch','partner'], true) ? $raw : 'partner'; else $value = sanitize_text_field($raw); update_post_meta($post_id, '_aiwest_directory_' . $key, $value); } } public static function admin_page($embedded = false) { if (!current_user_can('manage_options')) return; $embedded = (bool) $embedded; $labels = self::labels(); $items_url = admin_url('edit.php?post_type=' . self::CPT); $add_url = admin_url('post-new.php?post_type=' . self::CPT); $settings_saved = (bool) AIWEST_Security::get_key('aiwest_directory_saved'); $seeded = (bool) AIWEST_Security::get_key('aiwest_directory_seeded'); $business_draft = (bool) AIWEST_Security::get_key('aiwest_directory_business_draft'); $demo_deleted = (bool) AIWEST_Security::get_key('aiwest_directory_demo_deleted'); $cleaned = (bool) AIWEST_Security::get_key('aiwest_directory_cleaned'); $page_created = (bool) (AIWEST_Security::get_key('aiwest_directory_page_created') || AIWEST_Security::get_key('aiwest_directory_page_repaired')); $directory_error = AIWEST_Security::get_text('aiwest_directory_error'); ?>

[aiwest_directory_map]

rawurlencode($page_id->get_error_message())])); exit; } wp_safe_redirect(self::admin_page_url(['aiwest_directory_page_repaired' => '1'])); exit; } public static function canonical_map_slug() { return 'ai-directory-map'; } public static function resolve_map_page($slug = '') { $slug = sanitize_title($slug ?: self::canonical_map_slug()); $stored_id = absint(get_option(self::PAGE_ID_OPTION, 0)); if ($stored_id) { $stored = get_post($stored_id); if ($stored instanceof WP_Post && $stored->post_type === 'page' && $stored->post_status !== 'trash' && $stored->post_name === $slug) { return $stored; } } $setting_id = self::map_page_id_from_setting(get_option('aiwest_directory_map_page_url', '')); if ($setting_id) { $setting_page = get_post($setting_id); if ($setting_page instanceof WP_Post && $setting_page->post_type === 'page' && $setting_page->post_status !== 'trash' && $setting_page->post_name === $slug) { return $setting_page; } } $page = get_page_by_path($slug, OBJECT, 'page'); if ($page instanceof WP_Post && $page->post_status !== 'trash') return $page; $pages = get_posts([ 'post_type' => 'page', 'post_status' => ['publish', 'draft', 'pending', 'private', 'future'], 'name' => $slug, 'posts_per_page' => 1, 'no_found_rows' => true, ]); return $pages ? $pages[0] : null; } public static function ensure_map_page($repair = true) { $labels = self::labels(); $slug = self::canonical_map_slug(); $page = self::resolve_map_page($slug); $title = trim((string) ($labels['directory_name'] ?? '')) ?: __('Locations & Partners Map', 'ai-webmaster-studio'); if ($page instanceof WP_Post) { $page_id = (int) $page->ID; if ($repair) { $content = trim((string) $page->post_content); if (!has_shortcode($content, 'aiwest_directory_map')) { $content = $content === '' ? self::build_map_page_content($labels) : $content . "\n\n[aiwest_directory_map show_header=\"no\"]"; } $content = self::normalize_map_page_content($content); $update = ['ID' => $page_id]; if ($content !== (string) $page->post_content) { $update['post_content'] = $content; } if (self::is_legacy_generated_map_title((string) $page->post_title)) { $update['post_title'] = $title; } if (count($update) > 1) { $updated = wp_update_post($update, true); if (is_wp_error($updated)) return $updated; } } } else { $page_id = wp_insert_post([ 'post_type' => 'page', 'post_status' => 'draft', 'post_title' => $title, 'post_name' => $slug, 'post_content' => self::build_map_page_content($labels), ], true); if (is_wp_error($page_id)) return $page_id; } $post = get_post((int) $page_id); if (!$post instanceof WP_Post) { return new WP_Error('aiwest_directory_map_page_missing', __('The map page could not be resolved after creation.', 'ai-webmaster-studio')); } update_option(self::PAGE_ID_OPTION, (int) $post->ID, false); update_option('aiwest_directory_map_page_url', '/' . $post->post_name . '/'); if (class_exists('AIWEST_Search_Optimization')) { AIWEST_Search_Optimization::finalize((int) $post->ID, (string) $post->post_content, [ 'post_type' => 'page', 'post_title' => (string) $post->post_title, ]); } return (int) $post->ID; } protected static function build_map_page_content($labels = []) { return '[aiwest_directory_map show_header="no"]'; } protected static function is_legacy_generated_map_title($title) { $title = strtolower(trim(wp_strip_all_tags((string) $title))); return in_array($title, ['directory map', 'ai directory map'], true); } protected static function normalize_map_page_content($content) { $content = (string) $content; // Remove only the exact wrapper generated by earlier plugin releases. $content = preg_replace( '/]*>.*?<\/section>\s*/is', '', $content ); $content = preg_replace_callback( '/\[(aiwest_directory_map|aiwest_company_map)([^\]]*)\]/i', static function($matches) { $tag = strtolower((string) $matches[1]); $attributes = trim((string) $matches[2]); if (preg_match('/\bshow_header\s*=\s*(["\']).*?\1/i', $attributes)) { $attributes = preg_replace('/\bshow_header\s*=\s*(["\']).*?\1/i', 'show_header="no"', $attributes); } else { $attributes = trim($attributes . ' show_header="no"'); } return '[' . $tag . ($attributes !== '' ? ' ' . $attributes : '') . ']'; }, $content ); $content = preg_replace( '/^\s*\s*(\[(?:aiwest_directory_map|aiwest_company_map)[^\]]*\])\s*<\/div>\s*$/is', '$1', (string) $content ); return trim((string) $content); } public static function migrate_map_page_presentation() { if (!current_user_can('manage_options')) return; if (get_option(self::PRESENTATION_MIGRATION_OPTION, '') === 'yes') return; $page = self::resolve_map_page(); if ($page instanceof WP_Post) { $labels = self::labels(); $title = trim((string) ($labels['directory_name'] ?? '')) ?: __('Locations & Partners Map', 'ai-webmaster-studio'); $content = self::normalize_map_page_content((string) $page->post_content); $update = ['ID' => (int) $page->ID]; if ($content !== (string) $page->post_content) { $update['post_content'] = $content; } if (self::is_legacy_generated_map_title((string) $page->post_title)) { $update['post_title'] = $title; } if (count($update) > 1) { wp_update_post($update); } } update_option(self::PRESENTATION_MIGRATION_OPTION, 'yes', false); } public static function save_settings() { if (!current_user_can('manage_options')) wp_die(esc_html(__('Insufficient permissions.', 'ai-webmaster-studio'))); check_admin_referer('aiwest_directory_save_settings'); update_option('aiwest_directory_name', AIWEST_Security::post_text('aiwest_directory_name', 'Locations & Partners Map')); update_option('aiwest_directory_item_singular', AIWEST_Security::post_text('aiwest_directory_item_singular', 'Location')); update_option('aiwest_directory_item_plural', AIWEST_Security::post_text('aiwest_directory_item_plural', 'Locations & Partners')); update_option('aiwest_directory_button_text', AIWEST_Security::post_text('aiwest_directory_button_text', 'Find with AI')); update_option('aiwest_directory_lead_button_text', AIWEST_Security::post_text('aiwest_directory_lead_button_text', 'Generate an inquiry for selected partners')); update_option('aiwest_directory_industry_context', AIWEST_Security::post_textarea('aiwest_directory_industry_context')); update_option('aiwest_directory_map_description', AIWEST_Security::post_textarea('aiwest_directory_map_description')); update_option('aiwest_directory_google_api_key', AIWEST_Security::post_text('aiwest_directory_google_api_key')); update_option('aiwest_directory_map_page_url', '/ai-directory-map/'); update_option('aiwest_directory_mode', self::sanitize_mode(AIWEST_Security::post_key('aiwest_directory_mode', 'business_location'))); update_option('aiwest_directory_layout', self::sanitize_layout(AIWEST_Security::post_key('aiwest_directory_layout', 'map_above'))); update_option('aiwest_directory_width', self::sanitize_width(AIWEST_Security::post_key('aiwest_directory_width', 'wide'))); update_option('aiwest_directory_height', self::sanitize_height(AIWEST_Security::post_text('aiwest_directory_height', '650'))); update_option('aiwest_directory_split_ratio', self::sanitize_split_ratio(AIWEST_Security::post_key('aiwest_directory_split_ratio', '70_30'))); update_option('aiwest_directory_directions_provider', self::sanitize_directions_provider(AIWEST_Security::post_key('aiwest_directory_directions_provider', 'google'))); $provider = AIWEST_Security::post_key('aiwest_directory_map_provider', 'osm'); update_option('aiwest_directory_map_provider', in_array($provider, ['osm','google'], true) ? $provider : 'osm'); $source = AIWEST_Security::post_key('aiwest_directory_recommendation_source', 'catalog_only'); update_option('aiwest_directory_recommendation_source', in_array($source, ['catalog_only','catalog_first_public_allowed'], true) ? $source : 'catalog_only'); $lead_flow = AIWEST_Security::post_key('aiwest_directory_lead_flow_mode', 'send_immediately'); update_option('aiwest_directory_lead_flow_mode', in_array($lead_flow, ['send_immediately','admin_review'], true) ? $lead_flow : 'send_immediately'); $from_name = AIWEST_Security::post_text('aiwest_directory_email_from_name', self::default_email_from_name()); $from_email = sanitize_email(AIWEST_Security::post_text('aiwest_directory_email_from_email', self::default_email_from_email())); update_option('aiwest_directory_email_from_name', $from_name !== '' ? $from_name : self::default_email_from_name()); update_option('aiwest_directory_email_from_email', is_email($from_email) ? $from_email : self::default_email_from_email()); $show_header = AIWEST_Security::post_key('aiwest_directory_show_header', 'yes'); update_option('aiwest_directory_show_header', $show_header === 'no' ? 'no' : 'yes'); wp_safe_redirect(add_query_arg('aiwest_directory_saved', '1', self::admin_page_url())); exit; } public static function seed_demo_items() { if (!current_user_can('manage_options')) wp_die(esc_html(__('Insufficient permissions.', 'ai-webmaster-studio'))); check_admin_referer('aiwest_directory_seed_demo'); $demo = [ 'new-york' => ['title'=>__('Demo Partner — New York', 'ai-webmaster-studio'), 'city'=>__('New York', 'ai-webmaster-studio'), 'region'=>__('New York', 'ai-webmaster-studio'), 'country'=>__('United States', 'ai-webmaster-studio'), 'lat'=>'40.712776', 'lng'=>'-74.005974', 'features'=>__('Editable partner example for map testing.', 'ai-webmaster-studio'), 'service_area'=>__('New York metropolitan area', 'ai-webmaster-studio'), 'priority'=>70], 'berlin' => ['title'=>__('Demo Partner — Berlin', 'ai-webmaster-studio'), 'city'=>__('Berlin', 'ai-webmaster-studio'), 'region'=>__('Berlin', 'ai-webmaster-studio'), 'country'=>__('Germany', 'ai-webmaster-studio'), 'lat'=>'52.520008', 'lng'=>'13.404954', 'features'=>__('Editable partner example for map testing.', 'ai-webmaster-studio'), 'service_area'=>__('Berlin and surrounding area', 'ai-webmaster-studio'), 'priority'=>75], 'shanghai' => ['title'=>__('Demo Partner — Shanghai', 'ai-webmaster-studio'), 'city'=>__('Shanghai', 'ai-webmaster-studio'), 'region'=>__('Shanghai', 'ai-webmaster-studio'), 'country'=>__('China', 'ai-webmaster-studio'), 'lat'=>'31.230391', 'lng'=>'121.473701', 'features'=>__('Editable partner example for map testing.', 'ai-webmaster-studio'), 'service_area'=>__('Shanghai metropolitan area', 'ai-webmaster-studio'), 'priority'=>72], ]; foreach ($demo as $key => $item) { // Demo records use a plugin-owned metadata key to prevent duplicates. $existing = get_posts([ 'post_type' => self::CPT, 'post_status' => ['publish','draft','pending','private'], 'numberposts' => 1, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Plugin-owned key prevents duplicate demo records. 'meta_key' => '_aiwest_directory_demo_key', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Bounded lookup for one known demo identifier. 'meta_value' => $key, 'fields' => 'ids', ]); $postarr = [ 'post_type' => self::CPT, 'post_status' => 'draft', 'post_title' => $item['title'], 'post_content' => __('Demo record for testing the map. Replace every example value and review the record before publishing.', 'ai-webmaster-studio'), ]; if ($existing) { $postarr['ID'] = (int)$existing[0]; $post_id = wp_update_post($postarr, true); } else { $post_id = wp_insert_post($postarr, true); } if ($post_id && !is_wp_error($post_id)) { foreach ($item as $k=>$v) if ($k !== 'title') update_post_meta($post_id, '_aiwest_directory_' . $k, $v); update_post_meta($post_id, '_aiwest_directory_visible', 'yes'); update_post_meta($post_id, '_aiwest_directory_status', 'partner'); update_post_meta($post_id, '_aiwest_directory_record_type', 'partner'); update_post_meta($post_id, '_aiwest_directory_demo', '1'); update_post_meta($post_id, '_aiwest_directory_demo_key', $key); } } wp_safe_redirect(add_query_arg('aiwest_directory_seeded', '1', self::admin_page_url())); exit; } public static function create_demo_business() { if (!current_user_can('manage_options')) wp_die(esc_html(__('Insufficient permissions.', 'ai-webmaster-studio'))); check_admin_referer('aiwest_directory_create_demo_business'); $existing = get_posts([ 'post_type' => self::CPT, 'post_status' => ['publish', 'draft', 'pending', 'private'], 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => true, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Bounded lookup for the plugin-owned demo identifier. 'meta_key' => '_aiwest_directory_demo_key', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Bounded lookup for one known demo identifier. 'meta_value' => 'business-headquarters', ]); $postarr = [ 'post_type' => self::CPT, 'post_status' => 'draft', 'post_title' => __('My Business Headquarters — replace demo data', 'ai-webmaster-studio'), 'post_content' => __('Replace this demo description, address and coordinates with the real business information before publishing.', 'ai-webmaster-studio'), ]; if ($existing) $postarr['ID'] = (int) $existing[0]; $post_id = $existing ? wp_update_post($postarr, true) : wp_insert_post($postarr, true); if (!is_wp_error($post_id) && $post_id) { $values = [ 'address' => '350 Fifth Avenue', 'city' => 'New York', 'region' => 'New York', 'country' => 'United States', 'lat' => '40.748817', 'lng' => '-73.985428', 'features' => __('Demo location — replace with your business address and description.', 'ai-webmaster-studio'), 'opening_hours' => __('Monday–Friday 09:00–17:00', 'ai-webmaster-studio'), 'priority' => '100', 'status' => 'draft', 'visible' => 'yes', 'record_type' => 'primary_business', ]; foreach ($values as $key => $value) update_post_meta((int) $post_id, '_aiwest_directory_' . $key, $value); update_post_meta((int) $post_id, '_aiwest_directory_demo', '1'); update_post_meta((int) $post_id, '_aiwest_directory_demo_key', 'business-headquarters'); } wp_safe_redirect(add_query_arg('aiwest_directory_business_draft', '1', self::admin_page_url())); exit; } public static function delete_demo_items() { if (!current_user_can('manage_options')) wp_die(esc_html(__('Insufficient permissions.', 'ai-webmaster-studio'))); check_admin_referer('aiwest_directory_delete_demo'); // Administrator cleanup is bounded and targets plugin-owned demo metadata. $posts = get_posts([ 'post_type' => self::CPT, 'post_status' => ['publish','draft','pending','private','trash'], 'numberposts' => 200, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Administrator cleanup targets plugin-owned demo records. 'meta_key' => '_aiwest_directory_demo', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Paired marker value for bounded cleanup. 'meta_value' => '1', 'fields' => 'ids', ]); foreach ($posts as $post_id) wp_delete_post((int)$post_id, true); wp_safe_redirect(add_query_arg('aiwest_directory_demo_deleted', '1', self::admin_page_url())); exit; } public static function cleanup_map_page() { if (!current_user_can('manage_options')) wp_die(esc_html(__('Insufficient permissions.', 'ai-webmaster-studio'))); check_admin_referer('aiwest_directory_cleanup_map_page'); $labels = self::labels(); $page_id = self::map_page_id_from_setting($labels['map_page_url']); if ($page_id) { $post = get_post($page_id); if ($post && $post->post_type === 'page') { $content = $post->post_content; $patterns = [ '/\[\s*Mapa Google z naniesionymi[^\]]*integracja do wdrożenia przez administratora\s*\]/u', '/

\s*\[\s*Mapa Google z naniesionymi[^<]*integracja do wdrożenia przez administratora\s*\]\s*<\/p>/u', ]; $new = preg_replace($patterns, '', $content); if (is_string($new) && $new !== $content) { wp_update_post(['ID'=>$page_id, 'post_content'=>$new]); } } } wp_safe_redirect(add_query_arg('aiwest_directory_cleaned', '1', self::admin_page_url())); exit; } protected static function map_page_id_from_setting($setting) { $setting = trim((string)$setting); if ($setting === '') return absint(get_option(self::PAGE_ID_OPTION, 0)); if (ctype_digit($setting)) return (int)$setting; $id = url_to_postid($setting); if (!$id && strpos($setting, '/') === 0) $id = url_to_postid(home_url($setting)); if (!$id) { $path = trim((string) wp_parse_url($setting, PHP_URL_PATH), '/'); $page = $path ? get_page_by_path($path, OBJECT, 'page') : null; if ($page instanceof WP_Post && $page->post_status !== 'trash') $id = (int) $page->ID; } return (int)$id; } protected static function virtual_demo_items($mode) { $headquarters = [ 'id' => 'demo-business-headquarters', 'title' => __('Demo Company Headquarters', 'ai-webmaster-studio'), 'excerpt' => __('Demo location — replace with your business address.', 'ai-webmaster-studio'), 'city' => __('New York', 'ai-webmaster-studio'), 'region' => __('New York', 'ai-webmaster-studio'), 'country' => __('United States', 'ai-webmaster-studio'), 'address' => '350 Fifth Avenue', 'lat' => 40.748817, 'lng' => -73.985428, 'phone' => '', 'email' => '', 'website' => '', 'opening_hours' => __('Monday–Friday 09:00–17:00', 'ai-webmaster-studio'), 'marker_label' => __('Demo HQ', 'ai-webmaster-studio'), 'features' => __('Demo location — replace with your business address and description.', 'ai-webmaster-studio'), 'service_area' => '', 'status' => 'draft', 'priority' => 100, 'record_type' => 'primary_business', 'permalink' => '', 'is_demo' => true, ]; if (!self::mode_uses_partners($mode)) return [$headquarters]; $partners = [ ['key' => 'new-york', 'title' => __('Demo Partner — New York', 'ai-webmaster-studio'), 'city' => __('New York', 'ai-webmaster-studio'), 'region' => __('New York', 'ai-webmaster-studio'), 'country' => __('United States', 'ai-webmaster-studio'), 'lat' => 40.712776, 'lng' => -74.005974], ['key' => 'berlin', 'title' => __('Demo Partner — Berlin', 'ai-webmaster-studio'), 'city' => __('Berlin', 'ai-webmaster-studio'), 'region' => __('Berlin', 'ai-webmaster-studio'), 'country' => __('Germany', 'ai-webmaster-studio'), 'lat' => 52.520008, 'lng' => 13.404954], ['key' => 'shanghai', 'title' => __('Demo Partner — Shanghai', 'ai-webmaster-studio'), 'city' => __('Shanghai', 'ai-webmaster-studio'), 'region' => __('Shanghai', 'ai-webmaster-studio'), 'country' => __('China', 'ai-webmaster-studio'), 'lat' => 31.230391, 'lng' => 121.473701], ]; $items = $mode === 'company_partners' ? [$headquarters] : []; foreach ($partners as $partner) { $items[] = [ 'id' => 'demo-partner-' . $partner['key'], 'title' => $partner['title'], 'excerpt' => __('Virtual demo record — add and publish a real partner to replace it.', 'ai-webmaster-studio'), 'city' => $partner['city'], 'region' => $partner['region'], 'country' => $partner['country'], 'address' => '', 'lat' => $partner['lat'], 'lng' => $partner['lng'], 'phone' => '', 'email' => '', 'website' => '', 'opening_hours' => '', 'marker_label' => __('Demo', 'ai-webmaster-studio'), 'features' => __('Virtual demo partner — not a real directory listing.', 'ai-webmaster-studio'), 'service_area' => '', 'status' => 'draft', 'priority' => 50, 'record_type' => 'partner', 'permalink' => '', 'is_demo' => true, ]; } return $items; } protected static function get_display_items($limit, $mode) { $items = self::get_items($limit, $mode); return $items ?: self::virtual_demo_items($mode); } protected static function directions_url($item, $provider = '') { if (!isset($item['lat'], $item['lng']) || !is_numeric($item['lat']) || !is_numeric($item['lng'])) return ''; $lat = number_format((float) $item['lat'], 6, '.', ''); $lng = number_format((float) $item['lng'], 6, '.', ''); $provider = self::sanitize_directions_provider($provider ?: get_option('aiwest_directory_directions_provider', 'google')); if ($provider === 'osm') return 'https://www.openstreetmap.org/directions?to=' . rawurlencode($lat . ',' . $lng); if ($provider === 'apple') return 'https://maps.apple.com/?daddr=' . rawurlencode($lat . ',' . $lng); return 'https://www.google.com/maps/dir/?api=1&destination=' . rawurlencode($lat . ',' . $lng); } protected static function positioned_items($items) { $positioned = []; foreach ((array) $items as $item) { if (!isset($item['lat'], $item['lng']) || !is_numeric($item['lat']) || !is_numeric($item['lng'])) continue; $positioned[] = $item; } return $positioned; } protected static function static_map_fallback($items) { $positioned = self::positioned_items($items); if (empty($positioned)) { return '

' . esc_html(__('The map will be displayed after records with coordinates are added. The results list remains available.', 'ai-webmaster-studio')) . '
'; } return '
' . esc_html(__('Loading interactive map...', 'ai-webmaster-studio')) . '
'; } protected static function static_results_fallback($items) { $items = array_slice((array) $items, 0, 30); if (empty($items)) { return '
' . esc_html(__('No directory records to display.', 'ai-webmaster-studio')) . '
'; } $html = ''; foreach ($items as $item) { $id = isset($item['id']) ? (string) $item['id'] : ''; $title = isset($item['title']) ? (string) $item['title'] : ''; $location = trim(implode(', ', array_filter([(string)($item['city'] ?? ''), (string)($item['region'] ?? ''), (string)($item['country'] ?? '')]))); $desc = (string)($item['features'] ?? ''); if ($desc === '') $desc = (string)($item['excerpt'] ?? ''); $is_demo = !empty($item['is_demo']); $html .= '
'; if ($is_demo) { $html .= '' . esc_html(__('Demo location — replace in map settings', 'ai-webmaster-studio')) . ''; } elseif (($item['record_type'] ?? 'partner') === 'partner') { $html .= ''; } $html .= '

' . esc_html($title) . '

'; if ($location !== '') $html .= '

' . esc_html($location) . '

'; if ($desc !== '') $html .= '

' . esc_html(wp_trim_words($desc, 24)) . '

'; if (!empty($item['opening_hours'])) $html .= '

' . esc_html($item['opening_hours']) . '

'; if (!empty($item['website'])) $html .= '

' . esc_html(__('Website', 'ai-webmaster-studio')) . '

'; $directions = self::directions_url($item); if ($directions !== '') $html .= '

' . esc_html(__('Get Directions', 'ai-webmaster-studio')) . '

'; $html .= '
'; } return $html; } public static function shortcode_legacy($atts = []) { return self::shortcode($atts); } public static function allowed_shortcode_html() { $allowed = wp_kses_allowed_html('post'); $allowed['section'] = [ 'class' => true, 'id' => true, 'data-map-id' => true, 'data-provider' => true, 'data-map-mode' => true, ]; $allowed['div'] = array_merge($allowed['div'] ?? [], [ 'class' => true, 'style' => true, 'hidden' => true, 'role' => true, 'aria-live' => true, 'aria-modal' => true, 'aria-label' => true, ]); $allowed['input'] = [ 'type' => true, 'class' => true, 'style' => true, 'placeholder' => true, 'value' => true, 'checked' => true, 'data-aiwest-dir-filter' => true, 'data-aiwest-lead' => true, ]; $allowed['textarea'] = [ 'class' => true, 'rows' => true, 'data-aiwest-lead' => true, ]; $allowed['button'] = [ 'type' => true, 'class' => true, 'aria-label' => true, 'data-id' => true, 'data-lat' => true, 'data-lng' => true, ]; $allowed['article'] = array_merge($allowed['article'] ?? [], [ 'class' => true, 'data-id' => true, 'style' => true, ]); $allowed['label'] = array_merge($allowed['label'] ?? [], [ 'class' => true, 'style' => true, ]); return $allowed; } public static function get_items($limit = 200, $mode = '') { $mode = self::sanitize_mode($mode ?: get_option('aiwest_directory_mode', 'business_location')); // Visibility is stored as plugin-owned post metadata for this dedicated CPT. $q = new WP_Query([ 'post_type' => self::CPT, 'post_status' => 'publish', 'posts_per_page' => absint($limit), 'orderby' => 'title', 'order' => 'ASC', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Visibility is plugin-owned metadata on a dedicated, bounded CPT query. 'meta_query' => [ 'relation' => 'OR', ['key'=>'_aiwest_directory_visible','compare'=>'NOT EXISTS'], ['key'=>'_aiwest_directory_visible','value'=>'yes','compare'=>'='], ], ]); $items = []; foreach ($q->posts as $p) { $lat = get_post_meta($p->ID, '_aiwest_directory_lat', true); $lng = get_post_meta($p->ID, '_aiwest_directory_lng', true); $record_type = get_post_meta($p->ID, '_aiwest_directory_record_type', true) ?: 'partner'; if ($mode === 'business_location' && $record_type !== 'primary_business') continue; if ($mode === 'business_locations' && !in_array($record_type, ['primary_business', 'branch'], true)) continue; if ($mode === 'partner_directory' && $record_type !== 'partner') continue; $items[] = [ 'id' => $p->ID, 'title' => get_the_title($p), 'excerpt' => wp_strip_all_tags(get_the_excerpt($p) ?: wp_trim_words($p->post_content, 24)), 'city' => get_post_meta($p->ID, '_aiwest_directory_city', true), 'region' => get_post_meta($p->ID, '_aiwest_directory_region', true), 'country' => get_post_meta($p->ID, '_aiwest_directory_country', true), 'address' => get_post_meta($p->ID, '_aiwest_directory_address', true), 'lat' => is_numeric($lat) ? round((float)$lat, 6) : null, 'lng' => is_numeric($lng) ? round((float)$lng, 6) : null, 'phone' => get_post_meta($p->ID, '_aiwest_directory_phone', true), 'email' => get_post_meta($p->ID, '_aiwest_directory_email', true), 'website' => get_post_meta($p->ID, '_aiwest_directory_website', true), 'opening_hours' => get_post_meta($p->ID, '_aiwest_directory_opening_hours', true), 'marker_label' => get_post_meta($p->ID, '_aiwest_directory_marker_label', true), 'features' => get_post_meta($p->ID, '_aiwest_directory_features', true), 'service_area' => get_post_meta($p->ID, '_aiwest_directory_service_area', true), 'status' => self::normalize_directory_status(get_post_meta($p->ID, '_aiwest_directory_status', true) ?: 'active'), 'priority' => (int)get_post_meta($p->ID, '_aiwest_directory_priority', true), 'record_type' => $record_type, 'permalink' => get_permalink($p), 'is_demo' => false, ]; } wp_reset_postdata(); return $items; } public static function shortcode($atts = []) { $atts = shortcode_atts(['limit'=>200, 'height'=>'', 'show_header'=>''], $atts, 'aiwest_directory_map'); $labels = self::labels(); $mode = self::sanitize_mode($labels['mode']); $items = self::get_display_items((int)$atts['limit'], $mode); foreach ($items as &$item) { $item['directions_url'] = self::directions_url($item, $labels['directions_provider']); } unset($item); $height = $atts['height'] !== '' ? self::sanitize_height($atts['height']) : $labels['height']; $uid = 'aiwest-dir-' . wp_rand(10000,99999); $provider = self::active_provider(); self::enqueue_frontend_assets(); wp_localize_script('aiwest-directory-map', 'AIWESTDirectoryMap', [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce(self::NONCE_ACTION), 'provider' => $provider, 'googleApiKey' => $provider === 'google' ? $labels['google_api_key'] : '', 'i18n' => [ 'ajaxUnavailable' => __('The WordPress AJAX endpoint is unavailable.', 'ai-webmaster-studio'), 'noResults'=>__('No results for the selected filters.', 'ai-webmaster-studio'), 'matching'=>__('AI is analyzing the match...', 'ai-webmaster-studio'), 'weak'=>__('No full match found. Showing the closest directory results.', 'ai-webmaster-studio'), 'leadSelect'=>__('Select at least one partner.', 'ai-webmaster-studio'), 'leadGenerating'=>__('Preparing inquiry...', 'ai-webmaster-studio'), 'leadSending'=>__('Sending inquiry...', 'ai-webmaster-studio'), 'selectForInquiry'=>__('Select for inquiry', 'ai-webmaster-studio'), 'website'=>__('Website', 'ai-webmaster-studio'), 'directions'=>__('Get Directions', 'ai-webmaster-studio'), 'demoBadge'=>__('Demo location — replace in map settings', 'ai-webmaster-studio'), 'selectedPrefix'=>__('Selected: ', 'ai-webmaster-studio'), 'canPrepareInquiry'=>__('You can prepare an inquiry.', 'ai-webmaster-studio'), 'selectPartnersHint'=>__('Select partners from the results list, then prepare an inquiry.', 'ai-webmaster-studio'), 'leadFormMissing'=>__('Inquiry form was not found on the page.', 'ai-webmaster-studio'), 'leadEditable'=>__('You can edit the inquiry before sending.', 'ai-webmaster-studio'), 'leadGenerateFailed'=>__('Could not prepare the message.', 'ai-webmaster-studio'), 'emailInvalid'=>__('Enter a valid email address.', 'ai-webmaster-studio'), 'messageRequired'=>__('Generate or enter the inquiry message before sending.', 'ai-webmaster-studio'), 'consentRequired'=>__('Confirm consent to send the inquiry to selected partners.', 'ai-webmaster-studio'), 'leadSaved'=>__('Inquiry saved.', 'ai-webmaster-studio'), 'leadSendFailed'=>__('Could not send the inquiry.', 'ai-webmaster-studio'), 'mapLoading'=>__('Loading map...', 'ai-webmaster-studio'), 'mapLoadFailed'=>__('Map could not be loaded. The results list remains available.', 'ai-webmaster-studio'), 'googleLoading'=>__('Loading Google Maps...', 'ai-webmaster-studio'), 'googleLoadFailed'=>__('Google Maps could not be loaded. The results list remains available.', 'ai-webmaster-studio'), 'matchFailed'=>__('Could not perform matching.', 'ai-webmaster-studio'), 'matcherConnectionFailed'=>__('Could not connect to AI Matcher.', 'ai-webmaster-studio'), 'staticMapLabel'=>__('Locations and partners map', 'ai-webmaster-studio'), 'directoryMap'=>__('Locations & Partners', 'ai-webmaster-studio'), 'openMap'=>__('open map', 'ai-webmaster-studio'), 'matchingStopwords'=>__('the,a,an,and,or,to,for,from,with,in,on,at,company,companies,find,search,looking,service,services,offer,partner,partners', 'ai-webmaster-studio'), ], ]); $instance_data = wp_json_encode( ['items' => $items, 'labels' => $labels, 'provider' => $provider], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); $instance_id = wp_json_encode($uid, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); if (is_string($instance_data) && is_string($instance_id)) { wp_add_inline_script( 'aiwest-directory-map', 'window.AIWESTDirectoryInstances=window.AIWESTDirectoryInstances||{};window.AIWESTDirectoryInstances[' . $instance_id . ']=' . $instance_data . ';', 'before' ); } $show_header = $atts['show_header'] !== '' ? ($atts['show_header'] !== 'no') : ($labels['show_header'] !== 'no'); $layout_class = 'aiwest-directory-layout-' . str_replace('_', '-', $labels['layout']); $width_class = 'aiwest-directory-width-' . $labels['width']; $alignment_class = $labels['width'] === 'full' ? 'alignfull' : ($labels['width'] === 'wide' ? 'alignwide' : ''); $ratio_class = 'aiwest-directory-ratio-' . str_replace('_', '-', $labels['split_ratio']); $partners_enabled = self::mode_uses_partners($mode); ob_start(); ?>

post_content, 'aiwest_directory_map')) return; $mode = self::sanitize_mode(get_option('aiwest_directory_mode', 'business_location')); if (!self::mode_uses_business($mode)) return; $items = self::get_items(100, $mode); $business = null; foreach ($items as $item) { if (($item['record_type'] ?? '') === 'primary_business') { $business = $item; break; } } if (!$business) return; $schema = [ '@context' => 'https://schema.org', '@type' => 'LocalBusiness', 'name' => (string) $business['title'], 'url' => !empty($business['website']) ? (string) $business['website'] : home_url('/'), ]; $address = array_filter([ 'streetAddress' => (string) ($business['address'] ?? ''), 'addressLocality' => (string) ($business['city'] ?? ''), 'addressRegion' => (string) ($business['region'] ?? ''), 'addressCountry' => (string) ($business['country'] ?? ''), ]); if ($address) $schema['address'] = array_merge(['@type' => 'PostalAddress'], $address); if (is_numeric($business['lat'] ?? null) && is_numeric($business['lng'] ?? null)) { $schema['geo'] = [ '@type' => 'GeoCoordinates', 'latitude' => round((float) $business['lat'], 6), 'longitude' => round((float) $business['lng'], 6), ]; } if (!empty($business['phone'])) $schema['telephone'] = (string) $business['phone']; if (!empty($business['opening_hours'])) $schema['openingHours'] = (string) $business['opening_hours']; $json = wp_json_encode($schema, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); if (is_string($json)) wp_print_inline_script_tag($json, ['type' => 'application/ld+json']); } public static function ajax_match() { check_ajax_referer(self::NONCE_ACTION, 'nonce'); $location = AIWEST_Security::post_text('location'); $need = AIWEST_Security::post_text('need'); $items = self::get_items(300); $inferred_location = self::infer_location_from_need($need, $items); if ($location === '' && $inferred_location !== '') $location = $inferred_location; $matches = self::score_items($items, $location, $need); $message = self::build_match_message($matches, $location, $need); wp_send_json_success(['matches'=>array_slice($matches,0,8), 'message'=>$message, 'location'=>$location]); } public static function ajax_generate_lead_message() { check_ajax_referer(self::NONCE_ACTION, 'nonce'); $location = AIWEST_Security::post_text('location'); $need = AIWEST_Security::post_text('need'); $ids = self::sanitize_selected_ids(AIWEST_Security::post_array('selected_ids')); $items = self::get_items_by_ids($ids); if (empty($items)) { wp_send_json_error(['message' => __('Select at least one directory partner.', 'ai-webmaster-studio')]); } $fallback = self::build_default_lead_message($location, $need, $items); $message = $fallback; if (!get_transient('aiwest_ai_draft_'.hash('sha256',AIWEST_Security::remote_address())) && class_exists('AIWEST_OpenAI') && AIWEST_OpenAI::get_api_key() && AIWEST_Site_Workspace::consume_public_draft()) { set_transient('aiwest_ai_draft_'.hash('sha256',AIWEST_Security::remote_address()),1,2*MINUTE_IN_SECONDS); $need=substr($need,0,2000);$location=substr($location,0,250); $labels = self::labels(); $partner_summary = []; foreach ($items as $item) { $partner_summary[] = $item['title'] . ' — ' . trim(($item['city'] ?? '') . ' ' . ($item['region'] ?? '') . '; ' . ($item['features'] ?? '')); } $prompt = "Prepare an editable inquiry from the user to selected directory partners. Use ".AIWEST_Languages::language_name(AIWEST_Languages::content_locale()).", a professional tone and specific questions. Do not use square brackets or empty placeholders. Do not promise cooperation; ask for an offer and terms." . "\n\n" . __('Administrator context:', 'ai-webmaster-studio') . "\n" . ($labels['industry_context'] ?: __('Universal services/partners directory.', 'ai-webmaster-studio')) . "\n\n" . sprintf( /* translators: %s: Location entered by the directory visitor. */ __('User location: %s', 'ai-webmaster-studio'), $location ) . "\n" . sprintf( /* translators: %s: Service or project need entered by the directory visitor. */ __('User need: %s', 'ai-webmaster-studio'), $need ) . "\n" . __('Selected partners:', 'ai-webmaster-studio') . "\n- " . implode("\n- ", $partner_summary); $ai = AIWEST_OpenAI::chat([ ['role' => 'system', 'content' => __('You are an assistant preparing safe, specific inquiry messages. Return only the message body, without comments. Do not add empty square-bracket placeholders.', 'ai-webmaster-studio')], ['role' => 'user', 'content' => $prompt], ], 0.35); if (!is_wp_error($ai) && trim((string)$ai) !== '') { $message = self::cleanup_lead_message(trim(wp_strip_all_tags((string)$ai)), $location, $need, '', '', ''); } } wp_send_json_success(['message' => $message]); } public static function ajax_submit_lead() { check_ajax_referer(self::NONCE_ACTION, 'nonce'); $name = AIWEST_Security::post_text('name'); $phone = AIWEST_Security::post_text('phone'); $email = sanitize_email(AIWEST_Security::post_text('email')); $location = AIWEST_Security::post_text('location'); $need = AIWEST_Security::post_text('need'); $message = AIWEST_Security::post_textarea('message'); $consent = AIWEST_Security::post_key('consent'); $ids = self::sanitize_selected_ids(AIWEST_Security::post_array('selected_ids')); $items = self::get_items_by_ids($ids); $labels = self::labels(); $flow_mode = $labels['lead_flow_mode'] === 'admin_review' ? 'admin_review' : 'send_immediately'; if (!$email || !is_email($email)) wp_send_json_error(['message' => __('Enter a valid email address.', 'ai-webmaster-studio')]); if ($consent !== 'yes') wp_send_json_error(['message' => __('Confirm consent before sending the inquiry.', 'ai-webmaster-studio')]); if (empty($items)) wp_send_json_error(['message' => __('Select at least one partner.', 'ai-webmaster-studio')]); $message = self::cleanup_lead_message($message, $location, $need, $email, $name, $phone); if (mb_strlen(trim($message)) < 20) wp_send_json_error(['message' => __('Inquiry message is too short.', 'ai-webmaster-studio')]); $ip = AIWEST_Security::remote_address(); $rate_key = 'aiwest_dir_lead_' . md5($ip . '|' . $email); if (get_transient($rate_key)) wp_send_json_error(['message' => __('An inquiry was sent recently. Please wait before sending another one.', 'ai-webmaster-studio')]); set_transient($rate_key, '1', MINUTE_IN_SECONDS); $partners_meta = []; $partner_emails = []; $seen_partners = []; foreach ($items as $item) { $title = sanitize_text_field($item['title'] ?? ''); $mail = sanitize_email($item['email'] ?? ''); $city = sanitize_text_field($item['city'] ?? ''); $key = self::normalize($title . '|' . $mail . '|' . $city); if (isset($seen_partners[$key])) continue; $seen_partners[$key] = true; $partners_meta[] = ['id'=>(int)$item['id'], 'title'=>$title, 'email'=>$mail, 'city'=>$city]; if ($mail && is_email($mail)) $partner_emails[] = $mail; } $subject = __('New Site Directory Inquiry', 'ai-webmaster-studio'); $partner_lines = []; foreach ($partners_meta as $i) { $line = $i['title']; if (!empty($i['city'])) $line .= ' — ' . $i['city']; if (!empty($i['email'])) $line .= ' <' . $i['email'] . '>'; $partner_lines[] = $line; } $partner_list = $partner_lines ? implode(" - ", $partner_lines) : __('No partners selected.', 'ai-webmaster-studio'); $body_parts = [__('A new inquiry was received from the directory form.', 'ai-webmaster-studio')]; if ($name) { $body_parts[] = sprintf( /* translators: %s: Name of the person submitting a directory inquiry. */ __('Name / contact: %s', 'ai-webmaster-studio'), $name ); } $body_parts[] = sprintf( /* translators: %s: Email address of the person submitting a directory inquiry. */ __('User email: %s', 'ai-webmaster-studio'), $email ); if ($phone) { $body_parts[] = sprintf( /* translators: %s: Phone number supplied with a directory inquiry. */ __('Phone: %s', 'ai-webmaster-studio'), $phone ); } if ($location) { $body_parts[] = sprintf( /* translators: %s: Location supplied with a directory inquiry. */ __('Location: %s', 'ai-webmaster-studio'), $location ); } if ($need) { $body_parts[] = sprintf( /* translators: %s: Service or project need supplied with a directory inquiry. */ __('Need: %s', 'ai-webmaster-studio'), $need ); } $body_parts[] = sprintf( /* translators: %s: Inquiry message body prepared by the visitor. */ __('Message: %s', 'ai-webmaster-studio'), $message ); $body_parts[] = __('Selected partners: - ', 'ai-webmaster-studio') . $partner_list; $body = implode(" ", $body_parts); $headers = self::email_headers($email); $sent_to_partners = 0; $admin_email = get_option('admin_email'); if ($flow_mode === 'send_immediately') { foreach (array_unique($partner_emails) as $to) { if (wp_mail($to, $subject, $body, $headers)) $sent_to_partners++; } } wp_mail($email, __('Copy of Directory Inquiry', 'ai-webmaster-studio'), $body, self::email_headers($admin_email)); if ($admin_email && is_email($admin_email)) { $admin_subject = $flow_mode === 'admin_review' ? __('New directory inquiry awaiting approval', 'ai-webmaster-studio') : __('New directory inquiry', 'ai-webmaster-studio'); wp_mail($admin_email, $admin_subject, $body, self::email_headers($email)); } $lead_status = 'new'; if ($flow_mode === 'admin_review') $lead_status = 'admin_review'; elseif ($sent_to_partners > 0) $lead_status = 'sent'; else $lead_status = 'no_partner_email'; $lead_id = wp_insert_post([ 'post_type' => self::LEAD_CPT, 'post_status' => 'private', 'post_title' => __('Directory inquiry', 'ai-webmaster-studio') . ' — ' . current_time('Y-m-d H:i'), 'post_content' => $message, ], true); if (!is_wp_error($lead_id) && $lead_id) { update_post_meta($lead_id, '_aiwest_lead_user_name', $name); update_post_meta($lead_id, '_aiwest_lead_user_phone', $phone); update_post_meta($lead_id, '_aiwest_lead_user_email', $email); update_post_meta($lead_id, '_aiwest_lead_location', $location); update_post_meta($lead_id, '_aiwest_lead_need', $need); update_post_meta($lead_id, '_aiwest_lead_partners', $partners_meta); update_post_meta($lead_id, '_aiwest_lead_sent_at', current_time('mysql')); update_post_meta($lead_id, '_aiwest_lead_sent_to_partners', $sent_to_partners); update_post_meta($lead_id, '_aiwest_lead_status', $lead_status); update_post_meta($lead_id, '_aiwest_lead_flow_mode', $flow_mode); } if ($flow_mode === 'admin_review') { $msg = __('The inquiry was saved and sent to the administrator. A copy was sent to the provided email address.', 'ai-webmaster-studio'); } elseif ($sent_to_partners === 0) { $msg = __('The inquiry was saved and sent to the administrator and the provided email address. Selected partners do not have email addresses yet.', 'ai-webmaster-studio'); } else { $msg = __('The inquiry was saved and sent to the selected partners. A copy was sent to the provided email address.', 'ai-webmaster-studio'); } wp_send_json_success(['message' => $msg, 'lead_id' => is_wp_error($lead_id) ? 0 : (int)$lead_id, 'status' => $lead_status]); } protected static function sanitize_selected_ids($raw) { if (is_string($raw)) { $decoded = json_decode(wp_unslash($raw), true); $raw = is_array($decoded) ? $decoded : explode(',', $raw); } if (!is_array($raw)) $raw = []; $ids = []; foreach ($raw as $id) { $id = absint($id); if ($id) $ids[] = $id; } return array_values(array_unique(array_slice($ids, 0, 20))); } protected static function get_items_by_ids($ids) { if (empty($ids)) return []; $all = self::get_items(500); $out = []; foreach ($all as $item) { if (in_array((int)$item['id'], $ids, true)) $out[] = $item; } return $out; } protected static function cleanup_lead_message($message, $location = '', $need = '', $email = '', $name = '', $phone = '') { $message = wp_strip_all_tags((string)$message); $replacements = [ '[proszę wpisać adres e-mail]' => $email, '[proszę wpisać numer telefonu]' => $phone, '[Imię i nazwisko]' => $name, '[Opcjonalnie: nazwa firmy]' => '', '[Opcjonalnie: dodatkowe informacje kontaktowe]' => '', '[proszę wpisać]' => '', ]; foreach ($replacements as $from => $to) $message = str_replace($from, (string)$to, $message); $message = preg_replace('/\[[^\]]*(proszę|prosze|opcjonalnie|uzupełnij|uzupelnij)[^\]]*\]/iu', '', $message); $lines = preg_split('/\R/u', $message); $clean = []; foreach ($lines as $line) { $trim = trim($line); if ($trim === '') { $clean[] = ''; continue; } if (preg_match('/^[-*]?\s*(typ inwestycji|technologia budowy|etap budowy|preferowany termin|dodatkowe informacje ode mnie)\s*:\s*$/iu', $trim)) continue; if (preg_match('/^[-*]?\s*(potrzeba|opis potrzeby)\s*:\s*$/iu', $trim)) continue; $clean[] = rtrim($line); } $message = implode("\n", $clean); $message = preg_replace("/\n{3,}/", "\n\n", $message); return trim($message); } protected static function build_default_lead_message($location, $need, $items) { $names = implode(', ', array_unique(array_map(function($i){ return $i['title']; }, $items))); $lines = [ __('Hello,', 'ai-webmaster-studio'), __('please prepare initial information or an offer regarding my request.', 'ai-webmaster-studio'), ]; if ($location) { $lines[] = sprintf( /* translators: %s: Location supplied with a directory inquiry. */ __('Location: %s', 'ai-webmaster-studio'), $location ); } if ($need) { $lines[] = sprintf( /* translators: %s: Description supplied with a directory inquiry. */ __('Need description: %s', 'ai-webmaster-studio'), $need ); } if ($names) { $lines[] = sprintf( /* translators: %s: Comma-separated list of directory partners receiving the inquiry. */ __('Selected inquiry recipients: %s', 'ai-webmaster-studio'), $names ); } $lines[] = __('Please provide information about:', 'ai-webmaster-studio'); $lines[] = __('1. Do you provide this scope in the specified location?', 'ai-webmaster-studio'); $lines[] = __('2. What exactly is included in the offer and what is charged separately?', 'ai-webmaster-studio'); $lines[] = __('3. What is the earliest possible start date?', 'ai-webmaster-studio'); $lines[] = __('4. What information is required to prepare a more accurate quote?', 'ai-webmaster-studio'); $lines[] = __('5. What are the schedule, payment method and cooperation terms?', 'ai-webmaster-studio'); $lines[] = __('6. Can you provide similar projects or relevant experience?', 'ai-webmaster-studio'); $lines[] = __('Regards', 'ai-webmaster-studio'); return implode(" ", $lines); } protected static function normalize($text) { return strtolower(remove_accents(wp_strip_all_tags((string) $text))); } protected static function query_tokens($text) { $text = self::normalize($text); $words = preg_split('/[^a-z0-9]+/u', $text); $stop_words = explode(',', __('the,a,an,and,or,to,for,from,with,in,on,at,company,companies,find,search,looking,service,services,offer,partner,partners', 'ai-webmaster-studio')); $stop_words = array_filter(array_map([__CLASS__, 'normalize'], $stop_words)); $stop = array_flip($stop_words); $out = []; foreach ($words as $w) { $w = trim($w); if (mb_strlen($w) < 3 || isset($stop[$w])) continue; $out[] = $w; } return array_values(array_unique($out)); } protected static function infer_location_from_need($need, $items) { $need_n = self::normalize($need); if ($need_n === '') return ''; foreach ($items as $item) { foreach ([$item['city'] ?? '', $item['region'] ?? ''] as $candidate) { $candidate_n = self::normalize($candidate); if ($candidate_n && mb_strpos($need_n, $candidate_n) !== false) return (string)$candidate; } } return ''; } public static function score_items($items, $location, $need) { $loc = self::normalize($location); $tokens = self::query_tokens($need); $has_query = ($loc !== '' || !empty($tokens)); $scored = []; foreach ($items as $item) { $hay = self::normalize(implode(' ', [$item['title'], $item['city'], $item['region'], $item['address'], $item['features'], $item['service_area'], $item['excerpt']])); $locHay = self::normalize($item['city'].' '.$item['region'].' '.$item['service_area'].' '.$item['address']); $score = min(20, max(0, (int)$item['priority'] / 5)); $signals = 0; $need_hits = 0; $loc_match = false; if ($loc && (mb_strpos($locHay, $loc) !== false || mb_strpos($hay, $loc) !== false)) { $score += 42; $signals++; $loc_match = true; } foreach ($tokens as $word) { if (mb_strpos($hay, $word) !== false) { $score += 14; $signals++; $need_hits++; } } if (in_array($item['status'], ['partner','featured'], true)) $score += $item['status'] === 'featured' ? 18 : 10; if (!empty($item['lat']) && !empty($item['lng'])) $score += 5; $item['score'] = min(100, (int)round($score)); $item['match_reason'] = self::match_reason($item, $location, $need, $loc_match, $need_hits, count($tokens)); $item['match_strength'] = ($loc_match && ($need_hits > 0 || empty($tokens))) ? 'strong' : (($signals > 0) ? 'partial' : 'weak'); if (!$has_query || $signals > 0) $scored[] = $item; } usort($scored, function($a,$b){ return ($b['score'] <=> $a['score']) ?: strcmp($a['title'], $b['title']); }); return $scored; } protected static function match_reason($item, $location, $need, $loc_match = false, $need_hits = 0, $need_total = 0) { $reasons = []; if ($location && $loc_match) $reasons[] = __('location match', 'ai-webmaster-studio'); if ($need && $need_hits > 0) $reasons[] = __('partial need match with description/services', 'ai-webmaster-studio'); if ($need && $need_total > 0 && $need_hits === 0) $reasons[] = __('location match without full need coverage', 'ai-webmaster-studio'); if (in_array($item['status'], ['partner','featured'], true)) $reasons[] = __('partner or featured status', 'ai-webmaster-studio'); if (!$reasons) $reasons[] = __('closest available directory match', 'ai-webmaster-studio'); return implode(', ', $reasons); } protected static function build_match_message($matches, $location, $need) { $source = get_option('aiwest_directory_recommendation_source', 'catalog_only'); if (!$matches) { $msg = __('No matching records were found in the administrator directory. Expand the location, refine the need or add more records to the directory.', 'ai-webmaster-studio'); if ($source === 'catalog_first_public_allowed') $msg .= "\n" . __('Public sources are allowed in settings as a future direction, but this version does not perform external web search yet.', 'ai-webmaster-studio'); return $msg; } $top = array_slice($matches, 0, 3); $has_strong = false; foreach ($top as $m) if (($m['match_strength'] ?? '') === 'strong') $has_strong = true; $lines = []; if (!$has_strong && ($location || $need)) $lines[] = __('No full location and need match found. Showing the closest results from the administrator directory:', 'ai-webmaster-studio'); else $lines[] = __('Best matches from the administrator directory:', 'ai-webmaster-studio'); foreach ($top as $i => $m) $lines[] = ($i+1) . '. ' . $m['title'] . ' — ' . $m['match_reason'] . ' (score: ' . $m['score'] . ').'; $lines[] = __('Recommendation considers location, description, features/services, status and administrator-defined priority.', 'ai-webmaster-studio'); if ($source === 'catalog_only') $lines[] = __('Source: only records added and approved in this site directory.', 'ai-webmaster-studio'); else $lines[] = __('Source: currently the site directory; public source handling may be added as a separate integration.', 'ai-webmaster-studio'); return implode("\n", $lines); } } AIWEST_Directory_Map::init(); function aiwest_directory_map_admin_page() { AIWEST_Directory_Map::admin_page(); }