'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 '
';
});
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 '';
});
}
}
\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');
}
});