Plugin packaging and docs
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
<?php
|
||||
/**
|
||||
* IGNY8 API Client Class
|
||||
*
|
||||
* Handles all communication with IGNY8 API v1.0
|
||||
* Follows WORDPRESS-PLUGIN-INTEGRATION.md guidelines
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Igny8API Class
|
||||
*/
|
||||
class Igny8API {
|
||||
|
||||
/**
|
||||
* API base URL
|
||||
* Note: Base is /api, endpoints should include /v1/ prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $base_url = 'https://api.igny8.com/api';
|
||||
|
||||
/**
|
||||
* API key (used as access token)
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $access_token = null;
|
||||
|
||||
/**
|
||||
* Whether authentication is via API key (always true now)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $api_key_auth = true;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* Only uses API key for authentication
|
||||
*/
|
||||
public function __construct() {
|
||||
if (function_exists('igny8_get_secure_option')) {
|
||||
$api_key = igny8_get_secure_option('igny8_api_key');
|
||||
} else {
|
||||
$api_key = get_option('igny8_api_key');
|
||||
}
|
||||
|
||||
// API key is the only authentication method
|
||||
if (!empty($api_key)) {
|
||||
$this->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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API base URL
|
||||
*
|
||||
* @return string API base URL
|
||||
*/
|
||||
public function get_api_base() {
|
||||
return $this->base_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
/**
|
||||
* Link Insertion Queue
|
||||
*
|
||||
* Queues and processes link recommendations from IGNY8 Linker
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue link insertion
|
||||
*
|
||||
* @param array $link_data Link data
|
||||
* @return int|false Queue ID or false on failure
|
||||
*/
|
||||
function igny8_queue_link_insertion($link_data) {
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$queue = get_option('igny8_link_queue', array());
|
||||
|
||||
$queue_item = array(
|
||||
'id' => 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 <a> 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 <a> tag
|
||||
$before = substr($content, 0, $position);
|
||||
$after = substr($content, $position + $length);
|
||||
|
||||
// Count unclosed <a> tags before this position
|
||||
$open_tags = substr_count($before, '<a');
|
||||
$close_tags = substr_count($before, '</a>');
|
||||
|
||||
// If not inside a link, replace it
|
||||
if ($open_tags <= $close_tags) {
|
||||
$link_html = '<a href="' . esc_url($target_url) . '">' . esc_html($anchor) . '</a>';
|
||||
$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<p><a href=\"" . esc_url($target_url) . "\">" . esc_html($anchor) . "</a></p>";
|
||||
$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');
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
/**
|
||||
* IGNY8 File Logger
|
||||
*
|
||||
* Provides file-based logging for all publish/sync workflows
|
||||
*
|
||||
* @package IGNY8_Bridge
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Igny8_Logger {
|
||||
|
||||
/**
|
||||
* Log directory path
|
||||
*/
|
||||
private static $log_dir = null;
|
||||
|
||||
/**
|
||||
* Initialize logger
|
||||
*/
|
||||
public static function init() {
|
||||
// Set log directory to plugin root/logs/publish-sync-logs
|
||||
self::$log_dir = dirname(dirname(__FILE__)) . '/logs/publish-sync-logs';
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!file_exists(self::$log_dir)) {
|
||||
wp_mkdir_p(self::$log_dir);
|
||||
}
|
||||
|
||||
// Ensure directory is writable
|
||||
if (!is_writable(self::$log_dir)) {
|
||||
@chmod(self::$log_dir, 0755);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write log message to file
|
||||
*
|
||||
* @param string $message Log message
|
||||
* @param string $level Log level (INFO, WARNING, ERROR)
|
||||
* @param string $log_file Log file name (without .log extension)
|
||||
*/
|
||||
public static function log($message, $level = 'INFO', $log_file = 'publish-sync') {
|
||||
if (self::$log_dir === null) {
|
||||
self::init();
|
||||
}
|
||||
|
||||
$timestamp = current_time('Y-m-d H:i:s');
|
||||
$formatted_message = "[{$timestamp}] [{$level}] {$message}\n";
|
||||
|
||||
$file_path = self::$log_dir . '/' . $log_file . '.log';
|
||||
|
||||
// Append to log file
|
||||
error_log($formatted_message, 3, $file_path);
|
||||
|
||||
// Also log to WordPress debug.log if WP_DEBUG is enabled
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log("[IGNY8] [{$level}] {$message}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log info message
|
||||
*/
|
||||
public static function info($message, $log_file = 'publish-sync') {
|
||||
self::log($message, 'INFO', $log_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warning message
|
||||
*/
|
||||
public static function warning($message, $log_file = 'publish-sync') {
|
||||
self::log($message, 'WARNING', $log_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error message
|
||||
*/
|
||||
public static function error($message, $log_file = 'publish-sync') {
|
||||
self::log($message, 'ERROR', $log_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log API request
|
||||
*/
|
||||
public static function api_request($method, $endpoint, $data = null) {
|
||||
$message = "API REQUEST: {$method} {$endpoint}";
|
||||
if ($data) {
|
||||
$message .= "\n Data: " . json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
self::log($message, 'INFO', 'wordpress-api');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log API response
|
||||
*/
|
||||
public static function api_response($status_code, $body) {
|
||||
$message = "API RESPONSE: HTTP {$status_code}";
|
||||
if ($body) {
|
||||
$body_str = is_string($body) ? $body : json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
$message .= "\n Body: " . substr($body_str, 0, 500);
|
||||
}
|
||||
self::log($message, 'INFO', 'wordpress-api');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log API error
|
||||
*/
|
||||
public static function api_error($error_message) {
|
||||
self::log("API ERROR: {$error_message}", 'ERROR', 'wordpress-api');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log webhook event
|
||||
*/
|
||||
public static function webhook($event_type, $data) {
|
||||
$message = "WEBHOOK EVENT: {$event_type}";
|
||||
if ($data) {
|
||||
$message .= "\n Data: " . json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
self::log($message, 'INFO', 'webhooks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log workflow separator
|
||||
*/
|
||||
public static function separator($title = '') {
|
||||
$line = str_repeat('=', 80);
|
||||
self::log($line);
|
||||
if ($title) {
|
||||
self::log($title);
|
||||
self::log($line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get log file contents
|
||||
*
|
||||
* @param string $log_file Log file name
|
||||
* @param int $lines Number of lines to read (default 100, 0 for all)
|
||||
* @return string Log contents
|
||||
*/
|
||||
public static function get_log_contents($log_file = 'publish-sync', $lines = 100) {
|
||||
if (self::$log_dir === null) {
|
||||
self::init();
|
||||
}
|
||||
|
||||
$file_path = self::$log_dir . '/' . $log_file . '.log';
|
||||
|
||||
if (!file_exists($file_path)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($lines === 0) {
|
||||
return file_get_contents($file_path);
|
||||
}
|
||||
|
||||
// Read last N lines efficiently
|
||||
$file = new SplFileObject($file_path, 'r');
|
||||
$file->seek(PHP_INT_MAX);
|
||||
$total_lines = $file->key() + 1;
|
||||
|
||||
$start_line = max(0, $total_lines - $lines);
|
||||
$file->seek($start_line);
|
||||
|
||||
$content = '';
|
||||
while (!$file->eof()) {
|
||||
$content .= $file->fgets();
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear log file
|
||||
*/
|
||||
public static function clear_log($log_file = 'publish-sync') {
|
||||
if (self::$log_dir === null) {
|
||||
self::init();
|
||||
}
|
||||
|
||||
$file_path = self::$log_dir . '/' . $log_file . '.log';
|
||||
|
||||
if (file_exists($file_path)) {
|
||||
@unlink($file_path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all log files
|
||||
*/
|
||||
public static function get_log_files() {
|
||||
if (self::$log_dir === null) {
|
||||
self::init();
|
||||
}
|
||||
|
||||
if (!is_dir(self::$log_dir)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$files = glob(self::$log_dir . '/*.log');
|
||||
$log_files = array();
|
||||
|
||||
foreach ($files as $file) {
|
||||
$log_files[] = array(
|
||||
'name' => basename($file, '.log'),
|
||||
'path' => $file,
|
||||
'size' => filesize($file),
|
||||
'modified' => filemtime($file),
|
||||
);
|
||||
}
|
||||
|
||||
return $log_files;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
Igny8_Logger::init();
|
||||
@@ -0,0 +1,641 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API Endpoints for IGNY8
|
||||
*
|
||||
* Provides endpoints for IGNY8 to query WordPress posts by content_id
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Igny8RestAPI Class
|
||||
*/
|
||||
class Igny8RestAPI {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action('rest_api_init', array($this, 'register_routes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API routes
|
||||
*/
|
||||
public function register_routes() {
|
||||
// Get post by IGNY8 content_id
|
||||
register_rest_route('igny8/v1', '/post-by-content-id/(?P<content_id>\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<task_id>\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 or post_id
|
||||
register_rest_route('igny8/v1', '/post-status/(?P<id>\d+)', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array($this, 'get_post_status'),
|
||||
'permission_callback' => array($this, 'check_permission'),
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'description' => 'WordPress post ID or IGNY8 content ID (tries both)'
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
// 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
|
||||
));
|
||||
|
||||
// Manual publish endpoint - for triggering WordPress publish from IGNY8
|
||||
// Route: /wp-json/igny8/v1/publish
|
||||
register_rest_route('igny8/v1', '/publish', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array($this, 'publish_content_to_wordpress'),
|
||||
'permission_callback' => array($this, 'check_permission'),
|
||||
'args' => array(
|
||||
'content_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'description' => 'IGNY8 content ID'
|
||||
),
|
||||
'task_id' => array(
|
||||
'required' => false,
|
||||
'type' => 'integer',
|
||||
'description' => 'IGNY8 task ID'
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 post ID or content_id
|
||||
* Accepts either WordPress post_id or IGNY8 content_id
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_post_status($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)
|
||||
);
|
||||
}
|
||||
|
||||
$id = intval($request['id']);
|
||||
$post = null;
|
||||
$lookup_method = null;
|
||||
|
||||
// First try as WordPress post ID
|
||||
if (post_type_exists('post') || post_type_exists('page')) {
|
||||
$post = get_post($id);
|
||||
if ($post) {
|
||||
$lookup_method = 'wordpress_post_id';
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try as IGNY8 content_id
|
||||
if (!$post) {
|
||||
$posts = get_posts(array(
|
||||
'meta_key' => '_igny8_content_id',
|
||||
'meta_value' => $id,
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => 1,
|
||||
'post_status' => 'any'
|
||||
));
|
||||
|
||||
if (!empty($posts)) {
|
||||
$post = $posts[0];
|
||||
$lookup_method = 'igny8_content_id';
|
||||
}
|
||||
}
|
||||
|
||||
if (!$post) {
|
||||
return rest_ensure_response(array(
|
||||
'success' => false,
|
||||
'message' => 'Post not found',
|
||||
'searched_id' => $id
|
||||
));
|
||||
}
|
||||
|
||||
return rest_ensure_response(array(
|
||||
'success' => true,
|
||||
'data' => array(
|
||||
'post_id' => $post->ID,
|
||||
'post_status' => $post->post_status,
|
||||
'post_title' => $post->post_title,
|
||||
'post_type' => $post->post_type,
|
||||
'post_modified' => $post->post_modified,
|
||||
'post_url' => get_permalink($post->ID),
|
||||
'wordpress_status' => $post->post_status,
|
||||
'igny8_status' => igny8_map_wp_status_to_igny8($post->post_status),
|
||||
'content_id' => get_post_meta($post->ID, '_igny8_content_id', true),
|
||||
'task_id' => get_post_meta($post->ID, '_igny8_task_id', true),
|
||||
'last_synced' => get_post_meta($post->ID, '_igny8_last_synced', true),
|
||||
'lookup_method' => $lookup_method
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post status by content_id (DEPRECATED - use get_post_status instead)
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_post_status_by_content_id($request) {
|
||||
// Redirect to new unified method
|
||||
return $this->get_post_status($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish content to WordPress
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function publish_content_to_wordpress($request) {
|
||||
// Check connection
|
||||
if (!igny8_is_connection_enabled()) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
null,
|
||||
'IGNY8 connection is disabled',
|
||||
'connection_disabled',
|
||||
null,
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
// DIAGNOSTIC: Log raw request body
|
||||
$raw_body = $request->get_body();
|
||||
error_log('========== RAW REQUEST BODY ==========');
|
||||
error_log($raw_body);
|
||||
error_log('======================================');
|
||||
|
||||
// Get content data from POST body (IGNY8 backend already sends everything)
|
||||
$content_data = $request->get_json_params();
|
||||
|
||||
// DIAGNOSTIC: Log parsed JSON
|
||||
error_log('========== PARSED JSON DATA ==========');
|
||||
error_log(print_r($content_data, true));
|
||||
error_log('======================================');
|
||||
|
||||
// Extract IDs for validation
|
||||
$content_id = isset($content_data['content_id']) ? $content_data['content_id'] : null;
|
||||
$task_id = isset($content_data['task_id']) ? $content_data['task_id'] : null;
|
||||
|
||||
// ALWAYS log incoming data for debugging
|
||||
error_log('========== IGNY8 PUBLISH REQUEST ==========');
|
||||
error_log('Content ID: ' . $content_id);
|
||||
error_log('Task ID: ' . $task_id);
|
||||
error_log('Title: ' . (isset($content_data['title']) ? $content_data['title'] : 'MISSING'));
|
||||
error_log('Content HTML: ' . (isset($content_data['content_html']) ? strlen($content_data['content_html']) . ' chars' : 'MISSING'));
|
||||
error_log('Categories: ' . (isset($content_data['categories']) ? json_encode($content_data['categories']) : 'MISSING'));
|
||||
error_log('Tags: ' . (isset($content_data['tags']) ? json_encode($content_data['tags']) : 'MISSING'));
|
||||
error_log('Featured Image: ' . (isset($content_data['featured_image_url']) ? $content_data['featured_image_url'] : 'MISSING'));
|
||||
error_log('Gallery Images: ' . (isset($content_data['gallery_images']) ? count($content_data['gallery_images']) . ' images' : 'MISSING'));
|
||||
error_log('SEO Title: ' . (isset($content_data['seo_title']) ? 'YES' : 'NO'));
|
||||
error_log('SEO Description: ' . (isset($content_data['seo_description']) ? 'YES' : 'NO'));
|
||||
error_log('Primary Keyword: ' . (isset($content_data['primary_keyword']) ? $content_data['primary_keyword'] : 'MISSING'));
|
||||
error_log('===========================================');
|
||||
|
||||
// Validate required fields
|
||||
if (empty($content_id)) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
null,
|
||||
'Missing content_id in request',
|
||||
'missing_content_id',
|
||||
null,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($content_data['title'])) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
null,
|
||||
'Missing title in request',
|
||||
'missing_title',
|
||||
null,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($content_data['content_html'])) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
null,
|
||||
'Missing content_html in request',
|
||||
'missing_content_html',
|
||||
null,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
if (defined('IGNY8_DEBUG') && IGNY8_DEBUG) {
|
||||
error_log('IGNY8 Publish Request - Content ID: ' . $content_id);
|
||||
error_log('IGNY8 Publish Request - Title: ' . $content_data['title']);
|
||||
error_log('IGNY8 Publish Request - Content HTML length: ' . strlen($content_data['content_html']));
|
||||
}
|
||||
|
||||
// Check if content already exists
|
||||
$existing_posts = get_posts(array(
|
||||
'meta_key' => '_igny8_content_id',
|
||||
'meta_value' => $content_id,
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => 1
|
||||
));
|
||||
|
||||
if (!empty($existing_posts)) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
array('post_id' => $existing_posts[0]->ID),
|
||||
'Content already exists as WordPress post',
|
||||
'content_exists',
|
||||
null,
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
// Create WordPress post
|
||||
$result = igny8_create_wordpress_post_from_task($content_data);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return $this->build_unified_response(
|
||||
false,
|
||||
null,
|
||||
'Failed to create WordPress post: ' . $result->get_error_message(),
|
||||
'post_creation_failed',
|
||||
null,
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
// Handle new return format (array with post_id and term_ids)
|
||||
if (is_array($result) && isset($result['post_id'])) {
|
||||
$post_id = $result['post_id'];
|
||||
$term_ids = $result['term_ids'] ?? array();
|
||||
} else {
|
||||
// Legacy format (just post_id)
|
||||
$post_id = $result;
|
||||
$term_ids = array();
|
||||
}
|
||||
|
||||
// Return success response with term_ids
|
||||
return $this->build_unified_response(
|
||||
true,
|
||||
array(
|
||||
'post_id' => $post_id,
|
||||
'post_url' => get_permalink($post_id),
|
||||
'post_status' => get_post_status($post_id),
|
||||
'content_id' => $content_id,
|
||||
'task_id' => $task_id,
|
||||
'term_ids' => $term_ids
|
||||
),
|
||||
'Content successfully published to WordPress',
|
||||
null,
|
||||
null,
|
||||
201
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize REST API
|
||||
new Igny8RestAPI();
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/**
|
||||
* Site Integration Class
|
||||
*
|
||||
* Manages site data collection and semantic mapping
|
||||
* Follows WORDPRESS-PLUGIN-INTEGRATION.md guidelines
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Igny8SiteIntegration Class
|
||||
*/
|
||||
class Igny8SiteIntegration {
|
||||
|
||||
/**
|
||||
* API instance
|
||||
*
|
||||
* @var Igny8API
|
||||
*/
|
||||
private $api;
|
||||
|
||||
/**
|
||||
* Site ID
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $site_id;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param int $site_id IGNY8 site ID
|
||||
*/
|
||||
public function __construct($site_id) {
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* IGNY8 Template Loader
|
||||
*
|
||||
* Loads custom template for IGNY8-generated content
|
||||
* Only applies to posts with _igny8_content_id meta field
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Igny8_Template_Loader {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
// Hook into template loading with high priority
|
||||
add_filter('single_template', [$this, 'load_igny8_template'], 99);
|
||||
|
||||
// Enqueue styles and scripts for IGNY8 template
|
||||
add_action('wp_enqueue_scripts', [$this, 'enqueue_template_assets']);
|
||||
|
||||
// Add body class for IGNY8 content
|
||||
add_filter('body_class', [$this, 'add_body_class']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current post is IGNY8-generated content
|
||||
*
|
||||
* @param int|null $post_id Post ID (optional, defaults to current post)
|
||||
* @return bool
|
||||
*/
|
||||
public function is_igny8_content($post_id = null) {
|
||||
if (!$post_id) {
|
||||
$post_id = get_the_ID();
|
||||
}
|
||||
|
||||
if (!$post_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if post has IGNY8 content ID meta
|
||||
$content_id = get_post_meta($post_id, '_igny8_content_id', true);
|
||||
|
||||
return !empty($content_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load IGNY8 custom template for IGNY8-generated posts
|
||||
*
|
||||
* @param string $template Default template path
|
||||
* @return string Template path
|
||||
*/
|
||||
public function load_igny8_template($template) {
|
||||
global $post;
|
||||
|
||||
// Only apply to single post views
|
||||
if (!is_singular('post')) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
// Only apply to IGNY8-generated content
|
||||
if (!$this->is_igny8_content($post->ID)) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
// Path to our custom template
|
||||
$custom_template = plugin_dir_path(dirname(__FILE__)) . 'templates/single-igny8-content.php';
|
||||
|
||||
// Use custom template if it exists
|
||||
if (file_exists($custom_template)) {
|
||||
return $custom_template;
|
||||
}
|
||||
|
||||
// Fallback to default template
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue styles and scripts for IGNY8 template
|
||||
*/
|
||||
public function enqueue_template_assets() {
|
||||
global $post;
|
||||
|
||||
// Only enqueue on single post pages
|
||||
if (!is_singular('post')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only enqueue for IGNY8 content
|
||||
if (!$this->is_igny8_content($post->ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enqueue custom styles
|
||||
wp_enqueue_style(
|
||||
'igny8-content-template',
|
||||
plugin_dir_url(dirname(__FILE__)) . 'templates/assets/css/igny8-content-template.css',
|
||||
array(),
|
||||
'1.0.0'
|
||||
);
|
||||
|
||||
// Enqueue custom JavaScript (if needed in future)
|
||||
wp_enqueue_script(
|
||||
'igny8-content-template',
|
||||
plugin_dir_url(dirname(__FILE__)) . 'templates/assets/js/igny8-content-template.js',
|
||||
array('jquery'),
|
||||
'1.0.0',
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add body class for IGNY8 content
|
||||
*
|
||||
* @param array $classes Current body classes
|
||||
* @return array Modified body classes
|
||||
*/
|
||||
public function add_body_class($classes) {
|
||||
global $post;
|
||||
|
||||
if (is_singular('post') && $this->is_igny8_content($post->ID)) {
|
||||
$classes[] = 'igny8-content';
|
||||
$classes[] = 'igny8-template-active';
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize template loader
|
||||
new Igny8_Template_Loader();
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Updater
|
||||
*
|
||||
* Handles automatic updates from IGNY8 API
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Igny8_Updater {
|
||||
|
||||
/**
|
||||
* Plugin slug
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $plugin_slug = 'igny8-wp-bridge';
|
||||
|
||||
/**
|
||||
* Plugin file
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $plugin_file;
|
||||
|
||||
/**
|
||||
* Current version
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $version;
|
||||
|
||||
/**
|
||||
* API URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $api_url;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $plugin_file Plugin file path
|
||||
* @param string $version Current version
|
||||
* @param string $api_url API URL
|
||||
*/
|
||||
public function __construct($plugin_file, $version, $api_url) {
|
||||
$this->plugin_file = $plugin_file;
|
||||
$this->version = $version;
|
||||
$this->api_url = trailingslashit($api_url);
|
||||
|
||||
// Hook into WordPress update system
|
||||
add_filter('pre_set_site_transient_update_plugins', array($this, 'check_for_updates'));
|
||||
add_filter('plugins_api', array($this, 'plugin_info'), 10, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for plugin updates
|
||||
*
|
||||
* @param object $transient Update transient
|
||||
* @return object Modified transient
|
||||
*/
|
||||
public function check_for_updates($transient) {
|
||||
if (empty($transient->checked)) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
// Get update info from IGNY8 API
|
||||
$update_info = $this->get_update_info();
|
||||
|
||||
if (!$update_info || !isset($update_info['update_available'])) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
if ($update_info['update_available'] === true) {
|
||||
$plugin_basename = plugin_basename($this->plugin_file);
|
||||
|
||||
$transient->response[$plugin_basename] = (object) array(
|
||||
'slug' => $this->plugin_slug,
|
||||
'new_version' => $update_info['latest_version'],
|
||||
'package' => $update_info['download_url'],
|
||||
'url' => $update_info['info_url'] ?? '',
|
||||
'tested' => $update_info['tested'] ?? '',
|
||||
'requires_php' => $update_info['requires_php'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
return $transient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide plugin information for update screen
|
||||
*
|
||||
* @param false|object|array $result The result object or array
|
||||
* @param string $action The type of information being requested
|
||||
* @param object $args Plugin API arguments
|
||||
* @return false|object Modified result
|
||||
*/
|
||||
public function plugin_info($result, $action, $args) {
|
||||
if ($action !== 'plugin_information') {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (!isset($args->slug) || $args->slug !== $this->plugin_slug) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Get plugin info from IGNY8 API
|
||||
$info = $this->get_plugin_info();
|
||||
|
||||
if (!$info) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return (object) array(
|
||||
'name' => $info['name'] ?? 'IGNY8 WordPress Bridge',
|
||||
'slug' => $this->plugin_slug,
|
||||
'version' => $info['version'] ?? $this->version,
|
||||
'author' => 'IGNY8',
|
||||
'author_profile' => 'https://igny8.com',
|
||||
'homepage' => 'https://igny8.com',
|
||||
'sections' => array(
|
||||
'description' => $info['description'] ?? '',
|
||||
'changelog' => $info['changelog'] ?? '',
|
||||
),
|
||||
'download_link' => $info['download_url'] ?? '',
|
||||
'tested' => $info['tested'] ?? '',
|
||||
'requires_php' => $info['requires_php'] ?? '7.4',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get update information from IGNY8 API
|
||||
*
|
||||
* @return array|false Update info or false on failure
|
||||
*/
|
||||
private function get_update_info() {
|
||||
$url = $this->api_url . $this->plugin_slug . '/check-update/';
|
||||
|
||||
$response = wp_remote_get($url, array(
|
||||
'timeout' => 10,
|
||||
'headers' => array(
|
||||
'X-IGNY8-Site-ID' => get_option('igny8_site_id'),
|
||||
'X-IGNY8-API-Key' => get_option('igny8_api_key'),
|
||||
),
|
||||
'body' => array(
|
||||
'current_version' => $this->version,
|
||||
),
|
||||
));
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
if ($code !== 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
$data = json_decode($body, true);
|
||||
|
||||
return $data ?: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin information from IGNY8 API
|
||||
*
|
||||
* @return array|false Plugin info or false on failure
|
||||
*/
|
||||
private function get_plugin_info() {
|
||||
$url = $this->api_url . $this->plugin_slug . '/info/';
|
||||
|
||||
$response = wp_remote_get($url, array(
|
||||
'timeout' => 10,
|
||||
));
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
if ($code !== 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
$data = json_decode($body, true);
|
||||
|
||||
return $data ?: false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/**
|
||||
* Webhook Activity Logs
|
||||
*
|
||||
* Logs webhook activity for auditing and debugging
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log webhook activity
|
||||
*
|
||||
* @param array $data Log data
|
||||
* @return string|false Log ID or false on failure
|
||||
*/
|
||||
function igny8_log_webhook_activity($data) {
|
||||
$logs = get_option('igny8_webhook_logs', array());
|
||||
|
||||
$log_entry = array(
|
||||
'id' => 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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
/**
|
||||
* IGNY8 Webhooks Handler
|
||||
*
|
||||
* Handles incoming webhooks from IGNY8 SaaS
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Igny8Webhooks Class
|
||||
*/
|
||||
class Igny8Webhooks {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action('rest_api_init', array($this, 'register_webhook_routes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register webhook REST routes
|
||||
*/
|
||||
public function register_webhook_routes() {
|
||||
// Main webhook endpoint
|
||||
register_rest_route('igny8/v1', '/event', array(
|
||||
'methods' => '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 API key authentication
|
||||
*
|
||||
* @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 API key from plugin settings
|
||||
$stored_api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||
|
||||
if (empty($stored_api_key)) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('API key not configured', 'igny8-bridge'),
|
||||
array('status' => 403)
|
||||
);
|
||||
}
|
||||
|
||||
// Check X-IGNY8-API-KEY header
|
||||
$header_api_key = $request->get_header('X-IGNY8-API-KEY');
|
||||
|
||||
// Also check Authorization Bearer header
|
||||
if (empty($header_api_key)) {
|
||||
$auth_header = $request->get_header('Authorization');
|
||||
if ($auth_header && strpos($auth_header, 'Bearer ') === 0) {
|
||||
$header_api_key = substr($auth_header, 7);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($header_api_key)) {
|
||||
igny8_log_webhook_activity(array(
|
||||
'event' => 'authentication_failed',
|
||||
'ip' => $request->get_header('X-Forwarded-For') ?: $request->get_header('Remote-Addr'),
|
||||
'error' => 'Missing API key'
|
||||
));
|
||||
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('Missing API key in request headers', 'igny8-bridge'),
|
||||
array('status' => 401)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify API key matches
|
||||
if (!hash_equals($stored_api_key, $header_api_key)) {
|
||||
igny8_log_webhook_activity(array(
|
||||
'event' => 'authentication_failed',
|
||||
'ip' => $request->get_header('X-Forwarded-For') ?: $request->get_header('Remote-Addr'),
|
||||
'error' => 'Invalid API key'
|
||||
));
|
||||
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('Invalid API key', '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();
|
||||
|
||||
882
plugins/wordpress/source/igny8-wp-bridge/includes/functions.php
Normal file
882
plugins/wordpress/source/igny8-wp-bridge/includes/functions.php
Normal file
@@ -0,0 +1,882 @@
|
||||
<?php
|
||||
/**
|
||||
* Helper Functions
|
||||
*
|
||||
* WordPress integration functions for IGNY8 Bridge
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encryption key for secure option storage
|
||||
*
|
||||
* @return string Binary key
|
||||
*/
|
||||
function igny8_get_encryption_key() {
|
||||
$salt = wp_salt('auth');
|
||||
return hash('sha256', 'igny8_bridge_' . $salt, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a value for storage
|
||||
*
|
||||
* @param string $value Plain text value
|
||||
* @return string Encrypted value with prefix or original value on failure
|
||||
*/
|
||||
function igny8_encrypt_value($value) {
|
||||
if ($value === '' || $value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!function_exists('openssl_encrypt')) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$iv = openssl_random_pseudo_bytes(16);
|
||||
$cipher = openssl_encrypt($value, 'AES-256-CBC', igny8_get_encryption_key(), OPENSSL_RAW_DATA, $iv);
|
||||
|
||||
if ($cipher === false) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return 'igny8|' . base64_encode($iv . $cipher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a stored value
|
||||
*
|
||||
* @param string $value Stored value
|
||||
* @return string Decrypted value or original on failure
|
||||
*/
|
||||
function igny8_decrypt_value($value) {
|
||||
if (!is_string($value) || strpos($value, 'igny8|') !== 0) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!function_exists('openssl_decrypt')) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$encoded = substr($value, 6);
|
||||
$data = base64_decode($encoded, true);
|
||||
|
||||
if ($data === false || strlen($data) <= 16) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$iv = substr($data, 0, 16);
|
||||
$cipher = substr($data, 16);
|
||||
|
||||
$plain = openssl_decrypt($cipher, 'AES-256-CBC', igny8_get_encryption_key(), OPENSSL_RAW_DATA, $iv);
|
||||
|
||||
return ($plain === false) ? $value : $plain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an option securely
|
||||
*
|
||||
* @param string $option Option name
|
||||
* @param string $value Value to store
|
||||
*/
|
||||
function igny8_store_secure_option($option, $value) {
|
||||
if ($value === null || $value === '') {
|
||||
delete_option($option);
|
||||
return;
|
||||
}
|
||||
|
||||
update_option($option, igny8_encrypt_value($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve secure option (with legacy fallback)
|
||||
*
|
||||
* @param string $option Option name
|
||||
* @return string Value
|
||||
*/
|
||||
function igny8_get_secure_option($option) {
|
||||
$stored = get_option($option);
|
||||
|
||||
if (!$stored) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = igny8_decrypt_value($stored);
|
||||
|
||||
return is_string($value) ? $value : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported post types for automation
|
||||
*
|
||||
* @return array Key => label
|
||||
*/
|
||||
function igny8_get_supported_post_types() {
|
||||
$types = array(
|
||||
'post' => __('Posts', 'igny8-bridge'),
|
||||
'page' => __('Pages', 'igny8-bridge'),
|
||||
);
|
||||
|
||||
if (post_type_exists('product')) {
|
||||
$types['product'] = __('Products', 'igny8-bridge');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the list of selectable post types.
|
||||
*
|
||||
* @param array $types
|
||||
*/
|
||||
return apply_filters('igny8_supported_post_types', $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled post types
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function igny8_get_enabled_post_types() {
|
||||
$saved = get_option('igny8_enabled_post_types');
|
||||
|
||||
if (is_array($saved) && !empty($saved)) {
|
||||
return $saved;
|
||||
}
|
||||
|
||||
return array_keys(igny8_get_supported_post_types());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configured control mode
|
||||
*
|
||||
* @return string mirror|hybrid
|
||||
*/
|
||||
function igny8_get_control_mode() {
|
||||
$mode = get_option('igny8_control_mode', 'mirror');
|
||||
return in_array($mode, array('mirror', 'hybrid'), true) ? $mode : 'mirror';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported taxonomies for syncing
|
||||
*
|
||||
* @return array Key => label
|
||||
*/
|
||||
function igny8_get_supported_taxonomies() {
|
||||
$taxonomies = array();
|
||||
|
||||
// Standard WordPress taxonomies
|
||||
if (taxonomy_exists('category')) {
|
||||
$taxonomies['category'] = __('Categories', 'igny8-bridge');
|
||||
}
|
||||
|
||||
if (taxonomy_exists('post_tag')) {
|
||||
$taxonomies['post_tag'] = __('Tags', 'igny8-bridge');
|
||||
}
|
||||
|
||||
// WooCommerce taxonomies
|
||||
if (taxonomy_exists('product_cat')) {
|
||||
$taxonomies['product_cat'] = __('Product Categories', 'igny8-bridge');
|
||||
}
|
||||
|
||||
if (taxonomy_exists('product_tag')) {
|
||||
$taxonomies['product_tag'] = __('Product Tags', 'igny8-bridge');
|
||||
}
|
||||
|
||||
if (taxonomy_exists('product_shipping_class')) {
|
||||
$taxonomies['product_shipping_class'] = __('Product Shipping Classes', 'igny8-bridge');
|
||||
}
|
||||
|
||||
// IGNY8 taxonomies (always include)
|
||||
if (taxonomy_exists('igny8_sectors')) {
|
||||
$taxonomies['igny8_sectors'] = __('IGNY8 Sectors', 'igny8-bridge');
|
||||
}
|
||||
|
||||
if (taxonomy_exists('igny8_clusters')) {
|
||||
$taxonomies['igny8_clusters'] = __('IGNY8 Clusters', 'igny8-bridge');
|
||||
}
|
||||
|
||||
// Get custom taxonomies (public only)
|
||||
$custom_taxonomies = get_taxonomies(array(
|
||||
'public' => true,
|
||||
'_builtin' => false
|
||||
), 'objects');
|
||||
|
||||
foreach ($custom_taxonomies as $taxonomy) {
|
||||
// Skip if already added above
|
||||
if (isset($taxonomies[$taxonomy->name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip post formats and other system taxonomies
|
||||
if (in_array($taxonomy->name, array('post_format', 'wp_theme', 'wp_template_part_area'), true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$taxonomies[$taxonomy->name] = $taxonomy->label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the list of selectable taxonomies.
|
||||
*
|
||||
* @param array $taxonomies
|
||||
*/
|
||||
return apply_filters('igny8_supported_taxonomies', $taxonomies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled taxonomies for syncing
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function igny8_get_enabled_taxonomies() {
|
||||
$saved = get_option('igny8_enabled_taxonomies');
|
||||
|
||||
if (is_array($saved) && !empty($saved)) {
|
||||
return $saved;
|
||||
}
|
||||
|
||||
// Default: enable common taxonomies
|
||||
return array('category', 'post_tag', 'product_cat', 'igny8_sectors', 'igny8_clusters');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a taxonomy is enabled for syncing
|
||||
*
|
||||
* @param string $taxonomy Taxonomy key
|
||||
* @return bool
|
||||
*/
|
||||
function igny8_is_taxonomy_enabled($taxonomy) {
|
||||
$taxonomies = igny8_get_enabled_taxonomies();
|
||||
return in_array($taxonomy, $taxonomies, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available automation modules
|
||||
*
|
||||
* @return array Key => label
|
||||
*/
|
||||
function igny8_get_available_modules() {
|
||||
$modules = array(
|
||||
'sites' => __('Sites (Data & Semantic Map)', 'igny8-bridge'),
|
||||
'planner' => __('Planner (Keywords & Briefs)', 'igny8-bridge'),
|
||||
'writer' => __('Writer (Tasks & Posts)', 'igny8-bridge'),
|
||||
'linker' => __('Linker (Internal Links)', 'igny8-bridge'),
|
||||
'optimizer' => __('Optimizer (Audits & Scores)', 'igny8-bridge'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the list of IGNY8 modules that can be toggled.
|
||||
*
|
||||
* @param array $modules
|
||||
*/
|
||||
return apply_filters('igny8_available_modules', $modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled modules
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function igny8_get_enabled_modules() {
|
||||
$saved = get_option('igny8_enabled_modules');
|
||||
|
||||
if (is_array($saved) && !empty($saved)) {
|
||||
return $saved;
|
||||
}
|
||||
|
||||
return array_keys(igny8_get_available_modules());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a module is enabled
|
||||
*
|
||||
* @param string $module Module key
|
||||
* @return bool
|
||||
*/
|
||||
function igny8_is_module_enabled($module) {
|
||||
$modules = igny8_get_enabled_modules();
|
||||
return in_array($module, $modules, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a post type is enabled for automation
|
||||
*
|
||||
* @param string $post_type Post type key
|
||||
* @return bool
|
||||
*/
|
||||
function igny8_is_post_type_enabled($post_type) {
|
||||
$post_types = igny8_get_enabled_post_types();
|
||||
return in_array($post_type, $post_types, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if IGNY8 connection is enabled
|
||||
* This is a master switch that disables all sync operations while preserving credentials
|
||||
*
|
||||
* @return bool True if connection is enabled
|
||||
*/
|
||||
if (!function_exists('igny8_log_error')) {
|
||||
function igny8_log_error($message) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('[IGNY8 Plugin] ' . $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('igny8_is_connection_enabled')) {
|
||||
function igny8_is_connection_enabled() {
|
||||
// Master toggle (defaults to true)
|
||||
$enabled = (bool) get_option('igny8_connection_enabled', 1);
|
||||
|
||||
if (!$enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prefer secure option helpers when available
|
||||
if (function_exists('igny8_get_secure_option')) {
|
||||
$api_key = igny8_get_secure_option('igny8_api_key');
|
||||
} else {
|
||||
$api_key = get_option('igny8_api_key');
|
||||
}
|
||||
|
||||
$site_id = get_option('igny8_site_id');
|
||||
|
||||
if (empty($api_key) || empty($site_id)) {
|
||||
igny8_log_error('Failed to connect to IGNY8 API: API key or Site ID not configured.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection state
|
||||
* Three states: not_connected, configured, connected
|
||||
*
|
||||
* @return string Connection state
|
||||
*/
|
||||
function igny8_get_connection_state() {
|
||||
$api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||
$integration_id = get_option('igny8_integration_id');
|
||||
$last_structure_sync = get_option('igny8_last_structure_sync');
|
||||
|
||||
if (empty($api_key)) {
|
||||
igny8_log_connection_state('not_connected', 'No API key found');
|
||||
return 'not_connected';
|
||||
}
|
||||
|
||||
if (!empty($api_key) && !empty($integration_id) && !empty($last_structure_sync)) {
|
||||
igny8_log_connection_state('connected', 'Fully connected and synced');
|
||||
return 'connected';
|
||||
}
|
||||
|
||||
igny8_log_connection_state('configured', 'API key set, pending structure sync');
|
||||
return 'configured';
|
||||
}
|
||||
|
||||
/**
|
||||
* Log connection state changes (without exposing API keys)
|
||||
*
|
||||
* @param string $state Connection state
|
||||
* @param string $message Additional context
|
||||
*/
|
||||
function igny8_log_connection_state($state, $message = '') {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log(sprintf('[IGNY8 Connection] State: %s | %s', $state, $message));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log sync operations (without exposing sensitive data)
|
||||
*
|
||||
* @param string $operation Operation name
|
||||
* @param array $context Context data
|
||||
*/
|
||||
function igny8_log_sync($operation, $context = array()) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
// Filter out sensitive keys
|
||||
$safe_context = array_diff_key($context, array_flip(['api_key', 'password', 'secret', 'token']));
|
||||
error_log(sprintf('[IGNY8 Sync] %s | Context: %s', $operation, json_encode($safe_context)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration for site scans
|
||||
*
|
||||
* @param array $overrides Override defaults
|
||||
* @return array
|
||||
*/
|
||||
function igny8_get_site_scan_settings($overrides = array()) {
|
||||
$defaults = array(
|
||||
'post_types' => igny8_get_enabled_post_types(),
|
||||
'include_products' => (bool) get_option('igny8_enable_woocommerce', class_exists('WooCommerce') ? 1 : 0),
|
||||
'per_page' => 100,
|
||||
'since' => null,
|
||||
'mode' => 'full',
|
||||
);
|
||||
|
||||
$settings = wp_parse_args($overrides, $defaults);
|
||||
|
||||
return apply_filters('igny8_site_scan_settings', $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register IGNY8 post meta fields
|
||||
*/
|
||||
function igny8_register_post_meta() {
|
||||
$post_types = array('post', 'page', 'product');
|
||||
|
||||
// Define all meta fields with proper schema for REST API
|
||||
$meta_fields = array(
|
||||
'_igny8_taxonomy_id' => array(
|
||||
'type' => 'integer',
|
||||
'description' => 'IGNY8 taxonomy ID linked to this post',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
'_igny8_attribute_id' => array(
|
||||
'type' => 'integer',
|
||||
'description' => 'IGNY8 attribute ID linked to this post',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
'_igny8_last_synced' => array(
|
||||
'type' => 'string',
|
||||
'description' => 'Last sync timestamp',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
)
|
||||
);
|
||||
|
||||
// Register each meta field for all relevant post types
|
||||
foreach ($meta_fields as $meta_key => $config) {
|
||||
foreach ($post_types as $post_type) {
|
||||
register_post_meta($post_type, $meta_key, $config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register IGNY8 taxonomies
|
||||
*/
|
||||
function igny8_register_taxonomies() {
|
||||
// Register sectors taxonomy (hierarchical) - only if not exists
|
||||
if (!taxonomy_exists('igny8_sectors')) {
|
||||
register_taxonomy('igny8_sectors', array('post', 'page', 'product'), array(
|
||||
'hierarchical' => true,
|
||||
'labels' => array(
|
||||
'name' => 'IGNY8 Sectors',
|
||||
'singular_name' => 'Sector',
|
||||
'menu_name' => 'Sectors',
|
||||
'all_items' => 'All Sectors',
|
||||
'edit_item' => 'Edit Sector',
|
||||
'view_item' => 'View Sector',
|
||||
'update_item' => 'Update Sector',
|
||||
'add_new_item' => 'Add New Sector',
|
||||
'new_item_name' => 'New Sector Name',
|
||||
'parent_item' => 'Parent Sector',
|
||||
'parent_item_colon' => 'Parent Sector:',
|
||||
'search_items' => 'Search Sectors',
|
||||
'not_found' => 'No sectors found',
|
||||
),
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_admin_column' => false,
|
||||
'show_in_nav_menus' => true,
|
||||
'show_tagcloud' => false,
|
||||
'show_in_rest' => true,
|
||||
'rewrite' => array(
|
||||
'slug' => 'sectors',
|
||||
'with_front' => false,
|
||||
),
|
||||
'capabilities' => array(
|
||||
'manage_terms' => 'manage_categories',
|
||||
'edit_terms' => 'manage_categories',
|
||||
'delete_terms' => 'manage_categories',
|
||||
'assign_terms' => 'edit_posts',
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Register clusters taxonomy (hierarchical) - only if not exists
|
||||
if (!taxonomy_exists('igny8_clusters')) {
|
||||
register_taxonomy('igny8_clusters', array('post', 'page', 'product'), array(
|
||||
'hierarchical' => true,
|
||||
'labels' => array(
|
||||
'name' => 'IGNY8 Clusters',
|
||||
'singular_name' => 'Cluster',
|
||||
'menu_name' => 'Clusters',
|
||||
'all_items' => 'All Clusters',
|
||||
'edit_item' => 'Edit Cluster',
|
||||
'view_item' => 'View Cluster',
|
||||
'update_item' => 'Update Cluster',
|
||||
'add_new_item' => 'Add New Cluster',
|
||||
'new_item_name' => 'New Cluster Name',
|
||||
'parent_item' => 'Parent Cluster',
|
||||
'parent_item_colon' => 'Parent Cluster:',
|
||||
'search_items' => 'Search Clusters',
|
||||
'not_found' => 'No clusters found',
|
||||
),
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_admin_column' => false,
|
||||
'show_in_nav_menus' => true,
|
||||
'show_tagcloud' => false,
|
||||
'show_in_rest' => true,
|
||||
'rewrite' => array(
|
||||
'slug' => 'clusters',
|
||||
'with_front' => false,
|
||||
),
|
||||
'capabilities' => array(
|
||||
'manage_terms' => 'manage_categories',
|
||||
'edit_terms' => 'manage_categories',
|
||||
'delete_terms' => 'manage_categories',
|
||||
'assign_terms' => 'edit_posts',
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map WordPress post status to IGNY8 task status
|
||||
*
|
||||
* @param string $wp_status WordPress post status
|
||||
* @return string IGNY8 task status
|
||||
*/
|
||||
function igny8_map_wp_status_to_igny8($wp_status) {
|
||||
$status_map = array(
|
||||
'publish' => 'completed',
|
||||
'draft' => 'draft',
|
||||
'pending' => 'pending',
|
||||
'private' => 'completed',
|
||||
'trash' => 'archived',
|
||||
'future' => 'scheduled'
|
||||
);
|
||||
|
||||
return isset($status_map[$wp_status]) ? $status_map[$wp_status] : 'draft';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if post is managed by IGNY8
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @return bool True if IGNY8 managed
|
||||
*/
|
||||
function igny8_is_igny8_managed_post($post_id) {
|
||||
$task_id = get_post_meta($post_id, '_igny8_task_id', true);
|
||||
return !empty($task_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post data for IGNY8 sync
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @return array|false Post data or false on failure
|
||||
*/
|
||||
function igny8_get_post_data_for_sync($post_id) {
|
||||
$post = get_post($post_id);
|
||||
|
||||
if (!$post) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $post_id,
|
||||
'title' => $post->post_title,
|
||||
'status' => $post->post_status,
|
||||
'url' => get_permalink($post_id),
|
||||
'modified' => $post->post_modified,
|
||||
'published' => $post->post_date,
|
||||
'author' => get_the_author_meta('display_name', $post->post_author),
|
||||
'word_count' => str_word_count(strip_tags($post->post_content)),
|
||||
'meta' => array(
|
||||
'task_id' => get_post_meta($post_id, '_igny8_task_id', true),
|
||||
'content_id' => get_post_meta($post_id, '_igny8_content_id', true),
|
||||
'cluster_id' => get_post_meta($post_id, '_igny8_cluster_id', true),
|
||||
'sector_id' => get_post_meta($post_id, '_igny8_sector_id', true),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule cron jobs
|
||||
*/
|
||||
function igny8_schedule_cron_jobs() {
|
||||
// Schedule daily post status sync (WordPress → IGNY8)
|
||||
if (!wp_next_scheduled('igny8_sync_post_statuses')) {
|
||||
wp_schedule_event(time(), 'daily', 'igny8_sync_post_statuses');
|
||||
}
|
||||
|
||||
// Schedule daily site data sync (incremental)
|
||||
if (!wp_next_scheduled('igny8_sync_site_data')) {
|
||||
wp_schedule_event(time(), 'daily', 'igny8_sync_site_data');
|
||||
}
|
||||
|
||||
// Schedule periodic full site scan (runs at most once per week)
|
||||
if (!wp_next_scheduled('igny8_full_site_scan')) {
|
||||
wp_schedule_event(time(), 'daily', 'igny8_full_site_scan');
|
||||
}
|
||||
|
||||
// Schedule hourly sync from IGNY8 (IGNY8 → WordPress)
|
||||
if (!wp_next_scheduled('igny8_sync_from_igny8')) {
|
||||
wp_schedule_event(time(), 'hourly', 'igny8_sync_from_igny8');
|
||||
}
|
||||
|
||||
// Schedule taxonomy sync
|
||||
if (!wp_next_scheduled('igny8_sync_taxonomies')) {
|
||||
wp_schedule_event(time(), 'twicedaily', 'igny8_sync_taxonomies');
|
||||
}
|
||||
|
||||
// Schedule keyword sync
|
||||
if (!wp_next_scheduled('igny8_sync_keywords')) {
|
||||
wp_schedule_event(time(), 'daily', 'igny8_sync_keywords');
|
||||
}
|
||||
|
||||
// Schedule site structure sync (daily - to keep post types, taxonomies counts up to date)
|
||||
if (!wp_next_scheduled('igny8_sync_site_structure')) {
|
||||
wp_schedule_event(time(), 'daily', 'igny8_sync_site_structure');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unschedule cron jobs
|
||||
*/
|
||||
function igny8_unschedule_cron_jobs() {
|
||||
$timestamp = wp_next_scheduled('igny8_sync_post_statuses');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_post_statuses');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_sync_site_data');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_site_data');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_sync_from_igny8');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_from_igny8');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_full_site_scan');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_full_site_scan');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_sync_taxonomies');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_taxonomies');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_sync_keywords');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_keywords');
|
||||
}
|
||||
|
||||
$timestamp = wp_next_scheduled('igny8_sync_site_structure');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'igny8_sync_site_structure');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WordPress site structure (post types and taxonomies with counts)
|
||||
*
|
||||
* @return array Site structure with post types and taxonomies
|
||||
*/
|
||||
function igny8_get_site_structure() {
|
||||
$post_types_data = array();
|
||||
$taxonomies_data = array();
|
||||
|
||||
// Get all registered post types
|
||||
$post_types = get_post_types(array('public' => true), 'objects');
|
||||
|
||||
foreach ($post_types as $post_type) {
|
||||
// Skip built-in post types we don't care about
|
||||
if (in_array($post_type->name, array('attachment'), true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$count = wp_count_posts($post_type->name);
|
||||
$total = 0;
|
||||
foreach ((array) $count as $status => $num) {
|
||||
if ($status !== 'auto-draft') {
|
||||
$total += (int) $num;
|
||||
}
|
||||
}
|
||||
|
||||
if ($total > 0 || in_array($post_type->name, array('post', 'page', 'product'), true)) {
|
||||
$post_types_data[$post_type->name] = array(
|
||||
'label' => $post_type->label ?: $post_type->name,
|
||||
'count' => $total,
|
||||
'enabled' => igny8_is_post_type_enabled($post_type->name),
|
||||
'fetch_limit' => 100,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get all registered taxonomies
|
||||
$taxonomies = get_taxonomies(array('public' => true), 'objects');
|
||||
|
||||
foreach ($taxonomies as $taxonomy) {
|
||||
// Skip built-in taxonomies we don't care about
|
||||
if (in_array($taxonomy->name, array('post_format'), true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$terms = get_terms(array(
|
||||
'taxonomy' => $taxonomy->name,
|
||||
'hide_empty' => false,
|
||||
'number' => 0,
|
||||
));
|
||||
|
||||
$count = is_array($terms) ? count($terms) : 0;
|
||||
|
||||
if ($count > 0 || in_array($taxonomy->name, array('category', 'post_tag', 'product_cat'), true)) {
|
||||
$taxonomies_data[$taxonomy->name] = array(
|
||||
'label' => $taxonomy->label ?: $taxonomy->name,
|
||||
'count' => $count,
|
||||
'enabled' => true,
|
||||
'fetch_limit' => 100,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'post_types' => $post_types_data,
|
||||
'taxonomies' => $taxonomies_data,
|
||||
'timestamp' => current_time('c'),
|
||||
);
|
||||
}
|
||||
|
||||
/* Duplicate function removed. See guarded implementation above. */
|
||||
|
||||
/**
|
||||
* Sync WordPress site structure to IGNY8 backend
|
||||
* Called after connection is established
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
function igny8_sync_site_structure_to_backend($integration_id = null) {
|
||||
// Get site ID from options
|
||||
$site_id = get_option('igny8_site_id');
|
||||
if (!$site_id) {
|
||||
igny8_log_sync('structure_sync_failed', array('reason' => 'No site ID found'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the site structure
|
||||
$structure = igny8_get_site_structure();
|
||||
if (empty($structure['post_types']) && empty($structure['taxonomies'])) {
|
||||
igny8_log_sync('structure_sync_skipped', array('reason' => 'No post types or taxonomies'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a temporary integration object to find the actual integration ID
|
||||
$api = new Igny8API();
|
||||
|
||||
if (!$api->is_authenticated()) {
|
||||
igny8_log_sync('structure_sync_failed', array('reason' => 'Not authenticated'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use provided integration_id if available, otherwise query for it
|
||||
$integration = null;
|
||||
if ($integration_id) {
|
||||
// Use the provided integration_id directly
|
||||
$integration = array('id' => $integration_id);
|
||||
igny8_log_sync('structure_sync_start', array('integration_id' => $integration_id, 'site_id' => $site_id));
|
||||
} else {
|
||||
// Fallback: Get integration_id from stored option
|
||||
$stored_integration_id = get_option('igny8_integration_id');
|
||||
if ($stored_integration_id) {
|
||||
$integration = array('id' => intval($stored_integration_id));
|
||||
igny8_log_sync('structure_sync_start', array('integration_id' => $stored_integration_id, 'site_id' => $site_id, 'source' => 'stored_option'));
|
||||
} else {
|
||||
// Last resort: Query for integrations
|
||||
$response = $api->get('/v1/integration/integrations/?site=' . $site_id);
|
||||
|
||||
if (!$response['success'] || empty($response['data'])) {
|
||||
igny8_log_sync('structure_sync_failed', array('reason' => 'No integrations found'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the first integration (should be WordPress integration)
|
||||
if (isset($response['data']['results']) && !empty($response['data']['results'])) {
|
||||
$integration = $response['data']['results'][0];
|
||||
} elseif (is_array($response['data']) && !empty($response['data'])) {
|
||||
$integration = $response['data'][0];
|
||||
}
|
||||
|
||||
if ($integration && !empty($integration['id'])) {
|
||||
// Store integration_id for future use
|
||||
update_option('igny8_integration_id', intval($integration['id']));
|
||||
igny8_log_sync('integration_id_saved', array('integration_id' => $integration['id']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$integration || empty($integration['id'])) {
|
||||
igny8_log_sync('structure_sync_failed', array('reason' => 'Invalid integration'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prepare the payload
|
||||
$payload = array(
|
||||
'post_types' => $structure['post_types'],
|
||||
'taxonomies' => $structure['taxonomies'],
|
||||
'timestamp' => $structure['timestamp'],
|
||||
'plugin_connection_enabled' => (bool) igny8_is_connection_enabled(),
|
||||
'two_way_sync_enabled' => (bool) get_option('igny8_enable_two_way_sync', 1),
|
||||
);
|
||||
|
||||
igny8_log_sync('structure_sync_sending', array(
|
||||
'post_types_count' => count($structure['post_types']),
|
||||
'taxonomies_count' => count($structure['taxonomies'])
|
||||
));
|
||||
|
||||
// Send to backend
|
||||
$endpoint = '/v1/integration/integrations/' . $integration['id'] . '/update-structure/';
|
||||
$update_response = $api->post($endpoint, $payload);
|
||||
|
||||
if ($update_response['success']) {
|
||||
igny8_log_sync('structure_sync_success', array(
|
||||
'post_types' => count($structure['post_types']),
|
||||
'taxonomies' => count($structure['taxonomies'])
|
||||
));
|
||||
update_option('igny8_last_structure_sync', current_time('timestamp'));
|
||||
return true;
|
||||
} else {
|
||||
igny8_log_sync('structure_sync_failed', array(
|
||||
'error' => $update_response['error'] ?? 'Unknown error',
|
||||
'http_status' => $update_response['http_status'] ?? 0
|
||||
));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('igny8_handle_rate_limit')) {
|
||||
function igny8_handle_rate_limit($response, $max_retries = 3) {
|
||||
if (isset($response['error']) && strpos($response['error'], 'Rate limit') !== false) {
|
||||
for ($attempt = 0; $attempt < $max_retries; $attempt++) {
|
||||
sleep(pow(2, $attempt)); // Exponential backoff
|
||||
$response = igny8_retry_request(); // Retry logic (to be implemented)
|
||||
if ($response['success']) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
igny8_log_error('Max retries exceeded for rate-limited request.');
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('igny8_retry_request')) {
|
||||
function igny8_retry_request() {
|
||||
// Placeholder for retry logic
|
||||
return ['success' => false, 'error' => 'Retry logic not implemented'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Helper Functions
|
||||
*
|
||||
* Helper functions for IGNY8 content template
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse content HTML into intro and H2 sections
|
||||
*
|
||||
* @param string $content HTML content
|
||||
* @return array ['intro' => string, 'sections' => array]
|
||||
*/
|
||||
function igny8_parse_content_sections($content) {
|
||||
if (empty($content)) {
|
||||
return ['intro' => '', 'sections' => []];
|
||||
}
|
||||
|
||||
// Use DOMDocument to parse HTML
|
||||
$dom = new DOMDocument('1.0', 'UTF-8');
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
// Wrap content in a div to ensure proper parsing
|
||||
$wrapped_content = '<div>' . $content . '</div>';
|
||||
$dom->loadHTML('<?xml encoding="UTF-8">' . $wrapped_content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
libxml_clear_errors();
|
||||
|
||||
$intro_html = '';
|
||||
$sections = [];
|
||||
$current_section = null;
|
||||
|
||||
// Get the wrapper div
|
||||
$xpath = new DOMXPath($dom);
|
||||
$nodes = $xpath->query('//div/*');
|
||||
|
||||
if ($nodes->length === 0) {
|
||||
return ['intro' => $content, 'sections' => []];
|
||||
}
|
||||
|
||||
// Iterate through all child nodes
|
||||
foreach ($nodes as $node) {
|
||||
// Check if node is an H2 heading
|
||||
if ($node->nodeName === 'h2') {
|
||||
// Save previous section if exists
|
||||
if ($current_section !== null) {
|
||||
$sections[] = $current_section;
|
||||
}
|
||||
|
||||
// Start new section
|
||||
$current_section = [
|
||||
'heading' => trim($node->textContent),
|
||||
'content' => '',
|
||||
'id' => sanitize_title($node->textContent)
|
||||
];
|
||||
} elseif ($current_section !== null) {
|
||||
// Add to current section
|
||||
$current_section['content'] .= $dom->saveHTML($node);
|
||||
} else {
|
||||
// Add to intro (before first H2)
|
||||
$intro_html .= $dom->saveHTML($node);
|
||||
}
|
||||
}
|
||||
|
||||
// Save last section
|
||||
if ($current_section !== null) {
|
||||
$sections[] = $current_section;
|
||||
}
|
||||
|
||||
return [
|
||||
'intro' => trim($intro_html),
|
||||
'sections' => $sections
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get in-article images from imported images meta
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @return array Indexed array of image data by position
|
||||
*/
|
||||
function igny8_get_in_article_images($post_id) {
|
||||
$imported_images = get_post_meta($post_id, '_igny8_imported_images', true);
|
||||
|
||||
if (empty($imported_images) || !is_array($imported_images)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$in_article_images = [];
|
||||
|
||||
foreach ($imported_images as $img) {
|
||||
// Skip featured images
|
||||
if (isset($img['is_featured']) && $img['is_featured']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$position = isset($img['position']) ? (int)$img['position'] : count($in_article_images) + 1;
|
||||
$in_article_images[$position] = $img;
|
||||
}
|
||||
|
||||
return $in_article_images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get featured image prompt from imported images meta
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @return string|null Image prompt or null
|
||||
*/
|
||||
function igny8_get_featured_image_prompt($post_id) {
|
||||
$imported_images = get_post_meta($post_id, '_igny8_imported_images', true);
|
||||
|
||||
if (empty($imported_images) || !is_array($imported_images)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($imported_images as $img) {
|
||||
if (isset($img['is_featured']) && $img['is_featured'] && isset($img['prompt'])) {
|
||||
return $img['prompt'];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format status label for display
|
||||
*
|
||||
* @param string $status Post status
|
||||
* @return string Formatted label
|
||||
*/
|
||||
function igny8_format_status_label($status) {
|
||||
$labels = [
|
||||
'draft' => 'Draft',
|
||||
'pending' => 'Pending Review',
|
||||
'publish' => 'Published',
|
||||
'private' => 'Private',
|
||||
'future' => 'Scheduled',
|
||||
'trash' => 'Trash'
|
||||
];
|
||||
|
||||
return isset($labels[$status]) ? $labels[$status] : ucfirst($status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status CSS class
|
||||
*
|
||||
* @param string $status Post status
|
||||
* @return string CSS class
|
||||
*/
|
||||
function igny8_get_status_class($status) {
|
||||
$classes = [
|
||||
'draft' => 'igny8-status-draft',
|
||||
'pending' => 'igny8-status-pending',
|
||||
'publish' => 'igny8-status-publish',
|
||||
'private' => 'igny8-status-private',
|
||||
'future' => 'igny8-status-future'
|
||||
];
|
||||
|
||||
return isset($classes[$status]) ? $classes[$status] : 'igny8-status-default';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate word count from content
|
||||
*
|
||||
* @param string $content HTML content
|
||||
* @return int Word count
|
||||
*/
|
||||
function igny8_calculate_word_count($content) {
|
||||
if (empty($content)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Strip HTML tags and count words
|
||||
$text = wp_strip_all_tags($content);
|
||||
return str_word_count($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse secondary keywords from meta
|
||||
*
|
||||
* @param string $keywords Comma-separated keywords
|
||||
* @return array Array of keywords
|
||||
*/
|
||||
function igny8_parse_keywords($keywords) {
|
||||
if (empty($keywords)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Split by comma and trim each keyword
|
||||
$keywords_array = array_map('trim', explode(',', $keywords));
|
||||
|
||||
// Remove empty values
|
||||
return array_filter($keywords_array);
|
||||
}
|
||||
Reference in New Issue
Block a user