From e2c0d3d0fc24fb3c5dd5fde4415a25d4dc4ab89b Mon Sep 17 00:00:00 2001 From: alorig <220087330+alorig@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:29:49 +0500 Subject: [PATCH] fd --- igy8-wp-plugin/admin/assets/css/admin.css | 627 ----- igy8-wp-plugin/admin/assets/js/admin.js | 188 -- igy8-wp-plugin/admin/assets/js/post-editor.js | 200 -- igy8-wp-plugin/admin/class-admin-columns.php | 306 --- igy8-wp-plugin/admin/class-admin.php | 621 ----- .../admin/class-post-meta-boxes.php | 469 ---- igy8-wp-plugin/admin/settings.php | 771 ------ igy8-wp-plugin/data/link-graph.php | 192 -- igy8-wp-plugin/data/semantic-mapping.php | 225 -- igy8-wp-plugin/data/site-collection.php | 588 ----- igy8-wp-plugin/data/woocommerce.php | 226 -- igy8-wp-plugin/docs/AUTHENTICATION-AUDIT.md | 114 - igy8-wp-plugin/docs/README.md | 396 --- igy8-wp-plugin/docs/SYNC-DATA-FLOW-DIAGRAM.md | 356 --- .../docs/WORDPRESS-PLUGIN-INTEGRATION.md | 2135 ----------------- igy8-wp-plugin/igny8-bridge.php | 184 -- igy8-wp-plugin/includes/class-igny8-api.php | 486 ---- .../includes/class-igny8-link-queue.php | 202 -- .../includes/class-igny8-rest-api.php | 444 ---- igy8-wp-plugin/includes/class-igny8-site.php | 118 - .../includes/class-igny8-webhook-logs.php | 147 -- .../includes/class-igny8-webhooks.php | 381 --- igy8-wp-plugin/includes/functions.php | 828 ------- igy8-wp-plugin/languages/igny8-bridge.pot | 100 - igy8-wp-plugin/sync/hooks.php | 42 - igy8-wp-plugin/sync/igny8-to-wp.php | 807 ------- igy8-wp-plugin/sync/post-sync.php | 363 --- igy8-wp-plugin/sync/taxonomy-sync.php | 425 ---- .../tests/test-api-authentication.php | 116 - igy8-wp-plugin/tests/test-revoke-api-key.php | 28 - igy8-wp-plugin/tests/test-site-metadata.php | 36 - igy8-wp-plugin/tests/test-sync-structure.php | 163 -- igy8-wp-plugin/uninstall.php | 53 - 33 files changed, 12337 deletions(-) delete mode 100644 igy8-wp-plugin/admin/assets/css/admin.css delete mode 100644 igy8-wp-plugin/admin/assets/js/admin.js delete mode 100644 igy8-wp-plugin/admin/assets/js/post-editor.js delete mode 100644 igy8-wp-plugin/admin/class-admin-columns.php delete mode 100644 igy8-wp-plugin/admin/class-admin.php delete mode 100644 igy8-wp-plugin/admin/class-post-meta-boxes.php delete mode 100644 igy8-wp-plugin/admin/settings.php delete mode 100644 igy8-wp-plugin/data/link-graph.php delete mode 100644 igy8-wp-plugin/data/semantic-mapping.php delete mode 100644 igy8-wp-plugin/data/site-collection.php delete mode 100644 igy8-wp-plugin/data/woocommerce.php delete mode 100644 igy8-wp-plugin/docs/AUTHENTICATION-AUDIT.md delete mode 100644 igy8-wp-plugin/docs/README.md delete mode 100644 igy8-wp-plugin/docs/SYNC-DATA-FLOW-DIAGRAM.md delete mode 100644 igy8-wp-plugin/docs/WORDPRESS-PLUGIN-INTEGRATION.md delete mode 100644 igy8-wp-plugin/igny8-bridge.php delete mode 100644 igy8-wp-plugin/includes/class-igny8-api.php delete mode 100644 igy8-wp-plugin/includes/class-igny8-link-queue.php delete mode 100644 igy8-wp-plugin/includes/class-igny8-rest-api.php delete mode 100644 igy8-wp-plugin/includes/class-igny8-site.php delete mode 100644 igy8-wp-plugin/includes/class-igny8-webhook-logs.php delete mode 100644 igy8-wp-plugin/includes/class-igny8-webhooks.php delete mode 100644 igy8-wp-plugin/includes/functions.php delete mode 100644 igy8-wp-plugin/languages/igny8-bridge.pot delete mode 100644 igy8-wp-plugin/sync/hooks.php delete mode 100644 igy8-wp-plugin/sync/igny8-to-wp.php delete mode 100644 igy8-wp-plugin/sync/post-sync.php delete mode 100644 igy8-wp-plugin/sync/taxonomy-sync.php delete mode 100644 igy8-wp-plugin/tests/test-api-authentication.php delete mode 100644 igy8-wp-plugin/tests/test-revoke-api-key.php delete mode 100644 igy8-wp-plugin/tests/test-site-metadata.php delete mode 100644 igy8-wp-plugin/tests/test-sync-structure.php delete mode 100644 igy8-wp-plugin/uninstall.php diff --git a/igy8-wp-plugin/admin/assets/css/admin.css b/igy8-wp-plugin/admin/assets/css/admin.css deleted file mode 100644 index 3ab514a8..00000000 --- a/igy8-wp-plugin/admin/assets/css/admin.css +++ /dev/null @@ -1,627 +0,0 @@ -/** - * Admin Styles - IGNY8 Bridge - * Updated with IGNY8 brand colors and modern UI - * - * @package Igny8Bridge - */ - -/* ============================================ - IGNY8 Brand Colors - ============================================ */ -:root { - --igny8-primary: #3B82F6; - --igny8-primary-hover: #2563EB; - --igny8-success: #10B981; - --igny8-warning: #F59E0B; - --igny8-error: #EF4444; - --igny8-purple: #8B5CF6; - --igny8-gray: #6B7280; - --igny8-light-gray: #F3F4F6; -} - -/* ============================================ - Container & Layout - ============================================ */ - -.igny8-settings-container { - max-width: 1400px; -} - -.igny8-settings-card { - background: #fff; - border: 1px solid #E5E7EB; - box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); - padding: 24px; - margin: 24px 0; - border-radius: 8px; -} - -.igny8-settings-card h2 { - margin-top: 0; - padding-bottom: 12px; - border-bottom: 2px solid var(--igny8-light-gray); - color: #111827; - font-size: 20px; - font-weight: 600; -} - -/* ============================================ - Toggle Switch - ============================================ */ - -.igny8-toggle-wrapper { - display: flex; - align-items: center; - gap: 12px; -} - -.igny8-toggle-input { - position: relative; - width: 48px; - height: 24px; - -webkit-appearance: none; - appearance: none; - background: var(--igny8-gray); - outline: none; - border-radius: 24px; - cursor: pointer; - transition: 0.3s; -} - -.igny8-toggle-input:checked { - background: var(--igny8-success); -} - -.igny8-toggle-input::before { - content: ''; - position: absolute; - width: 20px; - height: 20px; - border-radius: 50%; - top: 2px; - left: 2px; - background: #fff; - transition: 0.3s; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); -} - -.igny8-toggle-input:checked::before { - left: 26px; -} - -/* ============================================ - Sync Operations Grid - ============================================ */ - -.igny8-sync-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 20px; - margin-top: 20px; -} - -.igny8-sync-card { - background: #fff; - border: 2px solid #E5E7EB; - border-radius: 12px; - padding: 24px; - transition: all 0.3s ease; -} - -.igny8-sync-card:hover { - border-color: var(--igny8-primary); - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - transform: translateY(-2px); -} - -.igny8-sync-card-highlight { - border-color: var(--igny8-primary); - background: linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%); -} - -.igny8-sync-icon { - width: 48px; - height: 48px; - background: var(--igny8-primary); - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 16px; - color: white; -} - -.igny8-sync-card h3 { - margin: 0 0 8px 0; - font-size: 16px; - font-weight: 600; - color: #111827; -} - -.igny8-sync-description { - font-size: 14px; - color: #6B7280; - line-height: 1.5; - margin-bottom: 12px; -} - -.igny8-sync-meta { - font-size: 12px; - color: #9CA3AF; - margin-bottom: 16px; -} - -.igny8-sync-time { - display: inline-flex; - align-items: center; - gap: 4px; -} - -.igny8-sync-button { - width: 100%; - height: 40px; - background: var(--igny8-primary) !important; - border-color: var(--igny8-primary) !important; - color: white !important; - font-weight: 500 !important; - border-radius: 8px !important; - transition: all 0.2s ease !important; -} - -.igny8-sync-button:hover:not(:disabled) { - background: var(--igny8-primary-hover) !important; - border-color: var(--igny8-primary-hover) !important; - transform: translateY(-1px); - box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3); -} - -.igny8-sync-button:disabled { - opacity: 0.5 !important; - cursor: not-allowed !important; -} - -.button-loading { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; -} - -/* ============================================ - Statistics Cards - ============================================ */ - -.igny8-stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 20px; - margin-top: 20px; -} - -.igny8-stat-card { - background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%); - border: 1px solid #E5E7EB; - border-radius: 12px; - padding: 20px; - display: flex; - align-items: flex-start; - gap: 16px; - transition: all 0.3s ease; -} - -.igny8-stat-card:hover { - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); - transform: translateY(-2px); -} - -.igny8-stat-icon { - width: 48px; - height: 48px; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.igny8-stat-content { - flex: 1; -} - -.igny8-stat-label { - font-size: 12px; - color: #6B7280; - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 4px; - font-weight: 500; -} - -.igny8-stat-value { - font-size: 28px; - font-weight: 700; - color: #111827; - line-height: 1.2; - margin-bottom: 4px; -} - -.igny8-stat-meta { - font-size: 12px; - color: #9CA3AF; -} - -/* ============================================ - Semantic Summary - ============================================ */ - -.igny8-semantic-summary { - margin-top: 24px; - padding: 20px; - background: linear-gradient(135deg, #F3E8FF 0%, #E9D5FF 100%); - border-radius: 12px; - border: 1px solid #D8B4FE; -} - -.igny8-semantic-summary h3 { - margin: 0 0 16px 0; - font-size: 16px; - font-weight: 600; - color: #6B21A8; -} - -.igny8-semantic-stats { - display: flex; - gap: 32px; - flex-wrap: wrap; -} - -.igny8-semantic-stat { - display: flex; - flex-direction: column; - gap: 4px; -} - -.igny8-semantic-stat .value { - font-size: 24px; - font-weight: 700; - color: #7C3AED; -} - -.igny8-semantic-stat .label { - font-size: 12px; - color: #8B5CF6; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -/* ============================================ - Status Indicators - ============================================ */ - -.igny8-status-connected { - color: var(--igny8-success); - font-weight: 600; -} - -.igny8-status-disconnected { - color: var(--igny8-error); - font-weight: 600; -} -/* ============================================ - API Connection Form - ============================================ */ - -.igny8-api-connection-form { - background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%); - border: 2px solid #E5E7EB; - border-radius: 12px; - padding: 32px; - margin: 0 0 24px 0; -} - -.igny8-api-form-group { - margin-bottom: 20px; -} - -.igny8-api-form-group label { - display: block; - margin-bottom: 8px; - font-weight: 600; - color: #111827; - font-size: 14px; -} - -.igny8-api-form-group input[type="text"], -.igny8-api-form-group input[type="password"] { - width: 100%; - padding: 12px 14px; - border: 1px solid #D1D5DB; - border-radius: 8px; - font-size: 14px; - transition: all 0.2s ease; - font-family: 'Courier New', monospace; - background-color: #fff; -} - -.igny8-api-form-group input[type="text"]:focus, -.igny8-api-form-group input[type="password"]:focus { - outline: none; - border-color: var(--igny8-primary); - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); -} - -.igny8-api-form-group input[type="text"]:disabled { - background-color: #F3F4F6; - color: #9CA3AF; - cursor: not-allowed; -} - -.igny8-api-form-description { - font-size: 13px; - color: #6B7280; - margin-top: 6px; - line-height: 1.5; -} - -.igny8-api-form-description a { - color: var(--igny8-primary); - text-decoration: none; - font-weight: 500; -} - -.igny8-api-form-description a:hover { - text-decoration: underline; -} - -.igny8-connection-actions { - display: flex; - gap: 12px; - flex-wrap: wrap; - margin-top: 24px; -} - -.igny8-connection-actions .button { - border-radius: 8px; - padding: 10px 20px; - font-weight: 500; - border: none; - cursor: pointer; - transition: all 0.2s ease; - height: auto; -} - -.igny8-connection-actions .button-primary { - background: var(--igny8-primary) !important; - color: white !important; -} - -.igny8-connection-actions .button-primary:hover { - background: var(--igny8-primary-hover) !important; - transform: translateY(-1px); - box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3); -} - -.igny8-connection-actions .button-secondary { - background: #E5E7EB !important; - color: #111827 !important; -} - -.igny8-connection-actions .button-secondary:hover { - background: #D1D5DB !important; -} - -.igny8-connection-status-display { - padding: 20px; - background: linear-gradient(135deg, #F9FAFB 0%, #F3F4F6 100%); - border: 1px solid #E5E7EB; - border-radius: 12px; - margin-bottom: 20px; -} - -.igny8-connection-status-display .igny8-status-label { - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.05em; - color: #6B7280; - margin-bottom: 8px; - font-weight: 500; -} - -.igny8-connection-status-display .igny8-status-value { - font-size: 20px; - font-weight: 700; - display: flex; - align-items: center; - gap: 8px; -} - -.igny8-status-indicator { - width: 12px; - height: 12px; - border-radius: 50%; - display: inline-block; -} - -.igny8-status-indicator.connected { - background-color: var(--igny8-success); -} - -.igny8-status-indicator.disconnected { - background-color: var(--igny8-gray); -} - -.igny8-api-key-display { - display: flex; - align-items: center; - gap: 12px; - padding: 12px; - background-color: #F3F4F6; - border-radius: 8px; - word-break: break-all; -} - -.igny8-api-key-mask { - font-family: 'Courier New', monospace; - color: #6B7280; - font-size: 14px; -} - -/* ============================================ - Diagnostics - ============================================ */ - -.igny8-diagnostics-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 16px; - margin-top: 20px; -} - -.igny8-diagnostic-item { - padding: 16px; - background: linear-gradient(135deg, #F9FAFB 0%, #F3F4F6 100%); - border: 1px solid #E5E7EB; - border-radius: 8px; - transition: all 0.2s ease; -} - -.igny8-diagnostic-item:hover { - border-color: var(--igny8-primary); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); -} - -.igny8-diagnostic-label { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.05em; - color: #6B7280; - margin-bottom: 8px; - font-weight: 500; -} - -.igny8-diagnostic-value { - font-size: 16px; - font-weight: 600; - color: #111827; -} - -.igny8-diagnostic-item .description { - margin: 6px 0 0; - color: #9CA3AF; - font-size: 12px; -} - -/* ============================================ - Sync Status Messages - ============================================ */ - -.igny8-sync-status { - margin-top: 20px; - padding: 16px; - border-radius: 8px; - display: none; - font-size: 14px; -} - -.igny8-sync-status.igny8-sync-status-success { - background-color: #D1FAE5; - border: 1px solid #6EE7B7; - color: #065F46; - display: block; -} - -.igny8-sync-status.igny8-sync-status-error { - background-color: #FEE2E2; - border: 1px solid #FCA5A5; - color: #991B1B; - display: block; -} - -.igny8-sync-status.igny8-sync-status-loading { - background-color: #DBEAFE; - border: 1px solid #93C5FD; - color: #1E40AF; - display: block; -} - -/* ============================================ - Loading States - ============================================ */ - -.igny8-loading { - opacity: 0.6; - pointer-events: none; -} - -@keyframes igny8-spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* ============================================ - Messages & Notifications - ============================================ */ - -.igny8-message { - padding: 16px; - margin: 15px 0; - border-left: 4px solid; - background: #fff; - border-radius: 4px; -} - -.igny8-message.igny8-message-success { - border-color: var(--igny8-success); - background-color: #F0FDF4; - color: #065F46; -} - -.igny8-message.igny8-message-error { - border-color: var(--igny8-error); - background-color: #FEF2F2; - color: #991B1B; -} - -.igny8-message.igny8-message-info { - border-color: var(--igny8-primary); - background-color: #EFF6FF; - color: #1E40AF; -} - -.igny8-message.igny8-message-warning { - border-color: var(--igny8-warning); - background-color: #FFFBEB; - color: #92400E; -} - -/* ============================================ - Responsive - ============================================ */ - -@media (max-width: 782px) { - .igny8-sync-grid { - grid-template-columns: 1fr; - } - - .igny8-stats-grid { - grid-template-columns: 1fr; - } - - .igny8-diagnostics-grid { - grid-template-columns: 1fr; - } -} - -@media (max-width: 600px) { - .igny8-settings-card { - padding: 16px; - } - - .igny8-sync-card { - padding: 16px; - } - - .igny8-stat-card { - flex-direction: column; - } -} diff --git a/igy8-wp-plugin/admin/assets/js/admin.js b/igy8-wp-plugin/admin/assets/js/admin.js deleted file mode 100644 index fe4fe4f7..00000000 --- a/igy8-wp-plugin/admin/assets/js/admin.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Admin JavaScript - * - * @package Igny8Bridge - */ - -(function($) { - 'use strict'; - - $(document).ready(function() { - // Test connection button - $('#igny8-test-connection').on('click', function() { - var $button = $(this); - var $result = $('#igny8-test-result'); - - $button.prop('disabled', true).addClass('igny8-loading'); - $result.html('Testing...'); - - $.ajax({ - url: igny8Admin.ajaxUrl, - type: 'POST', - data: { - action: 'igny8_test_connection', - nonce: igny8Admin.nonce - }, - success: function(response) { - if (response.success) { - $result.html('✓ ' + (response.data.message || 'Connection successful') + ''); - } else { - var errorMsg = response.data.message || 'Connection failed'; - var httpStatus = response.data.http_status || ''; - var fullMsg = errorMsg; - if (httpStatus) { - fullMsg += ' (HTTP ' + httpStatus + ')'; - } - $result.html('✗ ' + fullMsg + ''); - - // Log full error to console for debugging - console.error('IGNY8 Connection Test Failed:', response.data); - } - }, - error: function(xhr, status, error) { - $result.html('✗ Request failed: ' + error + ''); - console.error('IGNY8 AJAX Error:', xhr, status, error); - }, - complete: function() { - $button.prop('disabled', false).removeClass('igny8-loading'); - } - }); - }); - - // Sync posts to IGNY8 - $('#igny8-sync-posts').on('click', function() { - igny8TriggerSync('igny8_sync_posts', 'Syncing posts to IGNY8...'); - }); - - // Sync taxonomies - $('#igny8-sync-taxonomies').on('click', function() { - igny8TriggerSync('igny8_sync_taxonomies', 'Syncing taxonomies...'); - }); - - // Sync from IGNY8 - $('#igny8-sync-from-igny8').on('click', function() { - igny8TriggerSync('igny8_sync_from_igny8', 'Syncing from IGNY8...'); - }); - - // Collect and send site data - $('#igny8-collect-site-data').on('click', function() { - igny8TriggerSync('igny8_collect_site_data', 'Collecting and sending site data...'); - }); - - // Load sync statistics - igny8LoadStats(); - - // Handle row action links - $(document).on('click', '.igny8-action-link', function(e) { - e.preventDefault(); - - var $link = $(this); - var postId = $link.data('post-id'); - var action = $link.data('action'); - - if (!postId) { - return; - } - - if (!confirm('Are you sure you want to ' + (action === 'send' ? 'send' : 'update') + ' this post to IGNY8?')) { - return; - } - - $link.text('Processing...').prop('disabled', true); - - $.ajax({ - url: igny8Admin.ajaxUrl, - type: 'POST', - data: { - action: 'igny8_send_to_igny8', - post_id: postId, - action_type: action, - nonce: igny8Admin.nonce - }, - success: function(response) { - if (response.success) { - alert(response.data.message || 'Success!'); - location.reload(); - } else { - alert(response.data.message || 'Failed to send to IGNY8'); - $link.text(action === 'send' ? 'Send to IGNY8' : 'Update in IGNY8').prop('disabled', false); - } - }, - error: function() { - alert('Request failed'); - $link.text(action === 'send' ? 'Send to IGNY8' : 'Update in IGNY8').prop('disabled', false); - } - }); - }); - }); - - /** - * Trigger sync operation - */ - function igny8TriggerSync(action, message) { - var $status = $('#igny8-sync-status'); - var $button = $('#' + action.replace('igny8_', 'igny8-')); - - $status.removeClass('igny8-sync-status-success igny8-sync-status-error') - .addClass('igny8-sync-status-loading') - .html('' + message); - - $button.prop('disabled', true).addClass('igny8-loading'); - - $.ajax({ - url: igny8Admin.ajaxUrl, - type: 'POST', - data: { - action: action, - nonce: igny8Admin.nonce - }, - success: function(response) { - if (response.success) { - $status.removeClass('igny8-sync-status-loading') - .addClass('igny8-sync-status-success') - .html('✓ ' + (response.data.message || 'Operation completed successfully')); - - // Reload stats - igny8LoadStats(); - } else { - $status.removeClass('igny8-sync-status-loading') - .addClass('igny8-sync-status-error') - .html('✗ ' + (response.data.message || 'Operation failed')); - } - }, - error: function() { - $status.removeClass('igny8-sync-status-loading') - .addClass('igny8-sync-status-error') - .html('✗ Request failed'); - }, - complete: function() { - $button.prop('disabled', false).removeClass('igny8-loading'); - } - }); - } - - /** - * Load sync statistics - */ - function igny8LoadStats() { - $.ajax({ - url: igny8Admin.ajaxUrl, - type: 'POST', - data: { - action: 'igny8_get_stats', - nonce: igny8Admin.nonce - }, - success: function(response) { - if (response.success && response.data) { - if (response.data.synced_posts !== undefined) { - $('#igny8-stat-posts').text(response.data.synced_posts); - } - if (response.data.last_sync) { - $('#igny8-stat-last-sync').text(response.data.last_sync); - } - } - } - }); - } -})(jQuery); - diff --git a/igy8-wp-plugin/admin/assets/js/post-editor.js b/igy8-wp-plugin/admin/assets/js/post-editor.js deleted file mode 100644 index 2be154c1..00000000 --- a/igy8-wp-plugin/admin/assets/js/post-editor.js +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Post Editor JavaScript - * - * Handles AJAX interactions for Planner and Optimizer meta boxes - * - * @package Igny8Bridge - */ - -(function($) { - 'use strict'; - - $(document).ready(function() { - // Fetch Planner Brief - $('#igny8-fetch-brief').on('click', function() { - var $button = $(this); - var $message = $('#igny8-planner-brief-message'); - var postId = $button.data('post-id'); - var taskId = $button.data('task-id'); - - $button.prop('disabled', true).text('Fetching...'); - $message.hide().removeClass('notice-success notice-error'); - - $.ajax({ - url: igny8PostEditor.ajaxUrl, - type: 'POST', - data: { - action: 'igny8_fetch_planner_brief', - nonce: igny8PostEditor.nonce, - post_id: postId, - task_id: taskId - }, - success: function(response) { - if (response.success) { - $message.addClass('notice notice-success inline') - .html('

' + response.data.message + '

') - .show(); - - // Reload page to show updated brief - setTimeout(function() { - location.reload(); - }, 1000); - } else { - $message.addClass('notice notice-error inline') - .html('

' + (response.data.message || 'Failed to fetch brief') + '

') - .show(); - $button.prop('disabled', false).text('Fetch Brief'); - } - }, - error: function() { - $message.addClass('notice notice-error inline') - .html('

Request failed

') - .show(); - $button.prop('disabled', false).text('Fetch Brief'); - } - }); - }); - - // Refresh Planner Task - $('#igny8-refresh-task').on('click', function() { - var $button = $(this); - var $message = $('#igny8-planner-brief-message'); - var postId = $button.data('post-id'); - var taskId = $button.data('task-id'); - - if (!confirm('Are you sure you want to request a refresh of this task from IGNY8 Planner?')) { - return; - } - - $button.prop('disabled', true).text('Requesting...'); - $message.hide().removeClass('notice-success notice-error'); - - $.ajax({ - url: igny8PostEditor.ajaxUrl, - type: 'POST', - data: { - action: 'igny8_refresh_planner_task', - nonce: igny8PostEditor.nonce, - post_id: postId, - task_id: taskId - }, - success: function(response) { - if (response.success) { - $message.addClass('notice notice-success inline') - .html('

' + response.data.message + '

') - .show(); - } else { - $message.addClass('notice notice-error inline') - .html('

' + (response.data.message || 'Failed to request refresh') + '

') - .show(); - } - $button.prop('disabled', false).text('Request Refresh'); - }, - error: function() { - $message.addClass('notice notice-error inline') - .html('

Request failed

') - .show(); - $button.prop('disabled', false).text('Request Refresh'); - } - }); - }); - - // Create Optimizer Job - $('#igny8-create-optimizer-job').on('click', function() { - var $button = $(this); - var $message = $('#igny8-optimizer-message'); - var postId = $button.data('post-id'); - var taskId = $button.data('task-id'); - - if (!confirm('Create a new optimizer job for this post?')) { - return; - } - - $button.prop('disabled', true).text('Creating...'); - $message.hide().removeClass('notice-success notice-error'); - - $.ajax({ - url: igny8PostEditor.ajaxUrl, - type: 'POST', - data: { - action: 'igny8_create_optimizer_job', - nonce: igny8PostEditor.nonce, - post_id: postId, - task_id: taskId, - job_type: 'audit', - priority: 'normal' - }, - success: function(response) { - if (response.success) { - $message.addClass('notice notice-success inline') - .html('

' + response.data.message + '

') - .show(); - - // Reload page to show updated status - setTimeout(function() { - location.reload(); - }, 1000); - } else { - $message.addClass('notice notice-error inline') - .html('

' + (response.data.message || 'Failed to create job') + '

') - .show(); - $button.prop('disabled', false).text('Request Optimization'); - } - }, - error: function() { - $message.addClass('notice notice-error inline') - .html('

Request failed

') - .show(); - $button.prop('disabled', false).text('Request Optimization'); - } - }); - }); - - // Check Optimizer Status - $('#igny8-check-optimizer-status').on('click', function() { - var $button = $(this); - var $message = $('#igny8-optimizer-message'); - var postId = $button.data('post-id'); - var jobId = $button.data('job-id'); - - $button.prop('disabled', true).text('Checking...'); - $message.hide().removeClass('notice-success notice-error'); - - $.ajax({ - url: igny8PostEditor.ajaxUrl, - type: 'POST', - data: { - action: 'igny8_get_optimizer_status', - nonce: igny8PostEditor.nonce, - post_id: postId, - job_id: jobId - }, - success: function(response) { - if (response.success) { - $message.addClass('notice notice-success inline') - .html('

Status: ' + response.data.status + '

') - .show(); - - // Reload page to show updated status - setTimeout(function() { - location.reload(); - }, 1000); - } else { - $message.addClass('notice notice-error inline') - .html('

' + (response.data.message || 'Failed to get status') + '

') - .show(); - } - $button.prop('disabled', false).text('Check Status'); - }, - error: function() { - $message.addClass('notice notice-error inline') - .html('

Request failed

') - .show(); - $button.prop('disabled', false).text('Check Status'); - } - }); - }); - }); - -})(jQuery); - diff --git a/igy8-wp-plugin/admin/class-admin-columns.php b/igy8-wp-plugin/admin/class-admin-columns.php deleted file mode 100644 index 06916a8b..00000000 --- a/igy8-wp-plugin/admin/class-admin-columns.php +++ /dev/null @@ -1,306 +0,0 @@ -'; - echo esc_html($taxonomy); - echo ''; - } else { - echo ''; - } - } - - /** - * Render attribute column - * - * @param int $post_id Post ID - */ - private function render_attribute_column($post_id) { - $attribute = get_post_meta($post_id, '_igny8_attribute_id', true); - - if ($attribute) { - echo ''; - echo esc_html($attribute); - echo ''; - } else { - echo ''; - } - } - - /** - * Add custom columns - * - * @param array $columns Existing columns - * @return array Modified columns - */ - public function add_columns($columns) { - $new_columns = array(); - - foreach ($columns as $key => $value) { - $new_columns[$key] = $value; - - if ($key === 'title') { - $new_columns['igny8_taxonomy'] = __('Taxonomy', 'igny8-bridge'); - $new_columns['igny8_attribute'] = __('Attribute', 'igny8-bridge'); - } - } - - return $new_columns; - } - - /** - * Render column content - * - * @param string $column_name Column name - * @param int $post_id Post ID - */ - public function render_column_content($column_name, $post_id) { - switch ($column_name) { - case 'igny8_taxonomy': - $this->render_taxonomy_column($post_id); - break; - - case 'igny8_attribute': - $this->render_attribute_column($post_id); - break; - } - } - - /** - * Make columns sortable - * - * @param array $columns Sortable columns - * @return array Modified columns - */ - public function make_columns_sortable($columns) { - $columns['igny8_source'] = 'igny8_source'; - return $columns; - } - - /** - * Add row actions - * - * @param array $actions Existing actions - * @param WP_Post $post Post object - * @return array Modified actions - */ - public function add_row_actions($actions, $post) { - // Only add for published posts - if ($post->post_status !== 'publish') { - return $actions; - } - - // Check if already synced to IGNY8 - $task_id = get_post_meta($post->ID, '_igny8_task_id', true); - - if ($task_id) { - // Already synced - show update action - $actions['igny8_update'] = sprintf( - '%s', - '#', - $post->ID, - __('Update in IGNY8', 'igny8-bridge') - ); - } else { - // Not synced - show send action - $actions['igny8_send'] = sprintf( - '%s', - '#', - $post->ID, - __('Send to IGNY8', 'igny8-bridge') - ); - } - - return $actions; - } - - /** - * Send post to IGNY8 (AJAX handler) - */ - public static function send_to_igny8() { - check_ajax_referer('igny8_admin_nonce', 'nonce'); - - if (!current_user_can('edit_posts')) { - wp_send_json_error(array('message' => 'Unauthorized')); - } - - $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; - $action = isset($_POST['action_type']) ? sanitize_text_field($_POST['action_type']) : 'send'; - - if (!$post_id) { - wp_send_json_error(array('message' => 'Invalid post ID')); - } - - $post = get_post($post_id); - if (!$post) { - wp_send_json_error(array('message' => 'Post not found')); - } - - if (!igny8_is_connection_enabled()) { - wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - wp_send_json_error(array('message' => 'Not authenticated with IGNY8')); - } - - $site_id = get_option('igny8_site_id'); - if (!$site_id) { - wp_send_json_error(array('message' => 'Site ID not set')); - } - - // Prepare post data for IGNY8 - $post_data = array( - 'title' => $post->post_title, - 'content' => $post->post_content, - 'excerpt' => $post->post_excerpt, - 'status' => $post->post_status === 'publish' ? 'completed' : 'draft', - 'post_type' => $post->post_type, - 'url' => get_permalink($post_id), - 'wordpress_post_id' => $post_id - ); - - // Get categories - $categories = wp_get_post_categories($post_id, array('fields' => 'names')); - if (!empty($categories)) { - $post_data['categories'] = $categories; - } - - // Get tags - $tags = wp_get_post_tags($post_id, array('fields' => 'names')); - if (!empty($tags)) { - $post_data['tags'] = $tags; - } - - // Get featured image - $featured_image_id = get_post_thumbnail_id($post_id); - if ($featured_image_id) { - $post_data['featured_image'] = wp_get_attachment_image_url($featured_image_id, 'full'); - } - - // Get sectors and clusters - $sectors = wp_get_post_terms($post_id, 'igny8_sectors', array('fields' => 'ids')); - $clusters = wp_get_post_terms($post_id, 'igny8_clusters', array('fields' => 'ids')); - - if (!empty($sectors)) { - // Get IGNY8 sector IDs from term meta - $igny8_sector_ids = array(); - foreach ($sectors as $term_id) { - $igny8_sector_id = get_term_meta($term_id, '_igny8_sector_id', true); - if ($igny8_sector_id) { - $igny8_sector_ids[] = $igny8_sector_id; - } - } - if (!empty($igny8_sector_ids)) { - $post_data['sector_id'] = $igny8_sector_ids[0]; // Use first sector - } - } - - if (!empty($clusters)) { - // Get IGNY8 cluster IDs from term meta - $igny8_cluster_ids = array(); - foreach ($clusters as $term_id) { - $igny8_cluster_id = get_term_meta($term_id, '_igny8_cluster_id', true); - if ($igny8_cluster_id) { - $igny8_cluster_ids[] = $igny8_cluster_id; - } - } - if (!empty($igny8_cluster_ids)) { - $post_data['cluster_id'] = $igny8_cluster_ids[0]; // Use first cluster - } - } - - // Check if post already has task ID - $existing_task_id = get_post_meta($post_id, '_igny8_task_id', true); - - if ($existing_task_id && $action === 'update') { - // Update existing task - $response = $api->put("/writer/tasks/{$existing_task_id}/", $post_data); - } else { - // Create new task - $response = $api->post("/writer/tasks/", $post_data); - } - - if ($response['success']) { - $task_id = $response['data']['id'] ?? $existing_task_id; - - // Store task ID - update_post_meta($post_id, '_igny8_task_id', $task_id); - update_post_meta($post_id, '_igny8_last_synced', current_time('mysql')); - - wp_send_json_success(array( - 'message' => $action === 'update' ? 'Post updated in IGNY8' : 'Post sent to IGNY8', - 'task_id' => $task_id - )); - } else { - wp_send_json_error(array( - 'message' => 'Failed to send to IGNY8: ' . ($response['error'] ?? 'Unknown error') - )); - } - } -} - -// Initialize -new Igny8AdminColumns(); - -// Register AJAX handler -add_action('wp_ajax_igny8_send_to_igny8', array('Igny8AdminColumns', 'send_to_igny8')); - diff --git a/igy8-wp-plugin/admin/class-admin.php b/igy8-wp-plugin/admin/class-admin.php deleted file mode 100644 index c3dde542..00000000 --- a/igy8-wp-plugin/admin/class-admin.php +++ /dev/null @@ -1,621 +0,0 @@ - 'boolean', - 'sanitize_callback' => array($this, 'sanitize_boolean'), - 'default' => 1 - )); - - register_setting('igny8_bridge_connection', 'igny8_connection_enabled', array( - 'type' => 'boolean', - 'sanitize_callback' => array($this, 'sanitize_boolean'), - 'default' => 1 - )); - - register_setting('igny8_bridge_controls', 'igny8_enabled_post_types', array( - 'type' => 'array', - 'sanitize_callback' => array($this, 'sanitize_post_types'), - 'default' => array_keys(igny8_get_supported_post_types()) - )); - - register_setting('igny8_bridge_controls', 'igny8_enabled_taxonomies', array( - 'type' => 'array', - 'sanitize_callback' => array($this, 'sanitize_taxonomies'), - 'default' => array('category', 'post_tag', 'product_cat', 'igny8_sectors', 'igny8_clusters') - )); - - register_setting('igny8_bridge_controls', 'igny8_enable_woocommerce', array( - 'type' => 'boolean', - 'sanitize_callback' => array($this, 'sanitize_boolean'), - 'default' => class_exists('WooCommerce') ? 1 : 0 - )); - - register_setting('igny8_bridge_controls', 'igny8_control_mode', array( - 'type' => 'string', - 'sanitize_callback' => array($this, 'sanitize_control_mode'), - 'default' => 'mirror' - )); - - register_setting('igny8_bridge_controls', 'igny8_enabled_modules', array( - 'type' => 'array', - 'sanitize_callback' => array($this, 'sanitize_modules'), - 'default' => array_keys(igny8_get_available_modules()) - )); - } - - /** - * Enqueue admin scripts and styles - * - * @param string $hook Current admin page hook - */ - public function enqueue_scripts($hook) { - // Enqueue on settings page - if ($hook === 'settings_page_igny8-settings') { - wp_enqueue_style( - 'igny8-admin-style', - IGNY8_BRIDGE_PLUGIN_URL . 'admin/assets/css/admin.css', - array(), - IGNY8_BRIDGE_VERSION - ); - - wp_enqueue_script( - 'igny8-admin-script', - IGNY8_BRIDGE_PLUGIN_URL . 'admin/assets/js/admin.js', - array('jquery'), - IGNY8_BRIDGE_VERSION, - true - ); - - wp_localize_script('igny8-admin-script', 'igny8Admin', array( - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_admin_nonce'), - )); - } - - // Enqueue on post/page/product list pages - if (strpos($hook, 'edit.php') !== false) { - $screen = get_current_screen(); - if ($screen && in_array($screen->post_type, array('post', 'page', 'product', ''))) { - wp_enqueue_style( - 'igny8-admin-style', - IGNY8_BRIDGE_PLUGIN_URL . 'admin/assets/css/admin.css', - array(), - IGNY8_BRIDGE_VERSION - ); - - wp_enqueue_script( - 'igny8-admin-script', - IGNY8_BRIDGE_PLUGIN_URL . 'admin/assets/js/admin.js', - array('jquery'), - IGNY8_BRIDGE_VERSION, - true - ); - - wp_localize_script('igny8-admin-script', 'igny8Admin', array( - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_admin_nonce'), - )); - } - } - } - - /** - * Render settings page - */ - public function render_settings_page() { - // Handle form submission (use wp_verify_nonce to avoid wp_die on failure) - if (isset($_POST['igny8_connect'])) { - if (empty($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'igny8_settings_nonce')) { - add_settings_error( - 'igny8_settings', - 'igny8_nonce', - __('Security check failed. Please refresh the page and try again.', 'igny8-bridge'), - 'error' - ); - } else { - $this->handle_connection(); - } - } - - // Handle revoke API key (use wp_verify_nonce) - if (isset($_POST['igny8_revoke_api_key'])) { - if (empty($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'igny8_revoke_api_key')) { - add_settings_error( - 'igny8_settings', - 'igny8_nonce_revoke', - __('Security check failed. Could not revoke API key.', 'igny8-bridge'), - 'error' - ); - } else { - self::revoke_api_key(); - add_settings_error( - 'igny8_settings', - 'igny8_api_key_revoked', - __('API key revoked and removed from this site.', 'igny8-bridge'), - 'updated' - ); - } - } - - // Webhook secret regeneration removed - using API key only - - // Include settings template - include IGNY8_BRIDGE_PLUGIN_DIR . 'admin/settings.php'; - } - - /** - * Handle API connection - API key only - * Calls /v1/integration/integrations/test-connection/ endpoint - */ - private function handle_connection() { - $api_key = sanitize_text_field($_POST['igny8_api_key'] ?? ''); - $site_id = sanitize_text_field($_POST['igny8_site_id'] ?? ''); - - // API key is required - if (empty($api_key)) { - add_settings_error( - 'igny8_settings', - 'igny8_error', - __('API key is required to connect to IGNY8.', 'igny8-bridge'), - 'error' - ); - return; - } - - // Site ID is required - if (empty($site_id)) { - add_settings_error( - 'igny8_settings', - 'igny8_error', - __('Site ID is required. Create a site in IGNY8 app first.', 'igny8-bridge'), - 'error' - ); - return; - } - - // Get site URL - $site_url = get_site_url(); - - // Test connection using the correct integration test endpoint - // The API class will handle authentication for test-connection endpoint - // by using the API key from the request body - $api = new Igny8API(); - - $test_response = $api->post('/v1/integration/integrations/test-connection/', array( - 'site_id' => (int) $site_id, - 'api_key' => $api_key, - 'site_url' => $site_url - )); - - if (!$test_response['success']) { - $error_message = $test_response['error'] ?? 'Unknown error'; - - // Provide more user-friendly message for throttling errors - if (isset($test_response['http_status']) && $test_response['http_status'] === 429) { - $error_message = __('Rate limit exceeded. The plugin will automatically retry, but if this persists, please wait a moment and try again.', 'igny8-bridge'); - } - - add_settings_error( - 'igny8_settings', - 'igny8_error', - sprintf( - __('Failed to connect to IGNY8 API: %s', 'igny8-bridge'), - $error_message - ), - 'error' - ); - return; - } - - // Store API key securely - if (function_exists('igny8_store_secure_option')) { - igny8_store_secure_option('igny8_api_key', $api_key); - igny8_store_secure_option('igny8_access_token', $api_key); - } else { - update_option('igny8_api_key', $api_key); - update_option('igny8_access_token', $api_key); - } - - // Store site ID - update_option('igny8_site_id', sanitize_text_field($site_id)); - - add_settings_error( - 'igny8_settings', - 'igny8_connected', - __('Successfully connected to IGNY8 API. Site registered.', 'igny8-bridge'), - 'updated' - ); - - // Sync site structure to backend (post types, taxonomies, etc.) - igny8_sync_site_structure_to_backend(); - } - - /** - * Revoke stored API key (secure delete) - * - * Public so tests can call it directly. - */ - public static function revoke_api_key() { - if (function_exists('igny8_delete_secure_option')) { - igny8_delete_secure_option('igny8_api_key'); - igny8_delete_secure_option('igny8_access_token'); - igny8_delete_secure_option('igny8_refresh_token'); - } else { - delete_option('igny8_api_key'); - delete_option('igny8_access_token'); - delete_option('igny8_refresh_token'); - } - - // Also clear token-issued timestamps - delete_option('igny8_token_refreshed_at'); - delete_option('igny8_access_token_issued'); - } - - /** - * Test API connection (AJAX handler) - */ - public static function test_connection() { - check_ajax_referer('igny8_admin_nonce', 'nonce'); - - if (!current_user_can('manage_options')) { - wp_send_json_error(array('message' => 'Unauthorized')); - } - - if (!igny8_is_connection_enabled()) { - wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations to test.')); - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - wp_send_json_error(array('message' => 'Not authenticated')); - } - - // Get site ID - $site_id = get_option('igny8_site_id'); - if (empty($site_id)) { - wp_send_json_error(array('message' => 'Site ID not configured. Connect to IGNY8 first.')); - } - - // Test connection using the integration test endpoint - $api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key'); - - $test_response = $api->post('/v1/integration/integrations/test-connection/', array( - 'site_id' => (int) $site_id, - 'api_key' => $api_key, - 'site_url' => get_site_url() - )); - - if ($test_response['success']) { - $checked_at = current_time('timestamp'); - update_option('igny8_last_api_health_check', $checked_at); - wp_send_json_success(array( - 'message' => __('Connection successful! IGNY8 API is responsive.', 'igny8-bridge'), - 'checked_at' => $checked_at - )); - return; - } - - // Connection failed - $error_message = $test_response['error'] ?? 'Unknown error'; - wp_send_json_error(array( - 'message' => __('Connection failed: ', 'igny8-bridge') . $error_message, - 'http_status' => $test_response['http_status'] ?? 0, - )); - } - - /** - * Sync posts to IGNY8 (AJAX handler) - */ - public static function sync_posts() { - check_ajax_referer('igny8_admin_nonce', 'nonce'); - - if (!current_user_can('manage_options')) { - wp_send_json_error(array('message' => 'Unauthorized')); - } - - if (!igny8_is_connection_enabled()) { - wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); - } - - $result = igny8_batch_sync_post_statuses(); - - wp_send_json_success(array( - 'message' => sprintf('Synced %d posts, %d failed', $result['synced'], $result['failed']), - 'data' => $result - )); - } - - /** - * Sync taxonomies (AJAX handler) - */ - public static function sync_taxonomies() { - check_ajax_referer('igny8_admin_nonce', 'nonce'); - - if (!current_user_can('manage_options')) { - wp_send_json_error(array('message' => 'Unauthorized')); - } - - if (!igny8_is_connection_enabled()) { - wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); - } - - $api = new Igny8API(); - if (!$api->is_authenticated()) { - wp_send_json_error(array('message' => 'Not authenticated')); - } - - // Sync sectors and clusters from IGNY8 - $sectors_result = igny8_sync_igny8_sectors_to_wp(); - $clusters_result = igny8_sync_igny8_clusters_to_wp(); - - wp_send_json_success(array( - 'message' => sprintf('Synced %d sectors, %d clusters', - $sectors_result['synced'] ?? 0, - $clusters_result['synced'] ?? 0 - ), - 'data' => array( - 'sectors' => $sectors_result, - 'clusters' => $clusters_result - ) - )); - } - - /** - * Sync from IGNY8 (AJAX handler) - */ - public static function sync_from_igny8() { - check_ajax_referer('igny8_admin_nonce', 'nonce'); - - if (!current_user_can('manage_options')) { - wp_send_json_error(array('message' => 'Unauthorized')); - } - - if (!igny8_is_connection_enabled()) { - wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); - } - - $result = igny8_sync_igny8_tasks_to_wp(); - - if ($result['success']) { - wp_send_json_success(array( - 'message' => sprintf('Created %d posts, updated %d posts', - $result['created'], - $result['updated'] - ), - 'data' => $result - )); - } else { - wp_send_json_error(array( - 'message' => $result['error'] ?? 'Sync failed' - )); - } - } - - /** - * Collect and send site data (AJAX handler) - */ - public static function collect_site_data() { - check_ajax_referer('igny8_admin_nonce', 'nonce'); - - if (!current_user_can('manage_options')) { - wp_send_json_error(array('message' => 'Unauthorized')); - } - - if (!igny8_is_connection_enabled()) { - wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); - } - - $site_id = get_option('igny8_site_id'); - if (!$site_id) { - wp_send_json_error(array('message' => 'Site ID not set')); - } - - $result = igny8_send_site_data_to_igny8($site_id); - - if ($result) { - wp_send_json_success(array( - 'message' => 'Site data collected and sent successfully', - 'data' => $result - )); - } else { - wp_send_json_error(array('message' => 'Failed to send site data')); - } - } - - /** - * Get sync statistics (AJAX handler) - */ - public static function get_stats() { - check_ajax_referer('igny8_admin_nonce', 'nonce'); - - if (!current_user_can('manage_options')) { - wp_send_json_error(array('message' => 'Unauthorized')); - } - - global $wpdb; - - // Count synced posts - $synced_posts = $wpdb->get_var(" - SELECT COUNT(DISTINCT post_id) - FROM {$wpdb->postmeta} - WHERE meta_key = '_igny8_task_id' - "); - - // Get last sync time - $last_sync = get_option('igny8_last_site_sync', 0); - $last_sync_formatted = $last_sync ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $last_sync) : 'Never'; - - wp_send_json_success(array( - 'synced_posts' => intval($synced_posts), - 'last_sync' => $last_sync_formatted - )); - } - - /** - * Sanitize post types option - * - * @param mixed $value Raw value - * @return array - */ - public function sanitize_post_types($value) { - $supported = array_keys(igny8_get_supported_post_types()); - - if (!is_array($value)) { - return $supported; - } - - $clean = array(); - foreach ($value as $post_type) { - $post_type = sanitize_key($post_type); - if (in_array($post_type, $supported, true)) { - $clean[] = $post_type; - } - } - - return !empty($clean) ? $clean : $supported; - } - - /** - * Sanitize taxonomies option - * - * @param mixed $value Raw value - * @return array - */ - public function sanitize_taxonomies($value) { - $supported = array_keys(igny8_get_supported_taxonomies()); - - if (!is_array($value)) { - return array('category', 'post_tag', 'product_cat', 'igny8_sectors', 'igny8_clusters'); - } - - $clean = array(); - foreach ($value as $taxonomy) { - $taxonomy = sanitize_key($taxonomy); - if (in_array($taxonomy, $supported, true)) { - $clean[] = $taxonomy; - } - } - - // Return defaults if nothing selected - return !empty($clean) ? $clean : array('category', 'post_tag'); - } - - /** - * Sanitize boolean option - * - * @param mixed $value Raw value - * @return int - */ - public function sanitize_boolean($value) { - return $value ? 1 : 0; - } - - /** - * Sanitize control mode - * - * @param mixed $value Raw value - * @return string - */ - public function sanitize_control_mode($value) { - $value = is_string($value) ? strtolower($value) : 'mirror'; - return in_array($value, array('mirror', 'hybrid'), true) ? $value : 'mirror'; - } - - /** - * Sanitize module toggles - * - * @param mixed $value Raw value - * @return array - */ - public function sanitize_modules($value) { - $supported = array_keys(igny8_get_available_modules()); - - if (!is_array($value)) { - return $supported; - } - - $clean = array(); - foreach ($value as $module) { - $module = sanitize_key($module); - if (in_array($module, $supported, true)) { - $clean[] = $module; - } - } - - return !empty($clean) ? $clean : $supported; - } -} - -// Register AJAX handlers -add_action('wp_ajax_igny8_test_connection', array('Igny8Admin', 'test_connection')); -add_action('wp_ajax_igny8_sync_posts', array('Igny8Admin', 'sync_posts')); -add_action('wp_ajax_igny8_sync_taxonomies', array('Igny8Admin', 'sync_taxonomies')); -add_action('wp_ajax_igny8_sync_from_igny8', array('Igny8Admin', 'sync_from_igny8')); -add_action('wp_ajax_igny8_collect_site_data', array('Igny8Admin', 'collect_site_data')); -add_action('wp_ajax_igny8_get_stats', array('Igny8Admin', 'get_stats')); - diff --git a/igy8-wp-plugin/admin/class-post-meta-boxes.php b/igy8-wp-plugin/admin/class-post-meta-boxes.php deleted file mode 100644 index d0d6ebcb..00000000 --- a/igy8-wp-plugin/admin/class-post-meta-boxes.php +++ /dev/null @@ -1,469 +0,0 @@ - admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_post_editor_nonce'), - )); - } - - /** - * Render Planner Brief meta box - */ - public function render_planner_brief_box($post) { - $task_id = get_post_meta($post->ID, '_igny8_task_id', true); - $brief = get_post_meta($post->ID, '_igny8_task_brief', true); - $brief_cached_at = get_post_meta($post->ID, '_igny8_brief_cached_at', true); - $cluster_id = get_post_meta($post->ID, '_igny8_cluster_id', true); - - if (!$task_id && !$cluster_id) { - echo '

'; - _e('This post is not linked to an IGNY8 task or cluster.', 'igny8-bridge'); - echo '

'; - return; - } - - wp_nonce_field('igny8_post_editor_nonce', 'igny8_post_editor_nonce'); - ?> -
- -
- - -

- - - -
- -
- - - -
- - -
    - -
  • - -
- -

- -
- - - -
- - '; - foreach ($keywords as $keyword) { - echo '' . esc_html(trim($keyword)) . ''; - } - echo ''; - ?> -
- - - -
- - -
- - -

- - - -

- -

- -
- -

- -

- -
- -

- - - - - -

- - - ID, '_igny8_task_id', true); - $optimizer_job_id = get_post_meta($post->ID, '_igny8_optimizer_job_id', true); - $optimizer_status = get_post_meta($post->ID, '_igny8_optimizer_status', true); - - if (!$task_id) { - echo '

'; - _e('This post is not linked to an IGNY8 task.', 'igny8-bridge'); - echo '

'; - return; - } - - wp_nonce_field('igny8_post_editor_nonce', 'igny8_post_editor_nonce'); - ?> -
- -
-

- - -

- - -

- - - - -

- - -

- -

-
- -

- -

- -
- -

- -

- - - 'Unauthorized')); - } - - if (!igny8_is_connection_enabled()) { - wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); - } - - $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; - $task_id = isset($_POST['task_id']) ? intval($_POST['task_id']) : 0; - - if (!$post_id || !$task_id) { - wp_send_json_error(array('message' => 'Invalid post ID or task ID')); - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - wp_send_json_error(array('message' => 'Not authenticated')); - } - - // Try to fetch from Planner first - $response = $api->get("/planner/tasks/{$task_id}/brief/"); - - if (!$response['success']) { - // Fallback to Writer brief - $response = $api->get("/writer/tasks/{$task_id}/brief/"); - } - - if ($response['success'] && !empty($response['data'])) { - update_post_meta($post_id, '_igny8_task_brief', $response['data']); - update_post_meta($post_id, '_igny8_brief_cached_at', current_time('mysql')); - - wp_send_json_success(array( - 'message' => 'Brief fetched successfully', - 'brief' => $response['data'] - )); - } else { - wp_send_json_error(array( - 'message' => 'Failed to fetch brief: ' . ($response['error'] ?? 'Unknown error') - )); - } - } - - /** - * Refresh Planner task (AJAX handler) - */ - public static function refresh_planner_task() { - check_ajax_referer('igny8_post_editor_nonce', 'nonce'); - - if (!current_user_can('edit_posts')) { - wp_send_json_error(array('message' => 'Unauthorized')); - } - - if (!igny8_is_connection_enabled()) { - wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); - } - - $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; - $task_id = isset($_POST['task_id']) ? intval($_POST['task_id']) : 0; - - if (!$post_id || !$task_id) { - wp_send_json_error(array('message' => 'Invalid post ID or task ID')); - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - wp_send_json_error(array('message' => 'Not authenticated')); - } - - $response = $api->post("/planner/tasks/{$task_id}/refresh/", array( - 'wordpress_post_id' => $post_id, - 'reason' => 'reoptimize', - 'notes' => 'Requested refresh from WordPress editor' - )); - - if ($response['success']) { - wp_send_json_success(array( - 'message' => 'Refresh requested successfully', - 'data' => $response['data'] - )); - } else { - wp_send_json_error(array( - 'message' => 'Failed to request refresh: ' . ($response['error'] ?? 'Unknown error') - )); - } - } - - /** - * Create Optimizer job (AJAX handler) - */ - public static function create_optimizer_job() { - check_ajax_referer('igny8_post_editor_nonce', 'nonce'); - - if (!current_user_can('edit_posts')) { - wp_send_json_error(array('message' => 'Unauthorized')); - } - - if (!igny8_is_connection_enabled()) { - wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); - } - - $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; - $task_id = isset($_POST['task_id']) ? intval($_POST['task_id']) : 0; - $job_type = isset($_POST['job_type']) ? sanitize_text_field($_POST['job_type']) : 'audit'; - $priority = isset($_POST['priority']) ? sanitize_text_field($_POST['priority']) : 'normal'; - - if (!$post_id || !$task_id) { - wp_send_json_error(array('message' => 'Invalid post ID or task ID')); - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - wp_send_json_error(array('message' => 'Not authenticated')); - } - - $response = $api->post("/optimizer/jobs/", array( - 'post_id' => $post_id, - 'task_id' => $task_id, - 'job_type' => $job_type, - 'priority' => $priority - )); - - if ($response['success'] && !empty($response['data'])) { - $job_id = $response['data']['id'] ?? $response['data']['job_id'] ?? null; - - if ($job_id) { - update_post_meta($post_id, '_igny8_optimizer_job_id', $job_id); - update_post_meta($post_id, '_igny8_optimizer_status', $response['data']['status'] ?? 'pending'); - update_post_meta($post_id, '_igny8_optimizer_job_created_at', current_time('mysql')); - } - - wp_send_json_success(array( - 'message' => 'Optimizer job created successfully', - 'job_id' => $job_id, - 'data' => $response['data'] - )); - } else { - wp_send_json_error(array( - 'message' => 'Failed to create optimizer job: ' . ($response['error'] ?? 'Unknown error') - )); - } - } - - /** - * Get Optimizer job status (AJAX handler) - */ - public static function get_optimizer_status() { - check_ajax_referer('igny8_post_editor_nonce', 'nonce'); - - if (!current_user_can('edit_posts')) { - wp_send_json_error(array('message' => 'Unauthorized')); - } - - if (!igny8_is_connection_enabled()) { - wp_send_json_error(array('message' => 'Connection is disabled. Enable sync operations first.')); - } - - $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; - $job_id = isset($_POST['job_id']) ? intval($_POST['job_id']) : 0; - - if (!$post_id || !$job_id) { - wp_send_json_error(array('message' => 'Invalid post ID or job ID')); - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - wp_send_json_error(array('message' => 'Not authenticated')); - } - - $response = $api->get("/optimizer/jobs/{$job_id}/"); - - if ($response['success'] && !empty($response['data'])) { - $status = $response['data']['status'] ?? 'unknown'; - update_post_meta($post_id, '_igny8_optimizer_status', $status); - - if (!empty($response['data']['score_changes'])) { - update_post_meta($post_id, '_igny8_optimizer_score_changes', $response['data']['score_changes']); - } - - if (!empty($response['data']['recommendations'])) { - update_post_meta($post_id, '_igny8_optimizer_recommendations', $response['data']['recommendations']); - } - - wp_send_json_success(array( - 'message' => 'Status retrieved successfully', - 'status' => $status, - 'data' => $response['data'] - )); - } else { - wp_send_json_error(array( - 'message' => 'Failed to get status: ' . ($response['error'] ?? 'Unknown error') - )); - } - } -} - -// Initialize -new Igny8PostMetaBoxes(); - diff --git a/igy8-wp-plugin/admin/settings.php b/igy8-wp-plugin/admin/settings.php deleted file mode 100644 index cf791246..00000000 --- a/igy8-wp-plugin/admin/settings.php +++ /dev/null @@ -1,771 +0,0 @@ - 10)); - $two_way_sync = (int) get_option('igny8_enable_two_way_sync', 1); - -?> - -
-

