0, 'failed' => 0, 'quota' => 0 ); } // Most-clicked first (ties → newer): when the plan can't hold every link, monitoring goes // to the ones actually earning — the same priority the panel's over-limit logic protects. $candidates = AffKeeper_Local::all( 'clicks', 'DESC' ); foreach ( $candidates as $link ) { if ( ! empty( $link->remote_id ) ) { continue; // already synced } if ( isset( $link->monitored ) && ! (int) $link->monitored ) { continue; // monitoring is off for this link — it neither syncs nor uses a slot } if ( $quota > 0 ) { ++$quota; // the account is full — every further push would 403 identically continue; } $id = self::push_one( $link ); if ( 'quota' === $id ) { ++$quota; } elseif ( $id ) { ++$synced; } else { ++$failed; } } return array( 'synced' => $synced, 'failed' => $failed, 'quota' => $quota ); } /** * Push a single local link to the account and record its remote_id. * * @param object $link Row from AffKeeper_Local. * @return string|false The remote id on success; 'quota' when the plan's monitored-link * limit blocked it (the redirect keeps working locally); false otherwise. */ public static function push_one( $link ) { $payload = array( 'destination_url' => $link->destination, 'is_affiliate' => ! empty( $link->is_affiliate ), 'source' => 'wordpress', 'external_url' => AffKeeper_Local::short_url( $link ), 'title' => (string) $link->title, 'clicks' => (int) $link->clicks, ); if ( ! empty( $link->source_url ) ) { $payload['source_url'] = $link->source_url; } $resp = AffKeeper_API::create_link( $payload ); if ( is_wp_error( $resp ) ) { $data = $resp->get_error_data(); if ( is_array( $data ) && 403 === (int) ( isset( $data['status'] ) ? $data['status'] : 0 ) ) { return 'quota'; } return false; } if ( empty( $resp['id'] ) ) { return false; } AffKeeper_Local::update( (int) $link->id, array( 'remote_id' => $resp['id'] ) ); // Ship the day-by-day history right away (WP has it locally) — the panel chart shouldn't // sit empty until the next click or hourly cron. AffKeeper_API::report_clicks( (string) $resp['id'], (int) $link->clicks, AffKeeper_Local::daily_map( (int) $link->id ) ); return (string) $resp['id']; } /** * Near-realtime click report: fire-and-forget the link's fresh count right after a redirect, * so the panel doesn't lag an hour behind. Non-blocking (adds ~a millisecond to the redirect, * response ignored) and debounced per link — a click burst sends one report per 5s; whatever * slips through is healed by the hourly push_clicks cron, which is the reliable path. * * @param object $link Local row (fresh, AFTER the click was counted). */ public static function push_click_fast( $link ) { if ( empty( $link->remote_id ) || ! AffKeeper_API::is_configured() ) { return; } $key = 'affkeeper_cpush_' . $link->remote_id; if ( false !== get_transient( $key ) ) { return; // debounced — the cron will reconcile the exact number } set_transient( $key, 1, 5 ); wp_remote_post( untrailingslashit( AffKeeper_API::base_url() ) . '/api/v1/links/' . rawurlencode( $link->remote_id ) . '/clicks', array( 'blocking' => false, 'timeout' => 2, 'headers' => array( 'Authorization' => 'Bearer ' . AffKeeper_API::api_key(), 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( array( 'clicks' => (int) $link->clicks, 'daily' => AffKeeper_Local::daily_map( (int) $link->id ), ) ), ) ); // Deliberately NOT recorded in affkeeper_clicks_synced: we can't know a non-blocking send // landed, and the hourly cron re-sending an absolute count is a harmless no-op. } /** * Forward ONE click's context (referrer + user agent; visitor IP only when the site opted in) * so the panel can break clicks down like for its own links. Fire-and-forget and best-effort: * totals are reconciled by the absolute counter either way, a lost event only means one click * stays in the "unattributed" bucket. * * @param object $link Local row. */ public static function push_click_event( $link ) { if ( empty( $link->remote_id ) || ! AffKeeper_API::is_configured() ) { return; } $body = array( 'referrer' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', 'ua' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', ); // Preferred: the country is already known ON this server (Cloudflare / host geo headers) — // send the two-letter code and the visitor's IP never leaves the site at all. $cc = ''; foreach ( array( 'HTTP_CF_IPCOUNTRY', 'GEOIP_COUNTRY_CODE', 'HTTP_X_COUNTRY_CODE', 'HTTP_X_APPENGINE_COUNTRY' ) as $k ) { if ( ! empty( $_SERVER[ $k ] ) ) { $cand = strtoupper( sanitize_text_field( wp_unslash( $_SERVER[ $k ] ) ) ); if ( 2 === strlen( $cand ) && 'XX' !== $cand && ctype_alpha( $cand ) ) { $cc = $cand; break; } } } if ( '' !== $cc ) { $body['country'] = $cc; } wp_remote_post( untrailingslashit( AffKeeper_API::base_url() ) . '/api/v1/links/' . rawurlencode( $link->remote_id ) . '/click-event', array( 'blocking' => false, 'timeout' => 2, 'headers' => array( 'Authorization' => 'Bearer ' . AffKeeper_API::api_key(), 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( $body ), ) ); } /** * Two-way sync, downward leg: apply panel-side edits (slug / destination) to the local rows * that actually serve the redirects, and confirm each one back to the account. * * The site is authoritative: a pending panel edit is applied only if it's valid here (slug * free); otherwise the panel gets its real state back plus a reason (shown as "WP rejected"). * Without a pending flag any drift is healed UPWARD (local state wins). A remote record that * disappeared (deleted in the panel) gets re-pushed so monitoring returns. * * @param array $map slug => link data from AffKeeper_API::link_map(). * @return bool True when any local link changed (caller should re-fetch the map). */ public static function pull_updates( $map ) { if ( ! AffKeeper_API::is_configured() || ! is_array( $map ) ) { return false; } $by_id = array(); foreach ( $map as $l ) { if ( ! empty( $l['id'] ) ) { $by_id[ (string) $l['id'] ] = $l; } } $changed = false; $repush = false; foreach ( AffKeeper_Local::all() as $ll ) { if ( empty( $ll->remote_id ) ) { continue; } if ( ! isset( $by_id[ (string) $ll->remote_id ] ) ) { // The panel record is gone — clear the stale pointer and re-push below, so the // link (which still redirects here) gets its monitoring back. AffKeeper_Local::update( (int) $ll->id, array( 'remote_id' => '' ) ); $repush = true; continue; } $r = $by_id[ (string) $ll->remote_id ]; if ( ! empty( $r['wp_pending'] ) ) { $desired_dest = isset( $r['destination_url'] ) ? (string) $r['destination_url'] : ''; $ext = isset( $r['external_url'] ) ? untrailingslashit( (string) $r['external_url'] ) : ''; $desired_slug = '' !== $ext ? sanitize_title( basename( (string) wp_parse_url( $ext, PHP_URL_PATH ) ) ) : ''; $data = array(); if ( '' !== $desired_dest && $desired_dest !== $ll->destination ) { $data['destination'] = $desired_dest; } if ( '' !== $desired_slug && $desired_slug !== $ll->slug ) { if ( AffKeeper_Local::slug_exists( $desired_slug, (int) $ll->id ) ) { AffKeeper_API::update_link( $ll->remote_id, array( 'external_url' => AffKeeper_Local::short_url( $ll ), /* translators: %s: the slug that could not be used. */ 'wp_error' => sprintf( __( 'Slug "%s" is already used on this WordPress site.', 'affkeeper' ), $desired_slug ), ) ); continue; } $data['slug'] = $desired_slug; } if ( ! empty( $data ) ) { AffKeeper_Local::update( (int) $ll->id, $data ); $changed = true; } $fresh = AffKeeper_Local::get( (int) $ll->id ); AffKeeper_API::update_link( $ll->remote_id, array( 'external_url' => AffKeeper_Local::short_url( $fresh ? $fresh : $ll ), 'wp_synced' => true, ) ); } else { // No pending edit → heal drift upward (covers changes made here while offline). $local_url = untrailingslashit( AffKeeper_Local::short_url( $ll ) ); $remote_url = isset( $r['external_url'] ) ? untrailingslashit( (string) $r['external_url'] ) : ''; $remote_dest = isset( $r['destination_url'] ) ? (string) $r['destination_url'] : ''; $up = array(); if ( '' !== $ll->destination && $remote_dest !== $ll->destination ) { $up['destination_url'] = $ll->destination; } if ( $remote_url !== $local_url ) { $up['external_url'] = $local_url; } if ( ! empty( $up ) ) { $up['wp_synced'] = true; AffKeeper_API::update_link( $ll->remote_id, $up ); } } } if ( $repush ) { self::push_unsynced(); } return $changed || $repush; } /** * Report each synced link's current click total to the account, so panel stats reflect the * clicks counted locally on /go/. Absolute + idempotent; only links whose count changed since * the last push are sent (tracked in an option, so no per-click API chatter). Runs on WP-Cron. * * @return int Number of links pushed this run. */ public static function push_clicks() { if ( ! AffKeeper_API::is_configured() ) { return 0; } $last = get_option( 'affkeeper_clicks_synced', array() ); if ( ! is_array( $last ) ) { $last = array(); } $pushed = 0; $changed = false; foreach ( AffKeeper_Local::all() as $link ) { if ( empty( $link->remote_id ) ) { continue; } $count = (int) $link->clicks; if ( isset( $last[ $link->remote_id ] ) && (int) $last[ $link->remote_id ] === $count ) { continue; // unchanged since last push } $resp = AffKeeper_API::report_clicks( $link->remote_id, $count, AffKeeper_Local::daily_map( (int) $link->id ) ); if ( ! is_wp_error( $resp ) ) { $last[ $link->remote_id ] = $count; $changed = true; ++$pushed; } } if ( $changed ) { update_option( 'affkeeper_clicks_synced', $last, false ); } return $pushed; } /** * How many local links still need syncing (drives the "Sync now" prompt). * * @return int */ public static function unsynced_count() { $n = 0; foreach ( AffKeeper_Local::all() as $link ) { if ( empty( $link->remote_id ) ) { ++$n; } } return $n; } }