10]); if (is_wp_error($res)) return NULL; $body = json_decode(wp_remote_retrieve_body($res), TRUE); if (!is_array($body) || empty($body['ok'])) return NULL; return $body; } function statjolt_status($fresh = FALSE) { $opt = statjolt_options(); if ($opt['user_id'] < 1) return NULL; if (!$fresh) { $cached = get_transient('statjolt_status'); if (is_array($cached)) return $cached; } $body = statjolt_api(['u' => $opt['user_id'], 'd' => statjolt_domain(), 'fresh' => $fresh ? 1 : 0]); if (!$body) return NULL; set_transient('statjolt_status', $body, 5 * MINUTE_IN_SECONDS); // The front end works from the stored badge position, with no network call. if (!empty($body['badge']) && $body['badge'] !== $opt['badge']) { $stored = (array)get_option('statjolt', []); $stored['badge'] = $body['badge']; update_option('statjolt', $stored); statjolt_options(TRUE); } return $body; } function statjolt_sidebars() { $out = []; foreach ((array)($GLOBALS['wp_registered_sidebars'] ?? []) as $id => $sb) { if (is_active_sidebar($id)) $out[$id] = $sb['name']; } return $out; } // WordPress gives us names, not meaning, so this is a guess the user can override. // Prefer a real sidebar, fall back to the first area of any kind. function statjolt_default_sidebar() { $first = ''; foreach (statjolt_sidebars() as $id => $name) { if ($first === '') $first = $id; if (!preg_match('/footer/i', $id)) return $id; } return $first; } // Each row is a name, a detector and a purger. Plain closures rather than arrow // functions, so the plugin still parses on PHP 7.0. function statjolt_flush_cache() { // These hooks belong to other cache plugins on purpose. Firing them is the whole point, // so the "prefix your own hooks" rule does not apply here. // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound $cachers = [ ['WP Rocket', function () { return function_exists('rocket_clean_domain'); }, function () { rocket_clean_domain(); }], ['LiteSpeed Cache', function () { return has_action('litespeed_purge_all'); }, function () { do_action('litespeed_purge_all'); }], ['W3 Total Cache', function () { return function_exists('w3tc_flush_all'); }, function () { w3tc_flush_all(); }], ['WP Super Cache', function () { return function_exists('wp_cache_clear_cache'); }, function () { wp_cache_clear_cache(); }], ['WP Fastest Cache', function () { return isset($GLOBALS['wp_fastest_cache']) && method_exists($GLOBALS['wp_fastest_cache'], 'deleteCache'); }, function () { $GLOBALS['wp_fastest_cache']->deleteCache(TRUE); }], ['SiteGround Optimizer', function () { return function_exists('sg_cachepress_purge_cache'); }, function () { sg_cachepress_purge_cache(); }], // Not the same plugin as WP Super Cache above, despite the name. ['Super Page Cache', function () { return method_exists('SPC\\Modules\\Cache_Controller', 'purge_all'); }, function () { \SPC\Modules\Cache_Controller::purge_all(); }], ['Hummingbird', function () { return has_action('wphb_clear_page_cache'); }, function () { do_action('wphb_clear_page_cache'); }], ['Breeze', function () { return has_action('breeze_clear_all_cache'); }, function () { do_action('breeze_clear_all_cache'); }], ['Cache Enabler', function () { return method_exists('Cache_Enabler', 'clear_total_cache'); }, function () { Cache_Enabler::clear_total_cache(); }], ['Autoptimize', function () { return method_exists('autoptimizeCache', 'clearall'); }, function () { autoptimizeCache::clearall(); }], ['WP Engine', function () { return method_exists('WpeCommon', 'purge_varnish_cache'); }, function () { WpeCommon::purge_varnish_cache(); }], ]; // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound $done = []; foreach ($cachers as $cacher) { list($name, $detect, $purge) = $cacher; if (!$detect()) continue; // A broken third party plugin must not take the save down with it. try { $purge(); $done[] = $name; } catch (\Throwable $e) { } } return $done; } // The outcome is remembered in its own option rather than a transient, because the screen // has to keep telling the truth. A one-time message would expire and leave the user with // the "clear it yourself" warning even though we had already cleared it for them. // A separate option name, so writing it cannot recurse into the hooks below. add_action('statjolt_purge', function () { update_option('statjolt_purged', statjolt_flush_cache(), FALSE); }); // The first save on a fresh install never reaches update_option_statjolt. Because the // setting is registered with a default, update_option() sees the option missing and hands // off to add_option() (wp-includes/option.php:926), which fires a different hook. add_action('add_option_statjolt', function () { do_action('statjolt_purge'); }); // Purge only when the rendered output actually changed. add_action('update_option_statjolt', function ($old, $new) { $keys = ['user_id', 'placement', 'sidebar', 'badge', 'exclude_logged_in']; $a = $b = []; foreach ($keys as $k) { $a[$k] = $old[$k] ?? NULL; $b[$k] = $new[$k] ?? NULL; } if ($a == $b) return; do_action('statjolt_purge'); }, 10, 2); add_action('admin_init', function () { register_setting('statjolt_group', 'statjolt', ['sanitize_callback' => 'statjolt_sanitize', 'default' => []]); if (get_transient('statjolt_activated')) { delete_transient('statjolt_activated'); // No form data here. We only check for a bulk activation, where redirecting is not allowed. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if (!isset($_GET['activate-multi']) && current_user_can('manage_options')) { wp_safe_redirect(admin_url('options-general.php?page=statjolt-analytics')); exit; } } }); // Because of register_setting(), EVERY update_option('statjolt') passes through here, not // just the form. A programmatic write hands over a complete array, so keep it as is, // otherwise the badge position would be silently lost. function statjolt_sanitize($input) { $input = (array)$input; $cur = array_merge(statjolt_defaults(), (array)get_option('statjolt', [])); if (!array_key_exists('paste', $input)) return array_merge($cur, $input); $out = $cur; $out['placement'] = in_array($input['placement'] ?? '', ['body', 'sidebar', 'shortcode'], TRUE) ? $input['placement'] : 'body'; $out['sidebar'] = sanitize_text_field($input['sidebar'] ?? ''); $out['exclude_logged_in'] = empty($input['exclude_logged_in']) ? 0 : 1; $raw = trim((string)($input['paste'] ?? '')); if ($raw === '') return $out; $id = statjolt_parse_id($raw); if (!$id) { add_settings_error('statjolt', 'statjolt_notfound', __('I could not find a StatJolt code in that. Use the copy button on StatJolt.', 'statjolt-analytics'), 'error'); return $out; } // Kept verbatim so the field can show back exactly what was pasted, nothing invented. // Escaped with esc_textarea() on the way out. Capped so a stray page paste cannot bloat // an autoloaded option. $out['paste_raw'] = substr(str_replace("\0", '', $raw), 0, 2000); // The field is prefilled with what was pasted, so most saves resubmit the same ID. if ($id === (int)$cur['user_id']) { $out['user_id'] = $id; return $out; } $check = statjolt_api(['u' => $id]); // With a mistyped ID the traffic would silently land in a stranger's account. if ($check && empty($check['exists'])) { add_settings_error('statjolt', 'statjolt_nouser', __('That StatJolt ID does not exist. Check that you pasted your own code.', 'statjolt-analytics'), 'error'); return $out; } $out['user_id'] = $id; delete_transient('statjolt_status'); if ($check) add_settings_error('statjolt', 'statjolt_saved', __('Saved.', 'statjolt-analytics'), 'success'); else add_settings_error('statjolt', 'statjolt_apidown', __('I could not reach statjolt.com, so the ID stays unchecked. I saved it anyway.', 'statjolt-analytics'), 'warning'); return $out; } add_action('admin_menu', function () { add_options_page('StatJolt Analytics', 'StatJolt', 'manage_options', 'statjolt-analytics', 'statjolt_page'); }); // On a network one StatJolt account usually owns every domain, so the network value is the // default for the subsites. A subsite's own ID always wins. add_action('network_admin_menu', function () { add_submenu_page('settings.php', 'StatJolt Analytics', 'StatJolt', 'manage_network_options', 'statjolt-analytics', 'statjolt_network_page'); }); add_action('network_admin_edit_statjolt_network', function () { if (!current_user_can('manage_network_options')) wp_die('', '', ['response' => 403]); check_admin_referer('statjolt_network'); // The sanitisation here is statjolt_parse_id() itself, which always returns an integer. // sanitize_textarea_field would strip the script tag and the data-user value with it. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $raw = trim((string)wp_unslash($_POST['statjolt_paste'] ?? '')); $id = statjolt_parse_id($raw); $net = (array)get_site_option('statjolt', []); if ($id) { $check = statjolt_api(['u' => $id]); if (!$check || !empty($check['exists'])) { $net['user_id'] = $id; $net['paste_raw'] = substr(str_replace("\0", '', $raw), 0, 2000); } } update_site_option('statjolt', $net); wp_safe_redirect(network_admin_url('settings.php?page=statjolt-analytics&updated=1')); exit; }); function statjolt_network_page() { if (!current_user_can('manage_network_options')) return; $net = (array)get_site_option('statjolt', []); ?>

'.esc_html__('Settings', 'statjolt-analytics').''); return $links; }); // Not dismissible: once dismissed, the plugin would silently measure nothing. add_action('admin_notices', function () { if (!current_user_can('manage_options')) return; if (statjolt_options()['user_id'] > 0) return; $screen = get_current_screen(); // WordPress builds this id from the menu slug, so it has to track it. if ($screen && $screen->id === 'settings_page_statjolt-analytics') return; echo '

'.esc_html__('StatJolt is not measuring yet. Paste your code to start it.', 'statjolt-analytics') .' '.esc_html__('Set up StatJolt', 'statjolt-analytics').'

'; }); add_action('admin_post_statjolt_recheck', function () { if (!current_user_can('manage_options')) wp_die('', '', ['response' => 403]); check_admin_referer('statjolt_recheck'); $tries = (int)get_transient('statjolt_rechecks'); if ($tries >= 5) set_transient('statjolt_toooften', 1, 60); else { set_transient('statjolt_rechecks', $tries + 1, HOUR_IN_SECONDS); delete_transient('statjolt_status'); statjolt_status(TRUE); } wp_safe_redirect(admin_url('options-general.php?page=statjolt-analytics')); exit; }); function statjolt_status_line($st) { $domain = statjolt_domain(); if (!$st) return ['warning', __('I could not reach statjolt.com just now.', 'statjolt-analytics')]; if (empty($st['exists'])) return ['error', __('This StatJolt ID does not exist.', 'statjolt-analytics')]; switch ($st['status']) { case 'receiving': $txt = __('Receiving data.', 'statjolt-analytics'); if (!empty($st['last_event'])) { /* translators: %s: time since the last event, e.g. "5 mins" */ $txt .= ' '.sprintf(__('Last event: %s ago.', 'statjolt-analytics'), human_time_diff((int)$st['last_event'])); } return ['success', $txt]; case 'blocked': /* translators: %s: the site's domain name */ return ['error', sprintf(__('%s is switched off in your StatJolt account.', 'statjolt-analytics'), $domain)]; case 'no_site': /* translators: %s: the site's domain name */ return ['warning', sprintf(__('The code is saved, but statjolt.com has not seen a visit from %s yet. Open your site in a private window.', 'statjolt-analytics'), $domain)]; default: return ['warning', empty($st['partial']) ? __('The code is in place. Waiting for your first visitor.', 'statjolt-analytics') : __('The code is in place. I could not check for fresh data just now.', 'statjolt-analytics')]; } } // Neither message is on a timer. Both stay true until data arrives and clearing the cache // is the one step that decides whether tracking starts working at all. function statjolt_cache_box($receiving) { if ($receiving) return; $purged = get_option('statjolt_purged', []); if (is_array($purged) && $purged) { echo '

'.esc_html__('I cleared your whole cache', 'statjolt-analytics').'
' .esc_html__('Nothing left for you to do. The StatJolt logo should show up on your site shortly.', 'statjolt-analytics').' ('.esc_html(implode(', ', $purged)).')

'; return; } echo '

'.esc_html__('Clear your whole cache now', 'statjolt-analytics').'
' .esc_html__('Until you do, your visitors keep getting the old page and nothing is measured.', 'statjolt-analytics').'

'; } function statjolt_page() { if (!current_user_can('manage_options')) return; $opt = statjolt_options(); $ready = $opt['user_id'] > 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only decides which view to show $editing = !$ready || isset($_GET['statjolt_edit']); $st = $ready ? statjolt_status() : NULL; $receiving = $st && !empty($st['exists']) && ($st['status'] ?? '') === 'receiving'; if (get_transient('statjolt_toooften')) { delete_transient('statjolt_toooften'); echo '

'.esc_html__('You are checking too often. Wait a minute.', 'statjolt-analytics').'

'; } ?>

→