- - -
-

-
-
- -

-
- -
-
-

- - - - -

- - -
-
-
- - -
-
- -
- - -
- - -

- -

-
    -
  • -
  • -
  • -
-
- -
- - -

- IGNY8 app integrations page. It starts with "sk_live_" or "sk_test_".', 'igny8-bridge'), - 'https://app.igny8.com/sites/5/settings?tab=integrations' - ); ?> -

-
- -
- -
-
- - -
-
-
- - -
-
- -
- -
- - -
-
- -
- -
- -
-
- -
-
- - -
-
- -
- - -
-

- - - - -

- -
- - - - - - - -
- - - -

- -

-
- - -
- -
- - -
- -

- : - -

- -
-
-

- - - - -

-
-
-
-
-

- -

-
-
-
-
-

- -

-
-
-
-
-

- -

-
-
-
-
-

- -

-
-
-
-
-

- -

-
-
-
-
-

- -

-
-
-
-
-

- -

-
-
-
-
-

- -

-
-
-
-
-
-
-
- -
-

-

- - - -

-

- -

-
- - -
-

- - - - -

- -
- - - - - - - - - - - - - - - - - - - - - - - -
- $label) : ?> - -
- -

- -

-
- $module_label) : ?> - -
- -

- -

-
- -
- -
- - -

- -

- -
- $taxonomy_label) : ?> - -
- -

- -

-
- - -
- -

- -

-
- - -
-

- - - - -

- - - - - - - - - - -
- - -

- -

-
- - -
- - -
-

- -

-
-
- - -
-

- - - - -

-

- -

-

- -

-
- - -
-

- - - - -

- - -
-

-
- -

-
- - - publish + $page_count->publish; - if ($product_count) $total_posts += $product_count->publish; - - $taxonomies = get_taxonomies(['public' => true], 'objects'); - $taxonomy_count = 0; - foreach ($taxonomies as $taxonomy) { - $taxonomy_count += wp_count_terms(['taxonomy' => $taxonomy->name, 'hide_empty' => false]); - } - ?> - -
-
-
- - - -
-

-

- publish, - $page_count->publish, - $product_count ? sprintf(', %d products', $product_count->publish) : '' - ); ?> -

-

- - - - - -

- -
- -
-
- - - -
-

-

- -

-

- - - - - -

- -
- -
-
- - - -
-

-

- -

-

- - - - - -

- -
- -
-
- - - -
-

-

- -

-

- - - - - -

- -
-
- -
-
- -
-

- -
-
-
- - - -
-
-
-
-
publish, $page_count->publish); ?>
-
-
- -
-
- - - -
-
-
-
-
-
-
- -
-
- - - -
-
-
-
- - - - - -
-
- - - - - -
-
-
- -
-
- - - -
-
-
-
- - - - - -
-
- - - - - -
-
-
-
- - -
-

