'wordpress-plugin', 'utm_medium' => 'plugin', 'utm_campaign' => $campaign, ], $url); } define('ADVOCATE_LOOP_EXT_API', 1); function advocate_loop_log(string $message, string $level = \AdvocateLoop\Core\Logger::INFO, array $context = []): void { \AdvocateLoop\Core\Logger::log($message, $level, $context); } add_filter('sanitize_option_advocate_loop_api_key', function($value) { return sanitize_text_field($value); }, 10, 1); add_filter('sanitize_option_advocate_loop_brand_id', function($value) { return sanitize_text_field($value); }, 10, 1); add_action('admin_notices', function() { $screen = function_exists('get_current_screen') ? get_current_screen() : null; if ($screen && strpos($screen->id, 'advocate-loop') !== false) { global $wp_settings_errors; if (isset($wp_settings_errors)) { foreach ($wp_settings_errors as $key => $error) { if (strpos($error['setting'], 'advocate_loop') !== false) { unset($wp_settings_errors[$key]); } } } } }, 1); add_action('admin_notices', function() { if (advocate_loop_is_connected() || !current_user_can('manage_options')) { return; } $screen = function_exists('get_current_screen') ? get_current_screen() : null; if ($screen && strpos($screen->id, 'advocate-loop') !== false) { return; } $cta = advocate_loop_connect_cta(); echo '
'; echo '

' . esc_html($cta['heading']) . ' ' . esc_html($cta['body']) . '

'; echo '

'; echo '' . esc_html($cta['primary_label']) . ' '; if (!empty($cta['secondary_url'])) { echo '' . esc_html($cta['secondary_label']) . ' '; } echo '' . esc_html__('Enter credentials', 'advocateloop') . ''; echo '

'; echo '
'; }); spl_autoload_register(function ($class) { $prefix = 'AdvocateLoop\\'; $base_dir = ADVOCATE_LOOP_DIR . 'includes/'; $len = strlen($prefix); if (strncmp($prefix, $class, $len) !== 0) { return; } $relative_class = substr($class, $len); $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php'; if (file_exists($file)) { require $file; } }); /** * Initialize the plugin. */ function advocate_loop_init(): void { $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); $admin = new \AdvocateLoop\Admin\Admin(); $admin->init(); if (!empty($api_key) && !empty($brand_id)) { $shortcodes = new \AdvocateLoop\Frontend\Shortcodes(); $shortcodes->init(); } else { if (!is_admin()) { add_action('wp_footer', function() { if (!current_user_can('manage_options')) { return; } $cta = advocate_loop_connect_cta(); echo '
'; echo '' . esc_html($cta['heading']) . ''; echo '' . esc_html($cta['body']) . '
'; echo '' . esc_html__('Open AdvocateLoop settings', 'advocateloop') . ''; echo '
'; }); } } \AdvocateLoop\Admin\Extensions::register_tab([ 'id' => 'connection', 'label' => __('Connection', 'advocateloop'), 'priority' => 10, 'icon' => '', 'render' => function () { include ADVOCATE_LOOP_DIR . 'includes/Admin/views/tab-connection.php'; }, ]); do_action('advocate_loop_register_extensions'); } add_action('plugins_loaded', 'advocate_loop_init'); \AdvocateLoop\Core\Presence::register(); register_activation_hook(__FILE__, [\AdvocateLoop\Core\Presence::class, 'on_activation']); register_deactivation_hook(__FILE__, [\AdvocateLoop\Core\Presence::class, 'on_deactivation']); register_activation_hook(__FILE__, function() { add_option('advocate_loop_api_key', ''); add_option('advocate_loop_brand_id', ''); add_option('advocate_loop_settings', '{}'); if (!wp_next_scheduled('advocate_loop_process_request_queue')) { wp_schedule_event(time(), 'advocate_loop_five_minutes', 'advocate_loop_process_request_queue'); } flush_rewrite_rules(); }); register_deactivation_hook(__FILE__, function() { flush_rewrite_rules(); wp_clear_scheduled_hook('advocate_loop_process_request_queue'); }); function advocate_loop_resolve_active_platform() { return \AdvocateLoop\Admin\Extensions::resolve_platform(); } function advocate_loop_is_connected(): bool { return (bool) get_option('advocate_loop_api_key') && (bool) get_option('advocate_loop_brand_id'); } function advocate_loop_ever_connected(): bool { return (bool) get_option('advocate_loop_ever_connected'); } /** * Build the call-to-action shown wherever the plugin is not connected. * * New install (never connected): lead with creating an AdvocateLoop account, * with a secondary path for users who already have one. Previously connected * but now disconnected: send straight to the API credentials page. * * @return array{heading:string,body:string,primary_label:string,primary_url:string,secondary_label?:string,secondary_url?:string} */ function advocate_loop_connect_cta(): array { $signup_url = ADVOCATE_LOOP_APP_URL . '/sign-up'; $api_url = ADVOCATE_LOOP_APP_URL . '/admin/developers'; if (advocate_loop_ever_connected()) { return [ 'heading' => __('AdvocateLoop is disconnected', 'advocateloop'), 'body' => __('Add your API credentials to reconnect this site.', 'advocateloop'), 'primary_label' => __('Get your API credentials', 'advocateloop'), 'primary_url' => $api_url, ]; } return [ 'heading' => __('Connect AdvocateLoop to get started', 'advocateloop'), 'body' => __('Create your free AdvocateLoop account, then add your API credentials here to start tracking referrals.', 'advocateloop'), 'primary_label' => __('Create your account', 'advocateloop'), 'primary_url' => $signup_url, 'secondary_label' => __('Already have an account? Get your credentials', 'advocateloop'), 'secondary_url' => $api_url, ]; } function advocate_loop_api_request(string $endpoint, string $method = 'GET', ?array $body = null) { $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); if (empty($api_key) || empty($brand_id)) { return new \WP_Error('not_configured', 'API credentials not configured'); } $args = [ 'method' => $method, 'headers' => [ 'X-API-Key' => $api_key, 'X-Brand-ID' => $brand_id, 'Content-Type' => 'application/json', ], 'timeout' => 15, ]; if ($body !== null && in_array($method, ['POST', 'PUT', 'PATCH'], true)) { $args['body'] = wp_json_encode($body); } return wp_remote_request(ADVOCATE_LOOP_API_URL . $endpoint, $args); } add_action('wp_enqueue_scripts', function() { $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); if (empty($api_key) || empty($brand_id)) { return; } wp_enqueue_script( 'advocate-loop-al', ADVOCATE_LOOP_API_URL . '/al.js', array(), ADVOCATE_LOOP_VERSION, true ); $al_config = array( 'brand' => $brand_id, 'api' => ADVOCATE_LOOP_API_URL, 'debug' => (defined('WP_DEBUG') && WP_DEBUG), 'platform' => advocate_loop_resolve_active_platform(), ); wp_add_inline_script( 'advocate-loop-al', 'window.AdvocateLoopConfig = ' . wp_json_encode($al_config) . ';', 'before' ); }); add_action('admin_enqueue_scripts', function($hook) { if (strpos($hook, 'advocate-loop') === false) { return; } wp_enqueue_style( 'advocate-loop-admin', ADVOCATE_LOOP_URL . 'assets/css/admin.css', [], ADVOCATE_LOOP_VERSION ); wp_add_inline_style('advocate-loop-admin', ' body[class*="advocate-loop"] #wpcontent { padding-left: 0; } body[class*="advocate-loop"] #wpbody-content { background: #ffffff; } body[class*="advocate-loop"] #wpbody-content > .notice:not(.al-notice), body[class*="advocate-loop"] #wpbody-content > .updated:not(.al-notice), body[class*="advocate-loop"] #wpbody-content > .error:not(.al-notice) { display: none; } @media (max-width: 782px) { body[class*="advocate-loop"] #wpcontent { padding-left: 0; } } '); }, 100); add_filter('plugin_action_links_' . plugin_basename(__FILE__), function($links) { $settings_link = '' . __('Settings', 'advocateloop') . ''; array_unshift($links, $settings_link); return $links; }); add_filter('plugin_row_meta', function($links, $file) { if ($file !== plugin_basename(ADVOCATE_LOOP_FILE)) { return $links; } // WordPress auto-generates the "Visit plugin site" link from the clean // Plugin URI header; rewrite just the displayed link to carry UTM, leaving // the canonical header untouched. $site_plain = esc_url(ADVOCATE_LOOP_SITE_URL); $site_utm = esc_url(advocate_loop_utm(ADVOCATE_LOOP_SITE_URL, 'plugins-screen-site')); foreach ($links as $i => $link) { if (strpos($link, $site_plain) !== false) { $links[$i] = str_replace($site_plain, $site_utm, $link); break; } } $links[] = '' . esc_html__('Documentation', 'advocateloop') . ''; return $links; }, 10, 2); add_action('admin_bar_menu', function($admin_bar) { if (!current_user_can('manage_options')) { return; } $brand_icon = ''; $admin_bar->add_menu(array( 'id' => 'advocate-loop', 'title' => $brand_icon . 'AdvocateLoop', 'href' => admin_url('admin.php?page=advocate-loop'), 'meta' => array('title' => __('AdvocateLoop', 'advocateloop')) )); $admin_bar->add_menu(array( 'parent' => 'advocate-loop', 'id' => 'advocate-loop-settings', 'title' => 'Settings', 'href' => admin_url('admin.php?page=advocate-loop') )); }, 100); /** * Get plugin settings. * * @param string|null $section Section name or null for all settings * @return array Settings array */ function advocate_loop_get_settings(?string $section = null): array { $settings = advocate_loop_read_settings(); if ($section === null) { return is_array($settings) ? $settings : []; } if (isset($settings[$section]) && is_array($settings[$section])) { return $settings[$section]; } return []; } /** * The whole advocate_loop_settings option as an array. It's stored as a JSON * string, but older versions could leave an array (or an empty value) behind, * so every read goes through here and accepts either. * * @return array */ function advocate_loop_read_settings(): array { $raw = get_option('advocate_loop_settings', '{}'); if (is_array($raw)) { return $raw; } if (is_string($raw) && $raw !== '') { $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : []; } return []; } function advocate_loop_update_settings(string $section, array $data): bool { $settings = advocate_loop_read_settings(); $settings[$section] = array_merge($settings[$section] ?? [], $data); return update_option('advocate_loop_settings', wp_json_encode($settings)); } function advocate_loop_get_brand_config(bool $force_refresh = false): array { $cache_key = 'advocate_loop_brand_config'; $cache_duration = HOUR_IN_SECONDS; if (!$force_refresh) { $cached = get_transient($cache_key); if ($cached !== false && is_array($cached)) { return $cached; } } $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); if (empty($api_key) || empty($brand_id)) { return [ '_error' => true, '_message' => 'API credentials not configured' ]; } $response = wp_remote_get( ADVOCATE_LOOP_API_URL . '/api/v1/brand/config', [ 'headers' => [ 'X-API-Key' => $api_key, 'X-Brand-ID' => $brand_id ], 'timeout' => 10 ] ); if (is_wp_error($response)) { \AdvocateLoop\Core\Logger::error('Failed to fetch brand config', [ 'error' => $response->get_error_message() ]); return [ '_error' => true, '_message' => 'API connection failed: ' . $response->get_error_message() ]; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['success']) && $body['success'] && isset($body['data'])) { $config = $body['data']; set_transient($cache_key, $config, $cache_duration); return $config; } return [ '_error' => true, '_message' => $body['error'] ?? 'Failed to load brand configuration' ]; } function advocate_loop_clear_config_cache(): void { delete_transient('advocate_loop_brand_config'); } add_action('rest_api_init', function() { register_rest_route('advocate-loop/v1', '/config/refresh', array( 'methods' => 'POST', 'callback' => function($request) { advocate_loop_clear_config_cache(); return new \WP_REST_Response(['success' => true, 'message' => 'Config cache cleared']); }, 'permission_callback' => function($request) { $api_key = (string) $request->get_header('X-API-Key'); $stored_key = (string) get_option('advocate_loop_api_key', ''); if ($api_key === '' || $stored_key === '' || !hash_equals($stored_key, $api_key)) { return new \WP_Error('advocate_loop_unauthorized', 'Unauthorized', array('status' => 401)); } return true; }, )); }); define('ADVOCATE_LOOP_QUEUE_MAX_ATTEMPTS', 3); define('ADVOCATE_LOOP_QUEUE_MAX_AGE_HOURS', 24); define('ADVOCATE_LOOP_QUEUE_OPTION_KEY', 'advocate_loop_request_queue'); function advocate_loop_queue_request(string $endpoint, string $method, array $body, array $headers = []): bool { $queue = get_option(ADVOCATE_LOOP_QUEUE_OPTION_KEY, []); if (!is_array($queue)) { $queue = []; } $request_id = 'req_' . uniqid() . '_' . time(); $queue[$request_id] = [ 'id' => $request_id, 'endpoint' => $endpoint, 'method' => $method, 'body' => $body, 'headers' => $headers, 'attempts' => 0, 'created_at' => time(), 'last_attempt' => null, 'last_error' => null ]; $result = update_option(ADVOCATE_LOOP_QUEUE_OPTION_KEY, $queue); if ($result) { \AdvocateLoop\Core\Logger::info('Queued failed request', [ 'request_id' => $request_id, 'endpoint' => $endpoint, 'queue_size' => count($queue) ]); } return $result; } function advocate_loop_get_queue(): array { $queue = get_option(ADVOCATE_LOOP_QUEUE_OPTION_KEY, []); return is_array($queue) ? $queue : []; } function advocate_loop_remove_from_queue(string $request_id): bool { $queue = get_option(ADVOCATE_LOOP_QUEUE_OPTION_KEY, []); if (!is_array($queue)) { return false; } if (isset($queue[$request_id])) { unset($queue[$request_id]); return update_option(ADVOCATE_LOOP_QUEUE_OPTION_KEY, $queue); } return false; } function advocate_loop_update_queue_item(string $request_id, array $updates): bool { $queue = get_option(ADVOCATE_LOOP_QUEUE_OPTION_KEY, []); if (!is_array($queue)) { return false; } if (isset($queue[$request_id])) { $queue[$request_id] = array_merge($queue[$request_id], $updates); return update_option(ADVOCATE_LOOP_QUEUE_OPTION_KEY, $queue); } return false; } function advocate_loop_process_queue(): void { $queue = advocate_loop_get_queue(); if (empty($queue)) { return; } $api_key = get_option('advocate_loop_api_key'); $brand_id = get_option('advocate_loop_brand_id'); if (empty($api_key) || empty($brand_id)) { \AdvocateLoop\Core\Logger::warning('Cannot process queue - API credentials not configured'); return; } $now = time(); $max_age_seconds = ADVOCATE_LOOP_QUEUE_MAX_AGE_HOURS * HOUR_IN_SECONDS; $processed = 0; $succeeded = 0; $removed = 0; \AdvocateLoop\Core\Logger::info('Processing queue', ['items' => count($queue)]); foreach ($queue as $request_id => $request) { if (($now - $request['created_at']) > $max_age_seconds) { advocate_loop_remove_from_queue($request_id); $removed++; \AdvocateLoop\Core\Logger::info('Removed stale request', [ 'request_id' => $request_id, 'age_hours' => round(($now - $request['created_at']) / HOUR_IN_SECONDS, 1) ]); continue; } if ($request['attempts'] >= ADVOCATE_LOOP_QUEUE_MAX_ATTEMPTS) { advocate_loop_remove_from_queue($request_id); $removed++; \AdvocateLoop\Core\Logger::warning('Removed request after max attempts', [ 'request_id' => $request_id, 'attempts' => $request['attempts'] ]); continue; } $processed++; $url = ADVOCATE_LOOP_API_URL . $request['endpoint']; $args = [ 'method' => $request['method'], 'headers' => array_merge( [ 'Content-Type' => 'application/json', 'X-API-Key' => $api_key, 'X-Brand-ID' => $brand_id, 'X-Queued-Request' => 'true', 'X-Original-Timestamp' => (string) $request['created_at'] ], $request['headers'] ?? [] ), 'body' => wp_json_encode($request['body']), 'timeout' => 15 ]; $response = wp_remote_request($url, $args); if (is_wp_error($response)) { advocate_loop_update_queue_item($request_id, [ 'attempts' => $request['attempts'] + 1, 'last_attempt' => $now, 'last_error' => $response->get_error_message() ]); \AdvocateLoop\Core\Logger::warning('Queue retry failed', [ 'request_id' => $request_id, 'error' => $response->get_error_message(), 'attempt' => $request['attempts'] + 1, 'max_attempts' => ADVOCATE_LOOP_QUEUE_MAX_ATTEMPTS ]); continue; } $status_code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($status_code >= 200 && $status_code < 300 && isset($body['success']) && $body['success']) { advocate_loop_remove_from_queue($request_id); $succeeded++; \AdvocateLoop\Core\Logger::info('Queue request succeeded', ['request_id' => $request_id]); } else { advocate_loop_update_queue_item($request_id, [ 'attempts' => $request['attempts'] + 1, 'last_attempt' => $now, 'last_error' => $body['error'] ?? "HTTP {$status_code}" ]); \AdvocateLoop\Core\Logger::warning('Queue retry returned error', [ 'request_id' => $request_id, 'error' => $body['error'] ?? "HTTP {$status_code}", 'attempt' => $request['attempts'] + 1, 'max_attempts' => ADVOCATE_LOOP_QUEUE_MAX_ATTEMPTS ]); } } \AdvocateLoop\Core\Logger::info('Queue processing complete', [ 'processed' => $processed, 'succeeded' => $succeeded, 'removed' => $removed ]); } add_filter('cron_schedules', function($schedules) { $schedules['advocate_loop_five_minutes'] = array( 'interval' => 5 * MINUTE_IN_SECONDS, 'display' => __('Every 5 Minutes', 'advocateloop') ); return $schedules; }); add_action('advocate_loop_process_request_queue', 'advocate_loop_process_queue'); add_action('init', function() { if (!wp_next_scheduled('advocate_loop_process_request_queue')) { wp_schedule_event(time(), 'advocate_loop_five_minutes', 'advocate_loop_process_request_queue'); } });