label */ function igny8_get_supported_post_types() { $types = array( 'post' => __('Posts', 'igny8-bridge'), 'page' => __('Pages', 'igny8-bridge'), ); if (post_type_exists('product')) { $types['product'] = __('Products', 'igny8-bridge'); } /** * Filter the list of selectable post types. * * @param array $types */ return apply_filters('igny8_supported_post_types', $types); } /** * Get enabled post types * * @return array */ function igny8_get_enabled_post_types() { $saved = get_option('igny8_enabled_post_types'); if (is_array($saved) && !empty($saved)) { return $saved; } return array_keys(igny8_get_supported_post_types()); } /** * Get configured control mode * * @return string mirror|hybrid */ function igny8_get_control_mode() { $mode = get_option('igny8_control_mode', 'mirror'); return in_array($mode, array('mirror', 'hybrid'), true) ? $mode : 'mirror'; } /** * Get supported taxonomies for syncing * * @return array Key => label */ function igny8_get_supported_taxonomies() { $taxonomies = array(); // Standard WordPress taxonomies if (taxonomy_exists('category')) { $taxonomies['category'] = __('Categories', 'igny8-bridge'); } if (taxonomy_exists('post_tag')) { $taxonomies['post_tag'] = __('Tags', 'igny8-bridge'); } // WooCommerce taxonomies if (taxonomy_exists('product_cat')) { $taxonomies['product_cat'] = __('Product Categories', 'igny8-bridge'); } if (taxonomy_exists('product_tag')) { $taxonomies['product_tag'] = __('Product Tags', 'igny8-bridge'); } if (taxonomy_exists('product_shipping_class')) { $taxonomies['product_shipping_class'] = __('Product Shipping Classes', 'igny8-bridge'); } // IGNY8 taxonomies (always include) if (taxonomy_exists('igny8_sectors')) { $taxonomies['igny8_sectors'] = __('IGNY8 Sectors', 'igny8-bridge'); } if (taxonomy_exists('igny8_clusters')) { $taxonomies['igny8_clusters'] = __('IGNY8 Clusters', 'igny8-bridge'); } // Get custom taxonomies (public only) $custom_taxonomies = get_taxonomies(array( 'public' => true, '_builtin' => false ), 'objects'); foreach ($custom_taxonomies as $taxonomy) { // Skip if already added above if (isset($taxonomies[$taxonomy->name])) { continue; } // Skip post formats and other system taxonomies if (in_array($taxonomy->name, array('post_format', 'wp_theme', 'wp_template_part_area'), true)) { continue; } $taxonomies[$taxonomy->name] = $taxonomy->label; } /** * Filter the list of selectable taxonomies. * * @param array $taxonomies */ return apply_filters('igny8_supported_taxonomies', $taxonomies); } /** * Get enabled taxonomies for syncing * * @return array */ function igny8_get_enabled_taxonomies() { $saved = get_option('igny8_enabled_taxonomies'); if (is_array($saved) && !empty($saved)) { return $saved; } // Default: enable common taxonomies return array('category', 'post_tag', 'product_cat', 'igny8_sectors', 'igny8_clusters'); } /** * Check if a taxonomy is enabled for syncing * * @param string $taxonomy Taxonomy key * @return bool */ function igny8_is_taxonomy_enabled($taxonomy) { $taxonomies = igny8_get_enabled_taxonomies(); return in_array($taxonomy, $taxonomies, true); } /** * Get available automation modules * * @return array Key => label */ function igny8_get_available_modules() { $modules = array( 'sites' => __('Sites (Data & Semantic Map)', 'igny8-bridge'), 'planner' => __('Planner (Keywords & Briefs)', 'igny8-bridge'), 'writer' => __('Writer (Tasks & Posts)', 'igny8-bridge'), 'linker' => __('Linker (Internal Links)', 'igny8-bridge'), 'optimizer' => __('Optimizer (Audits & Scores)', 'igny8-bridge'), ); /** * Filter the list of IGNY8 modules that can be toggled. * * @param array $modules */ return apply_filters('igny8_available_modules', $modules); } /** * Get enabled modules * * @return array */ function igny8_get_enabled_modules() { $saved = get_option('igny8_enabled_modules'); if (is_array($saved) && !empty($saved)) { return $saved; } return array_keys(igny8_get_available_modules()); } /** * Check if a module is enabled * * @param string $module Module key * @return bool */ function igny8_is_module_enabled($module) { $modules = igny8_get_enabled_modules(); return in_array($module, $modules, true); } /** * Check if a post type is enabled for automation * * @param string $post_type Post type key * @return bool */ function igny8_is_post_type_enabled($post_type) { $post_types = igny8_get_enabled_post_types(); return in_array($post_type, $post_types, true); } /** * Check if IGNY8 connection is enabled * This is a master switch that disables all sync operations while preserving credentials * * @return bool True if connection is enabled */ if (!function_exists('igny8_log_error')) { function igny8_log_error($message) { if (defined('WP_DEBUG') && WP_DEBUG) { error_log('[IGNY8 Plugin] ' . $message); } } } if (!function_exists('igny8_is_connection_enabled')) { function igny8_is_connection_enabled() { // Master toggle (defaults to true) $enabled = (bool) get_option('igny8_connection_enabled', 1); if (!$enabled) { return false; } // Prefer secure option helpers when available if (function_exists('igny8_get_secure_option')) { $api_key = igny8_get_secure_option('igny8_api_key'); } else { $api_key = get_option('igny8_api_key'); } $site_id = get_option('igny8_site_id'); if (empty($api_key) || empty($site_id)) { igny8_log_error('Failed to connect to IGNY8 API: API key or Site ID not configured.'); return false; } return true; } } /** * Get connection state * Three states: not_connected, configured, connected * * @return string Connection state */ function igny8_get_connection_state() { $api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key'); if (empty($api_key)) { igny8_log_connection_state('not_connected', 'No API key found'); return 'not_connected'; } igny8_log_connection_state('connected', 'API key configured'); return 'connected'; } /** * Log connection state changes (without exposing API keys) * * @param string $state Connection state * @param string $message Additional context */ function igny8_log_connection_state($state, $message = '') { if (defined('WP_DEBUG') && WP_DEBUG) { error_log(sprintf('[IGNY8 Connection] State: %s | %s', $state, $message)); } } /** * Log sync operations (without exposing sensitive data) * * @param string $operation Operation name * @param array $context Context data */ function igny8_log_sync($operation, $context = array()) { if (defined('WP_DEBUG') && WP_DEBUG) { // Filter out sensitive keys $safe_context = array_diff_key($context, array_flip(['api_key', 'password', 'secret', 'token'])); error_log(sprintf('[IGNY8 Sync] %s | Context: %s', $operation, json_encode($safe_context))); } } /** * Get configuration for site scans * * @param array $overrides Override defaults * @return array */ function igny8_get_site_scan_settings($overrides = array()) { $defaults = array( 'post_types' => igny8_get_enabled_post_types(), 'include_products' => (bool) get_option('igny8_enable_woocommerce', class_exists('WooCommerce') ? 1 : 0), 'per_page' => 100, 'since' => null, 'mode' => 'full', ); $settings = wp_parse_args($overrides, $defaults); return apply_filters('igny8_site_scan_settings', $settings); } /** * Register IGNY8 post meta fields */ function igny8_register_post_meta() { $post_types = array('post', 'page', 'product'); // Define all meta fields with proper schema for REST API $meta_fields = array( '_igny8_taxonomy_id' => array( 'type' => 'integer', 'description' => 'IGNY8 taxonomy ID linked to this post', 'single' => true, 'show_in_rest' => true, ), '_igny8_attribute_id' => array( 'type' => 'integer', 'description' => 'IGNY8 attribute ID linked to this post', 'single' => true, 'show_in_rest' => true, ), '_igny8_last_synced' => array( 'type' => 'string', 'description' => 'Last sync timestamp', 'single' => true, 'show_in_rest' => true, ) ); // Register each meta field for all relevant post types foreach ($meta_fields as $meta_key => $config) { foreach ($post_types as $post_type) { register_post_meta($post_type, $meta_key, $config); } } } /** * Register IGNY8 taxonomies */ function igny8_register_taxonomies() { // Register sectors taxonomy (hierarchical) - only if not exists if (!taxonomy_exists('igny8_sectors')) { register_taxonomy('igny8_sectors', array('post', 'page', 'product'), array( 'hierarchical' => true, 'labels' => array( 'name' => 'IGNY8 Sectors', 'singular_name' => 'Sector', 'menu_name' => 'Sectors', 'all_items' => 'All Sectors', 'edit_item' => 'Edit Sector', 'view_item' => 'View Sector', 'update_item' => 'Update Sector', 'add_new_item' => 'Add New Sector', 'new_item_name' => 'New Sector Name', 'parent_item' => 'Parent Sector', 'parent_item_colon' => 'Parent Sector:', 'search_items' => 'Search Sectors', 'not_found' => 'No sectors found', ), 'public' => true, 'show_ui' => true, 'show_admin_column' => false, 'show_in_nav_menus' => true, 'show_tagcloud' => false, 'show_in_rest' => true, 'rewrite' => array( 'slug' => 'sectors', 'with_front' => false, ), 'capabilities' => array( 'manage_terms' => 'manage_categories', 'edit_terms' => 'manage_categories', 'delete_terms' => 'manage_categories', 'assign_terms' => 'edit_posts', ), )); } // Register clusters taxonomy (hierarchical) - only if not exists if (!taxonomy_exists('igny8_clusters')) { register_taxonomy('igny8_clusters', array('post', 'page', 'product'), array( 'hierarchical' => true, 'labels' => array( 'name' => 'IGNY8 Clusters', 'singular_name' => 'Cluster', 'menu_name' => 'Clusters', 'all_items' => 'All Clusters', 'edit_item' => 'Edit Cluster', 'view_item' => 'View Cluster', 'update_item' => 'Update Cluster', 'add_new_item' => 'Add New Cluster', 'new_item_name' => 'New Cluster Name', 'parent_item' => 'Parent Cluster', 'parent_item_colon' => 'Parent Cluster:', 'search_items' => 'Search Clusters', 'not_found' => 'No clusters found', ), 'public' => true, 'show_ui' => true, 'show_admin_column' => false, 'show_in_nav_menus' => true, 'show_tagcloud' => false, 'show_in_rest' => true, 'rewrite' => array( 'slug' => 'clusters', 'with_front' => false, ), 'capabilities' => array( 'manage_terms' => 'manage_categories', 'edit_terms' => 'manage_categories', 'delete_terms' => 'manage_categories', 'assign_terms' => 'edit_posts', ), )); } } /** * Map WordPress post status to IGNY8 task status * * @param string $wp_status WordPress post status * @return string IGNY8 task status */ function igny8_map_wp_status_to_igny8($wp_status) { $status_map = array( 'publish' => 'completed', 'draft' => 'draft', 'pending' => 'pending', 'private' => 'completed', 'trash' => 'archived', 'future' => 'scheduled' ); return isset($status_map[$wp_status]) ? $status_map[$wp_status] : 'draft'; } /** * Check if post is managed by IGNY8 * * @param int $post_id Post ID * @return bool True if IGNY8 managed */ function igny8_is_igny8_managed_post($post_id) { $task_id = get_post_meta($post_id, '_igny8_task_id', true); return !empty($task_id); } /** * Get post data for IGNY8 sync * * @param int $post_id Post ID * @return array|false Post data or false on failure */ function igny8_get_post_data_for_sync($post_id) { $post = get_post($post_id); if (!$post) { return false; } return array( 'id' => $post_id, 'title' => $post->post_title, 'status' => $post->post_status, 'url' => get_permalink($post_id), 'modified' => $post->post_modified, 'published' => $post->post_date, 'author' => get_the_author_meta('display_name', $post->post_author), 'word_count' => str_word_count(strip_tags($post->post_content)), 'meta' => array( 'task_id' => get_post_meta($post_id, '_igny8_task_id', true), 'content_id' => get_post_meta($post_id, '_igny8_content_id', true), 'cluster_id' => get_post_meta($post_id, '_igny8_cluster_id', true), 'sector_id' => get_post_meta($post_id, '_igny8_sector_id', true), ) ); } /** * Schedule cron jobs */ function igny8_schedule_cron_jobs() { // Schedule daily post status sync (WordPress → IGNY8) if (!wp_next_scheduled('igny8_sync_post_statuses')) { wp_schedule_event(time(), 'daily', 'igny8_sync_post_statuses'); } // Schedule daily site data sync (incremental) if (!wp_next_scheduled('igny8_sync_site_data')) { wp_schedule_event(time(), 'daily', 'igny8_sync_site_data'); } // Schedule periodic full site scan (runs at most once per week) if (!wp_next_scheduled('igny8_full_site_scan')) { wp_schedule_event(time(), 'daily', 'igny8_full_site_scan'); } // Schedule hourly sync from IGNY8 (IGNY8 → WordPress) if (!wp_next_scheduled('igny8_sync_from_igny8')) { wp_schedule_event(time(), 'hourly', 'igny8_sync_from_igny8'); } // Schedule taxonomy sync if (!wp_next_scheduled('igny8_sync_taxonomies')) { wp_schedule_event(time(), 'twicedaily', 'igny8_sync_taxonomies'); } // Schedule keyword sync if (!wp_next_scheduled('igny8_sync_keywords')) { wp_schedule_event(time(), 'daily', 'igny8_sync_keywords'); } // Schedule site structure sync (daily - to keep post types, taxonomies counts up to date) if (!wp_next_scheduled('igny8_sync_site_structure')) { wp_schedule_event(time(), 'daily', 'igny8_sync_site_structure'); } } /** * Unschedule cron jobs */ function igny8_unschedule_cron_jobs() { $timestamp = wp_next_scheduled('igny8_sync_post_statuses'); if ($timestamp) { wp_unschedule_event($timestamp, 'igny8_sync_post_statuses'); } $timestamp = wp_next_scheduled('igny8_sync_site_data'); if ($timestamp) { wp_unschedule_event($timestamp, 'igny8_sync_site_data'); } $timestamp = wp_next_scheduled('igny8_sync_from_igny8'); if ($timestamp) { wp_unschedule_event($timestamp, 'igny8_sync_from_igny8'); } $timestamp = wp_next_scheduled('igny8_full_site_scan'); if ($timestamp) { wp_unschedule_event($timestamp, 'igny8_full_site_scan'); } $timestamp = wp_next_scheduled('igny8_sync_taxonomies'); if ($timestamp) { wp_unschedule_event($timestamp, 'igny8_sync_taxonomies'); } $timestamp = wp_next_scheduled('igny8_sync_keywords'); if ($timestamp) { wp_unschedule_event($timestamp, 'igny8_sync_keywords'); } $timestamp = wp_next_scheduled('igny8_sync_site_structure'); if ($timestamp) { wp_unschedule_event($timestamp, 'igny8_sync_site_structure'); } } /** * Get WordPress site structure (post types and taxonomies with counts) * * @return array Site structure with post types and taxonomies */ function igny8_get_site_structure() { $post_types_data = array(); $taxonomies_data = array(); // Get all registered post types $post_types = get_post_types(array('public' => true), 'objects'); foreach ($post_types as $post_type) { // Skip built-in post types we don't care about if (in_array($post_type->name, array('attachment'), true)) { continue; } $count = wp_count_posts($post_type->name); $total = 0; foreach ((array) $count as $status => $num) { if ($status !== 'auto-draft') { $total += (int) $num; } } if ($total > 0 || in_array($post_type->name, array('post', 'page', 'product'), true)) { $post_types_data[$post_type->name] = array( 'label' => $post_type->label ?: $post_type->name, 'count' => $total, 'enabled' => igny8_is_post_type_enabled($post_type->name), 'fetch_limit' => 100, ); } } // Get all registered taxonomies $taxonomies = get_taxonomies(array('public' => true), 'objects'); foreach ($taxonomies as $taxonomy) { // Skip built-in taxonomies we don't care about if (in_array($taxonomy->name, array('post_format'), true)) { continue; } $terms = get_terms(array( 'taxonomy' => $taxonomy->name, 'hide_empty' => false, 'number' => 0, )); $count = is_array($terms) ? count($terms) : 0; if ($count > 0 || in_array($taxonomy->name, array('category', 'post_tag', 'product_cat'), true)) { $taxonomies_data[$taxonomy->name] = array( 'label' => $taxonomy->label ?: $taxonomy->name, 'count' => $count, 'enabled' => true, 'fetch_limit' => 100, ); } } return array( 'post_types' => $post_types_data, 'taxonomies' => $taxonomies_data, 'timestamp' => current_time('c'), ); } /* Duplicate function removed. See guarded implementation above. */ /** * Sync WordPress site structure to IGNY8 backend * Called after connection is established * * @return bool True on success, false on failure */ function igny8_sync_site_structure_to_backend($integration_id = null) { // Get site ID from options $site_id = get_option('igny8_site_id'); if (!$site_id) { igny8_log_sync('structure_sync_failed', array('reason' => 'No site ID found')); return false; } // Get the site structure $structure = igny8_get_site_structure(); if (empty($structure['post_types']) && empty($structure['taxonomies'])) { igny8_log_sync('structure_sync_skipped', array('reason' => 'No post types or taxonomies')); return false; } // Create a temporary integration object to find the actual integration ID $api = new Igny8API(); if (!$api->is_authenticated()) { igny8_log_sync('structure_sync_failed', array('reason' => 'Not authenticated')); return false; } // Use provided integration_id if available, otherwise query for it $integration = null; if ($integration_id) { // Use the provided integration_id directly $integration = array('id' => $integration_id); igny8_log_sync('structure_sync_start', array('integration_id' => $integration_id, 'site_id' => $site_id)); } else { // Fallback: Get integration_id from stored option $stored_integration_id = get_option('igny8_integration_id'); if ($stored_integration_id) { $integration = array('id' => intval($stored_integration_id)); igny8_log_sync('structure_sync_start', array('integration_id' => $stored_integration_id, 'site_id' => $site_id, 'source' => 'stored_option')); } else { // Last resort: Query for integrations $response = $api->get('/v1/integration/integrations/?site=' . $site_id); if (!$response['success'] || empty($response['data'])) { igny8_log_sync('structure_sync_failed', array('reason' => 'No integrations found')); return false; } // Get the first integration (should be WordPress integration) if (isset($response['data']['results']) && !empty($response['data']['results'])) { $integration = $response['data']['results'][0]; } elseif (is_array($response['data']) && !empty($response['data'])) { $integration = $response['data'][0]; } if ($integration && !empty($integration['id'])) { // Store integration_id for future use update_option('igny8_integration_id', intval($integration['id'])); igny8_log_sync('integration_id_saved', array('integration_id' => $integration['id'])); } } } if (!$integration || empty($integration['id'])) { igny8_log_sync('structure_sync_failed', array('reason' => 'Invalid integration')); return false; } // Prepare the payload $payload = array( 'post_types' => $structure['post_types'], 'taxonomies' => $structure['taxonomies'], 'timestamp' => $structure['timestamp'], 'plugin_connection_enabled' => (bool) igny8_is_connection_enabled(), 'two_way_sync_enabled' => (bool) get_option('igny8_enable_two_way_sync', 1), ); igny8_log_sync('structure_sync_sending', array( 'post_types_count' => count($structure['post_types']), 'taxonomies_count' => count($structure['taxonomies']) )); // Send to backend $endpoint = '/v1/integration/integrations/' . $integration['id'] . '/update-structure/'; $update_response = $api->post($endpoint, $payload); if ($update_response['success']) { igny8_log_sync('structure_sync_success', array( 'post_types' => count($structure['post_types']), 'taxonomies' => count($structure['taxonomies']) )); update_option('igny8_last_structure_sync', current_time('timestamp')); return true; } else { igny8_log_sync('structure_sync_failed', array( 'error' => $update_response['error'] ?? 'Unknown error', 'http_status' => $update_response['http_status'] ?? 0 )); return false; } } if (!function_exists('igny8_handle_rate_limit')) { function igny8_handle_rate_limit($response, $max_retries = 3) { if (isset($response['error']) && strpos($response['error'], 'Rate limit') !== false) { for ($attempt = 0; $attempt < $max_retries; $attempt++) { sleep(pow(2, $attempt)); // Exponential backoff $response = igny8_retry_request(); // Retry logic (to be implemented) if ($response['success']) { return $response; } } igny8_log_error('Max retries exceeded for rate-limited request.'); } return $response; } } if (!function_exists('igny8_retry_request')) { function igny8_retry_request() { // Placeholder for retry logic return ['success' => false, 'error' => 'Retry logic not implemented']; } }