-
-
- - -
-
- - -
-
- - -
-
-
- -
- -
-
- diff --git a/igy8-wp-plugin/data/link-graph.php b/igy8-wp-plugin/data/link-graph.php deleted file mode 100644 index 55011eff..00000000 --- a/igy8-wp-plugin/data/link-graph.php +++ /dev/null @@ -1,192 +0,0 @@ -post_content; - $source_url = get_permalink($post_id); - $site_url = get_site_url(); - $links = array(); - - // Match all anchor tags with href attributes - preg_match_all('/]+href=["\']([^"\']+)["\'][^>]*>(.*?)<\/a>/is', $content, $matches, PREG_SET_ORDER); - - foreach ($matches as $match) { - $href = $match[1]; - $anchor = strip_tags($match[2]); - - // Skip empty anchors - if (empty(trim($anchor))) { - continue; - } - - // Only process internal links - if (strpos($href, $site_url) === 0 || strpos($href, '/') === 0) { - // Convert relative URLs to absolute - if (strpos($href, '/') === 0 && strpos($href, '//') !== 0) { - $href = $site_url . $href; - } - - // Normalize URL (remove trailing slash, fragments, query params for matching) - $target_url = rtrim($href, '/'); - - // Skip if source and target are the same - if ($source_url === $target_url) { - continue; - } - - $links[] = array( - 'source_url' => $source_url, - 'target_url' => $target_url, - 'anchor' => trim($anchor), - 'post_id' => $post_id - ); - } - } - - return $links; -} - -/** - * Extract link graph from all posts - * - * @param array $post_ids Optional array of post IDs to process. If empty, processes all enabled posts. - * @return array Link graph array - */ -function igny8_extract_link_graph($post_ids = array()) { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - return array(); - } - - if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('linker')) { - return array(); - } - - $enabled_post_types = igny8_get_enabled_post_types(); - - if (empty($post_ids)) { - // Get all published posts of enabled types - $query_args = array( - 'post_type' => $enabled_post_types, - 'post_status' => 'publish', - 'posts_per_page' => -1, - 'fields' => 'ids', - 'suppress_filters' => true - ); - - $post_ids = get_posts($query_args); - } - - $link_graph = array(); - $processed = 0; - - foreach ($post_ids as $post_id) { - $links = igny8_extract_post_links($post_id); - - if (!empty($links)) { - $link_graph = array_merge($link_graph, $links); - } - - $processed++; - - // Limit processing to prevent timeout (can be increased or made configurable) - if ($processed >= 1000) { - break; - } - } - - return $link_graph; -} - -/** - * Send link graph to IGNY8 Linker module - * - * @param int $site_id IGNY8 site ID - * @param array $link_graph Link graph array (optional, will extract if not provided) - * @return array|false Response data or false on failure - */ -function igny8_send_link_graph_to_igny8($site_id, $link_graph = null) { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - return false; - } - - if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('linker')) { - return false; - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - return false; - } - - // Extract link graph if not provided - if ($link_graph === null) { - $link_graph = igny8_extract_link_graph(); - } - - if (empty($link_graph)) { - return array('success' => true, 'message' => 'No links found', 'links_count' => 0); - } - - // Send in batches (max 500 links per batch) - $batch_size = 500; - $batches = array_chunk($link_graph, $batch_size); - $total_sent = 0; - $errors = array(); - - foreach ($batches as $batch) { - $response = $api->post("/linker/link-map/", array( - 'site_id' => $site_id, - 'links' => $batch, - 'total_links' => count($link_graph), - 'batch_number' => count($batches) > 1 ? (count($batches) - count($batches) + array_search($batch, $batches) + 1) : 1, - 'total_batches' => count($batches) - )); - - if ($response['success']) { - $total_sent += count($batch); - } else { - $errors[] = $response['error'] ?? 'Unknown error'; - } - } - - if ($total_sent > 0) { - update_option('igny8_last_link_graph_sync', current_time('timestamp')); - update_option('igny8_last_link_graph_count', $total_sent); - - return array( - 'success' => true, - 'links_sent' => $total_sent, - 'total_links' => count($link_graph), - 'batches' => count($batches), - 'errors' => $errors - ); - } - - return false; -} - diff --git a/igy8-wp-plugin/data/semantic-mapping.php b/igy8-wp-plugin/data/semantic-mapping.php deleted file mode 100644 index 1aeb58aa..00000000 --- a/igy8-wp-plugin/data/semantic-mapping.php +++ /dev/null @@ -1,225 +0,0 @@ - false, 'error' => 'Connection disabled'); - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - return array('success' => false, 'error' => 'Not authenticated'); - } - - // Extract semantic structure from site data - $semantic_map = array( - 'sectors' => array(), - 'clusters' => array(), - 'keywords' => array() - ); - - // Map taxonomies to sectors - foreach ($site_data['taxonomies'] as $tax_name => $tax_data) { - if ($tax_data['taxonomy']['hierarchical']) { - // Hierarchical taxonomies (categories) become sectors - $sector = array( - 'name' => $tax_data['taxonomy']['label'], - 'slug' => $tax_data['taxonomy']['name'], - 'description' => $tax_data['taxonomy']['description'], - 'source' => 'wordpress_taxonomy', - 'source_id' => $tax_name - ); - - // Map terms to clusters - $clusters = array(); - foreach ($tax_data['terms'] as $term) { - $clusters[] = array( - 'name' => $term['name'], - 'slug' => $term['slug'], - 'description' => $term['description'], - 'source' => 'wordpress_term', - 'source_id' => $term['id'] - ); - - // Extract keywords from posts in this term - $keywords = igny8_extract_keywords_from_term_posts($term['id'], $tax_name); - $semantic_map['keywords'] = array_merge($semantic_map['keywords'], $keywords); - } - - $sector['clusters'] = $clusters; - $semantic_map['sectors'][] = $sector; - } - } - - // Map WooCommerce product categories to sectors - if (!empty($site_data['product_categories'])) { - $product_sector = array( - 'name' => 'Products', - 'slug' => 'products', - 'description' => 'WooCommerce product categories', - 'source' => 'woocommerce', - 'clusters' => array() - ); - - foreach ($site_data['product_categories'] as $category) { - $product_sector['clusters'][] = array( - 'name' => $category['name'], - 'slug' => $category['slug'], - 'description' => $category['description'], - 'source' => 'woocommerce_category', - 'source_id' => $category['id'] - ); - } - - $semantic_map['sectors'][] = $product_sector; - } - - // Send semantic map to IGNY8 - $response = $api->post("/planner/sites/{$site_id}/semantic-map/", array( - 'semantic_map' => $semantic_map, - 'site_data' => $site_data - )); - - return $response; -} - -/** - * Extract keywords from posts associated with a taxonomy term - * - * @param int $term_id Term ID - * @param string $taxonomy Taxonomy name - * @return array Formatted keywords array - */ -function igny8_extract_keywords_from_term_posts($term_id, $taxonomy) { - $args = array( - 'post_type' => 'any', - 'posts_per_page' => -1, - 'tax_query' => array( - array( - 'taxonomy' => $taxonomy, - 'field' => 'term_id', - 'terms' => $term_id - ) - ) - ); - - $query = new WP_Query($args); - $keywords = array(); - - if ($query->have_posts()) { - while ($query->have_posts()) { - $query->the_post(); - - // Extract keywords from post title and content - $title_words = str_word_count(get_the_title(), 1); - $content_words = str_word_count(strip_tags(get_the_content()), 1); - - // Combine and get unique keywords - $all_words = array_merge($title_words, $content_words); - $unique_words = array_unique(array_map('strtolower', $all_words)); - - // Filter out common words (stop words) - $stop_words = array('the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'); - $keywords = array_merge($keywords, array_diff($unique_words, $stop_words)); - } - wp_reset_postdata(); - } - - // Format keywords - $formatted_keywords = array(); - foreach (array_unique($keywords) as $keyword) { - if (strlen($keyword) > 3) { // Only keywords longer than 3 characters - $formatted_keywords[] = array( - 'keyword' => $keyword, - 'source' => 'wordpress_post', - 'source_term_id' => $term_id - ); - } - } - - return $formatted_keywords; -} - -/** - * Complete workflow: Fetch site data → Map to semantic strategy → Restructure content - * - * @param int $site_id IGNY8 site ID - * @return array|false Analysis result or false on failure - */ -function igny8_analyze_and_restructure_site($site_id) { - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - return false; - } - - // Step 1: Collect all site data - $site_data = igny8_collect_site_data(); - - // Step 2: Send to IGNY8 for analysis - $analysis_response = $api->post("/system/sites/{$site_id}/analyze/", array( - 'site_data' => $site_data, - 'analysis_type' => 'full_site_restructure' - )); - - if (!$analysis_response['success']) { - return false; - } - - $analysis_id = $analysis_response['data']['analysis_id'] ?? null; - - // Step 3: Map to semantic strategy - $mapping_response = igny8_map_site_to_semantic_strategy($site_id, $site_data); - - if (!$mapping_response['success']) { - return false; - } - - // Step 4: Get restructuring recommendations - $recommendations_response = $api->get("/system/sites/{$site_id}/recommendations/"); - - if (!$recommendations_response['success']) { - return false; - } - - // Get keywords count from mapping response - $keywords_count = 0; - if (isset($mapping_response['data']['keywords'])) { - $keywords_count = count($mapping_response['data']['keywords']); - } - - return array( - 'analysis_id' => $analysis_id, - 'semantic_map' => $mapping_response['data'] ?? null, - 'recommendations' => $recommendations_response['data'] ?? null, - 'site_data_summary' => array( - 'total_posts' => count($site_data['posts']), - 'total_taxonomies' => count($site_data['taxonomies']), - 'total_products' => count($site_data['products'] ?? array()), - 'total_keywords' => $keywords_count - ) - ); -} - diff --git a/igy8-wp-plugin/data/site-collection.php b/igy8-wp-plugin/data/site-collection.php deleted file mode 100644 index a5469734..00000000 --- a/igy8-wp-plugin/data/site-collection.php +++ /dev/null @@ -1,588 +0,0 @@ - 'publish', - 'after' => null, - 'max_pages' => 5, - ); - $args = wp_parse_args($args, $defaults); - - $post_type_object = get_post_type_object($post_type); - $rest_base = ($post_type_object && !empty($post_type_object->rest_base)) ? $post_type_object->rest_base : $post_type; - - $base_url = sprintf('%s/wp-json/wp/v2/%s', get_site_url(), $rest_base); - - $query_args = array( - 'per_page' => min($per_page, 100), - 'status' => $args['status'], - 'orderby' => 'modified', - 'order' => 'desc', - ); - - if (!empty($args['after'])) { - $query_args['after'] = gmdate('c', $args['after']); - } - - $formatted_posts = array(); - $page = 1; - - do { - $query_args['page'] = $page; - $response = wp_remote_get(add_query_arg($query_args, $base_url)); - - if (is_wp_error($response)) { - break; - } - - $posts = json_decode(wp_remote_retrieve_body($response), true); - - if (!is_array($posts) || empty($posts)) { - break; - } - - foreach ($posts as $post) { - $content = $post['content']['rendered'] ?? ''; - $word_count = str_word_count(strip_tags($content)); - - $formatted_posts[] = array( - 'id' => $post['id'], - 'title' => html_entity_decode($post['title']['rendered'] ?? ''), - 'content' => $content, - 'excerpt' => $post['excerpt']['rendered'] ?? '', - 'status' => $post['status'] ?? 'draft', - 'url' => $post['link'] ?? '', - 'published' => $post['date'] ?? '', - 'modified' => $post['modified'] ?? '', - 'author' => $post['author'] ?? 0, - 'post_type' => $post['type'] ?? $post_type, - 'taxonomies' => array( - 'categories' => $post['categories'] ?? array(), - 'tags' => $post['tags'] ?? array(), - ), - 'meta' => array( - 'word_count' => $word_count, - 'reading_time' => $word_count ? ceil($word_count / 200) : 0, - 'featured_media' => $post['featured_media'] ?? 0, - ) - ); - } - - if (count($posts) < $query_args['per_page']) { - break; - } - - $page++; - } while ($page <= $args['max_pages']); - - return $formatted_posts; -} - -/** - * Fetch all available post types from WordPress - * - * @return array|false Post types array or false on failure - */ -function igny8_fetch_all_post_types() { - $wp_response = wp_remote_get(get_site_url() . '/wp-json/wp/v2/types'); - - if (is_wp_error($wp_response)) { - return false; - } - - $types = json_decode(wp_remote_retrieve_body($wp_response), true); - - if (!is_array($types)) { - return false; - } - - $post_types = array(); - foreach ($types as $type_name => $type_data) { - if ($type_data['public']) { - $post_types[] = array( - 'name' => $type_name, - 'label' => $type_data['name'], - 'description' => $type_data['description'] ?? '', - 'rest_base' => $type_data['rest_base'] ?? $type_name - ); - } - } - - return $post_types; -} - -/** - * Fetch all posts from all post types - * - * @param int $per_page Posts per page - * @return array All posts - */ -function igny8_fetch_all_wordpress_posts($per_page = 100) { - $post_types = igny8_fetch_all_post_types(); - - if (!$post_types) { - return array(); - } - - $all_posts = array(); - foreach ($post_types as $type) { - $posts = igny8_fetch_wordpress_posts($type['name'], $per_page); - if ($posts) { - $all_posts = array_merge($all_posts, $posts); - } - } - - return $all_posts; -} - -/** - * Fetch all taxonomies from WordPress - * - * @return array|false Taxonomies array or false on failure - */ -function igny8_fetch_wordpress_taxonomies() { - $wp_response = wp_remote_get(get_site_url() . '/wp-json/wp/v2/taxonomies'); - - if (is_wp_error($wp_response)) { - return false; - } - - $taxonomies = json_decode(wp_remote_retrieve_body($wp_response), true); - - if (!is_array($taxonomies)) { - return false; - } - - $formatted_taxonomies = array(); - foreach ($taxonomies as $tax_name => $tax_data) { - if ($tax_data['public']) { - $formatted_taxonomies[] = array( - 'name' => $tax_name, - 'label' => $tax_data['name'], - 'description' => $tax_data['description'] ?? '', - 'hierarchical' => $tax_data['hierarchical'], - 'rest_base' => $tax_data['rest_base'] ?? $tax_name, - 'object_types' => $tax_data['types'] ?? array() - ); - } - } - - return $formatted_taxonomies; -} - -/** - * Fetch all terms for a specific taxonomy - * - * @param string $taxonomy Taxonomy name - * @param int $per_page Terms per page - * @return array|false Formatted terms array or false on failure - */ -function igny8_fetch_taxonomy_terms($taxonomy, $per_page = 100) { - $taxonomy_obj = get_taxonomy($taxonomy); - $rest_base = ($taxonomy_obj && !empty($taxonomy_obj->rest_base)) ? $taxonomy_obj->rest_base : $taxonomy; - - $base_url = sprintf('%s/wp-json/wp/v2/%s', get_site_url(), $rest_base); - - $formatted_terms = array(); - $page = 1; - - do { - $response = wp_remote_get(add_query_arg(array( - 'per_page' => min($per_page, 100), - 'page' => $page - ), $base_url)); - - if (is_wp_error($response)) { - break; - } - - $terms = json_decode(wp_remote_retrieve_body($response), true); - - if (!is_array($terms) || empty($terms)) { - break; - } - - foreach ($terms as $term) { - $formatted_terms[] = array( - 'id' => $term['id'], - 'name' => $term['name'], - 'slug' => $term['slug'], - 'description' => $term['description'] ?? '', - 'count' => $term['count'], - 'parent' => $term['parent'] ?? 0, - 'taxonomy' => $taxonomy, - 'url' => $term['link'] ?? '' - ); - } - - if (count($terms) < min($per_page, 100)) { - break; - } - - $page++; - } while (true); - - return $formatted_terms; -} - -/** - * Fetch all terms from all taxonomies - * - * @param int $per_page Terms per page - * @return array All terms organized by taxonomy - */ -function igny8_fetch_all_taxonomy_terms($per_page = 100) { - $taxonomies = igny8_fetch_wordpress_taxonomies(); - - if (!$taxonomies) { - return array(); - } - - $all_terms = array(); - foreach ($taxonomies as $taxonomy) { - $terms = igny8_fetch_taxonomy_terms($taxonomy['rest_base'], $per_page); - if ($terms) { - $all_terms[$taxonomy['name']] = $terms; - } - } - - return $all_terms; -} - -/** - * Collect all WordPress site data for IGNY8 semantic mapping - * - * @return array Complete site data - */ -function igny8_collect_site_data($args = array()) { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - return array('disabled' => true, 'reason' => 'connection_disabled'); - } - - if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('sites')) { - return array('disabled' => true); - } - - $settings = igny8_get_site_scan_settings($args); - - $site_data = array( - 'site_url' => get_site_url(), - 'site_name' => get_bloginfo('name'), - 'site_description' => get_bloginfo('description'), - 'collected_at' => current_time('mysql'), - 'settings' => $settings, - 'posts' => array(), - 'taxonomies' => array(), - 'products' => array(), - 'product_categories' => array(), - 'product_attributes' => array() - ); - - foreach ((array) $settings['post_types'] as $post_type) { - if (!post_type_exists($post_type) || !igny8_is_post_type_enabled($post_type)) { - continue; - } - - $posts = igny8_fetch_wordpress_posts($post_type, $settings['per_page'], array( - 'after' => $settings['since'], - 'status' => 'publish' - )); - - if ($posts) { - $site_data['posts'] = array_merge($site_data['posts'], $posts); - } - } - - $tracked_taxonomies = array('category', 'post_tag', 'igny8_sectors', 'igny8_clusters'); - - // Get enabled taxonomies from settings - if (function_exists('igny8_get_enabled_taxonomies')) { - $enabled_taxonomies = igny8_get_enabled_taxonomies(); - if (!empty($enabled_taxonomies)) { - $tracked_taxonomies = $enabled_taxonomies; - } - } - - foreach ($tracked_taxonomies as $taxonomy) { - if (!taxonomy_exists($taxonomy)) { - continue; - } - - $terms = igny8_fetch_taxonomy_terms($taxonomy, 100); - if ($terms) { - $tax_obj = get_taxonomy($taxonomy); - $site_data['taxonomies'][$taxonomy] = array( - 'taxonomy' => array( - 'name' => $taxonomy, - 'label' => $tax_obj ? $tax_obj->label : $taxonomy, - 'description' => $tax_obj->description ?? '', - 'hierarchical' => $tax_obj ? $tax_obj->hierarchical : false, - ), - 'terms' => $terms - ); - } - } - - if (!empty($settings['include_products']) && function_exists('igny8_is_woocommerce_active') && igny8_is_woocommerce_active()) { - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'data/woocommerce.php'; - - $products = igny8_fetch_woocommerce_products(100); - if ($products) { - $site_data['products'] = $products; - } - - $product_categories = igny8_fetch_product_categories(100); - if ($product_categories) { - $site_data['product_categories'] = $product_categories; - } - - $product_attributes = igny8_fetch_product_attributes(); - if ($product_attributes) { - $site_data['product_attributes'] = $product_attributes; - } - } - - // Extract link graph if Linker module is enabled - if (function_exists('igny8_is_module_enabled') && igny8_is_module_enabled('linker')) { - $post_ids = wp_list_pluck($site_data['posts'], 'id'); - $link_graph = igny8_extract_link_graph($post_ids); - - if (!empty($link_graph)) { - $site_data['link_graph'] = $link_graph; - } - } - - $site_data['summary'] = array( - 'posts' => count($site_data['posts']), - 'taxonomies' => count($site_data['taxonomies']), - 'products' => count($site_data['products']), - 'links' => isset($site_data['link_graph']) ? count($site_data['link_graph']) : 0 - ); - - update_option('igny8_last_site_snapshot', array( - 'timestamp' => current_time('timestamp'), - 'summary' => $site_data['summary'] - )); - - return $site_data; -} - -/** - * Send WordPress site data to IGNY8 for semantic strategy mapping - * - * @param int $site_id IGNY8 site ID - * @return array|false Response data or false on failure - */ -function igny8_send_site_data_to_igny8($site_id, $site_data = null, $args = array()) { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - return false; - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - return false; - } - - // Collect all site data if not provided - if (empty($site_data)) { - $site_data = igny8_collect_site_data($args); - } - - if (empty($site_data) || isset($site_data['disabled'])) { - return false; - } - - // Send to IGNY8 API - $response = $api->post("/system/sites/{$site_id}/import/", array( - 'site_data' => $site_data, - 'import_type' => $args['mode'] ?? 'full_site_scan' - )); - - if ($response['success']) { - // Store import ID for tracking - update_option('igny8_last_site_import_id', $response['data']['import_id'] ?? null); - update_option('igny8_last_site_sync', current_time('timestamp')); - - // Send link graph separately to Linker module if available - if (!empty($site_data['link_graph']) && function_exists('igny8_is_module_enabled') && igny8_is_module_enabled('linker')) { - $link_result = igny8_send_link_graph_to_igny8($site_id, $site_data['link_graph']); - if ($link_result) { - error_log(sprintf('IGNY8: Sent %d links to Linker module', $link_result['links_sent'] ?? 0)); - } - } - - return $response['data']; - } else { - error_log("IGNY8: Failed to send site data: " . ($response['error'] ?? 'Unknown error')); - return false; - } -} - -/** - * Sync only changed posts/taxonomies since last sync - * - * @param int $site_id IGNY8 site ID - * @return array|false Sync result or false on failure - */ -function igny8_sync_incremental_site_data($site_id, $settings = array()) { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - return array('synced' => 0, 'message' => 'Connection disabled'); - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - return false; - } - - $settings = igny8_get_site_scan_settings(wp_parse_args($settings, array('mode' => 'incremental'))); - $since = $settings['since'] ?? intval(get_option('igny8_last_site_sync', 0)); - - $formatted_posts = array(); - - foreach ((array) $settings['post_types'] as $post_type) { - if (!post_type_exists($post_type) || !igny8_is_post_type_enabled($post_type)) { - continue; - } - - $query_args = array( - 'post_type' => $post_type, - 'post_status' => array('publish', 'pending', 'draft', 'future'), - 'posts_per_page' => -1, - 'orderby' => 'modified', - 'order' => 'DESC', - 'suppress_filters' => true, - ); - - if ($since) { - $query_args['date_query'] = array( - array( - 'column' => 'post_modified_gmt', - 'after' => gmdate('Y-m-d H:i:s', $since) - ) - ); - } - - $posts = get_posts($query_args); - - foreach ($posts as $post) { - $word_count = str_word_count(strip_tags($post->post_content)); - - $formatted_posts[] = array( - 'id' => $post->ID, - 'title' => get_the_title($post), - 'content' => $post->post_content, - 'status' => $post->post_status, - 'modified' => $post->post_modified_gmt, - 'post_type' => $post->post_type, - 'url' => get_permalink($post), - 'taxonomies' => array( - 'categories' => wp_get_post_terms($post->ID, 'category', array('fields' => 'ids')), - 'tags' => wp_get_post_terms($post->ID, 'post_tag', array('fields' => 'ids')), - ), - 'meta' => array( - 'task_id' => get_post_meta($post->ID, '_igny8_task_id', true), - 'cluster_id' => get_post_meta($post->ID, '_igny8_cluster_id', true), - 'sector_id' => get_post_meta($post->ID, '_igny8_sector_id', true), - 'word_count' => $word_count, - ) - ); - } - } - - if (empty($formatted_posts)) { - return array('synced' => 0, 'message' => 'No changes since last sync'); - } - - $response = $api->post("/system/sites/{$site_id}/sync/", array( - 'posts' => $formatted_posts, - 'sync_type' => 'incremental', - 'last_sync' => $since, - 'post_types' => $settings['post_types'] - )); - - if ($response['success']) { - update_option('igny8_last_site_sync', current_time('timestamp')); - update_option('igny8_last_incremental_site_sync', array( - 'timestamp' => current_time('timestamp'), - 'count' => count($formatted_posts) - )); - - return array( - 'synced' => count($formatted_posts), - 'message' => 'Incremental sync completed' - ); - } - - return false; -} - -/** - * Run a full site scan and semantic mapping - * - * @param int $site_id IGNY8 site ID - * @param array $settings Scan settings - * @return array|false - */ -function igny8_perform_full_site_scan($site_id, $settings = array()) { - $site_data = igny8_collect_site_data($settings); - - if (empty($site_data) || isset($site_data['disabled'])) { - return false; - } - - $import = igny8_send_site_data_to_igny8($site_id, $site_data, array('mode' => 'full_site_scan')); - - if (!$import) { - return false; - } - - update_option('igny8_last_full_site_scan', current_time('timestamp')); - - // Map to semantic strategy (requires Planner module) - if (!function_exists('igny8_is_module_enabled') || igny8_is_module_enabled('planner')) { - $map_response = igny8_map_site_to_semantic_strategy($site_id, $site_data); - if (!empty($map_response['success'])) { - update_option('igny8_last_semantic_map', current_time('timestamp')); - update_option('igny8_last_semantic_map_summary', array( - 'sectors' => count($map_response['data']['sectors'] ?? array()), - 'keywords' => count($map_response['data']['keywords'] ?? array()) - )); - } - } - - // Send link graph to Linker module if available - if (!empty($site_data['link_graph']) && function_exists('igny8_is_module_enabled') && igny8_is_module_enabled('linker')) { - $link_result = igny8_send_link_graph_to_igny8($site_id, $site_data['link_graph']); - if ($link_result) { - error_log(sprintf('IGNY8: Sent %d links to Linker module during full scan', $link_result['links_sent'] ?? 0)); - } - } - - return $import; -} - diff --git a/igy8-wp-plugin/data/woocommerce.php b/igy8-wp-plugin/data/woocommerce.php deleted file mode 100644 index 7f01218e..00000000 --- a/igy8-wp-plugin/data/woocommerce.php +++ /dev/null @@ -1,226 +0,0 @@ - $headers - )); - - if (is_wp_error($wp_response)) { - return false; - } - - $products = json_decode(wp_remote_retrieve_body($wp_response), true); - - if (!is_array($products)) { - return false; - } - - $formatted_products = array(); - foreach ($products as $product) { - $formatted_products[] = array( - 'id' => $product['id'], - 'name' => $product['name'], - 'slug' => $product['slug'], - 'sku' => $product['sku'], - 'type' => $product['type'], - 'status' => $product['status'], - 'description' => $product['description'], - 'short_description' => $product['short_description'], - 'price' => $product['price'], - 'regular_price' => $product['regular_price'], - 'sale_price' => $product['sale_price'], - 'on_sale' => $product['on_sale'], - 'stock_status' => $product['stock_status'], - 'stock_quantity' => $product['stock_quantity'], - 'categories' => $product['categories'] ?? array(), - 'tags' => $product['tags'] ?? array(), - 'images' => $product['images'] ?? array(), - 'attributes' => $product['attributes'] ?? array(), - 'variations' => $product['variations'] ?? array(), - 'url' => $product['permalink'] - ); - } - - return $formatted_products; -} - -/** - * Fetch WooCommerce product categories - * - * @param int $per_page Categories per page - * @return array|false Formatted categories array or false on failure - */ -function igny8_fetch_product_categories($per_page = 100) { - if (!igny8_is_woocommerce_active()) { - return false; - } - - $consumer_key = get_option('woocommerce_api_consumer_key', ''); - $consumer_secret = get_option('woocommerce_api_consumer_secret', ''); - - $headers = array(); - if ($consumer_key && $consumer_secret) { - $headers['Authorization'] = 'Basic ' . base64_encode($consumer_key . ':' . $consumer_secret); - } - - $wp_response = wp_remote_get(sprintf( - '%s/wp-json/wc/v3/products/categories?per_page=%d', - get_site_url(), - $per_page - ), array( - 'headers' => $headers - )); - - if (is_wp_error($wp_response)) { - return false; - } - - $categories = json_decode(wp_remote_retrieve_body($wp_response), true); - - if (!is_array($categories)) { - return false; - } - - $formatted_categories = array(); - foreach ($categories as $category) { - $formatted_categories[] = array( - 'id' => $category['id'], - 'name' => $category['name'], - 'slug' => $category['slug'], - 'description' => $category['description'] ?? '', - 'count' => $category['count'], - 'parent' => $category['parent'] ?? 0, - 'image' => $category['image']['src'] ?? null - ); - } - - return $formatted_categories; -} - -/** - * Fetch WooCommerce product attributes - * - * @return array|false Formatted attributes array or false on failure - */ -function igny8_fetch_product_attributes() { - if (!igny8_is_woocommerce_active()) { - return false; - } - - $consumer_key = get_option('woocommerce_api_consumer_key', ''); - $consumer_secret = get_option('woocommerce_api_consumer_secret', ''); - - $headers = array(); - if ($consumer_key && $consumer_secret) { - $headers['Authorization'] = 'Basic ' . base64_encode($consumer_key . ':' . $consumer_secret); - } - - $wp_response = wp_remote_get( - get_site_url() . '/wp-json/wc/v3/products/attributes', - array( - 'headers' => $headers - ) - ); - - if (is_wp_error($wp_response)) { - return false; - } - - $attributes = json_decode(wp_remote_retrieve_body($wp_response), true); - - if (!is_array($attributes)) { - return false; - } - - $formatted_attributes = array(); - foreach ($attributes as $attribute) { - // Get attribute terms - $terms_response = wp_remote_get(sprintf( - '%s/wp-json/wc/v3/products/attributes/%d/terms', - get_site_url(), - $attribute['id'] - ), array( - 'headers' => $headers - )); - - $terms = array(); - if (!is_wp_error($terms_response)) { - $terms_data = json_decode(wp_remote_retrieve_body($terms_response), true); - if (is_array($terms_data)) { - foreach ($terms_data as $term) { - $terms[] = array( - 'id' => $term['id'], - 'name' => $term['name'], - 'slug' => $term['slug'] - ); - } - } - } - - $formatted_attributes[] = array( - 'id' => $attribute['id'], - 'name' => $attribute['name'], - 'slug' => $attribute['slug'], - 'type' => $attribute['type'], - 'order_by' => $attribute['order_by'], - 'has_archives' => $attribute['has_archives'], - 'terms' => $terms - ); - } - - return $formatted_attributes; -} - diff --git a/igy8-wp-plugin/docs/AUTHENTICATION-AUDIT.md b/igy8-wp-plugin/docs/AUTHENTICATION-AUDIT.md deleted file mode 100644 index e2232b20..00000000 --- a/igy8-wp-plugin/docs/AUTHENTICATION-AUDIT.md +++ /dev/null @@ -1,114 +0,0 @@ -# Authentication System Audit - IGNY8 WordPress Plugin - -**Date**: 2025-01-XX -**Status**: ✅ Fixed - -## Issue Summary - -The WordPress plugin was showing "Failed to connect to IGNY8 API: Not authenticated" error when attempting to connect, even when valid Site ID and API Key were provided. - -## Root Cause - -The WordPress plugin's `Igny8API::post()` method was checking for authentication (`is_authenticated()`) **before** making the API request. During initial connection setup, no API key is stored yet, so the check failed and returned "Not authenticated" error without ever making the request to the backend. - -## Authentication Flow - -### Expected Flow -1. User enters Site ID and API Key in WordPress plugin settings -2. Plugin sends POST request to `/v1/integration/integrations/test-connection/` with: - - `site_id` in body - - `api_key` in body - - `site_url` in body - - `Authorization: Bearer {api_key}` header -3. Backend verifies: - - Site exists - - API key in body matches site's `wp_api_key` field -4. If valid, connection succeeds and API key is stored in WordPress - -### Previous Flow (Broken) -1. User enters Site ID and API Key -2. Plugin creates `Igny8API` instance (no API key stored yet) -3. Plugin calls `$api->post()` which checks `is_authenticated()` -4. Check fails → returns "Not authenticated" error immediately -5. Request never reaches backend - -## Fixes Applied - -### 1. WordPress Plugin - API Class (`includes/class-igny8-api.php`) - -**Change**: Modified `post()` method to allow unauthenticated requests to `test-connection` endpoint when API key is provided in request body. - -```php -// Special case: test-connection endpoint allows API key in request body -// So we don't require pre-authentication for this endpoint -$is_test_connection = (strpos($endpoint, 'test-connection') !== false); -$has_api_key_in_data = !empty($data['api_key']); -$was_authenticated = $this->is_authenticated(); - -// If not authenticated, check if this is a test-connection with API key in data -if (!$was_authenticated) { - if ($is_test_connection && $has_api_key_in_data) { - // Temporarily set the API key for this request - $temp_api_key = $this->access_token; - $this->access_token = $data['api_key']; - } else { - return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401); - } -} -``` - -**Result**: Plugin can now make test-connection requests even without pre-stored API key. - -### 2. WordPress Plugin - Admin Class (`admin/class-admin.php`) - -**Change**: Cleaned up `handle_connection()` method to remove unnecessary workarounds. - -**Result**: Cleaner code that relies on API class to handle authentication properly. - -### 3. Backend - Integration Views (`backend/igny8_core/modules/integration/views.py`) - -**Change**: Improved error messages to provide more helpful feedback: - -- If API key not configured on site: "API key not configured for this site. Please generate an API key in the IGNY8 app and ensure it is saved to the site." -- If API key doesn't match: "Invalid API key. The provided API key does not match the one stored for this site." - -**Result**: Users get clearer error messages when authentication fails. - -## Backend Authentication Details - -### Test-Connection Endpoint -- **URL**: `POST /api/v1/integration/integrations/test-connection/` -- **Permission**: `AllowAny` (no authentication required via DRF auth classes) -- **Authentication Logic**: - 1. Check if user is authenticated via session/JWT and site belongs to user's account - 2. If not, check if API key in request body matches site's `wp_api_key` field - 3. If neither, return 403 error - -### API Key Authentication Class -- **Class**: `APIKeyAuthentication` in `backend/igny8_core/api/authentication.py` -- **Method**: Validates API key from `Authorization: Bearer {api_key}` header -- **Usage**: Used for authenticated API requests after initial connection - -## Testing Checklist - -- [x] Plugin can connect with valid Site ID and API Key -- [x] Plugin shows appropriate error for invalid Site ID -- [x] Plugin shows appropriate error for invalid API Key -- [x] Plugin shows appropriate error when API key not configured on site -- [x] API key is stored securely after successful connection -- [x] Subsequent API requests use stored API key for authentication - -## Security Considerations - -1. **API Key Storage**: API keys are stored using secure storage helpers when available (`igny8_store_secure_option`) -2. **API Key Transmission**: API keys are sent in both request body and Authorization header for test-connection -3. **Validation**: Backend validates API key matches site's stored key before allowing connection -4. **Error Messages**: Error messages don't leak sensitive information about API key format or site existence - -## Related Files - -- `igy8-wp-plugin/includes/class-igny8-api.php` - API client class -- `igy8-wp-plugin/admin/class-admin.php` - Admin interface and connection handling -- `backend/igny8_core/modules/integration/views.py` - Backend test-connection endpoint -- `backend/igny8_core/api/authentication.py` - Backend authentication classes - diff --git a/igy8-wp-plugin/docs/README.md b/igy8-wp-plugin/docs/README.md deleted file mode 100644 index fa7604b8..00000000 --- a/igy8-wp-plugin/docs/README.md +++ /dev/null @@ -1,396 +0,0 @@ -# IGNY8 WordPress Bridge Plugin - -**Version**: 1.0.0 -**Last Updated**: 2025-10-17 -**Requires**: WordPress 5.0+, PHP 7.4+ - ---- - -## Overview - -The IGNY8 WordPress Bridge Plugin is a **lightweight synchronization interface** that connects WordPress sites to the IGNY8 API. This plugin acts as a bridge, not a content management system, using WordPress native structures (taxonomies, post meta) to sync data bidirectionally with IGNY8. - -### Key Principles - -- ✅ **No Custom Database Tables** - Uses WordPress native taxonomies and post meta -- ✅ **Lightweight Bridge** - Minimal code, maximum efficiency -- ✅ **Two-Way Sync** - WordPress ↔ IGNY8 API synchronization -- ✅ **WordPress Native** - Leverages existing WordPress structures -- ✅ **API-First** - IGNY8 API is the source of truth - ---- - -## Features - -### Core Functionality - -1. **API Authentication** - - Secure token management - - Automatic token refresh - - Encrypted credential storage - -2. **Two-Way Synchronization** - - WordPress → IGNY8: Post status changes sync to IGNY8 tasks - - IGNY8 → WordPress: Content published from IGNY8 creates WordPress posts - -3. **Taxonomy Mapping** - - WordPress taxonomies → IGNY8 Sectors/Clusters - - Hierarchical taxonomies map to IGNY8 Sectors - - Taxonomy terms map to IGNY8 Clusters - -4. **Post Meta Integration** - - `_igny8_task_id` - Links WordPress posts to IGNY8 tasks - - `_igny8_cluster_id` - Links posts to IGNY8 clusters - - `_igny8_sector_id` - Links posts to IGNY8 sectors - - `_igny8_keyword_ids` - Links posts to IGNY8 keywords - -5. **Site Data Collection** - - Automatic collection of WordPress posts, taxonomies, products - - Semantic mapping to IGNY8 structure - - WooCommerce integration support - -6. **Status Mapping** - - WordPress post status → IGNY8 task status - - Automatic sync on post save/publish/status change - ---- - -## Installation - -### Requirements - -- WordPress 5.0 or higher -- PHP 7.4 or higher -- WordPress REST API enabled -- IGNY8 API account credentials - -### Installation Steps - -1. **Download/Clone Plugin** - ```bash - git clone [repository-url] - cd igny8-ai-os - ``` - -2. **Install in WordPress** - - Copy the `igny8-ai-os` folder to `/wp-content/plugins/` - - Or create a symlink for development - -3. **Activate Plugin** - - Go to WordPress Admin → Plugins - - Activate "IGNY8 WordPress Bridge" - -4. **Configure API Connection** - - Go to Settings → IGNY8 API - - Enter your IGNY8 email and password - - Click "Connect to IGNY8" - ---- - -## Configuration - -### API Settings - -Navigate to **Settings → IGNY8 API** to configure: - -- **Email**: Your IGNY8 account email -- **Password**: Your IGNY8 account password -- **Site ID**: Your IGNY8 site ID (auto-detected after connection) - -### WordPress Integration - -The plugin automatically: - -1. **Registers Taxonomies** (if needed): - - `sectors` - Maps to IGNY8 Sectors - - `clusters` - Maps to IGNY8 Clusters - -2. **Registers Post Meta Fields**: - - `_igny8_task_id` - - `_igny8_cluster_id` - - `_igny8_sector_id` - - `_igny8_keyword_ids` - - `_igny8_content_id` - -3. **Sets Up WordPress Hooks**: - - `save_post` - Syncs post changes to IGNY8 - - `publish_post` - Updates keywords on publish - - `transition_post_status` - Handles status changes - ---- - -## Usage - -### Basic Workflow - -#### 1. Connect to IGNY8 API - -```php -// Automatically handled via Settings page -// Or programmatically: -$api = new Igny8API(); -$api->login('your@email.com', 'password'); -``` - -#### 2. Sync WordPress Site Data - -```php -// Collect and send site data to IGNY8 -$site_id = get_option('igny8_site_id'); -igny8_send_site_data_to_igny8($site_id); -``` - -#### 3. WordPress → IGNY8 Sync - -When you save/publish a WordPress post: - -1. Plugin checks for `_igny8_task_id` in post meta -2. If found, syncs post status to IGNY8 task -3. If published, updates related keywords to 'mapped' status - -#### 4. IGNY8 → WordPress Sync - -When content is published from IGNY8: - -1. IGNY8 triggers webhook (or scheduled sync) -2. Plugin creates WordPress post via `wp_insert_post()` -3. Post meta saved with `_igny8_task_id` -4. IGNY8 task updated with WordPress post ID - ---- - -## WordPress Structures Used - -### Taxonomies - -- **`sectors`** (hierarchical) - - Maps to IGNY8 Sectors - - Can be created manually or synced from IGNY8 - -- **`clusters`** (hierarchical) - - Maps to IGNY8 Clusters - - Can be created manually or synced from IGNY8 - -- **Native Taxonomies** - - `category` - Can map to IGNY8 Sectors - - `post_tag` - Can be used for keyword extraction - -### Post Meta Fields - -All stored in WordPress `wp_postmeta` table: - -- `_igny8_task_id` (integer) - IGNY8 task ID -- `_igny8_cluster_id` (integer) - IGNY8 cluster ID -- `_igny8_sector_id` (integer) - IGNY8 sector ID -- `_igny8_keyword_ids` (array) - Array of IGNY8 keyword IDs -- `_igny8_content_id` (integer) - IGNY8 content ID -- `_igny8_last_synced` (datetime) - Last sync timestamp - -### Post Status Mapping - -| WordPress Status | IGNY8 Task Status | -|------------------|-------------------| -| `publish` | `completed` | -| `draft` | `draft` | -| `pending` | `pending` | -| `private` | `completed` | -| `trash` | `archived` | -| `future` | `scheduled` | - ---- - -## API Reference - -### Main Classes - -#### `Igny8API` - -Main API client class for all IGNY8 API interactions. - -```php -$api = new Igny8API(); - -// Login -$api->login('email@example.com', 'password'); - -// Get keywords -$response = $api->get('/planner/keywords/'); - -// Create task -$response = $api->post('/writer/tasks/', $data); - -// Update task -$response = $api->put('/writer/tasks/123/', $data); -``` - -#### `Igny8WordPressSync` - -Handles two-way synchronization between WordPress and IGNY8. - -```php -$sync = new Igny8WordPressSync(); -// Automatically hooks into WordPress post actions -``` - -#### `Igny8SiteIntegration` - -Manages site data collection and semantic mapping. - -```php -$integration = new Igny8SiteIntegration($site_id); -$result = $integration->full_site_scan(); -``` - -### Main Functions - -- `igny8_login($email, $password)` - Authenticate with IGNY8 -- `igny8_sync_post_status_to_igny8($post_id, $post, $update)` - Sync post to IGNY8 -- `igny8_collect_site_data()` - Collect all WordPress site data -- `igny8_send_site_data_to_igny8($site_id)` - Send site data to IGNY8 -- `igny8_map_site_to_semantic_strategy($site_id, $site_data)` - Map to semantic structure - -### Site Metadata Endpoint (Plugin) - -The plugin exposes a discovery endpoint that your IGNY8 app can call to learn which WordPress post types and taxonomies exist and how many items each contains. - -- Endpoint: `GET /wp-json/igny8/v1/site-metadata/` -- Auth: Plugin-level connection must be enabled and authenticated (plugin accepts stored API key or access token) -- Response: IGNY8 unified response format (`success`, `data`, `message`, `request_id`) - -Example response: - -```json -{ - "success": true, - "data": { - "post_types": { - "post": { "label": "Posts", "count": 123 }, - "page": { "label": "Pages", "count": 12 } - }, - "taxonomies": { - "category": { "label": "Categories", "count": 25 }, - "post_tag": { "label": "Tags", "count": 102 } - }, - "generated_at": 1700553600 - }, - "message": "Site metadata retrieved", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - ---- - -## File Structure - -``` -igny8-ai-os/ -├── igny8-bridge.php # Main plugin file -├── README.md # This file -├── docs/ # Documentation hub -│ ├── README.md # Index of available docs -│ ├── WORDPRESS-PLUGIN-INTEGRATION.md -│ ├── wp-bridge-implementation-plan.md -│ ├── missing-saas-api-endpoints.md -│ ├── STATUS_SYNC_DOCUMENTATION.md -│ └── STYLE_GUIDE.md -├── includes/ -│ ├── class-igny8-api.php # API client class -│ ├── class-igny8-sync.php # Sync handler class -│ ├── class-igny8-site.php # Site integration class -│ └── functions.php # Helper functions -├── admin/ -│ ├── class-admin.php # Admin interface -│ ├── settings.php # Settings page -│ └── assets/ -│ ├── css/ -│ └── js/ -├── sync/ -│ ├── hooks.php # WordPress hooks -│ ├── post-sync.php # Post synchronization -│ └── taxonomy-sync.php # Taxonomy synchronization -├── data/ -│ ├── site-collection.php # Site data collection -│ └── semantic-mapping.php # Semantic mapping -└── uninstall.php # Uninstall handler -``` - ---- - -## Development - -### Code Standards - -- Follow WordPress Coding Standards -- Use WordPress native functions -- No custom database tables -- All data in WordPress native structures - -### Testing - -```bash -# Run WordPress unit tests -phpunit - -# Test API connection -wp eval 'var_dump((new Igny8API())->login("test@example.com", "password"));' -``` - ---- - -## Troubleshooting - -### Authentication Issues - -**Problem**: Cannot connect to IGNY8 API - -**Solutions**: -1. Verify email and password are correct -2. Check API endpoint is accessible -3. Check WordPress REST API is enabled -4. Review error logs in WordPress debug log - -### Sync Issues - -**Problem**: Posts not syncing to IGNY8 - -**Solutions**: -1. Verify `_igny8_task_id` exists in post meta -2. Check API token is valid (not expired) -3. Review WordPress hooks are firing -4. Check error logs - -### Token Expiration - -**Problem**: Token expires frequently - -**Solution**: Plugin automatically refreshes tokens. If issues persist, check token refresh logic. - ---- - -## Support - -- **Documentation**: See `WORDPRESS-PLUGIN-INTEGRATION.md` -- **API Documentation**: https://api.igny8.com/docs -- **Issues**: [GitHub Issues](repository-url/issues) - ---- - -## License - -[Your License Here] - ---- - -## Changelog - -### 1.0.0 - 2025-10-17 -- Initial release -- API authentication -- Two-way sync -- Site data collection -- Semantic mapping - ---- - -**Last Updated**: 2025-10-17 - diff --git a/igy8-wp-plugin/docs/SYNC-DATA-FLOW-DIAGRAM.md b/igy8-wp-plugin/docs/SYNC-DATA-FLOW-DIAGRAM.md deleted file mode 100644 index 1bf0a778..00000000 --- a/igy8-wp-plugin/docs/SYNC-DATA-FLOW-DIAGRAM.md +++ /dev/null @@ -1,356 +0,0 @@ -# WordPress Plugin ↔ IGNY8 Backend Sync - Data Flow Diagram - -## Complete Sync Journey - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ WORDPRESS ADMIN - Connection Setup │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ User Input: │ -│ ┌───────────────────────────────────────────────────────────────┐ │ -│ │ Email: dev@igny8.com │ │ -│ │ Password: **** │ │ -│ │ API Key: **** │ │ -│ └───────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ WORDPRESS PLUGIN - Authentication (class-admin.php handle_connection()) │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 1. POST /auth/login/ (with email + password) │ -│ ↓ │ -│ 2. Store: access_token, refresh_token │ -│ ↓ │ -│ 3. GET /system/sites/ (authenticated) │ -│ ↓ │ -│ 4. Store: site_id (extracted from first site) │ -│ ↓ │ -│ ✅ Connection complete! User sees success message │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ WORDPRESS PLUGIN - Gather Site Structure (igny8_sync_site_structure_to_backend) -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Step 1: Query for Integration ID │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ GET /v1/integration/integrations/ │ │ -│ │ ?site={site_id} │ │ -│ │ &platform=wordpress ← NEW: Platform filter │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -│ ↓ │ -│ Step 2: Extract Integration ID (handle multiple response formats) │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ Response Format Handling: │ │ -│ │ • Paginated: data.results[0] ← Django REST Framework │ │ -│ │ • Array: data[0] ← Alternative format │ │ -│ │ • Object: data ← Direct single object │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -│ ↓ │ -│ Step 3: Gather WordPress Content Structure │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ Post Types (igny8_get_site_structure): │ │ -│ │ ├─ post → "Posts" (count: 50) │ │ -│ │ ├─ page → "Pages" (count: 10) │ │ -│ │ └─ product → "Products" (count: 100) │ │ -│ │ │ │ -│ │ Taxonomies: │ │ -│ │ ├─ category → "Categories" (count: 12) │ │ -│ │ ├─ post_tag → "Tags" (count: 89) │ │ -│ │ └─ product_cat → "Product Categories" (count: 15) │ │ -│ │ │ │ -│ │ Metadata: │ │ -│ │ ├─ timestamp (ISO 8601 format) ← NEW │ │ -│ │ ├─ site_url (WordPress domain) ← NEW │ │ -│ │ └─ wordpress_version (e.g., 6.4) ← NEW │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -│ │ -│ With Enhanced Debug Logging (if WP_DEBUG or IGNY8_DEBUG enabled): │ -│ • Log: Integration ID retrieved │ -│ • Log: Structure gathered successfully │ -│ • Log: Ready to sync │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ WORDPRESS → IGNY8 BACKEND - Push Structure (class-igny8-api.php post()) │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ POST /v1/integration/integrations/{integration_id}/update-structure/ │ -│ │ -│ Headers: │ -│ ├─ Authorization: Bearer {access_token} │ -│ └─ Content-Type: application/json │ -│ │ -│ Request Body: │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ { │ │ -│ │ "post_types": { │ │ -│ │ "post": { │ │ -│ │ "label": "Posts", │ │ -│ │ "count": 50, │ │ -│ │ "enabled": true, │ │ -│ │ "fetch_limit": 100 │ │ -│ │ }, │ │ -│ │ "page": {...}, │ │ -│ │ "product": {...} │ │ -│ │ }, │ │ -│ │ "taxonomies": { │ │ -│ │ "category": { │ │ -│ │ "label": "Categories", │ │ -│ │ "count": 12, │ │ -│ │ "enabled": true, │ │ -│ │ "fetch_limit": 100 │ │ -│ │ }, │ │ -│ │ "post_tag": {...}, │ │ -│ │ "product_cat": {...} │ │ -│ │ }, │ │ -│ │ "timestamp": "2025-11-22T10:15:30+00:00", │ │ -│ │ "plugin_connection_enabled": true, │ │ -│ │ "two_way_sync_enabled": true │ │ -│ │ } │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -│ │ -│ Debug Logging (NEW - Post Request Logging): │ -│ ├─ Log: Request URL │ -│ ├─ Log: Request payload (sanitized) │ -│ ├─ Log: Response status code │ -│ ├─ Log: Response body (first 500 chars) │ -│ └─ Log: Success/error with integration ID │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ IGNY8 BACKEND - Store Structure (modules/integration/views.py │ -│ update_site_structure action) │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 1. Authenticate request │ -│ ├─ Check Bearer token │ -│ └─ Verify user owns this integration │ -│ │ -│ 2. Extract payload │ -│ ├─ post_types │ -│ ├─ taxonomies │ -│ ├─ timestamp (optional, defaults to now) │ -│ ├─ plugin_connection_enabled │ -│ └─ two_way_sync_enabled │ -│ │ -│ 3. Store in SiteIntegration.config_json │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ config_json = { │ │ -│ │ "content_types": { │ │ -│ │ "post_types": {...}, │ │ -│ │ "taxonomies": {...}, │ │ -│ │ "last_structure_fetch": "2025-11-22T10:15:30+00:00" │ │ -│ │ }, │ │ -│ │ "plugin_connection_enabled": true, │ │ -│ │ "two_way_sync_enabled": true, │ │ -│ │ ... other config fields ... │ │ -│ │ } │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -│ │ -│ 4. Return Success Response │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ { │ │ -│ │ "success": true, │ │ -│ │ "data": { │ │ -│ │ "message": "Site structure updated successfully", │ │ -│ │ "post_types_count": 3, │ │ -│ │ "taxonomies_count": 3, │ │ -│ │ "last_structure_fetch": "2025-11-22T10:15:30+00:00" │ │ -│ │ } │ │ -│ │ } │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -│ │ -│ 5. Database save │ -│ └─ SiteIntegration record updated │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ WORDPRESS PLUGIN - Confirm Success & Update Options │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 1. Response Received (success == true) │ -│ ├─ Show success message to user │ -│ ├─ Log: "Site structure synced successfully" │ -│ └─ Update option: igny8_last_structure_sync = timestamp │ -│ │ -│ 2. New Options Created: │ -│ ├─ igny8_structure_synced = 1 (flag for status checking) │ -│ └─ igny8_last_structure_sync = unix timestamp │ -│ │ -│ 3. User Feedback: │ -│ ├─ "Successfully connected to IGNY8 API" │ -│ ├─ "Site structure synced successfully" ← NEW MESSAGE │ -│ └─ Or: "Connected but structure sync will be retried" (non-blocking) │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ IGNY8 FRONTEND - Fetch & Display Content Types │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 1. User navigates to Site Settings → Content Types Tab │ -│ │ -│ 2. Frontend queries backend: │ -│ GET /v1/integration/integrations/{integration_id}/content-types/ │ -│ │ -│ 3. Backend processes request (content_types_summary action): │ -│ ├─ Get stored content_types from config_json │ -│ ├─ Count synced items in Content model │ -│ ├─ Count synced items in ContentTaxonomy model │ -│ ├─ Compute synced_count for each post type │ -│ └─ Compute synced_count for each taxonomy │ -│ │ -│ 4. Backend Response: │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ { │ │ -│ │ "success": true, │ │ -│ │ "data": { │ │ -│ │ "post_types": { │ │ -│ │ "post": { │ │ -│ │ "label": "Posts", │ │ -│ │ "count": 50, ← Total in WordPress │ │ -│ │ "synced_count": 30, ← Synced to IGNY8 │ │ -│ │ "enabled": true, │ │ -│ │ "fetch_limit": 100 │ │ -│ │ }, │ │ -│ │ "page": {...}, │ │ -│ │ "product": {...} │ │ -│ │ }, │ │ -│ │ "taxonomies": { │ │ -│ │ "category": { │ │ -│ │ "label": "Categories", │ │ -│ │ "count": 12, ← Total in WordPress │ │ -│ │ "synced_count": 12, ← Synced to IGNY8 │ │ -│ │ "enabled": true, │ │ -│ │ "fetch_limit": 100 │ │ -│ │ }, │ │ -│ │ "post_tag": {...}, │ │ -│ │ "product_cat": {...} │ │ -│ │ }, │ │ -│ │ "last_structure_fetch": "2025-11-22T10:15:30+00:00", │ │ -│ │ "plugin_connection_enabled": true, │ │ -│ │ "two_way_sync_enabled": true │ │ -│ │ } │ │ -│ │ } │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -│ │ -│ 5. Frontend Renders: │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ Content Types │ │ -│ │ ┌──────────────────────────────────────────────────────────┐ │ │ -│ │ │ Post Types │ │ │ -│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ -│ │ │ │ Posts 50 total · 30 synced │ │ │ │ -│ │ │ │ Enabled Limit: 100 │ │ │ │ -│ │ │ └────────────────────────────────────────────────────┘ │ │ │ -│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ -│ │ │ │ Pages 10 total · 8 synced │ │ │ │ -│ │ │ │ Enabled Limit: 100 │ │ │ │ -│ │ │ └────────────────────────────────────────────────────┘ │ │ │ -│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ -│ │ │ │ Products 100 total · 45 synced │ │ │ │ -│ │ │ │ Enabled Limit: 100 │ │ │ │ -│ │ │ └────────────────────────────────────────────────────┘ │ │ │ -│ │ └──────────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ ┌──────────────────────────────────────────────────────────┐ │ │ -│ │ │ Taxonomies │ │ │ -│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ -│ │ │ │ Categories 12 total · 12 synced │ │ │ │ -│ │ │ │ Enabled Limit: 100 │ │ │ │ -│ │ │ └────────────────────────────────────────────────────┘ │ │ │ -│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ -│ │ │ │ Tags 89 total · 60 synced │ │ │ │ -│ │ │ │ Enabled Limit: 100 │ │ │ │ -│ │ │ └────────────────────────────────────────────────────┘ │ │ │ -│ │ └──────────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ Structure last fetched: 2025-11-22 10:15:30 UTC │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Daily Cron Job - Automatic Updates - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ WordPress Cron - Daily Schedule (igny8_sync_site_structure) │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Every 24 hours: │ -│ ├─ Trigger: do_action('igny8_sync_site_structure') │ -│ ├─ Call: igny8_sync_site_structure_to_backend() │ -│ ├─ Same flow as above (Get structure → Push to backend) │ -│ ├─ Updates counts and structure if changed │ -│ └─ Ensures frontend always has current data │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Error Handling & Logging Flow - -``` -┌──────────────────────────────────────────────────────────────────────────┐ -│ Error Detection & Logging │ -├──────────────────────────────────────────────────────────────────────────┤ -│ │ -│ If query fails: │ -│ ├─ Log: "Failed to fetch integrations. Error: [details]" │ -│ └─ Return: false (non-blocking) │ -│ │ -│ If integration not found: │ -│ ├─ Log: "Could not find valid WordPress integration for site {id}" │ -│ ├─ Log: "Response data: [full response]" │ -│ └─ Return: false (non-blocking) │ -│ │ -│ If POST fails: │ -│ ├─ Log: "Failed to sync site structure to integration {id}" │ -│ ├─ Log: "Error: [error message]" │ -│ ├─ Log: "Full response: [response JSON]" │ -│ └─ Return: false (non-blocking) │ -│ │ -│ If successful: │ -│ ├─ Log: "Site structure synced successfully to integration {id}" │ -│ ├─ Update: igny8_structure_synced option │ -│ ├─ Update: igny8_last_structure_sync timestamp │ -│ └─ Return: true │ -│ │ -│ All logs go to: wp-content/debug.log │ -│ To enable: define('WP_DEBUG_LOG', true) in wp-config.php │ -│ │ -└──────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Summary - -✅ **Reliable bidirectional data flow** -- WordPress → Backend: Structure pushed on connection and daily -- Backend → Frontend: Structure retrieved and displayed with sync counts -- All steps logged and error-handled -- Non-blocking approach ensures connection always succeeds - -✅ **User visibility** -- Clear success/failure messages -- Debug logs provide troubleshooting info -- Frontend shows current status and counts - -✅ **Maintenance** -- Automatic daily updates keep data fresh -- Error handling prevents sync failures from breaking the system -- Complete audit trail in logs - diff --git a/igy8-wp-plugin/docs/WORDPRESS-PLUGIN-INTEGRATION.md b/igy8-wp-plugin/docs/WORDPRESS-PLUGIN-INTEGRATION.md deleted file mode 100644 index e35f99eb..00000000 --- a/igy8-wp-plugin/docs/WORDPRESS-PLUGIN-INTEGRATION.md +++ /dev/null @@ -1,2135 +0,0 @@ -# WordPress Plugin Integration Guide - -**Version**: 1.0.0 -**Last Updated**: 2025-11-17 - -Complete guide for integrating WordPress plugins with IGNY8 API v1.0. - ---- - -## Overview - -This guide helps WordPress plugin developers integrate with the IGNY8 API using the unified response format. - ---- - -## Implementation Roadmap (2025-11 refresh) - -The bridge now follows a hands-off model: once a site connects and saves the recommended settings, all data collection and sync flows run automatically via cron/webhooks. The detailed engineering schedule lives in `docs/wp-bridge-implementation-plan.md`; use it for sprint planning and status tracking. - -### Module Integration Matrix - -| Area | WordPress Bridge Responsibilities | SaaS API / Endpoint | Status | -| --- | --- | --- | --- | -| Admin & Auth | Store creds, expose post-type/Woo toggles, control mode, diagnostics panel | `/auth/login/`, `/auth/refresh/`, `/system/sites/{id}/settings/` (planned) | Auth live; remote-settings endpoint pending | -| Taxonomies & Keywords | Mirror sectors/clusters as custom taxonomies, attach keywords to post meta, render read-only badges | `/planner/sectors/`, `/planner/clusters/`, `/planner/keywords/` | In progress | -| Writer Tasks | Pull new tasks, create drafts, push status/URL updates, cache briefs | `/writer/tasks/`, `/writer/tasks/{id}/`, `/writer/tasks/{id}/brief/` (pending) | Push path live; brief endpoint pending | -| Site Data & Semantic Map | Scheduled full/incremental scans, submit site payloads, store semantic metadata | `/system/sites/{id}/import/`, `/planner/sites/{id}/semantic-map/` | Collection live; semantic-map read pending | -| Planner / Linker / Optimizer Hooks | Attach briefs, export link graph, accept link recommendations & optimizer jobs | `/planner/tasks/{id}/refresh/`, `/linker/link-map/`, `/optimizer/jobs/` | Requires new SaaS endpoints | -| Webhooks & Automation | Provide secured WP REST endpoints for SaaS events (task ready, link suggestion, optimizer action) | SaaS → `/wp-json/igny8/v1/event` | WP side planned; SaaS needs outbound hooks | -| Monitoring & Tooling | WP-CLI commands, logging, admin notices, health widget | `/system/ping/`, `/system/sites/{id}/status/` | Ping/status endpoints pending | - -Reference `docs/missing-saas-api-endpoints.md` for any API work the SaaS team must complete before the bridge can fully automate these areas. - ---- - -## Authentication - -### Getting Access Token - -```php -function igny8_login($email, $password) { - $response = wp_remote_post('https://api.igny8.com/api/v1/auth/login/', [ - 'headers' => [ - 'Content-Type' => 'application/json' - ], - 'body' => json_encode([ - 'email' => $email, - 'password' => $password - ]) - ]); - - $body = json_decode(wp_remote_retrieve_body($response), true); - - if ($body['success']) { - // Store tokens - update_option('igny8_access_token', $body['data']['access']); - update_option('igny8_refresh_token', $body['data']['refresh']); - return $body['data']['access']; - } else { - return new WP_Error('login_failed', $body['error']); - } -} -``` - -### Using Access Token - -```php -function igny8_get_headers() { - $token = get_option('igny8_access_token'); - - if (!$token) { - return false; - } - - return [ - 'Authorization' => 'Bearer ' . $token, - 'Content-Type' => 'application/json' - ]; -} -``` - -Note (required): The bridge now requires all three credentials to be provided in Settings → IGNY8 API: **Email**, **Password**, and **API Key**. These map to WordPress options `igny8_email`, `igny8_access_token`/`igny8_refresh_token`, and `igny8_api_key`. The API key will be stored with `igny8_store_secure_option()` when available; if any required credential is missing the plugin will not establish the connection. - ---- - -## API Client Class - -### Complete PHP Implementation - -```php -class Igny8API { - private $base_url = 'https://api.igny8.com/api/v1'; - private $access_token = null; - private $refresh_token = null; - - public function __construct() { - $this->access_token = get_option('igny8_access_token'); - $this->refresh_token = get_option('igny8_refresh_token'); - } - - /** - * Login and store tokens - */ - public function login($email, $password) { - $response = wp_remote_post($this->base_url . '/auth/login/', [ - 'headers' => [ - 'Content-Type' => 'application/json' - ], - 'body' => json_encode([ - 'email' => $email, - 'password' => $password - ]) - ]); - - $body = $this->parse_response($response); - - if ($body['success']) { - $this->access_token = $body['data']['access']; - $this->refresh_token = $body['data']['refresh']; - - update_option('igny8_access_token', $this->access_token); - update_option('igny8_refresh_token', $this->refresh_token); - - return true; - } - - return false; - } - - /** - * Refresh access token - */ - public function refresh_token() { - if (!$this->refresh_token) { - return false; - } - - $response = wp_remote_post($this->base_url . '/auth/refresh/', [ - 'headers' => [ - 'Content-Type' => 'application/json' - ], - 'body' => json_encode([ - 'refresh' => $this->refresh_token - ]) - ]); - - $body = $this->parse_response($response); - - if ($body['success']) { - $this->access_token = $body['data']['access']; - $this->refresh_token = $body['data']['refresh']; - - update_option('igny8_access_token', $this->access_token); - update_option('igny8_refresh_token', $this->refresh_token); - - return true; - } - - return false; - } - - /** - * Parse unified API response - */ - private function parse_response($response) { - if (is_wp_error($response)) { - return [ - 'success' => false, - 'error' => $response->get_error_message() - ]; - } - - $body = json_decode(wp_remote_retrieve_body($response), true); - $status_code = wp_remote_retrieve_response_code($response); - - // Handle non-JSON responses - if (!$body) { - return [ - 'success' => false, - 'error' => 'Invalid response format' - ]; - } - - // Check if response follows unified format - if (isset($body['success'])) { - return $body; - } - - // Legacy format - wrap in unified format - if ($status_code >= 200 && $status_code < 300) { - return [ - 'success' => true, - 'data' => $body - ]; - } else { - return [ - 'success' => false, - 'error' => $body['detail'] ?? 'Unknown error' - ]; - } - } - - /** - * Get headers with authentication - */ - private function get_headers() { - if (!$this->access_token) { - throw new Exception('Not authenticated'); - } - - return [ - 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json' - ]; - } - - /** - * Make GET request - */ - public function get($endpoint) { - $response = wp_remote_get($this->base_url . $endpoint, [ - 'headers' => $this->get_headers() - ]); - - $body = $this->parse_response($response); - - // Handle 401 - token expired - if (!$body['success'] && wp_remote_retrieve_response_code($response) == 401) { - // Try to refresh token - if ($this->refresh_token()) { - // Retry request - $response = wp_remote_get($this->base_url . $endpoint, [ - 'headers' => $this->get_headers() - ]); - $body = $this->parse_response($response); - } - } - - return $body; - } - - /** - * Make POST request - */ - public function post($endpoint, $data) { - $response = wp_remote_post($this->base_url . $endpoint, [ - 'headers' => $this->get_headers(), - 'body' => json_encode($data) - ]); - - $body = $this->parse_response($response); - - // Handle 401 - token expired - if (!$body['success'] && wp_remote_retrieve_response_code($response) == 401) { - // Try to refresh token - if ($this->refresh_token()) { - // Retry request - $response = wp_remote_post($this->base_url . $endpoint, [ - 'headers' => $this->get_headers(), - 'body' => json_encode($data) - ]); - $body = $this->parse_response($response); - } - } - - return $body; - } - - /** - * Make PUT request - */ - public function put($endpoint, $data) { - $response = wp_remote_request($this->base_url . $endpoint, [ - 'method' => 'PUT', - 'headers' => $this->get_headers(), - 'body' => json_encode($data) - ]); - - return $this->parse_response($response); - } - - /** - * Make DELETE request - */ - public function delete($endpoint) { - $response = wp_remote_request($this->base_url . $endpoint, [ - 'method' => 'DELETE', - 'headers' => $this->get_headers() - ]); - - return $this->parse_response($response); - } -} -``` - ---- - -## Usage Examples - -### Get Keywords - -```php -$api = new Igny8API(); - -// Get keywords -$response = $api->get('/planner/keywords/'); - -if ($response['success']) { - $keywords = $response['results']; - $count = $response['count']; - - foreach ($keywords as $keyword) { - echo $keyword['name'] . '
'; - } -} else { - echo 'Error: ' . $response['error']; -} -``` - -### Create Keyword - -```php -$api = new Igny8API(); - -$data = [ - 'seed_keyword_id' => 1, - 'site_id' => 1, - 'sector_id' => 1, - 'status' => 'active' -]; - -$response = $api->post('/planner/keywords/', $data); - -if ($response['success']) { - $keyword = $response['data']; - echo 'Created keyword: ' . $keyword['id']; -} else { - echo 'Error: ' . $response['error']; - if (isset($response['errors'])) { - foreach ($response['errors'] as $field => $errors) { - echo $field . ': ' . implode(', ', $errors) . '
'; - } - } -} -``` - -### Handle Pagination - -```php -$api = new Igny8API(); - -function get_all_keywords($api) { - $all_keywords = []; - $page = 1; - - do { - $response = $api->get("/planner/keywords/?page={$page}&page_size=100"); - - if ($response['success']) { - $all_keywords = array_merge($all_keywords, $response['results']); - $page++; - } else { - break; - } - } while ($response['next']); - - return $all_keywords; -} - -$keywords = get_all_keywords($api); -``` - -### Handle Rate Limiting - -```php -function make_rate_limited_request($api, $endpoint, $max_retries = 3) { - for ($attempt = 0; $attempt < $max_retries; $attempt++) { - $response = $api->get($endpoint); - - // Check if rate limited - if (!$response['success'] && isset($response['error'])) { - if (strpos($response['error'], 'Rate limit') !== false) { - // Wait before retry - sleep(pow(2, $attempt)); // Exponential backoff - continue; - } - } - - return $response; - } - - return ['success' => false, 'error' => 'Max retries exceeded']; -} -``` - ---- - -## Error Handling - -### Unified Error Handling - -```php -function handle_api_response($response) { - if ($response['success']) { - return $response['data'] ?? $response['results']; - } else { - $error_message = $response['error']; - - // Log error with request ID - error_log(sprintf( - 'IGNY8 API Error: %s (Request ID: %s)', - $error_message, - $response['request_id'] ?? 'unknown' - )); - - // Handle field-specific errors - if (isset($response['errors'])) { - foreach ($response['errors'] as $field => $errors) { - error_log(" {$field}: " . implode(', ', $errors)); - } - } - - return new WP_Error('igny8_api_error', $error_message, $response); - } -} -``` - ---- - -## Best Practices - -### 1. Store Tokens Securely - -```php -// Use WordPress options API with encryption -function save_token($token) { - // Encrypt token before storing - $encrypted = base64_encode($token); - update_option('igny8_access_token', $encrypted, false); -} - -function get_token() { - $encrypted = get_option('igny8_access_token'); - return base64_decode($encrypted); -} -``` - -### 2. Implement Token Refresh - -```php -function ensure_valid_token($api) { - // Check if token is about to expire (refresh 1 minute before) - // Token expires in 15 minutes, refresh at 14 minutes - $last_refresh = get_option('igny8_token_refreshed_at', 0); - - if (time() - $last_refresh > 14 * 60) { - if ($api->refresh_token()) { - update_option('igny8_token_refreshed_at', time()); - } - } -} -``` - -### 3. Cache Responses - -```php -function get_cached_keywords($api, $cache_key = 'igny8_keywords', $ttl = 300) { - $cached = get_transient($cache_key); - - if ($cached !== false) { - return $cached; - } - - $response = $api->get('/planner/keywords/'); - - if ($response['success']) { - $keywords = $response['results']; - set_transient($cache_key, $keywords, $ttl); - return $keywords; - } - - return false; -} -``` - -### 4. Handle Rate Limits - -```php -function check_rate_limit($response) { - // Note: WordPress wp_remote_* doesn't expose all headers easily - // Consider using cURL or checking response for 429 status - - if (isset($response['error']) && strpos($response['error'], 'Rate limit') !== false) { - // Wait and retry - sleep(60); - return true; // Should retry - } - - return false; -} -``` - ---- - -## WordPress Admin Integration - -### Settings Page - -```php -function igny8_settings_page() { - ?> -
-

IGNY8 API Settings

-
- - - - - - - - - - -
API Email
API Password
- -
-
- login($_POST['igny8_email'], $_POST['igny8_password'])) { - update_option('igny8_email', $_POST['igny8_email']); - add_settings_error('igny8_settings', 'igny8_connected', 'Successfully connected to IGNY8 API', 'updated'); - } else { - add_settings_error('igny8_settings', 'igny8_error', 'Failed to connect to IGNY8 API', 'error'); - } - } -} -add_action('admin_init', 'igny8_save_settings'); -``` - -#### Recommended Controls - -- **Post Type Toggles** – store JSON option (e.g., `igny8_enabled_post_types`) so the bridge only syncs what the SaaS site expects (`post`, `page`, `product`, custom CPTs). -- **WooCommerce Switches** – enable/disable product sync, inventory fields, and taxonomy export. -- **Control Mode Selector** – choose between `mirror` (IGNY8 authoritative; WP read-only) and `hybrid` (WP edits allowed and synced back). -- **Automation Status Panel** – show last cron run, last webhook received, token age, and any queued failures. - -Once these settings are saved, cron jobs (`igny8_schedule_cron_jobs`) and webhook endpoints take over automatically—no manual “sync” buttons are needed outside of troubleshooting. The exact flow chart lives in `docs/wp-bridge-implementation-plan.md`. - ---- - -## Testing - -### Unit Tests - -```php -class TestIgny8API extends WP_UnitTestCase { - public function test_login() { - $api = new Igny8API(); - $result = $api->login('test@example.com', 'password'); - - $this->assertTrue($result); - $this->assertNotEmpty(get_option('igny8_access_token')); - } - - public function test_get_keywords() { - $api = new Igny8API(); - $response = $api->get('/planner/keywords/'); - - $this->assertTrue($response['success']); - $this->assertArrayHasKey('results', $response); - $this->assertArrayHasKey('count', $response); - } -} -``` - ---- - -## Troubleshooting - -### Issue: Authentication Fails - -**Check**: -1. Email and password are correct -2. Account is active -3. API endpoint is accessible - -### Issue: Token Expires Frequently - -**Solution**: Implement automatic token refresh before expiration. - -### Issue: Rate Limited - -**Solution**: Implement request throttling and caching. - ---- - -## WordPress Hooks and Two-Way Sync - -### Overview - -The integration supports **two-way synchronization**: -- **IGNY8 → WordPress**: Publishing content from IGNY8 to WordPress -- **WordPress → IGNY8**: Syncing WordPress post status changes back to IGNY8 - -### WordPress Post Hooks - -#### 1. Post Save Hook (`save_post`) - -Hook into WordPress post saves to sync status back to IGNY8: - -```php -/** - * Sync WordPress post status to IGNY8 when post is saved - */ -add_action('save_post', 'igny8_sync_post_status_to_igny8', 10, 3); - -function igny8_sync_post_status_to_igny8($post_id, $post, $update) { - // Skip autosaves and revisions - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { - return; - } - - if (wp_is_post_revision($post_id)) { - return; - } - - // Only sync IGNY8-managed posts - $task_id = get_post_meta($post_id, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - // Get post status - $post_status = $post->post_status; - - // Map WordPress status to IGNY8 task status - $task_status_map = [ - 'publish' => 'completed', - 'draft' => 'draft', - 'pending' => 'pending', - 'private' => 'completed', - 'trash' => 'archived' - ]; - - $task_status = $task_status_map[$post_status] ?? 'draft'; - - // Sync to IGNY8 API - $api = new Igny8API(); - $response = $api->put("/writer/tasks/{$task_id}/", [ - 'status' => $task_status, - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - ]); - - if ($response['success']) { - error_log("IGNY8: Synced post {$post_id} status to task {$task_id}"); - } else { - error_log("IGNY8: Failed to sync post status: " . $response['error']); - } -} -``` - -#### 2. Post Publish Hook (`publish_post`) - -Update keyword status when content is published: - -```php -/** - * Update keyword status when WordPress post is published - */ -add_action('publish_post', 'igny8_update_keywords_on_post_publish', 10, 1); -add_action('publish_page', 'igny8_update_keywords_on_post_publish', 10, 1); -add_action('draft_to_publish', 'igny8_update_keywords_on_post_publish', 10, 1); -add_action('future_to_publish', 'igny8_update_keywords_on_post_publish', 10, 1); - -function igny8_update_keywords_on_post_publish($post_id) { - // Get task ID from post meta - $task_id = get_post_meta($post_id, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - $api = new Igny8API(); - - // Get task details to find associated cluster/keywords - $task_response = $api->get("/writer/tasks/{$task_id}/"); - - if (!$task_response['success']) { - return; - } - - $task = $task_response['data']; - $cluster_id = $task['cluster_id'] ?? null; - - if ($cluster_id) { - // Get keywords in this cluster - $keywords_response = $api->get("/planner/keywords/?cluster_id={$cluster_id}"); - - if ($keywords_response['success']) { - $keywords = $keywords_response['results']; - - // Update each keyword status to 'mapped' - foreach ($keywords as $keyword) { - $api->put("/planner/keywords/{$keyword['id']}/", [ - 'status' => 'mapped' - ]); - } - } - } - - // Update task status to completed - $api->put("/writer/tasks/{$task_id}/", [ - 'status' => 'completed', - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - ]); -} -``` - -#### 3. Post Status Change Hook (`transition_post_status`) - -Handle all post status transitions: - -```php -/** - * Sync post status changes to IGNY8 - */ -add_action('transition_post_status', 'igny8_sync_post_status_transition', 10, 3); - -function igny8_sync_post_status_transition($new_status, $old_status, $post) { - // Skip if status hasn't changed - if ($new_status === $old_status) { - return; - } - - // Only sync IGNY8-managed posts - $task_id = get_post_meta($post->ID, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - $api = new Igny8API(); - - // Map WordPress status to IGNY8 task status - $status_map = [ - 'publish' => 'completed', - 'draft' => 'draft', - 'pending' => 'pending', - 'private' => 'completed', - 'trash' => 'archived', - 'future' => 'scheduled' - ]; - - $task_status = $status_map[$new_status] ?? 'draft'; - - // Sync to IGNY8 - $response = $api->put("/writer/tasks/{$task_id}/", [ - 'status' => $task_status, - 'assigned_post_id' => $post->ID, - 'post_url' => get_permalink($post->ID) - ]); - - if ($response['success']) { - do_action('igny8_post_status_synced', $post->ID, $task_id, $new_status); - } -} -``` - -### Fetching WordPress Post Status - -#### Get Post Status from WordPress - -```php -/** - * Get WordPress post status and sync to IGNY8 - */ -function igny8_fetch_and_sync_post_status($post_id) { - $post = get_post($post_id); - - if (!$post) { - return false; - } - - // Get post status - $wp_status = $post->post_status; - - // Get additional post data - $post_data = [ - 'id' => $post_id, - 'status' => $wp_status, - 'title' => $post->post_title, - 'url' => get_permalink($post_id), - 'modified' => $post->post_modified, - 'published' => $post->post_date - ]; - - // Get task ID - $task_id = get_post_meta($post_id, '_igny8_task_id', true); - - if (!$task_id) { - return false; - } - - // Sync to IGNY8 - $api = new Igny8API(); - - // Map WordPress status to IGNY8 status - $status_map = [ - 'publish' => 'completed', - 'draft' => 'draft', - 'pending' => 'pending', - 'private' => 'completed', - 'trash' => 'archived' - ]; - - $task_status = $status_map[$wp_status] ?? 'draft'; - - $response = $api->put("/writer/tasks/{$task_id}/", [ - 'status' => $task_status, - 'assigned_post_id' => $post_id, - 'post_url' => $post_data['url'] - ]); - - return $response['success']; -} -``` - -#### Batch Sync Post Statuses - -```php -/** - * Sync all IGNY8-managed posts status to IGNY8 API - */ -function igny8_batch_sync_post_statuses() { - global $wpdb; - - // Get all posts with IGNY8 task ID - $posts = $wpdb->get_results(" - SELECT p.ID, p.post_status, p.post_title, pm.meta_value as task_id - FROM {$wpdb->posts} p - INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id - WHERE pm.meta_key = '_igny8_task_id' - AND p.post_type IN ('post', 'page') - AND p.post_status != 'trash' - "); - - $api = new Igny8API(); - $synced = 0; - $failed = 0; - - foreach ($posts as $post_data) { - $post_id = $post_data->ID; - $task_id = intval($post_data->task_id); - $wp_status = $post_data->post_status; - - // Map status - $status_map = [ - 'publish' => 'completed', - 'draft' => 'draft', - 'pending' => 'pending', - 'private' => 'completed' - ]; - - $task_status = $status_map[$wp_status] ?? 'draft'; - - // Sync to IGNY8 - $response = $api->put("/writer/tasks/{$task_id}/", [ - 'status' => $task_status, - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - ]); - - if ($response['success']) { - $synced++; - } else { - $failed++; - error_log("IGNY8: Failed to sync post {$post_id}: " . $response['error']); - } - } - - return [ - 'synced' => $synced, - 'failed' => $failed, - 'total' => count($posts) - ]; -} -``` - -### Complete Two-Way Sync Example - -```php -/** - * Complete two-way sync implementation - */ -class Igny8WordPressSync { - private $api; - - public function __construct() { - $this->api = new Igny8API(); - - // WordPress → IGNY8 hooks - add_action('save_post', [$this, 'sync_post_to_igny8'], 10, 3); - add_action('publish_post', [$this, 'update_keywords_on_publish'], 10, 1); - add_action('transition_post_status', [$this, 'sync_status_transition'], 10, 3); - - // IGNY8 → WordPress (when content is published from IGNY8) - add_action('igny8_content_published', [$this, 'create_wordpress_post'], 10, 1); - } - - /** - * WordPress → IGNY8: Sync post changes to IGNY8 - */ - public function sync_post_to_igny8($post_id, $post, $update) { - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { - return; - } - - if (wp_is_post_revision($post_id)) { - return; - } - - $task_id = get_post_meta($post_id, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - $status_map = [ - 'publish' => 'completed', - 'draft' => 'draft', - 'pending' => 'pending', - 'private' => 'completed', - 'trash' => 'archived' - ]; - - $task_status = $status_map[$post->post_status] ?? 'draft'; - - $response = $this->api->put("/writer/tasks/{$task_id}/", [ - 'status' => $task_status, - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - ]); - - if ($response['success']) { - error_log("IGNY8: Synced post {$post_id} to task {$task_id}"); - } - } - - /** - * WordPress → IGNY8: Update keywords when post is published - */ - public function update_keywords_on_publish($post_id) { - $task_id = get_post_meta($post_id, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - // Get task to find cluster - $task_response = $this->api->get("/writer/tasks/{$task_id}/"); - if (!$task_response['success']) { - return; - } - - $task = $task_response['data']; - $cluster_id = $task['cluster_id'] ?? null; - - if ($cluster_id) { - // Update keywords in cluster to 'mapped' - $keywords_response = $this->api->get("/planner/keywords/?cluster_id={$cluster_id}"); - if ($keywords_response['success']) { - foreach ($keywords_response['results'] as $keyword) { - $this->api->put("/planner/keywords/{$keyword['id']}/", [ - 'status' => 'mapped' - ]); - } - } - } - - // Update task status - $this->api->put("/writer/tasks/{$task_id}/", [ - 'status' => 'completed', - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - ]); - } - - /** - * WordPress → IGNY8: Handle status transitions - */ - public function sync_status_transition($new_status, $old_status, $post) { - if ($new_status === $old_status) { - return; - } - - $task_id = get_post_meta($post->ID, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - $status_map = [ - 'publish' => 'completed', - 'draft' => 'draft', - 'pending' => 'pending', - 'private' => 'completed', - 'trash' => 'archived' - ]; - - $task_status = $status_map[$new_status] ?? 'draft'; - - $this->api->put("/writer/tasks/{$task_id}/", [ - 'status' => $task_status, - 'assigned_post_id' => $post->ID, - 'post_url' => get_permalink($post->ID) - ]); - } - - /** - * IGNY8 → WordPress: Create WordPress post from IGNY8 content - */ - public function create_wordpress_post($content_data) { - $post_data = [ - 'post_title' => $content_data['title'], - 'post_content' => $content_data['content'], - 'post_status' => $content_data['status'] ?? 'draft', - 'post_type' => 'post', - 'meta_input' => [ - '_igny8_task_id' => $content_data['task_id'], - '_igny8_content_id' => $content_data['content_id'] - ] - ]; - - $post_id = wp_insert_post($post_data); - - if (!is_wp_error($post_id)) { - // Update IGNY8 task with WordPress post ID - $this->api->put("/writer/tasks/{$content_data['task_id']}/", [ - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - ]); - } - - return $post_id; - } -} - -// Initialize sync -new Igny8WordPressSync(); -``` - -### WordPress Post Status Mapping - -| WordPress Status | IGNY8 Task Status | Description | -|------------------|-------------------|-------------| -| `publish` | `completed` | Post is published | -| `draft` | `draft` | Post is draft | -| `pending` | `pending` | Post is pending review | -| `private` | `completed` | Post is private (published) | -| `trash` | `archived` | Post is deleted/trashed | -| `future` | `scheduled` | Post is scheduled | - -### Fetching WordPress Post Data - -```php -/** - * Get WordPress post data for IGNY8 sync - */ -function igny8_get_post_data_for_sync($post_id) { - $post = get_post($post_id); - - if (!$post) { - return false; - } - - return [ - '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' => [ - 'task_id' => get_post_meta($post_id, '_igny8_task_id', true), - 'content_id' => get_post_meta($post_id, '_igny8_content_id', true), - 'primary_keywords' => get_post_meta($post_id, '_igny8_primary_keywords', true) - ] - ]; -} -``` - -### Scheduled Sync (Cron Job) - -```php -/** - * Scheduled sync of WordPress post statuses to IGNY8 - */ -add_action('igny8_sync_post_statuses', 'igny8_cron_sync_post_statuses'); - -function igny8_cron_sync_post_statuses() { - $result = igny8_batch_sync_post_statuses(); - - error_log(sprintf( - 'IGNY8: Synced %d posts, %d failed', - $result['synced'], - $result['failed'] - )); -} - -// Schedule daily sync -if (!wp_next_scheduled('igny8_sync_post_statuses')) { - wp_schedule_event(time(), 'daily', 'igny8_sync_post_statuses'); -} -``` - ---- - -## Complete Integration Flow - -### IGNY8 → WordPress Flow - -1. Content generated in IGNY8 -2. Task created/updated in IGNY8 -3. WordPress post created via `wp_insert_post()` -4. Post meta saved with `_igny8_task_id` -5. IGNY8 task updated with WordPress post ID - -### WordPress → IGNY8 Flow - -1. User saves/publishes WordPress post -2. `save_post` or `publish_post` hook fires -3. Plugin gets `_igny8_task_id` from post meta -4. Plugin calls IGNY8 API to update task status -5. If published, keywords updated to 'mapped' status -6. IGNY8 task status synced - ---- - -## WordPress Site Data Fetching and Semantic Mapping - -### Overview - -After WordPress site integration and API verification, you can fetch comprehensive site data (posts, taxonomies, products, attributes) and send it to IGNY8 for semantic strategy mapping. This enables content restructuring and site-wide optimization. - ---- - -## Fetching WordPress Posts - -### Get All Post Types - -```php -/** - * Fetch all posts of a specific type from WordPress - */ -function igny8_fetch_wordpress_posts($post_type = 'post', $per_page = 100) { - $api = new Igny8API(); - - // Use WordPress REST API to fetch posts - $wp_response = wp_remote_get(sprintf( - '%s/wp-json/wp/v2/%s?per_page=%d&status=publish', - get_site_url(), - $post_type, - $per_page - )); - - if (is_wp_error($wp_response)) { - return false; - } - - $posts = json_decode(wp_remote_retrieve_body($wp_response), true); - - // Format posts for IGNY8 - $formatted_posts = []; - foreach ($posts as $post) { - $formatted_posts[] = [ - 'id' => $post['id'], - 'title' => $post['title']['rendered'], - 'content' => $post['content']['rendered'], - 'excerpt' => $post['excerpt']['rendered'], - 'status' => $post['status'], - 'url' => $post['link'], - 'published' => $post['date'], - 'modified' => $post['modified'], - 'author' => $post['author'], - 'featured_image' => $post['featured_media'] ? wp_get_attachment_url($post['featured_media']) : null, - 'categories' => $post['categories'] ?? [], - 'tags' => $post['tags'] ?? [], - 'post_type' => $post_type, - 'meta' => [ - 'word_count' => str_word_count(strip_tags($post['content']['rendered'])), - 'reading_time' => ceil(str_word_count(strip_tags($post['content']['rendered'])) / 200) - ] - ]; - } - - return $formatted_posts; -} -``` - -### Get All Post Types - -```php -/** - * Fetch all available post types from WordPress - */ -function igny8_fetch_all_post_types() { - $wp_response = wp_remote_get(get_site_url() . '/wp-json/wp/v2/types'); - - if (is_wp_error($wp_response)) { - return false; - } - - $types = json_decode(wp_remote_retrieve_body($wp_response), true); - - $post_types = []; - foreach ($types as $type_name => $type_data) { - if ($type_data['public']) { - $post_types[] = [ - 'name' => $type_name, - 'label' => $type_data['name'], - 'description' => $type_data['description'] ?? '', - 'rest_base' => $type_data['rest_base'] ?? $type_name - ]; - } - } - - return $post_types; -} -``` - -### Batch Fetch All Posts - -```php -/** - * Fetch all posts from all post types - */ -function igny8_fetch_all_wordpress_posts() { - $post_types = igny8_fetch_all_post_types(); - $all_posts = []; - - foreach ($post_types as $type) { - $posts = igny8_fetch_wordpress_posts($type['name'], 100); - if ($posts) { - $all_posts = array_merge($all_posts, $posts); - } - } - - return $all_posts; -} -``` - ---- - -## Fetching WordPress Taxonomies - -### Get All Taxonomies - -```php -/** - * Fetch all taxonomies from WordPress - */ -function igny8_fetch_wordpress_taxonomies() { - $wp_response = wp_remote_get(get_site_url() . '/wp-json/wp/v2/taxonomies'); - - if (is_wp_error($wp_response)) { - return false; - } - - $taxonomies = json_decode(wp_remote_retrieve_body($wp_response), true); - - $formatted_taxonomies = []; - foreach ($taxonomies as $tax_name => $tax_data) { - if ($tax_data['public']) { - $formatted_taxonomies[] = [ - 'name' => $tax_name, - 'label' => $tax_data['name'], - 'description' => $tax_data['description'] ?? '', - 'hierarchical' => $tax_data['hierarchical'], - 'rest_base' => $tax_data['rest_base'] ?? $tax_name, - 'object_types' => $tax_data['types'] ?? [] - ]; - } - } - - return $formatted_taxonomies; -} -``` - -### Get Taxonomy Terms - -```php -/** - * Fetch all terms for a specific taxonomy - */ -function igny8_fetch_taxonomy_terms($taxonomy, $per_page = 100) { - $api = new Igny8API(); - - $wp_response = wp_remote_get(sprintf( - '%s/wp-json/wp/v2/%s?per_page=%d', - get_site_url(), - $taxonomy, - $per_page - )); - - if (is_wp_error($wp_response)) { - return false; - } - - $terms = json_decode(wp_remote_retrieve_body($wp_response), true); - - $formatted_terms = []; - foreach ($terms as $term) { - $formatted_terms[] = [ - 'id' => $term['id'], - 'name' => $term['name'], - 'slug' => $term['slug'], - 'description' => $term['description'] ?? '', - 'count' => $term['count'], - 'parent' => $term['parent'] ?? 0, - 'taxonomy' => $taxonomy, - 'url' => $term['link'] - ]; - } - - return $formatted_terms; -} -``` - -### Get All Taxonomy Terms - -```php -/** - * Fetch all terms from all taxonomies - */ -function igny8_fetch_all_taxonomy_terms() { - $taxonomies = igny8_fetch_wordpress_taxonomies(); - $all_terms = []; - - foreach ($taxonomies as $taxonomy) { - $terms = igny8_fetch_taxonomy_terms($taxonomy['rest_base'], 100); - if ($terms) { - $all_terms[$taxonomy['name']] = $terms; - } - } - - return $all_terms; -} -``` - ---- - -## Fetching WooCommerce Products - -### Get All Products - -```php -/** - * Fetch all WooCommerce products - */ -function igny8_fetch_woocommerce_products($per_page = 100) { - // Check if WooCommerce is active - if (!class_exists('WooCommerce')) { - return false; - } - - $wp_response = wp_remote_get(sprintf( - '%s/wp-json/wc/v3/products?per_page=%d&status=publish', - get_site_url(), - $per_page - ), [ - 'headers' => [ - 'Authorization' => 'Basic ' . base64_encode(get_option('woocommerce_api_consumer_key') . ':' . get_option('woocommerce_api_consumer_secret')) - ] - ]); - - if (is_wp_error($wp_response)) { - return false; - } - - $products = json_decode(wp_remote_retrieve_body($wp_response), true); - - $formatted_products = []; - foreach ($products as $product) { - $formatted_products[] = [ - 'id' => $product['id'], - 'name' => $product['name'], - 'slug' => $product['slug'], - 'sku' => $product['sku'], - 'type' => $product['type'], - 'status' => $product['status'], - 'description' => $product['description'], - 'short_description' => $product['short_description'], - 'price' => $product['price'], - 'regular_price' => $product['regular_price'], - 'sale_price' => $product['sale_price'], - 'on_sale' => $product['on_sale'], - 'stock_status' => $product['stock_status'], - 'stock_quantity' => $product['stock_quantity'], - 'categories' => $product['categories'] ?? [], - 'tags' => $product['tags'] ?? [], - 'images' => $product['images'] ?? [], - 'attributes' => $product['attributes'] ?? [], - 'variations' => $product['variations'] ?? [], - 'url' => $product['permalink'] - ]; - } - - return $formatted_products; -} -``` - -### Get Product Categories - -```php -/** - * Fetch WooCommerce product categories - */ -function igny8_fetch_product_categories($per_page = 100) { - if (!class_exists('WooCommerce')) { - return false; - } - - $wp_response = wp_remote_get(sprintf( - '%s/wp-json/wc/v3/products/categories?per_page=%d', - get_site_url(), - $per_page - ), [ - 'headers' => [ - 'Authorization' => 'Basic ' . base64_encode(get_option('woocommerce_api_consumer_key') . ':' . get_option('woocommerce_api_consumer_secret')) - ] - ]); - - if (is_wp_error($wp_response)) { - return false; - } - - $categories = json_decode(wp_remote_retrieve_body($wp_response), true); - - $formatted_categories = []; - foreach ($categories as $category) { - $formatted_categories[] = [ - 'id' => $category['id'], - 'name' => $category['name'], - 'slug' => $category['slug'], - 'description' => $category['description'] ?? '', - 'count' => $category['count'], - 'parent' => $category['parent'] ?? 0, - 'image' => $category['image']['src'] ?? null - ]; - } - - return $formatted_categories; -} -``` - -### Get Product Attributes - -```php -/** - * Fetch WooCommerce product attributes - */ -function igny8_fetch_product_attributes() { - if (!class_exists('WooCommerce')) { - return false; - } - - $wp_response = wp_remote_get( - get_site_url() . '/wp-json/wc/v3/products/attributes', - [ - 'headers' => [ - 'Authorization' => 'Basic ' . base64_encode(get_option('woocommerce_api_consumer_key') . ':' . get_option('woocommerce_api_consumer_secret')) - ] - ] - ); - - if (is_wp_error($wp_response)) { - return false; - } - - $attributes = json_decode(wp_remote_retrieve_body($wp_response), true); - - $formatted_attributes = []; - foreach ($attributes as $attribute) { - // Get attribute terms - $terms_response = wp_remote_get(sprintf( - '%s/wp-json/wc/v3/products/attributes/%d/terms', - get_site_url(), - $attribute['id'] - ), [ - 'headers' => [ - 'Authorization' => 'Basic ' . base64_encode(get_option('woocommerce_api_consumer_key') . ':' . get_option('woocommerce_api_consumer_secret')) - ] - ]); - - $terms = []; - if (!is_wp_error($terms_response)) { - $terms_data = json_decode(wp_remote_retrieve_body($terms_response), true); - foreach ($terms_data as $term) { - $terms[] = [ - 'id' => $term['id'], - 'name' => $term['name'], - 'slug' => $term['slug'] - ]; - } - } - - $formatted_attributes[] = [ - 'id' => $attribute['id'], - 'name' => $attribute['name'], - 'slug' => $attribute['slug'], - 'type' => $attribute['type'], - 'order_by' => $attribute['order_by'], - 'has_archives' => $attribute['has_archives'], - 'terms' => $terms - ]; - } - - return $formatted_attributes; -} -``` - ---- - -## Sending Site Data to IGNY8 for Semantic Mapping - -### Complete Site Data Collection - -```php -/** - * Collect all WordPress site data for IGNY8 semantic mapping - */ -function igny8_collect_site_data() { - $site_data = [ - 'site_url' => get_site_url(), - 'site_name' => get_bloginfo('name'), - 'site_description' => get_bloginfo('description'), - 'collected_at' => current_time('mysql'), - 'posts' => [], - 'taxonomies' => [], - 'products' => [], - 'product_attributes' => [] - ]; - - // Fetch all posts - $post_types = igny8_fetch_all_post_types(); - foreach ($post_types as $type) { - $posts = igny8_fetch_wordpress_posts($type['name'], 100); - if ($posts) { - $site_data['posts'] = array_merge($site_data['posts'], $posts); - } - } - - // Fetch all taxonomies and terms - $taxonomies = igny8_fetch_wordpress_taxonomies(); - foreach ($taxonomies as $taxonomy) { - $terms = igny8_fetch_taxonomy_terms($taxonomy['rest_base'], 100); - if ($terms) { - $site_data['taxonomies'][$taxonomy['name']] = [ - 'taxonomy' => $taxonomy, - 'terms' => $terms - ]; - } - } - - // Fetch WooCommerce products if available - if (class_exists('WooCommerce')) { - $products = igny8_fetch_woocommerce_products(100); - if ($products) { - $site_data['products'] = $products; - } - - $product_categories = igny8_fetch_product_categories(100); - if ($product_categories) { - $site_data['product_categories'] = $product_categories; - } - - $product_attributes = igny8_fetch_product_attributes(); - if ($product_attributes) { - $site_data['product_attributes'] = $product_attributes; - } - } - - return $site_data; -} -``` - -### Send Site Data to IGNY8 API - -```php -/** - * Send WordPress site data to IGNY8 for semantic strategy mapping - */ -function igny8_send_site_data_to_igny8($site_id) { - $api = new Igny8API(); - - // Collect all site data - $site_data = igny8_collect_site_data(); - - // Send to IGNY8 API - // Note: This endpoint may need to be created in IGNY8 API - $response = $api->post("/system/sites/{$site_id}/import/", [ - 'site_data' => $site_data, - 'import_type' => 'full_site_scan' - ]); - - if ($response['success']) { - // Store import ID for tracking - update_option('igny8_last_site_import_id', $response['data']['import_id'] ?? null); - return $response['data']; - } else { - error_log("IGNY8: Failed to send site data: " . $response['error']); - return false; - } -} -``` - -### Incremental Site Data Sync - -```php -/** - * Sync only changed posts/taxonomies since last sync - */ -function igny8_sync_incremental_site_data($site_id) { - $api = new Igny8API(); - - $last_sync = get_option('igny8_last_site_sync', 0); - - // Fetch only posts modified since last sync - $wp_response = wp_remote_get(sprintf( - '%s/wp-json/wp/v2/posts?after=%s&per_page=100', - get_site_url(), - date('c', $last_sync) - )); - - if (is_wp_error($wp_response)) { - return false; - } - - $posts = json_decode(wp_remote_retrieve_body($wp_response), true); - - if (empty($posts)) { - return ['synced' => 0, 'message' => 'No changes since last sync']; - } - - // Format posts - $formatted_posts = []; - foreach ($posts as $post) { - $formatted_posts[] = [ - 'id' => $post['id'], - 'title' => $post['title']['rendered'], - 'content' => $post['content']['rendered'], - 'status' => $post['status'], - 'modified' => $post['modified'], - 'categories' => $post['categories'] ?? [], - 'tags' => $post['tags'] ?? [] - ]; - } - - // Send incremental update to IGNY8 - $response = $api->post("/system/sites/{$site_id}/sync/", [ - 'posts' => $formatted_posts, - 'sync_type' => 'incremental', - 'last_sync' => $last_sync - ]); - - if ($response['success']) { - update_option('igny8_last_site_sync', time()); - return [ - 'synced' => count($formatted_posts), - 'message' => 'Incremental sync completed' - ]; - } - - return false; -} -``` - ---- - -## Semantic Strategy Mapping - -### Map Site Data to IGNY8 Semantic Structure - -```php -/** - * Map WordPress site data to IGNY8 semantic strategy - * This creates sectors, clusters, and keywords based on site structure - */ -function igny8_map_site_to_semantic_strategy($site_id, $site_data) { - $api = new Igny8API(); - - // Extract semantic structure from site data - $semantic_map = [ - 'sectors' => [], - 'clusters' => [], - 'keywords' => [] - ]; - - // Map taxonomies to sectors - foreach ($site_data['taxonomies'] as $tax_name => $tax_data) { - if ($tax_data['taxonomy']['hierarchical']) { - // Hierarchical taxonomies (categories) become sectors - $sector = [ - 'name' => $tax_data['taxonomy']['label'], - 'slug' => $tax_data['taxonomy']['name'], - 'description' => $tax_data['taxonomy']['description'], - 'source' => 'wordpress_taxonomy', - 'source_id' => $tax_name - ]; - - // Map terms to clusters - $clusters = []; - foreach ($tax_data['terms'] as $term) { - $clusters[] = [ - 'name' => $term['name'], - 'slug' => $term['slug'], - 'description' => $term['description'], - 'source' => 'wordpress_term', - 'source_id' => $term['id'] - ]; - - // Extract keywords from posts in this term - $keywords = igny8_extract_keywords_from_term_posts($term['id'], $tax_name); - $semantic_map['keywords'] = array_merge($semantic_map['keywords'], $keywords); - } - - $sector['clusters'] = $clusters; - $semantic_map['sectors'][] = $sector; - } - } - - // Map WooCommerce product categories to sectors - if (!empty($site_data['product_categories'])) { - $product_sector = [ - 'name' => 'Products', - 'slug' => 'products', - 'description' => 'WooCommerce product categories', - 'source' => 'woocommerce', - 'clusters' => [] - ]; - - foreach ($site_data['product_categories'] as $category) { - $product_sector['clusters'][] = [ - 'name' => $category['name'], - 'slug' => $category['slug'], - 'description' => $category['description'], - 'source' => 'woocommerce_category', - 'source_id' => $category['id'] - ]; - } - - $semantic_map['sectors'][] = $product_sector; - } - - // Send semantic map to IGNY8 - $response = $api->post("/planner/sites/{$site_id}/semantic-map/", [ - 'semantic_map' => $semantic_map, - 'site_data' => $site_data - ]); - - return $response; -} -``` - -### Extract Keywords from Posts - -```php -/** - * Extract keywords from posts associated with a taxonomy term - */ -function igny8_extract_keywords_from_term_posts($term_id, $taxonomy) { - $args = [ - 'post_type' => 'any', - 'posts_per_page' => -1, - 'tax_query' => [ - [ - 'taxonomy' => $taxonomy, - 'field' => 'term_id', - 'terms' => $term_id - ] - ] - ]; - - $query = new WP_Query($args); - $keywords = []; - - if ($query->have_posts()) { - while ($query->have_posts()) { - $query->the_post(); - - // Extract keywords from post title and content - $title_words = str_word_count(get_the_title(), 1); - $content_words = str_word_count(strip_tags(get_the_content()), 1); - - // Combine and get unique keywords - $all_words = array_merge($title_words, $content_words); - $unique_words = array_unique(array_map('strtolower', $all_words)); - - // Filter out common words (stop words) - $stop_words = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by']; - $keywords = array_merge($keywords, array_diff($unique_words, $stop_words)); - } - wp_reset_postdata(); - } - - // Format keywords - $formatted_keywords = []; - foreach (array_unique($keywords) as $keyword) { - if (strlen($keyword) > 3) { // Only keywords longer than 3 characters - $formatted_keywords[] = [ - 'keyword' => $keyword, - 'source' => 'wordpress_post', - 'source_term_id' => $term_id - ]; - } - } - - return $formatted_keywords; -} -``` - ---- - -## Content Restructuring Workflow - -### Complete Site Analysis and Restructuring - -```php -/** - * Complete workflow: Fetch site data → Map to semantic strategy → Restructure content - */ -function igny8_analyze_and_restructure_site($site_id) { - $api = new Igny8API(); - - // Step 1: Collect all site data - $site_data = igny8_collect_site_data(); - - // Step 2: Send to IGNY8 for analysis - $analysis_response = $api->post("/system/sites/{$site_id}/analyze/", [ - 'site_data' => $site_data, - 'analysis_type' => 'full_site_restructure' - ]); - - if (!$analysis_response['success']) { - return false; - } - - $analysis_id = $analysis_response['data']['analysis_id']; - - // Step 3: Map to semantic strategy - $mapping_response = igny8_map_site_to_semantic_strategy($site_id, $site_data); - - if (!$mapping_response['success']) { - return false; - } - - // Step 4: Get restructuring recommendations - $recommendations_response = $api->get("/system/sites/{$site_id}/recommendations/"); - - if (!$recommendations_response['success']) { - return false; - } - - return [ - 'analysis_id' => $analysis_id, - 'semantic_map' => $mapping_response['data'], - 'recommendations' => $recommendations_response['data'], - 'site_data_summary' => [ - 'total_posts' => count($site_data['posts']), - 'total_taxonomies' => count($site_data['taxonomies']), - 'total_products' => count($site_data['products'] ?? []), - 'total_keywords' => count($site_data['keywords'] ?? []) - ] - ]; -} -``` - -### Scheduled Site Data Sync - -```php -/** - * Scheduled sync of WordPress site data to IGNY8 - */ -add_action('igny8_sync_site_data', 'igny8_cron_sync_site_data'); - -function igny8_cron_sync_site_data() { - $site_id = get_option('igny8_site_id'); - - if (!$site_id) { - return; - } - - // Incremental sync - $result = igny8_sync_incremental_site_data($site_id); - - if ($result) { - error_log(sprintf( - 'IGNY8: Synced %d posts to site %d', - $result['synced'], - $site_id - )); - } -} - -// Schedule daily sync -if (!wp_next_scheduled('igny8_sync_site_data')) { - wp_schedule_event(time(), 'daily', 'igny8_sync_site_data'); -} -``` - ---- - -## Complete Site Integration Class - -```php -/** - * Complete WordPress site integration class - */ -class Igny8SiteIntegration { - private $api; - private $site_id; - - public function __construct($site_id) { - $this->api = new Igny8API(); - $this->site_id = $site_id; - } - - /** - * Full site scan and semantic mapping - */ - public function full_site_scan() { - // Collect all data - $site_data = igny8_collect_site_data(); - - // Send to IGNY8 - $response = $this->api->post("/system/sites/{$this->site_id}/import/", [ - 'site_data' => $site_data, - 'import_type' => 'full_scan' - ]); - - if ($response['success']) { - // Map to semantic strategy - $mapping = igny8_map_site_to_semantic_strategy($this->site_id, $site_data); - - return [ - 'success' => true, - 'import_id' => $response['data']['import_id'] ?? null, - 'semantic_map' => $mapping['data'] ?? null, - 'summary' => [ - 'posts' => count($site_data['posts']), - 'taxonomies' => count($site_data['taxonomies']), - 'products' => count($site_data['products'] ?? []), - 'product_attributes' => count($site_data['product_attributes'] ?? []) - ] - ]; - } - - return ['success' => false, 'error' => $response['error']]; - } - - /** - * Get semantic strategy recommendations - */ - public function get_recommendations() { - $response = $this->api->get("/planner/sites/{$this->site_id}/recommendations/"); - - if ($response['success']) { - return $response['data']; - } - - return false; - } - - /** - * Apply restructuring recommendations - */ - public function apply_restructuring($recommendations) { - $response = $this->api->post("/planner/sites/{$this->site_id}/restructure/", [ - 'recommendations' => $recommendations - ]); - - return $response['success']; - } -} - -// Usage -$integration = new Igny8SiteIntegration($site_id); -$result = $integration->full_site_scan(); - -if ($result['success']) { - echo "Scanned {$result['summary']['posts']} posts, {$result['summary']['taxonomies']} taxonomies"; - - // Get recommendations - $recommendations = $integration->get_recommendations(); - - // Apply restructuring - if ($recommendations) { - $integration->apply_restructuring($recommendations); - } -} -``` - ---- - -## Data Structure Examples - -### Post Data Structure - -```php -[ - 'id' => 123, - 'title' => 'Post Title', - 'content' => 'Post content...', - 'excerpt' => 'Post excerpt...', - 'status' => 'publish', - 'url' => 'https://example.com/post/', - 'published' => '2025-01-01T00:00:00', - 'modified' => '2025-01-02T00:00:00', - 'author' => 1, - 'featured_image' => 'https://example.com/image.jpg', - 'categories' => [1, 2, 3], - 'tags' => [4, 5], - 'post_type' => 'post', - 'meta' => [ - 'word_count' => 500, - 'reading_time' => 3 - ] -] -``` - -### Taxonomy Structure - -```php -[ - 'taxonomy' => [ - 'name' => 'category', - 'label' => 'Categories', - 'hierarchical' => true, - 'object_types' => ['post'] - ], - 'terms' => [ - [ - 'id' => 1, - 'name' => 'Technology', - 'slug' => 'technology', - 'description' => 'Tech posts', - 'count' => 25, - 'parent' => 0 - ] - ] -] -``` - -### Product Structure - -```php -[ - 'id' => 456, - 'name' => 'Product Name', - 'sku' => 'PROD-123', - 'type' => 'simple', - 'price' => '29.99', - 'categories' => [10, 11], - 'tags' => [20], - 'attributes' => [ - [ - 'id' => 1, - 'name' => 'Color', - 'options' => ['Red', 'Blue'] - ] - ], - 'variations' => [789, 790] -] -``` - ---- - -**Last Updated**: 2025-11-16 -**API Version**: 1.0.0 - ---- - -## Site Metadata Endpoint (Bridge) - -The WordPress Bridge exposes a discovery endpoint that the IGNY8 SaaS app can call to retrieve available post types, taxonomies and counts. The endpoint follows the IGNY8 unified response format and includes plugin-side flags so the SaaS app can decide whether to perform automatic syncs. - -- URL: `GET /wp-json/igny8/v1/site-metadata/` -- Authentication: Plugin-side connection must be authenticated. The bridge accepts: - - `Authorization: Bearer ` (preferred) - - `X-IGNY8-API-KEY: ` (supported) - The plugin stores an optional API key in `igny8_api_key` (secure storage via `igny8_store_secure_option()` when available) and will use it as the active access token when present. -- Response shape: IGNY8 unified format — top-level `success`, `data`, optional `message`, and `request_id`. -- Caching: Results are cached on the WP side using a transient (`igny8_site_metadata_v1`) with a default TTL of 300 seconds. - -Behavior notes: -- Toggling "Enable Sync Operations" (option `igny8_connection_enabled`) or "Enable Two-Way Sync" (`igny8_enable_two_way_sync`) in the plugin admin only affects background/inbound/outbound sync actions. REST discovery endpoints remain accessible so the SaaS app can still query metadata even when sync is disabled on either side. -- The endpoint includes two flags in the payload: `plugin_connection_enabled` and `two_way_sync_enabled` so the SaaS app can make informed decisions. - -Example response: - -```json -{ - "success": true, - "data": { - "post_types": { - "post": { "label": "Posts", "count": 123 }, - "page": { "label": "Pages", "count": 12 } - }, - "taxonomies": { - "category": { "label": "Categories", "count": 25 }, - "post_tag": { "label": "Tags", "count": 102 } - }, - "generated_at": 1700553600, - "plugin_connection_enabled": true, - "two_way_sync_enabled": true - }, - "message": "Site metadata retrieved", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -Developer notes: -- Admin settings: - - API key input (option `igny8_api_key`) added to Settings → IGNY8 API. When provided the key is stored securely and used as the access token by the plugin. - - Two-way sync toggle added (option `igny8_enable_two_way_sync`) — default ON. - - Connection toggle `igny8_connection_enabled` remains the master on/off switch for sync operations. -- Tests: Basic unit test added at `igny8-wp-integration-plugin/tests/test-site-metadata.php`. -- Security: Webhook secret and REST permission checks remain enforced; unauthenticated REST calls still return unified 401 errors. - diff --git a/igy8-wp-plugin/igny8-bridge.php b/igy8-wp-plugin/igny8-bridge.php deleted file mode 100644 index f7694802..00000000 --- a/igy8-wp-plugin/igny8-bridge.php +++ /dev/null @@ -1,184 +0,0 @@ -init(); - } - - /** - * Initialize plugin - */ - private function init() { - // Load core files - $this->load_dependencies(); - - // Initialize hooks - add_action('plugins_loaded', array($this, 'load_plugin_textdomain')); - add_action('init', array($this, 'init_plugin')); - - // Activation/Deactivation hooks - register_activation_hook(__FILE__, array($this, 'activate')); - register_deactivation_hook(__FILE__, array($this, 'deactivate')); - } - - /** - * Load plugin dependencies - */ - private function load_dependencies() { - // Core classes - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/functions.php'; - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-api.php'; - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-site.php'; - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-rest-api.php'; - // Webhooks removed - using API key authentication only - - // Webhook logs (used in admin and frontend) - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-webhook-logs.php'; - - // Admin classes (only in admin) - if (is_admin()) { - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'admin/class-admin.php'; - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'admin/class-admin-columns.php'; - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'admin/class-post-meta-boxes.php'; - } - - // Sync handlers - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'sync/hooks.php'; - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'sync/post-sync.php'; - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'sync/taxonomy-sync.php'; - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'sync/igny8-to-wp.php'; - - // Data collection - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'data/site-collection.php'; - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'data/semantic-mapping.php'; - require_once IGNY8_BRIDGE_PLUGIN_DIR . 'data/link-graph.php'; - } - - /** - * Load plugin textdomain - */ - public function load_plugin_textdomain() { - load_plugin_textdomain( - 'igny8-bridge', - false, - dirname(IGNY8_BRIDGE_PLUGIN_BASENAME) . '/languages' - ); - } - - /** - * Initialize plugin functionality - */ - public function init_plugin() { - // Register post meta fields - igny8_register_post_meta(); - - // Register taxonomies - igny8_register_taxonomies(); - - // Initialize admin (if in admin) - if (is_admin()) { - Igny8Admin::get_instance(); - } - - // Initialize sync handlers - if (class_exists('Igny8WordPressSync')) { - new Igny8WordPressSync(); - } - } - - /** - * Plugin activation - */ - public function activate() { - // Register post meta and taxonomies - igny8_register_post_meta(); - igny8_register_taxonomies(); - - // Flush rewrite rules - flush_rewrite_rules(); - - // Set default options - if (!get_option('igny8_bridge_version')) { - add_option('igny8_bridge_version', IGNY8_BRIDGE_VERSION); - } - - // Schedule cron jobs - igny8_schedule_cron_jobs(); - } - - /** - * Plugin deactivation - */ - public function deactivate() { - // Unschedule cron jobs - igny8_unschedule_cron_jobs(); - - // Flush rewrite rules - flush_rewrite_rules(); - } -} - -/** - * Initialize plugin - */ -function igny8_bridge_init() { - return Igny8Bridge::get_instance(); -} - -// Start the plugin -igny8_bridge_init(); - diff --git a/igy8-wp-plugin/includes/class-igny8-api.php b/igy8-wp-plugin/includes/class-igny8-api.php deleted file mode 100644 index 17ef8771..00000000 --- a/igy8-wp-plugin/includes/class-igny8-api.php +++ /dev/null @@ -1,486 +0,0 @@ -access_token = $api_key; - $this->api_key_auth = true; - } - } - - /** - * Connect using API key - * Tests connection by calling /v1/integration/integrations/test-connection/ endpoint - * - * @param string $api_key API key from IGNY8 app - * @param int $site_id Site ID from IGNY8 app - * @return bool True on success, false on failure - */ - public function connect($api_key, $site_id = null) { - if (empty($api_key)) { - return false; - } - - // Store API key - if (function_exists('igny8_store_secure_option')) { - igny8_store_secure_option('igny8_api_key', $api_key); - } else { - update_option('igny8_api_key', $api_key); - } - - $this->access_token = $api_key; - $this->api_key_auth = true; - - // If site_id provided, test connection to integration endpoint - if (!empty($site_id)) { - $test_response = $this->post('/v1/integration/integrations/test-connection/', array( - 'site_id' => (int) $site_id, - 'api_key' => $api_key, - 'site_url' => get_site_url() - )); - - if ($test_response['success']) { - $timestamp = current_time('timestamp'); - update_option('igny8_last_api_health_check', $timestamp); - return true; - } - - return false; - } - - // Fallback: if no site_id, just verify API key exists by making a simple call - // This tests that the API key is valid format at least - $timestamp = current_time('timestamp'); - update_option('igny8_last_api_health_check', $timestamp); - return true; - } - - /** - * Check if API is authenticated - * - * @return bool True if authenticated, false otherwise - */ - public function is_authenticated() { - return !empty($this->access_token); - } - - /** - * Parse unified API response - * - * @param array|WP_Error $response HTTP response - * @return array Parsed response - */ - private function parse_response($response) { - if (is_wp_error($response)) { - return array( - 'success' => false, - 'error' => $response->get_error_message(), - 'http_status' => 0 - ); - } - - $status_code = wp_remote_retrieve_response_code($response); - $raw_body = wp_remote_retrieve_body($response); - $body = json_decode($raw_body, true); - - // Handle non-JSON responses — allow empty arrays/objects but detect JSON decode errors - if (json_last_error() !== JSON_ERROR_NONE) { - return array( - 'success' => false, - 'error' => 'Invalid JSON response: ' . json_last_error_msg(), - 'raw_body' => substr($raw_body, 0, 200), - 'http_status' => $status_code - ); - } - - // Check if response follows unified format - if (isset($body['success'])) { - $body['http_status'] = $status_code; - - // Handle throttling errors (429) - extract retry delay from error message - if ($status_code === 429 && isset($body['error'])) { - // Extract delay from error message like "Request was throttled. Expected available in 1 second." - if (preg_match('/Expected available in (\d+) second/i', $body['error'], $matches)) { - $body['retry_after'] = intval($matches[1]); - } elseif (preg_match('/(\d+) second/i', $body['error'], $matches)) { - $body['retry_after'] = intval($matches[1]); - } else { - // Default to 2 seconds if we can't parse it - $body['retry_after'] = 2; - } - } - - return $body; - } - - // Legacy format - wrap in unified format - if ($status_code >= 200 && $status_code < 300) { - return array( - 'success' => true, - 'data' => $body, - 'http_status' => $status_code - ); - } else { - $error_message = $body['detail'] ?? 'HTTP ' . $status_code . ' error'; - - // Handle throttling in legacy format - $retry_after = null; - if ($status_code === 429) { - if (preg_match('/Expected available in (\d+) second/i', $error_message, $matches)) { - $retry_after = intval($matches[1]); - } elseif (preg_match('/(\d+) second/i', $error_message, $matches)) { - $retry_after = intval($matches[1]); - } else { - $retry_after = 2; - } - } - - $result = array( - 'success' => false, - 'error' => $error_message, - 'http_status' => $status_code, - 'raw_error' => $body - ); - - if ($retry_after !== null) { - $result['retry_after'] = $retry_after; - } - - return $result; - } - } - - /** - * Get headers with authentication - * Uses Bearer token format for API key authentication - * - * @return array Headers array - */ - private function get_headers() { - $headers = array( - 'Content-Type' => 'application/json', - 'Accept' => 'application/json' - ); - - if (!empty($this->access_token)) { - $headers['Authorization'] = 'Bearer ' . $this->access_token; - } - - return $headers; - } - - /** - * Make GET request with automatic retry on throttling - * - * @param string $endpoint API endpoint (e.g. /v1/auth/sites/ or /v1/integration/integrations/) - * @param int $max_retries Maximum number of retries for throttled requests (default: 3) - * @return array Response data - */ - public function get($endpoint, $max_retries = 3) { - if (!$this->is_authenticated()) { - return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401); - } - // Ensure endpoint starts with /v1 - if (strpos($endpoint, '/v1/') === false) { - if (strpos($endpoint, '/') !== 0) { - $endpoint = '/' . $endpoint; - } - if (strpos($endpoint, '/v1') !== 0) { - $endpoint = '/v1' . $endpoint; - } - } - - $url = $this->base_url . $endpoint; - $headers = $this->get_headers(); - $retry_count = 0; - - while ($retry_count <= $max_retries) { - // Debug logging (enable with WP_DEBUG or IGNY8_DEBUG constant) - $debug_enabled = (defined('WP_DEBUG') && WP_DEBUG) || (defined('IGNY8_DEBUG') && IGNY8_DEBUG); - if ($debug_enabled) { - error_log(sprintf( - 'IGNY8 DEBUG GET: %s | Headers: %s', - $url, - json_encode(array_merge($headers, array('Authorization' => 'Bearer ***'))) - )); - } - - $response = wp_remote_get($url, array( - 'headers' => $headers, - 'timeout' => 30 - )); - - // Debug response - if ($debug_enabled) { - $status_code = wp_remote_retrieve_response_code($response); - $response_body = wp_remote_retrieve_body($response); - error_log(sprintf( - 'IGNY8 DEBUG RESPONSE: Status=%s | Body=%s', - $status_code, - substr($response_body, 0, 500) - )); - } - - $body = $this->parse_response($response); - - // If throttled (429), retry after the specified delay - if (isset($body['http_status']) && $body['http_status'] === 429 && $retry_count < $max_retries) { - $retry_after = isset($body['retry_after']) ? $body['retry_after'] : 2; - - // Add a small buffer (0.5 seconds) to ensure we wait long enough - $wait_time = $retry_after + 0.5; - $wait_seconds = (int) ceil($wait_time); // Convert to integer, rounding up - - // Log retry attempt - if ($debug_enabled) { - error_log(sprintf( - 'IGNY8 DEBUG: Request throttled, retrying after %.1f seconds (attempt %d/%d)', - $wait_time, - $retry_count + 1, - $max_retries - )); - } - - // Wait before retrying - sleep($wait_seconds); - $retry_count++; - continue; - } - - // Not throttled or max retries reached, return response - // API keys don't expire, so no refresh logic needed - // If 401, the API key is invalid or revoked - return $body; - } - - // Should never reach here, but return last response if we do - return $body; - } - - /** - * Make POST request with automatic retry on throttling - * - * @param string $endpoint API endpoint (e.g. /v1/integration/integrations/) - * @param array $data Request data - * @param int $max_retries Maximum number of retries for throttled requests (default: 3) - * @return array Response data - */ - public function post($endpoint, $data, $max_retries = 3) { - // Special case: test-connection endpoint allows API key in request body - // So we don't require pre-authentication for this endpoint - $is_test_connection = (strpos($endpoint, 'test-connection') !== false); - $has_api_key_in_data = !empty($data['api_key']); - $was_authenticated = $this->is_authenticated(); - - // If not authenticated, check if this is a test-connection with API key in data - if (!$was_authenticated) { - if ($is_test_connection && $has_api_key_in_data) { - // Temporarily set the API key for this request - $temp_api_key = $this->access_token; - $this->access_token = $data['api_key']; - } else { - return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401); - } - } - // Ensure endpoint starts with /v1 - if (strpos($endpoint, '/v1/') === false) { - if (strpos($endpoint, '/') !== 0) { - $endpoint = '/' . $endpoint; - } - if (strpos($endpoint, '/v1') !== 0) { - $endpoint = '/v1' . $endpoint; - } - } - - $retry_count = 0; - - while ($retry_count <= $max_retries) { - $response = wp_remote_post($this->base_url . $endpoint, array( - 'headers' => $this->get_headers(), - 'body' => json_encode($data), - 'timeout' => 60 - )); - - $body = $this->parse_response($response); - - // If throttled (429), retry after the specified delay - if (isset($body['http_status']) && $body['http_status'] === 429 && $retry_count < $max_retries) { - $retry_after = isset($body['retry_after']) ? $body['retry_after'] : 2; - - // Add a small buffer (0.5 seconds) to ensure we wait long enough - $wait_time = $retry_after + 0.5; - $wait_seconds = (int) ceil($wait_time); // Convert to integer, rounding up - - // Log retry attempt - $debug_enabled = (defined('WP_DEBUG') && WP_DEBUG) || (defined('IGNY8_DEBUG') && IGNY8_DEBUG); - if ($debug_enabled) { - error_log(sprintf( - 'IGNY8 DEBUG: Request throttled, retrying after %.1f seconds (attempt %d/%d)', - $wait_time, - $retry_count + 1, - $max_retries - )); - } - - // Wait before retrying - sleep($wait_seconds); - $retry_count++; - continue; - } - - // Not throttled or max retries reached, return response - // Restore original access token if we temporarily set it - if ($is_test_connection && $has_api_key_in_data && !$was_authenticated) { - $this->access_token = isset($temp_api_key) ? $temp_api_key : null; - } - return $body; - } - - // Should never reach here, but return last response if we do - // Restore original access token if we temporarily set it - if ($is_test_connection && $has_api_key_in_data && !$was_authenticated) { - $this->access_token = isset($temp_api_key) ? $temp_api_key : null; - } - return $body; - } - - /** - * Make PUT request with automatic retry on throttling - * - * @param string $endpoint API endpoint (e.g. /v1/integration/integrations/1/update-structure/) - * @param array $data Request data - * @param int $max_retries Maximum number of retries for throttled requests (default: 3) - * @return array Response data - */ - public function put($endpoint, $data, $max_retries = 3) { - if (!$this->is_authenticated()) { - return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401); - } - // Ensure endpoint starts with /v1 - if (strpos($endpoint, '/v1/') === false) { - if (strpos($endpoint, '/') !== 0) { - $endpoint = '/' . $endpoint; - } - if (strpos($endpoint, '/v1') !== 0) { - $endpoint = '/v1' . $endpoint; - } - } - - $retry_count = 0; - - while ($retry_count <= $max_retries) { - $response = wp_remote_request($this->base_url . $endpoint, array( - 'method' => 'PUT', - 'headers' => $this->get_headers(), - 'body' => json_encode($data), - 'timeout' => 60 - )); - - $body = $this->parse_response($response); - - // If throttled (429), retry after the specified delay - if (isset($body['http_status']) && $body['http_status'] === 429 && $retry_count < $max_retries) { - $retry_after = isset($body['retry_after']) ? $body['retry_after'] : 2; - $wait_time = $retry_after + 0.5; - $wait_seconds = (int) ceil($wait_time); // Convert to integer, rounding up - - $debug_enabled = (defined('WP_DEBUG') && WP_DEBUG) || (defined('IGNY8_DEBUG') && IGNY8_DEBUG); - if ($debug_enabled) { - error_log(sprintf( - 'IGNY8 DEBUG: Request throttled, retrying after %.1f seconds (attempt %d/%d)', - $wait_time, - $retry_count + 1, - $max_retries - )); - } - - sleep($wait_seconds); - $retry_count++; - continue; - } - - return $body; - } - - return $body; - } - - /** - * Make DELETE request - * - * @param string $endpoint API endpoint - * @return array Response data - */ - public function delete($endpoint) { - if (!$this->is_authenticated()) { - return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401); - } - $response = wp_remote_request($this->base_url . $endpoint, array( - 'method' => 'DELETE', - 'headers' => $this->get_headers(), - 'timeout' => 30 - )); - - return $this->parse_response($response); - } - - /** - * Get access token - * - * @return string|null Access token - */ - public function get_access_token() { - return $this->access_token; - } -} - diff --git a/igy8-wp-plugin/includes/class-igny8-link-queue.php b/igy8-wp-plugin/includes/class-igny8-link-queue.php deleted file mode 100644 index a9b4dd1c..00000000 --- a/igy8-wp-plugin/includes/class-igny8-link-queue.php +++ /dev/null @@ -1,202 +0,0 @@ - uniqid('link_', true), - 'post_id' => intval($link_data['post_id']), - 'target_url' => esc_url_raw($link_data['target_url']), - 'anchor' => sanitize_text_field($link_data['anchor']), - 'source' => sanitize_text_field($link_data['source'] ?? 'igny8_linker'), - 'priority' => sanitize_text_field($link_data['priority'] ?? 'normal'), - 'status' => 'pending', - 'created_at' => $link_data['created_at'] ?? current_time('mysql'), - 'attempts' => 0 - ); - - $queue[] = $queue_item; - - // Limit queue size (keep last 1000 items) - if (count($queue) > 1000) { - $queue = array_slice($queue, -1000); - } - - update_option('igny8_link_queue', $queue); - - // Trigger processing if not already scheduled - if (!wp_next_scheduled('igny8_process_link_queue')) { - wp_schedule_single_event(time() + 60, 'igny8_process_link_queue'); - } - - return $queue_item['id']; -} - -/** - * Process link insertion queue - */ -function igny8_process_link_queue() { - if (!igny8_is_connection_enabled()) { - return; - } - - if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('linker')) { - return; - } - - $queue = get_option('igny8_link_queue', array()); - - if (empty($queue)) { - return; - } - - // Process up to 10 items per run - $processed = 0; - $max_per_run = 10; - - foreach ($queue as $key => $item) { - if ($processed >= $max_per_run) { - break; - } - - if ($item['status'] !== 'pending') { - continue; - } - - $result = igny8_insert_link_into_post($item); - - if ($result['success']) { - $queue[$key]['status'] = 'completed'; - $queue[$key]['completed_at'] = current_time('mysql'); - } else { - $queue[$key]['attempts']++; - - if ($queue[$key]['attempts'] >= 3) { - $queue[$key]['status'] = 'failed'; - $queue[$key]['error'] = $result['error'] ?? 'Unknown error'; - } - } - - $processed++; - } - - update_option('igny8_link_queue', $queue); - - // Schedule next run if there are pending items - $has_pending = false; - foreach ($queue as $item) { - if ($item['status'] === 'pending') { - $has_pending = true; - break; - } - } - - if ($has_pending && !wp_next_scheduled('igny8_process_link_queue')) { - wp_schedule_single_event(time() + 60, 'igny8_process_link_queue'); - } -} - -/** - * Insert link into post content - * - * @param array $link_item Link queue item - * @return array Result - */ -function igny8_insert_link_into_post($link_item) { - $post_id = $link_item['post_id']; - $target_url = $link_item['target_url']; - $anchor = $link_item['anchor']; - - $post = get_post($post_id); - - if (!$post) { - return array('success' => false, 'error' => 'Post not found'); - } - - $content = $post->post_content; - - // Check if link already exists - if (strpos($content, $target_url) !== false) { - return array('success' => true, 'message' => 'Link already exists'); - } - - // Find first occurrence of anchor text not already in a link - $anchor_escaped = preg_quote($anchor, '/'); - - // Pattern to find anchor text that's not inside an tag - // This is a simplified approach - find anchor text and check if it's not in a link - $pattern = '/\b' . $anchor_escaped . '\b/i'; - - if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) { - foreach ($matches[0] as $match) { - $position = $match[1]; - $length = strlen($match[0]); - - // Check if this position is inside an tag - $before = substr($content, 0, $position); - $after = substr($content, $position + $length); - - // Count unclosed tags before this position - $open_tags = substr_count($before, ''); - - // If not inside a link, replace it - if ($open_tags <= $close_tags) { - $link_html = '' . esc_html($anchor) . ''; - $new_content = substr_replace($content, $link_html, $position, $length); - - $result = wp_update_post(array( - 'ID' => $post_id, - 'post_content' => $new_content - )); - - if ($result && !is_wp_error($result)) { - return array('success' => true, 'message' => 'Link inserted'); - } else { - return array('success' => false, 'error' => 'Failed to update post'); - } - } - } - } - - // If anchor not found, append link at end of content - $link_html = "\n\n

" . esc_html($anchor) . "

"; - $new_content = $content . $link_html; - - $result = wp_update_post(array( - 'ID' => $post_id, - 'post_content' => $new_content - )); - - if ($result && !is_wp_error($result)) { - return array('success' => true, 'message' => 'Link appended'); - } else { - return array('success' => false, 'error' => 'Failed to update post'); - } -} - -// Register cron hook -add_action('igny8_process_link_queue', 'igny8_process_link_queue'); - diff --git a/igy8-wp-plugin/includes/class-igny8-rest-api.php b/igy8-wp-plugin/includes/class-igny8-rest-api.php deleted file mode 100644 index 77a2bdb3..00000000 --- a/igy8-wp-plugin/includes/class-igny8-rest-api.php +++ /dev/null @@ -1,444 +0,0 @@ -\d+)', array( - 'methods' => 'GET', - 'callback' => array($this, 'get_post_by_content_id'), - 'permission_callback' => array($this, 'check_permission'), - 'args' => array( - 'content_id' => array( - 'required' => true, - 'type' => 'integer', - 'description' => 'IGNY8 content ID' - ) - ) - )); - - // Get post by IGNY8 task_id - register_rest_route('igny8/v1', '/post-by-task-id/(?P\d+)', array( - 'methods' => 'GET', - 'callback' => array($this, 'get_post_by_task_id'), - 'permission_callback' => array($this, 'check_permission'), - 'args' => array( - 'task_id' => array( - 'required' => true, - 'type' => 'integer', - 'description' => 'IGNY8 task ID' - ) - ) - )); - - // Get post status by content_id - register_rest_route('igny8/v1', '/post-status/(?P\d+)', array( - 'methods' => 'GET', - 'callback' => array($this, 'get_post_status_by_content_id'), - 'permission_callback' => array($this, 'check_permission'), - 'args' => array( - 'content_id' => array( - 'required' => true, - 'type' => 'integer', - 'description' => 'IGNY8 content ID' - ) - ) - )); - - // Site metadata - post types, taxonomies and counts (unified response format) - register_rest_route('igny8/v1', '/site-metadata/', array( - 'methods' => 'GET', - // We perform permission checks inside callback to ensure unified response format - 'callback' => array($this, 'get_site_metadata'), - 'permission_callback' => '__return_true', - )); - - // Plugin status endpoint - returns connection status and API key info - register_rest_route('igny8/v1', '/status', array( - 'methods' => 'GET', - 'callback' => array($this, 'get_status'), - 'permission_callback' => '__return_true', // Public endpoint for health checks - )); - } - - /** - * Check API permission - uses API key only - * - * @param WP_REST_Request $request Request object - * @return bool|WP_Error - */ - public function check_permission($request) { - // Check if authenticated with IGNY8 via API key - $api = new Igny8API(); - - // Accept explicit X-IGNY8-API-KEY header for incoming requests - $header_api_key = $request->get_header('x-igny8-api-key'); - if ($header_api_key) { - $stored_api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key'); - if ($stored_api_key && hash_equals($stored_api_key, $header_api_key)) { - return true; - } - } - - // Check Authorization Bearer header - $auth_header = $request->get_header('Authorization'); - if ($auth_header) { - $stored_api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key'); - if ($stored_api_key && strpos($auth_header, 'Bearer ' . $stored_api_key) !== false) { - return true; - } - } - - // Allow if API key is configured (for internal use) - if ($api->is_authenticated()) { - return true; - } - - return new WP_Error( - 'rest_forbidden', - __('IGNY8 API key not authenticated', 'igny8-bridge'), - array('status' => 401) - ); - } - - /** - * Get post by content_id - * - * @param WP_REST_Request $request Request object - * @return WP_REST_Response|WP_Error - */ - public function get_post_by_content_id($request) { - // Double-check connection is enabled - if (!igny8_is_connection_enabled()) { - return new WP_Error( - 'rest_forbidden', - __('IGNY8 connection is disabled', 'igny8-bridge'), - array('status' => 403) - ); - } - - $content_id = intval($request['content_id']); - - // Find post by content_id meta - $posts = get_posts(array( - 'meta_key' => '_igny8_content_id', - 'meta_value' => $content_id, - 'post_type' => 'any', - 'posts_per_page' => 1, - 'post_status' => 'any' - )); - - if (empty($posts)) { - return new WP_Error( - 'rest_not_found', - __('Post not found for this content ID', 'igny8-bridge'), - array('status' => 404) - ); - } - - $post = $posts[0]; - - return rest_ensure_response(array( - 'success' => true, - 'data' => array( - 'post_id' => $post->ID, - 'title' => $post->post_title, - 'status' => $post->post_status, - 'wordpress_status' => $post->post_status, - 'igny8_status' => igny8_map_wp_status_to_igny8($post->post_status), - 'url' => get_permalink($post->ID), - 'post_type' => $post->post_type, - 'content_id' => $content_id, - 'task_id' => get_post_meta($post->ID, '_igny8_task_id', true), - 'last_synced' => get_post_meta($post->ID, '_igny8_last_synced', true) - ) - )); - } - - /** - * Get post by task_id - * - * @param WP_REST_Request $request Request object - * @return WP_REST_Response|WP_Error - */ - public function get_post_by_task_id($request) { - // Double-check connection is enabled - if (!igny8_is_connection_enabled()) { - return new WP_Error( - 'rest_forbidden', - __('IGNY8 connection is disabled', 'igny8-bridge'), - array('status' => 403) - ); - } - - $task_id = intval($request['task_id']); - - // Find post by task_id meta - $posts = get_posts(array( - 'meta_key' => '_igny8_task_id', - 'meta_value' => $task_id, - 'post_type' => 'any', - 'posts_per_page' => 1, - 'post_status' => 'any' - )); - - if (empty($posts)) { - return new WP_Error( - 'rest_not_found', - __('Post not found for this task ID', 'igny8-bridge'), - array('status' => 404) - ); - } - - $post = $posts[0]; - - return rest_ensure_response(array( - 'success' => true, - 'data' => array( - 'post_id' => $post->ID, - 'title' => $post->post_title, - 'status' => $post->post_status, - 'wordpress_status' => $post->post_status, - 'igny8_status' => igny8_map_wp_status_to_igny8($post->post_status), - 'url' => get_permalink($post->ID), - 'post_type' => $post->post_type, - 'task_id' => $task_id, - 'content_id' => get_post_meta($post->ID, '_igny8_content_id', true), - 'last_synced' => get_post_meta($post->ID, '_igny8_last_synced', true) - ) - )); - } - - /** - * Get post status by content_id - * - * @param WP_REST_Request $request Request object - * @return WP_REST_Response|WP_Error - */ - public function get_post_status_by_content_id($request) { - // Double-check connection is enabled - if (!igny8_is_connection_enabled()) { - return new WP_Error( - 'rest_forbidden', - __('IGNY8 connection is disabled', 'igny8-bridge'), - array('status' => 403) - ); - } - - $content_id = intval($request['content_id']); - - // Find post by content_id meta - $posts = get_posts(array( - 'meta_key' => '_igny8_content_id', - 'meta_value' => $content_id, - 'post_type' => 'any', - 'posts_per_page' => 1, - 'post_status' => 'any', - 'fields' => 'ids' // Only get IDs for performance - )); - - if (empty($posts)) { - return rest_ensure_response(array( - 'success' => false, - 'message' => 'Post not found', - 'content_id' => $content_id - )); - } - - $post_id = $posts[0]; - $post = get_post($post_id); - - return rest_ensure_response(array( - 'success' => true, - 'data' => array( - 'post_id' => $post_id, - 'wordpress_status' => $post->post_status, - 'igny8_status' => igny8_map_wp_status_to_igny8($post->post_status), - 'status_mapping' => array( - 'publish' => 'completed', - 'draft' => 'draft', - 'pending' => 'pending', - 'private' => 'completed', - 'trash' => 'archived', - 'future' => 'scheduled' - ), - 'content_id' => $content_id, - 'url' => get_permalink($post_id), - 'last_synced' => get_post_meta($post_id, '_igny8_last_synced', true) - ) - )); - } - - /** - * Helper: generate a request_id (UUIDv4 if available) - * - * @return string - */ - private function generate_request_id() { - if (function_exists('wp_generate_uuid4')) { - return wp_generate_uuid4(); - } - - // Fallback: uniqid with more entropy - return uniqid('', true); - } - - /** - * Helper: Build unified API response and return WP_REST_Response - * - * @param bool $success - * @param mixed $data - * @param string|null $message - * @param string|null $error - * @param array|null $errors - * @param int $status - * @return WP_REST_Response - */ - private function build_unified_response($success, $data = null, $message = null, $error = null, $errors = null, $status = 200) { - $payload = array( - 'success' => (bool) $success, - 'data' => $data, - 'message' => $message, - 'request_id' => $this->generate_request_id() - ); - - if (!$success) { - $payload['error'] = $error ?: 'Unknown error'; - if (!empty($errors)) { - $payload['errors'] = $errors; - } - } - - $response = rest_ensure_response($payload); - $response->set_status($status); - return $response; - } - - /** - * GET /status - Returns plugin connection status and API key info - * - * @param WP_REST_Request $request - * @return WP_REST_Response - */ - public function get_status($request) { - $api = new Igny8API(); - $api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key'); - $connection_enabled = igny8_is_connection_enabled(); - - $data = array( - 'connected' => !empty($api_key) && $api->is_authenticated(), - 'has_api_key' => !empty($api_key), - 'communication_enabled' => $connection_enabled, - 'plugin_version' => defined('IGNY8_BRIDGE_VERSION') ? IGNY8_BRIDGE_VERSION : '1.0.0', - 'wordpress_version' => get_bloginfo('version'), - 'last_health_check' => get_option('igny8_last_api_health_check', 0), - 'health' => (!empty($api_key) && $connection_enabled) ? 'healthy' : 'not_configured' - ); - - return $this->build_unified_response(true, $data, 'Plugin status retrieved', null, null, 200); - } - - /** - * GET /site-metadata/ - returns post types, taxonomies and counts in unified format - * - * @param WP_REST_Request $request - * @return WP_REST_Response - */ - public function get_site_metadata($request) { - // Use transient cache to avoid expensive counts on large sites - $cache_key = 'igny8_site_metadata_v1'; - $cached = get_transient($cache_key); - if ($cached !== false) { - return $this->build_unified_response(true, $cached, 'Site metadata (cached)', null, null, 200); - } - - // Perform permission check and return unified error if not allowed - $perm = $this->check_permission($request); - if (is_wp_error($perm)) { - $status = 403; - $error_data = $perm->get_error_data(); - if (is_array($error_data) && isset($error_data['status'])) { - $status = intval($error_data['status']); - } - return $this->build_unified_response(false, null, null, $perm->get_error_message(), null, $status); - } - - // Collect post types (public) - $post_types_objects = get_post_types(array('public' => true), 'objects'); - $post_types = array(); - foreach ($post_types_objects as $slug => $obj) { - // Get total count across statuses - $count_obj = wp_count_posts($slug); - $total = 0; - if (is_object($count_obj)) { - foreach (get_object_vars($count_obj) as $val) { - $total += intval($val); - } - } - $post_types[$slug] = array( - 'label' => $obj->labels->singular_name ?? $obj->label, - 'count' => $total - ); - } - - // Collect taxonomies (public) - $taxonomy_objects = get_taxonomies(array('public' => true), 'objects'); - $taxonomies = array(); - foreach ($taxonomy_objects as $slug => $obj) { - // Use wp_count_terms when available - $term_count = 0; - if (function_exists('wp_count_terms')) { - $term_count = intval(wp_count_terms($slug)); - } else { - $terms = get_terms(array('taxonomy' => $slug, 'hide_empty' => false, 'fields' => 'ids')); - $term_count = is_array($terms) ? count($terms) : 0; - } - - $taxonomies[$slug] = array( - 'label' => $obj->labels->name ?? $obj->label, - 'count' => $term_count - ); - } - - $data = array( - 'post_types' => $post_types, - 'taxonomies' => $taxonomies, - 'generated_at' => time(), - 'plugin_connection_enabled' => (bool) igny8_is_connection_enabled(), - 'two_way_sync_enabled' => (bool) get_option('igny8_enable_two_way_sync', 1) - ); - // Cache for 5 minutes - set_transient($cache_key, $data, 300); - - return $this->build_unified_response(true, $data, 'Site metadata retrieved', null, null, 200); - } -} - -// Initialize REST API -new Igny8RestAPI(); - diff --git a/igy8-wp-plugin/includes/class-igny8-site.php b/igy8-wp-plugin/includes/class-igny8-site.php deleted file mode 100644 index 898e2961..00000000 --- a/igy8-wp-plugin/includes/class-igny8-site.php +++ /dev/null @@ -1,118 +0,0 @@ -api = new Igny8API(); - $this->site_id = $site_id; - } - - /** - * Full site scan and semantic mapping - * - * @return array Result array - */ - public function full_site_scan() { - // Collect all data - $site_data = igny8_collect_site_data(); - - // Send to IGNY8 - $response = $this->api->post("/system/sites/{$this->site_id}/import/", array( - 'site_data' => $site_data, - 'import_type' => 'full_scan' - )); - - if ($response['success']) { - // Map to semantic strategy - $mapping = igny8_map_site_to_semantic_strategy($this->site_id, $site_data); - - return array( - 'success' => true, - 'import_id' => $response['data']['import_id'] ?? null, - 'semantic_map' => $mapping['data'] ?? null, - 'summary' => array( - 'posts' => count($site_data['posts']), - 'taxonomies' => count($site_data['taxonomies']), - 'products' => count($site_data['products'] ?? array()), - 'product_attributes' => count($site_data['product_attributes'] ?? array()) - ) - ); - } - - return array('success' => false, 'error' => $response['error'] ?? 'Unknown error'); - } - - /** - * Get semantic strategy recommendations - * - * @return array|false Recommendations or false on failure - */ - public function get_recommendations() { - $response = $this->api->get("/planner/sites/{$this->site_id}/recommendations/"); - - if ($response['success']) { - return $response['data']; - } - - return false; - } - - /** - * Apply restructuring recommendations - * - * @param array $recommendations Recommendations array - * @return bool True on success - */ - public function apply_restructuring($recommendations) { - $response = $this->api->post("/planner/sites/{$this->site_id}/restructure/", array( - 'recommendations' => $recommendations - )); - - return $response['success']; - } - - /** - * Sync incremental site data - * - * @return array|false Sync result or false on failure - */ - public function sync_incremental() { - return igny8_sync_incremental_site_data($this->site_id); - } -} - diff --git a/igy8-wp-plugin/includes/class-igny8-webhook-logs.php b/igy8-wp-plugin/includes/class-igny8-webhook-logs.php deleted file mode 100644 index a7fddf32..00000000 --- a/igy8-wp-plugin/includes/class-igny8-webhook-logs.php +++ /dev/null @@ -1,147 +0,0 @@ - uniqid('webhook_', true), - 'event' => sanitize_text_field($data['event'] ?? 'unknown'), - 'data' => $data['data'] ?? null, - 'ip' => sanitize_text_field($data['ip'] ?? ''), - 'user_agent' => sanitize_text_field($data['user_agent'] ?? ''), - 'status' => sanitize_text_field($data['status'] ?? 'received'), - 'response' => $data['response'] ?? null, - 'error' => sanitize_text_field($data['error'] ?? ''), - 'received_at' => current_time('mysql'), - 'processed_at' => $data['processed_at'] ?? null - ); - - $logs[] = $log_entry; - - // Keep only last 500 logs - if (count($logs) > 500) { - $logs = array_slice($logs, -500); - } - - update_option('igny8_webhook_logs', $logs); - - return $log_entry['id']; -} - -/** - * Update webhook log entry - * - * @param string $log_id Log ID - * @param array $updates Updates to apply - * @return bool Success - */ -function igny8_update_webhook_log($log_id, $updates) { - $logs = get_option('igny8_webhook_logs', array()); - - foreach ($logs as $key => $log) { - if ($log['id'] === $log_id) { - foreach ($updates as $field => $value) { - if ($field === 'status') { - $logs[$key][$field] = sanitize_text_field($value); - } elseif ($field === 'response') { - $logs[$key][$field] = $value; - } elseif ($field === 'processed_at') { - $logs[$key][$field] = sanitize_text_field($value); - } else { - $logs[$key][$field] = $value; - } - } - - update_option('igny8_webhook_logs', $logs); - return true; - } - } - - return false; -} - -/** - * Get webhook logs - * - * @param array $args Query arguments - * @return array Logs - */ -function igny8_get_webhook_logs($args = array()) { - $defaults = array( - 'limit' => 50, - 'event' => null, - 'status' => null - ); - - $args = wp_parse_args($args, $defaults); - $logs = get_option('igny8_webhook_logs', array()); - - // Reverse to get newest first - $logs = array_reverse($logs); - - // Filter by event - if ($args['event']) { - $logs = array_filter($logs, function($log) use ($args) { - return $log['event'] === $args['event']; - }); - } - - // Filter by status - if ($args['status']) { - $logs = array_filter($logs, function($log) use ($args) { - return $log['status'] === $args['status']; - }); - } - - // Limit results - if ($args['limit'] > 0) { - $logs = array_slice($logs, 0, $args['limit']); - } - - return array_values($logs); -} - -/** - * Clear old webhook logs - * - * @param int $days_old Delete logs older than this many days - * @return int Number of logs deleted - */ -function igny8_clear_old_webhook_logs($days_old = 30) { - $logs = get_option('igny8_webhook_logs', array()); - $cutoff = strtotime("-{$days_old} days"); - $deleted = 0; - - foreach ($logs as $key => $log) { - $log_time = strtotime($log['received_at']); - if ($log_time < $cutoff) { - unset($logs[$key]); - $deleted++; - } - } - - if ($deleted > 0) { - update_option('igny8_webhook_logs', array_values($logs)); - } - - return $deleted; -} - diff --git a/igy8-wp-plugin/includes/class-igny8-webhooks.php b/igy8-wp-plugin/includes/class-igny8-webhooks.php deleted file mode 100644 index ccf2c9be..00000000 --- a/igy8-wp-plugin/includes/class-igny8-webhooks.php +++ /dev/null @@ -1,381 +0,0 @@ - 'POST', - 'callback' => array($this, 'handle_webhook'), - 'permission_callback' => array($this, 'verify_webhook_secret'), - 'args' => array( - 'event' => array( - 'required' => true, - 'type' => 'string', - 'description' => 'Event type' - ), - 'data' => array( - 'required' => true, - 'type' => 'object', - 'description' => 'Event data' - ) - ) - )); - } - - /** - * Verify webhook shared secret - * - * @param WP_REST_Request $request Request object - * @return bool|WP_Error - */ - public function verify_webhook_secret($request) { - // First check if connection is enabled - if (!igny8_is_connection_enabled()) { - return new WP_Error( - 'rest_forbidden', - __('IGNY8 connection is disabled', 'igny8-bridge'), - array('status' => 403) - ); - } - - // Get shared secret from settings - $shared_secret = igny8_get_webhook_secret(); - - if (empty($shared_secret)) { - return new WP_Error( - 'rest_forbidden', - __('Webhook secret not configured', 'igny8-bridge'), - array('status' => 403) - ); - } - - // Check X-IGNY8-Signature header - $signature = $request->get_header('X-IGNY8-Signature'); - - if (empty($signature)) { - return new WP_Error( - 'rest_forbidden', - __('Missing webhook signature', 'igny8-bridge'), - array('status' => 401) - ); - } - - // Verify signature - $body = $request->get_body(); - $expected_signature = hash_hmac('sha256', $body, $shared_secret); - - if (!hash_equals($expected_signature, $signature)) { - igny8_log_webhook_activity(array( - 'event' => 'authentication_failed', - 'ip' => $request->get_header('X-Forwarded-For') ?: $request->get_header('Remote-Addr'), - 'error' => 'Invalid signature' - )); - - return new WP_Error( - 'rest_forbidden', - __('Invalid webhook signature', 'igny8-bridge'), - array('status' => 401) - ); - } - - return true; - } - - /** - * Handle incoming webhook - * - * @param WP_REST_Request $request Request object - * @return WP_REST_Response|WP_Error - */ - public function handle_webhook($request) { - // Double-check connection is enabled - if (!igny8_is_connection_enabled()) { - return new WP_Error( - 'rest_forbidden', - __('IGNY8 connection is disabled', 'igny8-bridge'), - array('status' => 403) - ); - } - - $event = $request->get_param('event'); - $data = $request->get_param('data'); - - if (empty($event) || empty($data)) { - return new WP_Error( - 'rest_invalid_param', - __('Missing event or data parameter', 'igny8-bridge'), - array('status' => 400) - ); - } - - // Log webhook receipt - $log_id = igny8_log_webhook_activity(array( - 'event' => $event, - 'data' => $data, - 'ip' => $request->get_header('X-Forwarded-For') ?: $request->get_header('Remote-Addr'), - 'user_agent' => $request->get_header('User-Agent'), - 'status' => 'received' - )); - - // Route to appropriate handler - $result = null; - - switch ($event) { - case 'task_published': - case 'task_completed': - $result = $this->handle_task_published($data); - break; - - case 'link_recommendation': - case 'insert_link': - $result = $this->handle_link_recommendation($data); - break; - - case 'optimizer_request': - case 'optimizer_job_completed': - $result = $this->handle_optimizer_request($data); - break; - - default: - $result = array( - 'success' => false, - 'error' => 'Unknown event type: ' . $event - ); - } - - // Update log with result - if ($log_id) { - igny8_update_webhook_log($log_id, array( - 'status' => $result['success'] ? 'processed' : 'failed', - 'response' => $result, - 'processed_at' => current_time('mysql') - )); - } - - return rest_ensure_response($result); - } - - /** - * Handle task published event - * - * @param array $data Event data - * @return array Result - */ - private function handle_task_published($data) { - if (!igny8_is_connection_enabled()) { - return array('success' => false, 'error' => 'Connection disabled'); - } - - if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('writer')) { - return array('success' => false, 'error' => 'Writer module disabled'); - } - - $task_id = $data['task_id'] ?? null; - - if (!$task_id) { - return array('success' => false, 'error' => 'Missing task_id'); - } - - // Check if post already exists - $existing_posts = get_posts(array( - 'meta_key' => '_igny8_task_id', - 'meta_value' => $task_id, - 'post_type' => 'any', - 'posts_per_page' => 1 - )); - - if (!empty($existing_posts)) { - // Post already exists, just update status if needed - $post_id = $existing_posts[0]->ID; - $status = $data['status'] ?? 'publish'; - - if ($status === 'publish' || $status === 'completed') { - wp_update_post(array( - 'ID' => $post_id, - 'post_status' => 'publish' - )); - } - - return array( - 'success' => true, - 'message' => 'Post updated', - 'post_id' => $post_id - ); - } - - // Fetch full task data and create post - $api = new Igny8API(); - $task_response = $api->get("/writer/tasks/{$task_id}/"); - - if (!$task_response['success']) { - return array( - 'success' => false, - 'error' => 'Failed to fetch task: ' . ($task_response['error'] ?? 'Unknown error') - ); - } - - $task = $task_response['data']; - $enabled_post_types = igny8_get_enabled_post_types(); - - $content_data = array( - 'task_id' => $task['id'], - 'title' => $task['title'] ?? 'Untitled', - 'content' => $task['content'] ?? '', - 'status' => $task['status'] ?? 'draft', - 'cluster_id' => $task['cluster_id'] ?? null, - 'sector_id' => $task['sector_id'] ?? null, - 'keyword_ids' => $task['keyword_ids'] ?? array(), - 'content_type' => $task['content_type'] ?? 'post', - 'categories' => $task['categories'] ?? array(), - 'tags' => $task['tags'] ?? array(), - 'featured_image' => $task['featured_image'] ?? null, - 'gallery_images' => $task['gallery_images'] ?? array(), - 'meta_title' => $task['meta_title'] ?? null, - 'meta_description' => $task['meta_description'] ?? null - ); - - $post_id = igny8_create_wordpress_post_from_task($content_data, $enabled_post_types); - - if (is_wp_error($post_id)) { - return array( - 'success' => false, - 'error' => $post_id->get_error_message() - ); - } - - return array( - 'success' => true, - 'message' => 'Post created', - 'post_id' => $post_id - ); - } - - /** - * Handle link recommendation event - * - * @param array $data Event data - * @return array Result - */ - private function handle_link_recommendation($data) { - if (!igny8_is_connection_enabled()) { - return array('success' => false, 'error' => 'Connection disabled'); - } - - if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('linker')) { - return array('success' => false, 'error' => 'Linker module disabled'); - } - - $post_id = $data['post_id'] ?? null; - $target_url = $data['target_url'] ?? null; - $anchor = $data['anchor'] ?? $data['anchor_text'] ?? null; - - if (!$post_id || !$target_url || !$anchor) { - return array( - 'success' => false, - 'error' => 'Missing required parameters: post_id, target_url, anchor' - ); - } - - // Queue link insertion - $queued = igny8_queue_link_insertion(array( - 'post_id' => intval($post_id), - 'target_url' => esc_url_raw($target_url), - 'anchor' => sanitize_text_field($anchor), - 'source' => 'igny8_linker', - 'priority' => $data['priority'] ?? 'normal', - 'created_at' => current_time('mysql') - )); - - if ($queued) { - return array( - 'success' => true, - 'message' => 'Link queued for insertion', - 'queue_id' => $queued - ); - } - - return array( - 'success' => false, - 'error' => 'Failed to queue link insertion' - ); - } - - /** - * Handle optimizer request event - * - * @param array $data Event data - * @return array Result - */ - private function handle_optimizer_request($data) { - if (!igny8_is_connection_enabled()) { - return array('success' => false, 'error' => 'Connection disabled'); - } - - if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('optimizer')) { - return array('success' => false, 'error' => 'Optimizer module disabled'); - } - - $post_id = $data['post_id'] ?? null; - $job_id = $data['job_id'] ?? null; - $status = $data['status'] ?? null; - $score_changes = $data['score_changes'] ?? null; - $recommendations = $data['recommendations'] ?? null; - - if (!$post_id) { - return array('success' => false, 'error' => 'Missing post_id'); - } - - // Update optimizer status if job_id provided - if ($job_id) { - update_post_meta($post_id, '_igny8_optimizer_job_id', $job_id); - } - - if ($status) { - update_post_meta($post_id, '_igny8_optimizer_status', $status); - } - - if ($score_changes) { - update_post_meta($post_id, '_igny8_optimizer_score_changes', $score_changes); - } - - if ($recommendations) { - update_post_meta($post_id, '_igny8_optimizer_recommendations', $recommendations); - } - - return array( - 'success' => true, - 'message' => 'Optimizer data updated', - 'post_id' => $post_id - ); - } -} - -// Initialize webhooks -new Igny8Webhooks(); - diff --git a/igy8-wp-plugin/includes/functions.php b/igy8-wp-plugin/includes/functions.php deleted file mode 100644 index 84dd327f..00000000 --- a/igy8-wp-plugin/includes/functions.php +++ /dev/null @@ -1,828 +0,0 @@ - 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 webhook shared secret - * - * @return string Webhook secret - */ -function igny8_get_webhook_secret() { - $secret = get_option('igny8_webhook_secret'); - - if (empty($secret)) { - // Generate secret if not exists - $secret = wp_generate_password(64, false); - update_option('igny8_webhook_secret', $secret); - } - - return $secret; -} - -/** - * Regenerate webhook secret - * - * @return string New secret - */ -function igny8_regenerate_webhook_secret() { - $secret = wp_generate_password(64, false); - update_option('igny8_webhook_secret', $secret); - return $secret; -} - -/** - * 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() { - // Get site ID from options - $site_id = get_option('igny8_site_id'); - if (!$site_id) { - error_log('IGNY8: No site ID found. Cannot sync structure.'); - return false; - } - - // Get the site structure - $structure = igny8_get_site_structure(); - if (empty($structure['post_types']) && empty($structure['taxonomies'])) { - error_log('IGNY8: No post types or taxonomies to sync.'); - return false; - } - - // Create a temporary integration object to find the actual integration ID - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - error_log('IGNY8: Not authenticated. Cannot sync structure.'); - return false; - } - - // Get integrations for this site - $response = $api->get('/v1/integration/integrations/?site=' . $site_id); - - if (!$response['success'] || empty($response['data'])) { - error_log('IGNY8: No integrations found for site. Response: ' . json_encode($response)); - return false; - } - - // Get the first integration (should be WordPress integration) - $integration = null; - 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'])) { - error_log('IGNY8: Could not find valid integration. Response: ' . json_encode($response)); - 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), - ); - - // Send to backend - $endpoint = '/v1/integration/integrations/' . $integration['id'] . '/update-structure/'; - $update_response = $api->post($endpoint, $payload); - - if ($update_response['success']) { - error_log('IGNY8: Site structure synced successfully.'); - update_option('igny8_last_structure_sync', current_time('timestamp')); - return true; - } else { - error_log('IGNY8: Failed to sync site structure. Error: ' . json_encode($update_response)); - 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']; - } -} - diff --git a/igy8-wp-plugin/languages/igny8-bridge.pot b/igy8-wp-plugin/languages/igny8-bridge.pot deleted file mode 100644 index 875327c0..00000000 --- a/igy8-wp-plugin/languages/igny8-bridge.pot +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (C) 2025 Your Name -# This file is distributed under the same license as the IGNY8 WordPress Bridge plugin. -msgid "" -msgstr "" -"Project-Id-Version: IGNY8 WordPress Bridge 1.0.0\n" -"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/igny8-bridge\n" -"POT-Creation-Date: 2025-10-17 12:00+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" - -#: admin/settings.php -msgid "IGNY8 API Settings" -msgstr "" - -#: admin/settings.php -msgid "IGNY8 API" -msgstr "" - -#: admin/settings.php -msgid "API Connection" -msgstr "" - -#: admin/settings.php -msgid "Email" -msgstr "" - -#: admin/settings.php -msgid "Your IGNY8 account email address." -msgstr "" - -#: admin/settings.php -msgid "Password" -msgstr "" - -#: admin/settings.php -msgid "Your IGNY8 account password." -msgstr "" - -#: admin/settings.php -msgid "Connect to IGNY8" -msgstr "" - -#: admin/settings.php -msgid "Connection Status" -msgstr "" - -#: admin/settings.php -msgid "Status" -msgstr "" - -#: admin/settings.php -msgid "Connected" -msgstr "" - -#: admin/settings.php -msgid "Site ID" -msgstr "" - -#: admin/settings.php -msgid "Not Connected" -msgstr "" - -#: admin/settings.php -msgid "Enter your IGNY8 credentials above and click \"Connect to IGNY8\" to establish a connection." -msgstr "" - -#: admin/settings.php -msgid "About" -msgstr "" - -#: admin/settings.php -msgid "The IGNY8 WordPress Bridge plugin connects your WordPress site to the IGNY8 API, enabling two-way synchronization of posts, taxonomies, and site data." -msgstr "" - -#: admin/settings.php -msgid "Version:" -msgstr "" - -#: admin/settings.php -msgid "Test Connection" -msgstr "" - -#: admin/class-admin.php -msgid "Email and password are required." -msgstr "" - -#: admin/class-admin.php -msgid "Successfully connected to IGNY8 API." -msgstr "" - -#: admin/class-admin.php -msgid "Failed to connect to IGNY8 API. Please check your credentials." -msgstr "" - diff --git a/igy8-wp-plugin/sync/hooks.php b/igy8-wp-plugin/sync/hooks.php deleted file mode 100644 index 5d503999..00000000 --- a/igy8-wp-plugin/sync/hooks.php +++ /dev/null @@ -1,42 +0,0 @@ - 'post', - 'page' => 'page', - 'product' => 'product', - 'article' => 'post', - 'blog' => 'post' - ); - - $post_type = isset($post_type_map[$content_type]) ? $post_type_map[$content_type] : $content_type; - $post_type = apply_filters('igny8_post_type_for_task', $post_type, $content_data); - - if (!post_type_exists($post_type)) { - $post_type = 'post'; - } - - return $post_type; -} - -/** - * Cache writer brief for a task - * - * @param int $task_id IGNY8 task ID - * @param int $post_id WordPress post ID - * @param Igny8API|null $api Optional API client - */ -function igny8_cache_task_brief($task_id, $post_id, $api = null) { - if (!$task_id || !$post_id) { - return; - } - - $api = $api ?: new Igny8API(); - if (!$api->is_authenticated()) { - return; - } - - $response = $api->get("/writer/tasks/{$task_id}/brief/"); - if ($response && !empty($response['success']) && !empty($response['data'])) { - update_post_meta($post_id, '_igny8_task_brief', $response['data']); - update_post_meta($post_id, '_igny8_brief_cached_at', current_time('mysql')); - } -} - -/** - * Create WordPress post from IGNY8 task/content - * - * @param array $content_data Content data from IGNY8 - * @param array $allowed_post_types Post types allowed to be created automatically - * @return int|WP_Error WordPress post ID or error - */ -function igny8_create_wordpress_post_from_task($content_data, $allowed_post_types = array()) { - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - return new WP_Error('igny8_not_authenticated', 'IGNY8 API not authenticated'); - } - - $post_type = igny8_resolve_post_type_for_task($content_data); - - if (!empty($allowed_post_types) && !in_array($post_type, $allowed_post_types, true)) { - return new WP_Error('igny8_post_type_disabled', sprintf('Post type %s is disabled for automation', $post_type)); - } - - // Prepare post data - $post_data = array( - 'post_title' => $content_data['title'] ?? 'Untitled', - 'post_content' => $content_data['content'] ?? '', - 'post_status' => igny8_map_igny8_status_to_wp($content_data['status'] ?? 'draft'), - 'post_type' => $post_type, - 'meta_input' => array() - ); - - // Add IGNY8 meta - if (!empty($content_data['task_id'])) { - $post_data['meta_input']['_igny8_task_id'] = $content_data['task_id']; - } - - if (!empty($content_data['content_id'])) { - $post_data['meta_input']['_igny8_content_id'] = $content_data['content_id']; - } - - if (!empty($content_data['cluster_id'])) { - $post_data['meta_input']['_igny8_cluster_id'] = $content_data['cluster_id']; - } - - if (!empty($content_data['sector_id'])) { - $post_data['meta_input']['_igny8_sector_id'] = $content_data['sector_id']; - } - - if (!empty($content_data['keyword_ids'])) { - $post_data['meta_input']['_igny8_keyword_ids'] = $content_data['keyword_ids']; - } - - // Create post - $post_id = wp_insert_post($post_data); - - if (is_wp_error($post_id)) { - error_log("IGNY8: Failed to create WordPress post: " . $post_id->get_error_message()); - return $post_id; - } - - // Assign taxonomies if cluster/sector IDs exist - if (!empty($content_data['cluster_id'])) { - // Find cluster term - $cluster_terms = get_terms(array( - 'taxonomy' => 'igny8_clusters', - 'meta_key' => '_igny8_cluster_id', - 'meta_value' => $content_data['cluster_id'], - 'hide_empty' => false - )); - - if (!is_wp_error($cluster_terms) && !empty($cluster_terms)) { - wp_set_post_terms($post_id, array($cluster_terms[0]->term_id), 'igny8_clusters'); - } - } - - if (!empty($content_data['sector_id'])) { - // Find sector term - $sector_terms = get_terms(array( - 'taxonomy' => 'igny8_sectors', - 'meta_key' => '_igny8_sector_id', - 'meta_value' => $content_data['sector_id'], - 'hide_empty' => false - )); - - if (!is_wp_error($sector_terms) && !empty($sector_terms)) { - wp_set_post_terms($post_id, array($sector_terms[0]->term_id), 'igny8_sectors'); - } - } - - // Handle categories - if (!empty($content_data['categories'])) { - $category_ids = igny8_process_categories($content_data['categories'], $post_id); - if (!empty($category_ids)) { - wp_set_post_terms($post_id, $category_ids, 'category'); - } - } - - // Handle tags - if (!empty($content_data['tags'])) { - $tag_ids = igny8_process_tags($content_data['tags'], $post_id); - if (!empty($tag_ids)) { - wp_set_post_terms($post_id, $tag_ids, 'post_tag'); - } - } - - // Handle featured image - if (!empty($content_data['featured_image'])) { - igny8_set_featured_image($post_id, $content_data['featured_image']); - } - - // Handle image gallery (1-5 images) - if (!empty($content_data['gallery_images'])) { - igny8_set_image_gallery($post_id, $content_data['gallery_images']); - } - - // Handle meta title and meta description (SEO) - if (!empty($content_data['meta_title'])) { - update_post_meta($post_id, '_yoast_wpseo_title', $content_data['meta_title']); - update_post_meta($post_id, '_seopress_titles_title', $content_data['meta_title']); - update_post_meta($post_id, '_aioseo_title', $content_data['meta_title']); - // Generic meta - update_post_meta($post_id, '_igny8_meta_title', $content_data['meta_title']); - } - - if (!empty($content_data['meta_description'])) { - update_post_meta($post_id, '_yoast_wpseo_metadesc', $content_data['meta_description']); - update_post_meta($post_id, '_seopress_titles_desc', $content_data['meta_description']); - update_post_meta($post_id, '_aioseo_description', $content_data['meta_description']); - // Generic meta - update_post_meta($post_id, '_igny8_meta_description', $content_data['meta_description']); - } - - // Get the actual WordPress post status (after creation) - $created_post = get_post($post_id); - $wp_status = $created_post ? $created_post->post_status : 'draft'; - - // Store WordPress status in meta for IGNY8 to read - update_post_meta($post_id, '_igny8_wordpress_status', $wp_status); - - // Map WordPress status back to IGNY8 status - $igny8_status = igny8_map_wp_status_to_igny8($wp_status); - - // Update IGNY8 task with WordPress post ID, URL, and status - if (!empty($content_data['task_id'])) { - $update_data = array( - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id), - 'wordpress_status' => $wp_status, // WordPress actual status (publish/pending/draft) - 'status' => $igny8_status, // IGNY8 mapped status (completed/pending/draft) - 'synced_at' => current_time('mysql'), - 'post_type' => $post_type, // WordPress post type - 'content_type' => $content_type // IGNY8 content type - ); - - // Include content_id if provided - if (!empty($content_data['content_id'])) { - $update_data['content_id'] = $content_data['content_id']; - } - - $response = $api->put("/writer/tasks/{$content_data['task_id']}/", $update_data); - - if ($response['success']) { - error_log("IGNY8: Updated task {$content_data['task_id']} with WordPress post {$post_id} (status: {$wp_status})"); - } else { - error_log("IGNY8: Failed to update task: " . ($response['error'] ?? 'Unknown error')); - } - } - - // Store content_id if provided (for IGNY8 to query) - if (!empty($content_data['content_id'])) { - update_post_meta($post_id, '_igny8_content_id', $content_data['content_id']); - } - - return $post_id; -} - -/** - * Map IGNY8 task status to WordPress post status - * - * @param string $igny8_status IGNY8 task status - * @return string WordPress post status - */ -function igny8_map_igny8_status_to_wp($igny8_status) { - $status_map = array( - 'completed' => 'publish', - 'draft' => 'draft', - 'pending' => 'pending', - 'scheduled' => 'future', - 'archived' => 'trash' - ); - - return isset($status_map[$igny8_status]) ? $status_map[$igny8_status] : 'draft'; -} - -/** - * Sync IGNY8 tasks to WordPress posts - * Fetches tasks from IGNY8 and creates/updates WordPress posts - * - * @param array $filters Optional filters (status, cluster_id, etc.) - * @return array Sync results - */ -function igny8_sync_igny8_tasks_to_wp($filters = array()) { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - return array('success' => false, 'error' => 'Connection disabled', 'disabled' => true); - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - return array('success' => false, 'error' => 'Not authenticated'); - } - - if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('writer')) { - return array('success' => true, 'created' => 0, 'updated' => 0, 'failed' => 0, 'skipped' => 0, 'total' => 0, 'disabled' => true); - } - - $site_id = get_option('igny8_site_id'); - if (!$site_id) { - return array('success' => false, 'error' => 'Site ID not configured'); - } - - $enabled_post_types = function_exists('igny8_get_enabled_post_types') ? igny8_get_enabled_post_types() : array('post', 'page'); - - // Build endpoint with filters - $endpoint = '/writer/tasks/'; - $query_params = array(); - - $query_params[] = 'site_id=' . intval($site_id); - - if (!empty($filters['status'])) { - $query_params[] = 'status=' . urlencode($filters['status']); - } - - if (!empty($filters['cluster_id'])) { - $query_params[] = 'cluster_id=' . intval($filters['cluster_id']); - } - - if (!empty($query_params)) { - $endpoint .= '?' . implode('&', $query_params); - } - - // Get tasks from IGNY8 - $response = $api->get($endpoint); - - if (!$response['success']) { - return array('success' => false, 'error' => $response['error'] ?? 'Unknown error'); - } - - $tasks = $response['data']['results'] ?? $response['data'] ?? $response['results'] ?? array(); - $created = 0; - $updated = 0; - $failed = 0; - $skipped = 0; - - foreach ($tasks as $task) { - // Check if post already exists - $existing_posts = get_posts(array( - 'meta_key' => '_igny8_task_id', - 'meta_value' => $task['id'], - 'post_type' => 'any', - 'posts_per_page' => 1 - )); - - if (!empty($existing_posts)) { - // Update existing post - $post_id = $existing_posts[0]->ID; - - $update_data = array( - 'ID' => $post_id, - 'post_title' => $task['title'] ?? get_the_title($post_id), - 'post_status' => igny8_map_igny8_status_to_wp($task['status'] ?? 'draft') - ); - - if (!empty($task['content'])) { - $update_data['post_content'] = $task['content']; - } - - $result = wp_update_post($update_data); - - // Update categories, tags, images, and meta - if ($result && !is_wp_error($result)) { - // Update categories - if (!empty($task['categories'])) { - $category_ids = igny8_process_categories($task['categories'], $post_id); - if (!empty($category_ids)) { - wp_set_post_terms($post_id, $category_ids, 'category'); - } - } - - // Update tags - if (!empty($task['tags'])) { - $tag_ids = igny8_process_tags($task['tags'], $post_id); - if (!empty($tag_ids)) { - wp_set_post_terms($post_id, $tag_ids, 'post_tag'); - } - } - - // Update featured image - if (!empty($task['featured_image']) || !empty($task['featured_media'])) { - igny8_set_featured_image($post_id, $task['featured_image'] ?? $task['featured_media']); - } - - // Update gallery - if (!empty($task['gallery_images']) || !empty($task['images'])) { - igny8_set_image_gallery($post_id, $task['gallery_images'] ?? $task['images']); - } - - // Update meta title and description - if (!empty($task['meta_title']) || !empty($task['seo_title'])) { - $meta_title = $task['meta_title'] ?? $task['seo_title']; - update_post_meta($post_id, '_yoast_wpseo_title', $meta_title); - update_post_meta($post_id, '_seopress_titles_title', $meta_title); - update_post_meta($post_id, '_aioseo_title', $meta_title); - update_post_meta($post_id, '_igny8_meta_title', $meta_title); - } - - if (!empty($task['meta_description']) || !empty($task['seo_description'])) { - $meta_desc = $task['meta_description'] ?? $task['seo_description']; - update_post_meta($post_id, '_yoast_wpseo_metadesc', $meta_desc); - update_post_meta($post_id, '_seopress_titles_desc', $meta_desc); - update_post_meta($post_id, '_aioseo_description', $meta_desc); - update_post_meta($post_id, '_igny8_meta_description', $meta_desc); - } - } - - if ($result && !is_wp_error($result)) { - igny8_cache_task_brief($task['id'], $post_id, $api); - $updated++; - } else { - $failed++; - } - } else { - // Create new post - $task_post_type = igny8_resolve_post_type_for_task($task); - if (!empty($enabled_post_types) && !in_array($task_post_type, $enabled_post_types, true)) { - $skipped++; - continue; - } - $content_data = array( - 'task_id' => $task['id'], - 'title' => $task['title'] ?? 'Untitled', - 'content' => $task['content'] ?? '', - 'status' => $task['status'] ?? 'draft', - 'cluster_id' => $task['cluster_id'] ?? null, - 'sector_id' => $task['sector_id'] ?? null, - 'keyword_ids' => $task['keyword_ids'] ?? array(), - 'content_type' => $task['content_type'] ?? $task['post_type'] ?? 'post', - 'post_type' => $task['post_type'] ?? null, // Keep for backward compatibility - 'categories' => $task['categories'] ?? array(), - 'tags' => $task['tags'] ?? array(), - 'featured_image' => $task['featured_image'] ?? $task['featured_media'] ?? null, - 'gallery_images' => $task['gallery_images'] ?? $task['images'] ?? array(), - 'meta_title' => $task['meta_title'] ?? $task['seo_title'] ?? null, - 'meta_description' => $task['meta_description'] ?? $task['seo_description'] ?? null - ); - - $post_id = igny8_create_wordpress_post_from_task($content_data, $enabled_post_types); - - if (is_wp_error($post_id)) { - if ($post_id->get_error_code() === 'igny8_post_type_disabled') { - $skipped++; - } else { - $failed++; - } - } elseif ($post_id) { - igny8_cache_task_brief($task['id'], $post_id, $api); - $created++; - } else { - $failed++; - } - } - } - - return array( - 'success' => true, - 'created' => $created, - 'updated' => $updated, - 'failed' => $failed, - 'skipped' => $skipped, - 'total' => count($tasks) - ); -} - -/** - * Handle webhook from IGNY8 (when content is published from IGNY8) - * This can be called via REST API endpoint or scheduled sync - * - * @param array $webhook_data Webhook data from IGNY8 - * @return int|false WordPress post ID or false on failure - */ -function igny8_handle_igny8_webhook($webhook_data) { - if (empty($webhook_data['task_id'])) { - return false; - } - - $api = new Igny8API(); - - // Get full task data from IGNY8 - $task_response = $api->get("/writer/tasks/{$webhook_data['task_id']}/"); - - if (!$task_response['success']) { - return false; - } - - $task = $task_response['data']; - - // Prepare content data - $content_data = array( - 'task_id' => $task['id'], - 'content_id' => $task['content_id'] ?? null, - 'title' => $task['title'] ?? 'Untitled', - 'content' => $task['content'] ?? '', - 'status' => $task['status'] ?? 'draft', - 'cluster_id' => $task['cluster_id'] ?? null, - 'sector_id' => $task['sector_id'] ?? null, - 'keyword_ids' => $task['keyword_ids'] ?? array(), - 'post_type' => $task['post_type'] ?? 'post' - ); - - return igny8_create_wordpress_post_from_task($content_data); -} - -/** - * Process categories from IGNY8 - * - * @param array $categories Category data (IDs, names, or slugs) - * @param int $post_id Post ID - * @return array Category term IDs - */ -function igny8_process_categories($categories, $post_id) { - $category_ids = array(); - - foreach ($categories as $category) { - $term_id = null; - - // If it's an ID - if (is_numeric($category)) { - $term = get_term($category, 'category'); - if ($term && !is_wp_error($term)) { - $term_id = $term->term_id; - } - } - // If it's an array with name/slug - elseif (is_array($category)) { - $name = $category['name'] ?? $category['slug'] ?? null; - $slug = $category['slug'] ?? sanitize_title($name); - - if ($name) { - // Try to find existing term - $term = get_term_by('slug', $slug, 'category'); - if (!$term) { - $term = get_term_by('name', $name, 'category'); - } - - // Create if doesn't exist - if (!$term || is_wp_error($term)) { - $term_result = wp_insert_term($name, 'category', array('slug' => $slug)); - if (!is_wp_error($term_result)) { - $term_id = $term_result['term_id']; - } - } else { - $term_id = $term->term_id; - } - } - } - // If it's a string (name or slug) - elseif (is_string($category)) { - $term = get_term_by('slug', $category, 'category'); - if (!$term) { - $term = get_term_by('name', $category, 'category'); - } - - if ($term && !is_wp_error($term)) { - $term_id = $term->term_id; - } else { - // Create new category - $term_result = wp_insert_term($category, 'category'); - if (!is_wp_error($term_result)) { - $term_id = $term_result['term_id']; - } - } - } - - if ($term_id) { - $category_ids[] = $term_id; - } - } - - return array_unique($category_ids); -} - -/** - * Process tags from IGNY8 - * - * @param array $tags Tag data (IDs, names, or slugs) - * @param int $post_id Post ID - * @return array Tag term IDs - */ -function igny8_process_tags($tags, $post_id) { - $tag_ids = array(); - - foreach ($tags as $tag) { - $term_id = null; - - // If it's an ID - if (is_numeric($tag)) { - $term = get_term($tag, 'post_tag'); - if ($term && !is_wp_error($term)) { - $term_id = $term->term_id; - } - } - // If it's an array with name/slug - elseif (is_array($tag)) { - $name = $tag['name'] ?? $tag['slug'] ?? null; - $slug = $tag['slug'] ?? sanitize_title($name); - - if ($name) { - // Try to find existing term - $term = get_term_by('slug', $slug, 'post_tag'); - if (!$term) { - $term = get_term_by('name', $name, 'post_tag'); - } - - // Create if doesn't exist - if (!$term || is_wp_error($term)) { - $term_result = wp_insert_term($name, 'post_tag', array('slug' => $slug)); - if (!is_wp_error($term_result)) { - $term_id = $term_result['term_id']; - } - } else { - $term_id = $term->term_id; - } - } - } - // If it's a string (name or slug) - elseif (is_string($tag)) { - $term = get_term_by('slug', $tag, 'post_tag'); - if (!$term) { - $term = get_term_by('name', $tag, 'post_tag'); - } - - if ($term && !is_wp_error($term)) { - $term_id = $term->term_id; - } else { - // Create new tag - $term_result = wp_insert_term($tag, 'post_tag'); - if (!is_wp_error($term_result)) { - $term_id = $term_result['term_id']; - } - } - } - - if ($term_id) { - $tag_ids[] = $term_id; - } - } - - return array_unique($tag_ids); -} - -/** - * Set featured image for post - * - * @param int $post_id Post ID - * @param string|array $image_data Image URL or array with image data - * @return int|false Attachment ID or false on failure - */ -function igny8_set_featured_image($post_id, $image_data) { - $image_url = is_array($image_data) ? ($image_data['url'] ?? $image_data['src'] ?? '') : $image_data; - - if (empty($image_url)) { - return false; - } - - // Check if image already exists - $attachment_id = igny8_get_attachment_by_url($image_url); - - if (!$attachment_id) { - // Download and attach image - $attachment_id = igny8_import_image($image_url, $post_id); - } - - if ($attachment_id) { - set_post_thumbnail($post_id, $attachment_id); - return $attachment_id; - } - - return false; -} - -/** - * Set image gallery for post (1-5 images) - * - * @param int $post_id Post ID - * @param array $gallery_images Array of image URLs or image data - * @return array Attachment IDs - */ -function igny8_set_image_gallery($post_id, $gallery_images) { - $attachment_ids = array(); - - // Limit to 5 images - $gallery_images = array_slice($gallery_images, 0, 5); - - foreach ($gallery_images as $image_data) { - $image_url = is_array($image_data) ? ($image_data['url'] ?? $image_data['src'] ?? '') : $image_data; - - if (empty($image_url)) { - continue; - } - - // Check if image already exists - $attachment_id = igny8_get_attachment_by_url($image_url); - - if (!$attachment_id) { - // Download and attach image - $attachment_id = igny8_import_image($image_url, $post_id); - } - - if ($attachment_id) { - $attachment_ids[] = $attachment_id; - } - } - - // Store gallery as post meta (WordPress native) - if (!empty($attachment_ids)) { - update_post_meta($post_id, '_igny8_gallery_images', $attachment_ids); - - // Also store in format compatible with plugins - update_post_meta($post_id, '_product_image_gallery', implode(',', $attachment_ids)); // WooCommerce - update_post_meta($post_id, '_gallery_images', $attachment_ids); // Generic - } - - return $attachment_ids; -} - -/** - * Get attachment ID by image URL - * - * @param string $image_url Image URL - * @return int|false Attachment ID or false - */ -function igny8_get_attachment_by_url($image_url) { - global $wpdb; - - $attachment_id = $wpdb->get_var($wpdb->prepare( - "SELECT ID FROM {$wpdb->posts} WHERE guid = %s AND post_type = 'attachment'", - $image_url - )); - - return $attachment_id ? intval($attachment_id) : false; -} - -/** - * Import image from URL and attach to post - * - * @param string $image_url Image URL - * @param int $post_id Post ID to attach to - * @return int|false Attachment ID or false on failure - */ -function igny8_import_image($image_url, $post_id) { - require_once(ABSPATH . 'wp-admin/includes/image.php'); - require_once(ABSPATH . 'wp-admin/includes/file.php'); - require_once(ABSPATH . 'wp-admin/includes/media.php'); - - // Download image - $tmp = download_url($image_url); - - if (is_wp_error($tmp)) { - error_log("IGNY8: Failed to download image {$image_url}: " . $tmp->get_error_message()); - return false; - } - - // Get file extension - $file_array = array( - 'name' => basename(parse_url($image_url, PHP_URL_PATH)), - 'tmp_name' => $tmp - ); - - // Upload to WordPress media library - $attachment_id = media_handle_sideload($file_array, $post_id); - - // Clean up temp file - @unlink($tmp); - - if (is_wp_error($attachment_id)) { - error_log("IGNY8: Failed to import image {$image_url}: " . $attachment_id->get_error_message()); - return false; - } - - return $attachment_id; -} - -/** - * Scheduled sync from IGNY8 to WordPress - * Fetches new/updated tasks from IGNY8 and creates/updates WordPress posts - */ -function igny8_cron_sync_from_igny8() { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - error_log('IGNY8: Connection disabled, skipping sync from IGNY8'); - return; - } - - $site_id = get_option('igny8_site_id'); - - if (!$site_id) { - error_log('IGNY8: Site ID not set, skipping sync from IGNY8'); - return; - } - - if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('writer')) { - error_log('IGNY8: Writer module disabled, skipping sync from IGNY8'); - return; - } - - // Get last sync time - $last_sync = get_option('igny8_last_sync_from_igny8', 0); - - // Sync only completed/published tasks - $filters = array( - 'status' => 'completed' - ); - - // If we have a last sync time, we could filter by updated date - // For now, sync all completed tasks (API should handle deduplication) - - $result = igny8_sync_igny8_tasks_to_wp($filters); - - if ($result['success']) { - update_option('igny8_last_sync_from_igny8', time()); - update_option('igny8_last_writer_sync', current_time('timestamp')); - error_log(sprintf( - 'IGNY8: Synced from IGNY8 - Created %d posts, updated %d posts, %d failed, %d skipped', - $result['created'], - $result['updated'], - $result['failed'], - $result['skipped'] ?? 0 - )); - } else { - error_log('IGNY8: Failed to sync from IGNY8: ' . ($result['error'] ?? 'Unknown error')); - } -} - diff --git a/igy8-wp-plugin/sync/post-sync.php b/igy8-wp-plugin/sync/post-sync.php deleted file mode 100644 index d8aad08d..00000000 --- a/igy8-wp-plugin/sync/post-sync.php +++ /dev/null @@ -1,363 +0,0 @@ -post_status; - - // Map WordPress status to IGNY8 task status - $task_status = igny8_map_wp_status_to_igny8($post_status); - - // Sync to IGNY8 API - $api = new Igny8API(); - - // Get content_id if available - $content_id = get_post_meta($post_id, '_igny8_content_id', true); - - $update_data = array( - 'status' => $task_status, - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id), - 'wordpress_status' => $post_status, // Actual WordPress status - 'synced_at' => current_time('mysql') - ); - - // Include content_id if available - if ($content_id) { - $update_data['content_id'] = $content_id; - } - - $response = $api->put("/writer/tasks/{$task_id}/", $update_data); - - if ($response['success']) { - // Update WordPress status in meta for IGNY8 to read - update_post_meta($post_id, '_igny8_wordpress_status', $post_status); - update_post_meta($post_id, '_igny8_last_synced', current_time('mysql')); - error_log("IGNY8: Synced post {$post_id} status ({$post_status}) to task {$task_id}"); - } else { - error_log("IGNY8: Failed to sync post status: " . ($response['error'] ?? 'Unknown error')); - } -} - -/** - * Update keyword status when WordPress post is published - * - * @param int $post_id Post ID - */ -function igny8_update_keywords_on_post_publish($post_id) { - // Get task ID from post meta - $task_id = get_post_meta($post_id, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - $api = new Igny8API(); - - // Get task details to find associated cluster/keywords - $task_response = $api->get("/writer/tasks/{$task_id}/"); - - if (!$task_response['success']) { - return; - } - - $task = $task_response['data']; - $cluster_id = $task['cluster_id'] ?? null; - - if ($cluster_id) { - // Get keywords in this cluster - $keywords_response = $api->get("/planner/keywords/?cluster_id={$cluster_id}"); - - if ($keywords_response['success']) { - $keywords = $keywords_response['results']; - - // Update each keyword status to 'mapped' - foreach ($keywords as $keyword) { - $api->put("/planner/keywords/{$keyword['id']}/", array( - 'status' => 'mapped' - )); - } - } - } - - // Update task status to completed - $api->put("/writer/tasks/{$task_id}/", array( - 'status' => 'completed', - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - )); - - update_post_meta($post_id, '_igny8_last_synced', current_time('mysql')); -} - -/** - * Sync post status changes to IGNY8 - * - * @param string $new_status New post status - * @param string $old_status Old post status - * @param WP_Post $post Post object - */ -function igny8_sync_post_status_transition($new_status, $old_status, $post) { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - return; - } - - // Skip if status hasn't changed - if ($new_status === $old_status) { - return; - } - - // Only sync IGNY8-managed posts - if (!igny8_is_igny8_managed_post($post->ID)) { - return; - } - - $task_id = get_post_meta($post->ID, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - $api = new Igny8API(); - - // Map WordPress status to IGNY8 task status - $task_status = igny8_map_wp_status_to_igny8($new_status); - - // Sync to IGNY8 - $response = $api->put("/writer/tasks/{$task_id}/", array( - 'status' => $task_status, - 'assigned_post_id' => $post->ID, - 'post_url' => get_permalink($post->ID) - )); - - if ($response['success']) { - update_post_meta($post->ID, '_igny8_last_synced', current_time('mysql')); - do_action('igny8_post_status_synced', $post->ID, $task_id, $new_status); - } -} - -/** - * Batch sync all IGNY8-managed posts status to IGNY8 API - * - * @return array Sync results - */ -function igny8_batch_sync_post_statuses() { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - return array( - 'synced' => 0, - 'failed' => 0, - 'total' => 0, - 'disabled' => true - ); - } - - global $wpdb; - - // Get all posts with IGNY8 task ID - $posts = $wpdb->get_results(" - SELECT p.ID, p.post_status, p.post_title, pm.meta_value as task_id - FROM {$wpdb->posts} p - INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id - WHERE pm.meta_key = '_igny8_task_id' - AND p.post_type IN ('post', 'page', 'product') - AND p.post_status != 'trash' - "); - - $api = new Igny8API(); - $synced = 0; - $failed = 0; - - foreach ($posts as $post_data) { - $post_id = $post_data->ID; - $task_id = intval($post_data->task_id); - $wp_status = $post_data->post_status; - - // Map status - $task_status = igny8_map_wp_status_to_igny8($wp_status); - - // Sync to IGNY8 - $response = $api->put("/writer/tasks/{$task_id}/", array( - 'status' => $task_status, - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - )); - - if ($response['success']) { - update_post_meta($post_id, '_igny8_last_synced', current_time('mysql')); - $synced++; - } else { - $failed++; - error_log("IGNY8: Failed to sync post {$post_id}: " . ($response['error'] ?? 'Unknown error')); - } - } - - return array( - 'synced' => $synced, - 'failed' => $failed, - 'total' => count($posts) - ); -} - -/** - * Scheduled sync of WordPress post statuses to IGNY8 - */ -function igny8_cron_sync_post_statuses() { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - error_log('IGNY8: Connection disabled, skipping post status sync'); - return; - } - - $result = igny8_batch_sync_post_statuses(); - - error_log(sprintf( - 'IGNY8: Synced %d posts, %d failed', - $result['synced'], - $result['failed'] - )); -} - -/** - * Scheduled sync of site data - */ -function igny8_cron_sync_site_data() { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - error_log('IGNY8: Connection disabled, skipping site data sync'); - return; - } - - $site_id = get_option('igny8_site_id'); - - if (!$site_id) { - error_log('IGNY8: Site ID not set, skipping site data sync'); - return; - } - - if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('sites')) { - error_log('IGNY8: Sites module disabled, skipping incremental site sync'); - return; - } - - $settings = igny8_get_site_scan_settings(array( - 'mode' => 'incremental', - 'since' => get_option('igny8_last_site_sync', 0) - )); - - $result = igny8_sync_incremental_site_data($site_id, $settings); - - if ($result) { - error_log(sprintf( - 'IGNY8: Synced %d posts to site %d', - $result['synced'], - $site_id - )); - } else { - error_log('IGNY8: Site data sync failed'); - } -} - -/** - * Scheduled full site scan (runs at most once per week) - */ -function igny8_cron_full_site_scan() { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - return; - } - - $site_id = get_option('igny8_site_id'); - - if (!$site_id) { - return; - } - - if (function_exists('igny8_is_module_enabled') && !igny8_is_module_enabled('sites')) { - return; - } - - $last_full = intval(get_option('igny8_last_full_site_scan', 0)); - if ($last_full && (time() - $last_full) < WEEK_IN_SECONDS) { - return; - } - - $settings = igny8_get_site_scan_settings(array( - 'mode' => 'full', - 'since' => null - )); - - $result = igny8_perform_full_site_scan($site_id, $settings); - - if ($result) { - error_log('IGNY8: Full site scan completed'); - } else { - error_log('IGNY8: Full site scan failed'); - } -} - -/** - * Two-way sync class - */ -class Igny8WordPressSync { - - /** - * API instance - * - * @var Igny8API - */ - private $api; - - /** - * Constructor - */ - public function __construct() { - $this->api = new Igny8API(); - - // WordPress → IGNY8 hooks are registered in hooks.php - // IGNY8 → WordPress hooks can be added here if needed - } -} - diff --git a/igy8-wp-plugin/sync/taxonomy-sync.php b/igy8-wp-plugin/sync/taxonomy-sync.php deleted file mode 100644 index 885a8550..00000000 --- a/igy8-wp-plugin/sync/taxonomy-sync.php +++ /dev/null @@ -1,425 +0,0 @@ -is_authenticated()) { - return false; - } - - $site_id = get_option('igny8_site_id'); - if (!$site_id) { - return false; - } - - // Get taxonomy data - $taxonomy_obj = get_taxonomy($taxonomy); - if (!$taxonomy_obj) { - return false; - } - - // Get all terms - $terms = get_terms(array( - 'taxonomy' => $taxonomy, - 'hide_empty' => false - )); - - if (is_wp_error($terms)) { - return false; - } - - // Format taxonomy data - $taxonomy_data = array( - 'name' => $taxonomy, - 'label' => $taxonomy_obj->label, - 'hierarchical' => $taxonomy_obj->hierarchical, - 'terms' => array() - ); - - foreach ($terms as $term) { - $taxonomy_data['terms'][] = array( - 'id' => $term->term_id, - 'name' => $term->name, - 'slug' => $term->slug, - 'description' => $term->description, - 'parent' => $term->parent - ); - } - - // Send to IGNY8 - $response = $api->post("/planner/sites/{$site_id}/taxonomies/", array( - 'taxonomy' => $taxonomy_data - )); - - return $response['success'] ? $response['data'] : false; -} - -/** - * Sync IGNY8 sectors to WordPress taxonomies - * - * @return array|false Sync result or false on failure - */ -function igny8_sync_igny8_sectors_to_wp() { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - return array('synced' => 0, 'total' => 0, 'skipped' => true, 'disabled' => true); - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - return false; - } - - $site_id = get_option('igny8_site_id'); - if (!$site_id) { - return false; - } - - // Respect module toggle - $enabled_modules = function_exists('igny8_get_enabled_modules') ? igny8_get_enabled_modules() : array(); - if (!in_array('planner', $enabled_modules, true)) { - return array('synced' => 0, 'total' => 0, 'skipped' => true); - } - - // Get sectors from IGNY8 - $response = $api->get("/planner/sites/{$site_id}/sectors/"); - - if (!$response['success']) { - return false; - } - - $sectors = $response['data']['results'] ?? $response['data'] ?? $response['results'] ?? array(); - $synced = 0; - - foreach ($sectors as $sector) { - $term = term_exists($sector['name'], 'igny8_sectors'); - if (!$term) { - $term = wp_insert_term( - $sector['name'], - 'igny8_sectors', - array( - 'description' => $sector['description'] ?? '', - 'slug' => $sector['slug'] ?? sanitize_title($sector['name']) - ) - ); - } else { - wp_update_term($term['term_id'], 'igny8_sectors', array( - 'description' => $sector['description'] ?? '', - 'slug' => $sector['slug'] ?? sanitize_title($sector['name']) - )); - } - - if (!is_wp_error($term)) { - $term_id = is_array($term) ? $term['term_id'] : $term; - update_term_meta($term_id, '_igny8_sector_id', $sector['id']); - $synced++; - } - } - - return array('synced' => $synced, 'total' => count($sectors)); -} - -/** - * Sync IGNY8 clusters to WordPress taxonomies - * - * @param int $sector_id Optional sector ID to filter clusters - * @return array|false Sync result or false on failure - */ -function igny8_sync_igny8_clusters_to_wp($sector_id = null) { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - return array('synced' => 0, 'total' => 0, 'skipped' => true, 'disabled' => true); - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - return false; - } - - $site_id = get_option('igny8_site_id'); - if (!$site_id) { - return false; - } - - $enabled_modules = function_exists('igny8_get_enabled_modules') ? igny8_get_enabled_modules() : array(); - if (!in_array('planner', $enabled_modules, true)) { - return array('synced' => 0, 'total' => 0, 'skipped' => true); - } - - // Build endpoint - $endpoint = "/planner/sites/{$site_id}/clusters/"; - if ($sector_id) { - $endpoint .= "?sector_id={$sector_id}"; - } - - // Get clusters from IGNY8 - $response = $api->get($endpoint); - - if (!$response['success']) { - return false; - } - - $clusters = $response['data']['results'] ?? $response['data'] ?? $response['results'] ?? array(); - $synced = 0; - - foreach ($clusters as $cluster) { - // Get parent sector term if sector_id exists - $parent = 0; - if (!empty($cluster['sector_id'])) { - $sector_terms = get_terms(array( - 'taxonomy' => 'igny8_sectors', - 'meta_key' => '_igny8_sector_id', - 'meta_value' => $cluster['sector_id'], - 'hide_empty' => false - )); - - if (!is_wp_error($sector_terms) && !empty($sector_terms)) { - $parent = $sector_terms[0]->term_id; - } - } - - $term_id = 0; - $existing = get_terms(array( - 'taxonomy' => 'igny8_clusters', - 'meta_key' => '_igny8_cluster_id', - 'meta_value' => $cluster['id'], - 'hide_empty' => false - )); - - if (!is_wp_error($existing) && !empty($existing)) { - $term_id = $existing[0]->term_id; - } - - if (!$term_id) { - $term = term_exists($cluster['name'], 'igny8_clusters'); - } else { - $term = array('term_id' => $term_id); - } - if (!$term) { - $term = wp_insert_term( - $cluster['name'], - 'igny8_clusters', - array( - 'description' => $cluster['description'] ?? '', - 'slug' => $cluster['slug'] ?? sanitize_title($cluster['name']), - 'parent' => $parent - ) - ); - } else { - wp_update_term($term['term_id'], 'igny8_clusters', array( - 'description' => $cluster['description'] ?? '', - 'slug' => $cluster['slug'] ?? sanitize_title($cluster['name']), - 'parent' => $parent - )); - } - - if (!is_wp_error($term)) { - $term_id = is_array($term) ? $term['term_id'] : $term; - update_term_meta($term_id, '_igny8_cluster_id', $cluster['id']); - if (!empty($cluster['sector_id'])) { - update_term_meta($term_id, '_igny8_sector_id', $cluster['sector_id']); - } - $synced++; - } - } - - return array('synced' => $synced, 'total' => count($clusters)); -} - -/** - * Cron handler: sync sectors/clusters automatically - * - * @return array - */ -function igny8_cron_sync_taxonomies() { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - error_log('IGNY8: Connection disabled, skipping taxonomy sync'); - return array('sectors' => array('skipped' => true), 'clusters' => array('skipped' => true)); - } - - $results = array( - 'sectors' => igny8_sync_igny8_sectors_to_wp(), - 'clusters' => igny8_sync_igny8_clusters_to_wp() - ); - - update_option('igny8_last_taxonomy_sync', current_time('timestamp')); - - return $results; -} - -/** - * Sync planner keywords for all referenced clusters - * - * @return array|false - */ -function igny8_sync_keywords_from_planner() { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - return array('synced_clusters' => 0, 'synced_posts' => 0, 'skipped' => true, 'disabled' => true); - } - - $api = new Igny8API(); - - if (!$api->is_authenticated()) { - return false; - } - - $site_id = get_option('igny8_site_id'); - if (!$site_id) { - return false; - } - - $enabled_modules = function_exists('igny8_get_enabled_modules') ? igny8_get_enabled_modules() : array(); - if (!in_array('planner', $enabled_modules, true)) { - return array('synced_clusters' => 0, 'synced_posts' => 0, 'skipped' => true); - } - - global $wpdb; - - $cluster_ids = $wpdb->get_col(" - SELECT DISTINCT meta_value - FROM {$wpdb->postmeta} - WHERE meta_key = '_igny8_cluster_id' - AND meta_value IS NOT NULL - AND meta_value != '' - "); - - if (empty($cluster_ids)) { - return array('synced_clusters' => 0, 'synced_posts' => 0); - } - - $enabled_post_types = function_exists('igny8_get_enabled_post_types') ? igny8_get_enabled_post_types() : array('post', 'page'); - - $synced_clusters = 0; - $synced_posts = 0; - - foreach ($cluster_ids as $cluster_id) { - $cluster_id = intval($cluster_id); - if (!$cluster_id) { - continue; - } - - $response = $api->get("/planner/keywords/?cluster_id={$cluster_id}&page_size=500"); - if (!$response['success']) { - continue; - } - - $keywords = $response['data']['results'] ?? $response['data'] ?? $response['results'] ?? array(); - $keyword_ids = array_map('intval', wp_list_pluck($keywords, 'id')); - - // Update cluster term meta - $cluster_terms = get_terms(array( - 'taxonomy' => 'igny8_clusters', - 'meta_key' => '_igny8_cluster_id', - 'meta_value' => $cluster_id, - 'hide_empty' => false - )); - - if (!is_wp_error($cluster_terms) && !empty($cluster_terms)) { - foreach ($cluster_terms as $term) { - update_term_meta($term->term_id, '_igny8_keyword_ids', $keyword_ids); - } - } - - // Update posts tied to this cluster - $posts = get_posts(array( - 'post_type' => $enabled_post_types, - 'meta_query' => array( - array( - 'key' => '_igny8_cluster_id', - 'value' => $cluster_id, - 'compare' => '=' - ) - ), - 'post_status' => 'any', - 'fields' => 'ids', - 'nopaging' => true - )); - - foreach ($posts as $post_id) { - update_post_meta($post_id, '_igny8_keyword_ids', $keyword_ids); - $synced_posts++; - } - - $synced_clusters++; - } - - return array( - 'synced_clusters' => $synced_clusters, - 'synced_posts' => $synced_posts - ); -} - -/** - * Cron handler: sync planner keywords - * - * @return array|false - */ -function igny8_cron_sync_keywords() { - // Skip if connection is disabled - if (!igny8_is_connection_enabled()) { - error_log('IGNY8: Connection disabled, skipping keyword sync'); - return array('synced_clusters' => 0, 'synced_posts' => 0, 'skipped' => true); - } - - $result = igny8_sync_keywords_from_planner(); - if ($result !== false) { - update_option('igny8_last_keyword_sync', current_time('timestamp')); - } - return $result; -} - -/** - * Map WordPress taxonomy term to IGNY8 cluster - * - * @param int $term_id Term ID - * @param string $taxonomy Taxonomy name - * @param int $cluster_id IGNY8 cluster ID - * @return bool True on success - */ -function igny8_map_term_to_cluster($term_id, $taxonomy, $cluster_id) { - if ($taxonomy !== 'igny8_clusters') { - return false; - } - - update_term_meta($term_id, '_igny8_cluster_id', $cluster_id); - return true; -} - -/** - * Map WordPress taxonomy to IGNY8 sector - * - * @param string $taxonomy Taxonomy name - * @param int $sector_id IGNY8 sector ID - * @return bool True on success - */ -function igny8_map_taxonomy_to_sector($taxonomy, $sector_id) { - // Store mapping in options - $mappings = get_option('igny8_taxonomy_sector_mappings', array()); - $mappings[$taxonomy] = $sector_id; - update_option('igny8_taxonomy_sector_mappings', $mappings); - - return true; -} - diff --git a/igy8-wp-plugin/tests/test-api-authentication.php b/igy8-wp-plugin/tests/test-api-authentication.php deleted file mode 100644 index 2e37e45d..00000000 --- a/igy8-wp-plugin/tests/test-api-authentication.php +++ /dev/null @@ -1,116 +0,0 @@ -getMessage() . "\n\n"; - exit(1); -} - -// Test 3: Check is_authenticated method exists -echo "Test 3: Check is_authenticated() method\n"; -if (method_exists($api, 'is_authenticated')) { - echo "✓ PASS: is_authenticated() method exists\n"; - $is_auth = $api->is_authenticated(); - echo " Status: " . ($is_auth ? "Authenticated" : "Not authenticated") . "\n\n"; -} else { - echo "✗ FAIL: is_authenticated() method not found\n\n"; - exit(1); -} - -// Test 4: Check connect method exists -echo "Test 4: Check connect() method\n"; -if (method_exists($api, 'connect')) { - echo "✓ PASS: connect() method exists\n\n"; -} else { - echo "✗ FAIL: connect() method not found\n\n"; - exit(1); -} - -// Test 5: Check get method exists -echo "Test 5: Check get() method\n"; -if (method_exists($api, 'get')) { - echo "✓ PASS: get() method exists\n\n"; -} else { - echo "✗ FAIL: get() method not found\n\n"; - exit(1); -} - -// Test 6: Test endpoint path normalization (mock test) -echo "Test 6: Endpoint path normalization logic\n"; -// This is a conceptual test - in reality we'd need to mock HTTP calls -echo "✓ PASS: Endpoint normalization should convert:\n"; -echo " /auth/sites/ → https://api.igny8.com/api/v1/auth/sites/\n"; -echo " /v1/auth/sites/ → https://api.igny8.com/api/v1/auth/sites/\n"; -echo " auth/sites → https://api.igny8.com/api/v1/auth/sites/\n\n"; - -// Test 7: Check if functions exist -echo "Test 7: Check helper functions\n"; -$functions = array( - 'igny8_store_secure_option', - 'igny8_get_secure_option', - 'igny8_is_connection_enabled' -); - -$missing = array(); -foreach ($functions as $func) { - if (!function_exists($func)) { - $missing[] = $func; - } -} - -if (empty($missing)) { - echo "✓ PASS: All helper functions available\n\n"; -} else { - echo "⚠ WARNING: Missing functions: " . implode(', ', $missing) . "\n"; - echo " These may be optional depending on WordPress configuration\n\n"; -} - -// Test 8: Summary -echo "=== Test Summary ===\n"; -echo "✓ API authentication module is properly structured\n"; -echo "✓ All required methods exist\n"; -echo "✓ Ready for integration testing\n\n"; - -echo "Next Steps:\n"; -echo "1. Go to WordPress Admin → IGNY8 API Settings\n"; -echo "2. Get your API key from: https://app.igny8.com/sites/{id}/settings?tab=integrations\n"; -echo "3. Paste the API key and click 'Connect to IGNY8'\n"; -echo "4. Check the debug.log for connection details if it fails\n"; -echo "\nExpected successful response:\n"; -echo " Authorization: Bearer sk_live_xxxxxxxxxxxxx\n"; -echo " GET https://api.igny8.com/api/v1/auth/sites/\n"; -echo " Response: {\"success\": true, \"data\": [{\"id\": 1, ...}]}\n"; -?> diff --git a/igy8-wp-plugin/tests/test-revoke-api-key.php b/igy8-wp-plugin/tests/test-revoke-api-key.php deleted file mode 100644 index 7bc2932b..00000000 --- a/igy8-wp-plugin/tests/test-revoke-api-key.php +++ /dev/null @@ -1,28 +0,0 @@ -assertFalse(get_option('igny8_api_key')); - $this->assertFalse(get_option('igny8_access_token')); - $this->assertFalse(get_option('igny8_refresh_token')); - $this->assertFalse(get_option('igny8_token_refreshed_at')); - } -} - - diff --git a/igy8-wp-plugin/tests/test-site-metadata.php b/igy8-wp-plugin/tests/test-site-metadata.php deleted file mode 100644 index ba793f10..00000000 --- a/igy8-wp-plugin/tests/test-site-metadata.php +++ /dev/null @@ -1,36 +0,0 @@ -set_header('Authorization', 'Bearer test-api-key-123'); - - $server = rest_get_server(); - $response = $server->dispatch($request); - - $this->assertEquals(200, $response->get_status()); - $data = $response->get_data(); - $this->assertNotEmpty($data); - $this->assertArrayHasKey('success', $data); - $this->assertTrue($data['success']); - $this->assertArrayHasKey('data', $data); - $this->assertArrayHasKey('post_types', $data['data']); - $this->assertArrayHasKey('taxonomies', $data['data']); - } -} - - diff --git a/igy8-wp-plugin/tests/test-sync-structure.php b/igy8-wp-plugin/tests/test-sync-structure.php deleted file mode 100644 index ad5e402f..00000000 --- a/igy8-wp-plugin/tests/test-sync-structure.php +++ /dev/null @@ -1,163 +0,0 @@ -is_authenticated()) { - echo "✅ API is authenticated\n"; -} else { - echo "❌ API is not authenticated. Credentials missing.\n"; - exit; -} -echo "\n"; - -// Test 3: Get site structure -echo "Test 3: Gathering Site Structure...\n"; -$structure = igny8_get_site_structure(); - -echo " Post Types Found: " . count($structure['post_types']) . "\n"; -foreach ($structure['post_types'] as $type => $data) { - echo " - $type: {$data['label']} ({$data['count']} items)\n"; -} - -echo "\n Taxonomies Found: " . count($structure['taxonomies']) . "\n"; -foreach ($structure['taxonomies'] as $tax => $data) { - echo " - $tax: {$data['label']} ({$data['count']} items)\n"; -} - -if (empty($structure['post_types']) && empty($structure['taxonomies'])) { - echo "❌ No content found to sync\n"; - exit; -} - -echo "✅ Site structure gathered successfully\n"; -echo "\n"; - -// Test 4: Query for integration -echo "Test 4: Querying for Integration...\n"; -$query_response = $api->get('/v1/integration/integrations/?site=' . $site_id . '&platform=wordpress'); - -echo " API Response Status: " . ($query_response['success'] ? 'Success' : 'Failed') . "\n"; -echo " HTTP Status: " . (isset($query_response['http_status']) ? $query_response['http_status'] : 'N/A') . "\n"; - -// Extract integration -$integration = null; -if (isset($query_response['data'])) { - $data = $query_response['data']; - - if (isset($data['results']) && !empty($data['results'])) { - $integration = $data['results'][0]; - echo " Response Format: Paginated (DRF)\n"; - } elseif (is_array($data) && isset($data[0])) { - $integration = $data[0]; - echo " Response Format: Direct Array\n"; - } elseif (is_array($data) && isset($data['id'])) { - $integration = $data; - echo " Response Format: Single Object\n"; - } -} - -if (!$integration || empty($integration['id'])) { - echo "❌ No integration found\n"; - if (isset($query_response['error'])) { - echo " Error: " . $query_response['error'] . "\n"; - } - exit; -} - -echo "✅ Integration found: ID {$integration['id']}\n"; -echo "\n"; - -// Test 5: Sync structure to backend -echo "Test 5: Syncing Structure to Backend...\n"; - -$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), -); - -$endpoint = '/v1/integration/integrations/' . $integration['id'] . '/update-structure/'; -echo " Endpoint: $endpoint\n"; -echo " Payload Size: " . strlen(json_encode($payload)) . " bytes\n"; - -$sync_response = $api->post($endpoint, $payload); - -echo " API Response Status: " . ($sync_response['success'] ? 'Success' : 'Failed') . "\n"; -echo " HTTP Status: " . (isset($sync_response['http_status']) ? $sync_response['http_status'] : 'N/A') . "\n"; - -if ($sync_response['success']) { - echo "✅ Structure synced successfully\n"; - if (isset($sync_response['data']['message'])) { - echo " Message: " . $sync_response['data']['message'] . "\n"; - } - if (isset($sync_response['data']['post_types_count'])) { - echo " Post Types Synced: " . $sync_response['data']['post_types_count'] . "\n"; - } - if (isset($sync_response['data']['taxonomies_count'])) { - echo " Taxonomies Synced: " . $sync_response['data']['taxonomies_count'] . "\n"; - } -} else { - echo "❌ Structure sync failed\n"; - if (isset($sync_response['error'])) { - echo " Error: " . $sync_response['error'] . "\n"; - } - if (isset($sync_response['raw_error'])) { - echo " Details: " . json_encode($sync_response['raw_error']) . "\n"; - } - exit; -} - -echo "\n"; - -// Test 6: Verify backend stored the data -echo "Test 6: Verifying Backend Stored Data...\n"; - -$verify_response = $api->get('/v1/integration/integrations/' . $integration['id'] . '/content-types/'); - -if ($verify_response['success'] && isset($verify_response['data'])) { - $data = $verify_response['data']; - echo " Post Types in Backend: " . count($data['post_types'] ?? []) . "\n"; - echo " Taxonomies in Backend: " . count($data['taxonomies'] ?? []) . "\n"; - echo " Last Structure Fetch: " . ($data['last_structure_fetch'] ?? 'Unknown') . "\n"; - echo "✅ Backend data verified\n"; -} else { - echo "⚠️ Could not verify backend data\n"; - if (isset($verify_response['error'])) { - echo " Error: " . $verify_response['error'] . "\n"; - } -} - -echo "\n"; -echo "=== Test Complete ===\n"; -echo "✅ All tests passed! Site structure sync is working.\n\n"; - diff --git a/igy8-wp-plugin/uninstall.php b/igy8-wp-plugin/uninstall.php deleted file mode 100644 index b0764afd..00000000 --- a/igy8-wp-plugin/uninstall.php +++ /dev/null @@ -1,53 +0,0 @@ -query(" - DELETE FROM {$wpdb->postmeta} - WHERE meta_key LIKE '_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'); -} - -// Note: Taxonomies and terms are NOT deleted -// They remain in WordPress for user reference -// Only the taxonomy registration is removed -